- 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/
183 lines
7.6 KiB
Python
183 lines
7.6 KiB
Python
# -*- 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'}
|