Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica
- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind - Frontend Homenest (versión alternativa) - Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp) - WACRM fork (Next.js 16 + Supabase) - Hermes + Bridge + Skills (Qwen3.6 via Nan Builders) - Scripts de migración y operación - Documentación extensiva en docs/
This commit is contained in:
182
odoo-addons/skeen_whatsapp/controllers/main.py
Normal file
182
odoo-addons/skeen_whatsapp/controllers/main.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
import json
|
||||
|
||||
|
||||
class SkeenWhatsAppController(http.Controller):
|
||||
"""Controladores API para integración con Hermes/WACRM"""
|
||||
|
||||
@http.route('/skeen/api/v1/available_slots', type='json', auth='api_key', methods=['POST'], csrf=False)
|
||||
def get_available_slots(self, service_id, date_from, date_to, **kw):
|
||||
"""Consultar slots disponibles para un servicio en un rango de fechas"""
|
||||
try:
|
||||
servicio = request.env['skeen.servicio'].browse(int(service_id))
|
||||
if not servicio.exists():
|
||||
return {'status': 'error', 'message': 'Servicio no encontrado'}
|
||||
|
||||
slots = []
|
||||
from datetime import datetime, timedelta
|
||||
current_date = datetime.strptime(date_from, '%Y-%m-%d').date()
|
||||
end_date = datetime.strptime(date_to, '%Y-%m-%d').date()
|
||||
|
||||
while current_date <= end_date:
|
||||
day_slots = request.env['skeen.cita'].get_available_slots(current_date, int(service_id))
|
||||
for slot in day_slots:
|
||||
slots.append({
|
||||
'date': current_date.strftime('%Y-%m-%d'),
|
||||
'time': slot['time_str'],
|
||||
'datetime': f"{current_date.strftime('%Y-%m-%d')} {slot['time_str']}",
|
||||
})
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return {'status': 'success', 'slots': slots}
|
||||
except Exception as e:
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
|
||||
@http.route('/skeen/api/v1/create_appointment', type='json', auth='api_key', methods=['POST'], csrf=False)
|
||||
def create_appointment(self, phone, name, service_id, date, time, **kw):
|
||||
"""Crear una nueva cita"""
|
||||
try:
|
||||
# Buscar o crear paciente
|
||||
partner = request.env['res.partner'].search([('phone', '=', phone)], limit=1)
|
||||
if not partner:
|
||||
partner = request.env['res.partner'].create({
|
||||
'name': name,
|
||||
'phone': phone,
|
||||
'is_patient': True,
|
||||
'source': 'whatsapp',
|
||||
})
|
||||
|
||||
# Convertir time a float
|
||||
time_parts = time.split(':')
|
||||
time_float = float(time_parts[0]) + float(time_parts[1]) / 60.0
|
||||
|
||||
# Crear cita
|
||||
cita = request.env['skeen.cita'].create({
|
||||
'partner_id': partner.id,
|
||||
'servicio_id': int(service_id),
|
||||
'date': date,
|
||||
'time': time_float,
|
||||
'state': 'confirmed',
|
||||
})
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'booking_id': cita.id,
|
||||
'reference': cita.name,
|
||||
'patient_id': partner.patient_id,
|
||||
}
|
||||
except Exception as e:
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
|
||||
@http.route('/skeen/api/v1/patient_balance', type='json', auth='api_key', methods=['POST'], csrf=False)
|
||||
def get_patient_balance(self, phone, **kw):
|
||||
"""Obtener saldo pendiente y monedero del paciente"""
|
||||
try:
|
||||
partner = request.env['res.partner'].search([('phone', '=', phone)], limit=1)
|
||||
if not partner:
|
||||
return {'status': 'error', 'message': 'Paciente no encontrado'}
|
||||
|
||||
# Citas pendientes de pago
|
||||
pending_citas = request.env['skeen.cita'].search([
|
||||
('partner_id', '=', partner.id),
|
||||
('payment_state', '!=', 'paid'),
|
||||
('state', 'in', ('confirmed', 'arrived', 'in_progress', 'done')),
|
||||
])
|
||||
|
||||
pending_balance = sum(c.servicio_price for c in pending_citas)
|
||||
|
||||
# Monedero
|
||||
monedero = request.env['skeen.monedero'].search([('partner_id', '=', partner.id)], limit=1)
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'pending_balance': pending_balance,
|
||||
'pending_invoices': [{
|
||||
'id': c.id,
|
||||
'reference': c.name,
|
||||
'service': c.servicio_id.name,
|
||||
'amount': c.servicio_price,
|
||||
'date': c.date.strftime('%Y-%m-%d'),
|
||||
} for c in pending_citas],
|
||||
'wallet_points': monedero.points if monedero else 0,
|
||||
'wallet_mxn': monedero.equivalent_mxn if monedero else 0,
|
||||
}
|
||||
except Exception as e:
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
|
||||
@http.route('/skeen/api/v1/wallet_redeem', type='json', auth='api_key', methods=['POST'], csrf=False)
|
||||
def redeem_wallet(self, phone, points, appointment_id=None, **kw):
|
||||
"""Redimir puntos del monedero"""
|
||||
try:
|
||||
partner = request.env['res.partner'].search([('phone', '=', phone)], limit=1)
|
||||
if not partner:
|
||||
return {'status': 'error', 'message': 'Paciente no encontrado'}
|
||||
|
||||
monedero = request.env['skeen.monedero'].search([('partner_id', '=', partner.id)], limit=1)
|
||||
if not monedero:
|
||||
return {'status': 'error', 'message': 'Monedero no encontrado'}
|
||||
|
||||
monedero.redeem_points(int(points))
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'remaining_points': monedero.points,
|
||||
'remaining_mxn': monedero.equivalent_mxn,
|
||||
}
|
||||
except Exception as e:
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
|
||||
@http.route('/skeen/api/v1/cancel_appointment', type='json', auth='api_key', methods=['POST'], csrf=False)
|
||||
def cancel_appointment(self, phone, booking_id=None, **kw):
|
||||
"""Cancelar una cita"""
|
||||
try:
|
||||
domain = [
|
||||
('partner_id.phone', '=', phone),
|
||||
('state', 'in', ('pending', 'confirmed')),
|
||||
]
|
||||
if booking_id:
|
||||
domain.append(('id', '=', int(booking_id)))
|
||||
|
||||
cita = request.env['skeen.cita'].search(domain, order='date asc', limit=1)
|
||||
if not cita:
|
||||
return {'status': 'error', 'message': 'No se encontró cita activa'}
|
||||
|
||||
cita.write({'state': 'cancelled'})
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'cancelled_booking': cita.name,
|
||||
}
|
||||
except Exception as e:
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
|
||||
@http.route('/skeen/api/v1/services', type='json', auth='api_key', methods=['GET', 'POST'], csrf=False)
|
||||
def list_services(self, **kw):
|
||||
"""Listar todos los servicios disponibles"""
|
||||
try:
|
||||
services = request.env['skeen.servicio'].search([('active', '=', True)])
|
||||
return {
|
||||
'status': 'success',
|
||||
'services': [{
|
||||
'id': s.id,
|
||||
'code': s.code,
|
||||
'name': s.name,
|
||||
'category': s.category,
|
||||
'price': s.price,
|
||||
'package_price': s.package_price,
|
||||
'package_sessions': s.package_sessions,
|
||||
'package_notes': s.package_notes or '',
|
||||
'duration_min': s.duration_min,
|
||||
'description': s.description,
|
||||
} for s in services],
|
||||
}
|
||||
except Exception as e:
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
|
||||
@http.route('/skeen/api/v1/health', type='json', auth='none', methods=['GET'], csrf=False)
|
||||
def health_check(self, **kw):
|
||||
"""Health check para monitoreo"""
|
||||
return {'status': 'ok', 'service': 'skeen-odoo-api', 'version': '1.0.0'}
|
||||
Reference in New Issue
Block a user