220 lines
8.6 KiB
Python
220 lines
8.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""API de licencia por plan (Configuración -> Licencia).
|
|
|
|
Solo administración del sistema (PUT) y administración/gerencia (GET/validar).
|
|
Nunca bloquea el login, la agenda ni la configuración.
|
|
"""
|
|
|
|
import json
|
|
|
|
from odoo import fields, http
|
|
from odoo.http import request, Response
|
|
|
|
from odoo.addons.skeen_whatsapp.controllers.auth import require_role, current_user
|
|
from odoo.addons.skeen_citas.clinic_settings import (
|
|
PLANES, MODULOS_BASE, MODULOS_GATEABLES, MODULO_LABEL, LIMITE_KEYS,
|
|
get_settings, set_settings, licencia_de, modulos_licencia,
|
|
licencia_bloqueo_activo, verificar_clave_licencia, licencia_desde_payload,
|
|
dias_para_vencer, LICENCIA_DEFAULT,
|
|
)
|
|
|
|
BASE = '/skeen/frontend/v1'
|
|
LIMITE_LABEL = {
|
|
'usuarios_max': 'Usuarios',
|
|
'sucursales_max': 'Sucursales',
|
|
'citas_mes_max': 'Citas del mes',
|
|
'almacenamiento_mb': 'Almacenamiento (MB)',
|
|
}
|
|
AVISO_DIAS = 15
|
|
|
|
|
|
def _json(data, status=200):
|
|
return Response(
|
|
json.dumps(data, default=str),
|
|
status=status,
|
|
mimetype='application/json',
|
|
headers={
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
},
|
|
)
|
|
|
|
|
|
def _body():
|
|
try:
|
|
raw = request.httprequest.get_data(as_text=True)
|
|
return json.loads(raw) if raw else {}
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def _puede_ver():
|
|
u = current_user(request.env)
|
|
return bool(u and u.role in ('admin', 'gerencia'))
|
|
|
|
|
|
def _puede_editar():
|
|
u = current_user(request.env)
|
|
return bool(u and u.role == 'admin')
|
|
|
|
|
|
def _uso(env):
|
|
"""Uso actual de cada límite frente a su tope."""
|
|
from datetime import date
|
|
hoy = date.today()
|
|
inicio = hoy.replace(day=1)
|
|
fin = hoy.replace(year=hoy.year + 1, month=1, day=1) if hoy.month == 12 \
|
|
else hoy.replace(month=hoy.month + 1, day=1)
|
|
env.cr.execute('SELECT COALESCE(SUM(file_size), 0) FROM ir_attachment')
|
|
bytes_almacen = env.cr.fetchone()[0] or 0
|
|
return {
|
|
'usuarios_max': env['skeen.frontend.user'].sudo().search_count([('active', '=', True)]),
|
|
'sucursales_max': env['skeen.sucursal'].sudo().search_count([('active', '=', True)]),
|
|
'citas_mes_max': env['skeen.cita'].sudo().search_count(
|
|
[('date', '>=', inicio), ('date', '<', fin)]),
|
|
'almacenamiento_mb': round(bytes_almacen / (1024.0 * 1024.0), 2),
|
|
}
|
|
|
|
|
|
def _estado(env):
|
|
settings = get_settings(env)
|
|
licencia = settings.get('licencia') or dict(LICENCIA_DEFAULT)
|
|
limites = licencia.get('limites') or {}
|
|
uso = _uso(env)
|
|
filas = []
|
|
for clave in LIMITE_KEYS:
|
|
tope = int(limites.get(clave) or 0)
|
|
actual = uso.get(clave) or 0
|
|
if tope <= 0:
|
|
pct = 0
|
|
estado = 'sin_limite'
|
|
else:
|
|
pct = round(min(100.0, (actual / float(tope)) * 100.0), 1)
|
|
estado = 'excedido' if actual > tope else (
|
|
'cerca' if pct >= 80 else 'ok')
|
|
filas.append({
|
|
'clave': clave,
|
|
'label': LIMITE_LABEL.get(clave, clave),
|
|
'uso': actual,
|
|
'limite': tope,
|
|
'porcentaje': pct,
|
|
'estado': estado,
|
|
})
|
|
permitidos = modulos_licencia(licencia)
|
|
modulos = []
|
|
for clave in list(MODULOS_BASE) + list(MODULOS_GATEABLES):
|
|
incluido = permitidos is None or clave in permitidos
|
|
modulos.append({
|
|
'clave': clave,
|
|
'label': MODULO_LABEL.get(clave, clave),
|
|
'base': clave in MODULOS_BASE,
|
|
'incluido': incluido,
|
|
})
|
|
dias = dias_para_vencer(licencia)
|
|
aplicada = bool(licencia.get('clave'))
|
|
return {
|
|
'licencia': {
|
|
'plan': licencia.get('plan') or '',
|
|
'plan_label': (PLANES.get(licencia.get('plan')) or {}).get('label', ''),
|
|
'modo': licencia.get('modo') or 'aviso',
|
|
'cliente': licencia.get('cliente') or '',
|
|
'vence': licencia.get('vence') or '',
|
|
'dias_restantes': dias,
|
|
'aplicada': aplicada,
|
|
'bloqueo_activo': licencia_bloqueo_activo(licencia),
|
|
'aviso_vence': bool(dias is not None and 0 <= dias <= AVISO_DIAS),
|
|
},
|
|
'limites': filas,
|
|
'modulos': modulos,
|
|
'planes': [{'clave': k, 'label': v['label'], 'modulos': v['modulos']}
|
|
for k, v in PLANES.items()],
|
|
'aviso': ('Sin licencia aplicada — todas las funciones habilitadas.'
|
|
if not aplicada else None),
|
|
}
|
|
|
|
|
|
class SkeenLicenciaController(http.Controller):
|
|
|
|
@http.route(BASE + '/licencia', type='http', auth='none',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
@require_role('gerencia')
|
|
def get_licencia(self, **kw):
|
|
try:
|
|
if str(request.httprequest.method).upper() == 'OPTIONS':
|
|
return _json({}, 200)
|
|
if not _puede_ver():
|
|
return _json({'status': 'error', 'message': 'Sin permiso'}, 403)
|
|
return _json({'status': 'success', **_estado(request.env)})
|
|
except Exception as e:
|
|
return _json({'status': 'error', 'message': str(e)}, 500)
|
|
|
|
@http.route(BASE + '/licencia', type='http', auth='none',
|
|
methods=['PUT', 'OPTIONS'], csrf=False)
|
|
@require_role('gerencia')
|
|
def put_licencia(self, **kw):
|
|
try:
|
|
if str(request.httprequest.method).upper() == 'OPTIONS':
|
|
return _json({}, 200)
|
|
if not _puede_editar():
|
|
return _json({'status': 'error',
|
|
'message': 'Solo administración puede aplicar licencias'}, 403)
|
|
data = _body()
|
|
env = request.env
|
|
if 'clave' in data:
|
|
clave = (data.get('clave') or '').strip()
|
|
if not clave:
|
|
# Quitar la licencia: todo vuelve a estar habilitado.
|
|
vacio = dict(LICENCIA_DEFAULT)
|
|
vacio['limites'] = {k: 0 for k in LIMITE_KEYS}
|
|
set_settings(env, {'licencia': vacio})
|
|
return _json({'status': 'success',
|
|
'message': 'Licencia retirada. Todo habilitado.',
|
|
**_estado(env)})
|
|
payload, error = verificar_clave_licencia(env, clave)
|
|
if error:
|
|
return _json({'status': 'error',
|
|
'message': '%s No se guardó ningún cambio.' % error}, 400)
|
|
modo = data.get('modo') or 'bloqueo'
|
|
lic = licencia_desde_payload(payload, clave, modo=modo)
|
|
set_settings(env, {'licencia': lic})
|
|
return _json({'status': 'success', 'message': 'Licencia aplicada.',
|
|
**_estado(env)})
|
|
# Cambios sin re-firmar (p. ej. pasar de aviso a bloqueo).
|
|
vals = {k: data[k] for k in ('modo', 'plan', 'cliente', 'vence',
|
|
'modulos', 'limites') if k in data}
|
|
if not vals:
|
|
return _json({'status': 'error', 'message': 'Nada que actualizar'}, 400)
|
|
set_settings(env, {'licencia': vals})
|
|
return _json({'status': 'success', 'message': 'Licencia actualizada.',
|
|
**_estado(env)})
|
|
except ValueError as e:
|
|
request.env.cr.rollback()
|
|
return _json({'status': 'error', 'message': str(e)}, 400)
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _json({'status': 'error', 'message': str(e)}, 500)
|
|
|
|
@http.route(BASE + '/licencia/validar', type='http', auth='none',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
@require_role('gerencia')
|
|
def validar_licencia(self, **kw):
|
|
try:
|
|
if str(request.httprequest.method).upper() == 'OPTIONS':
|
|
return _json({}, 200)
|
|
if not _puede_ver():
|
|
return _json({'status': 'error', 'message': 'Sin permiso'}, 403)
|
|
data = _body()
|
|
payload, error = verificar_clave_licencia(request.env, data.get('clave'))
|
|
dias = dias_para_vencer({'vence': (payload or {}).get('vence')})
|
|
return _json({
|
|
'status': 'success' if not error else 'error',
|
|
'valida': error is None,
|
|
'error': error,
|
|
'payload': payload,
|
|
'dias_restantes': dias,
|
|
'aviso_vence': bool(dias is not None and 0 <= dias <= AVISO_DIAS),
|
|
}, 200 if not error else 400)
|
|
except Exception as e:
|
|
return _json({'status': 'error', 'message': str(e)}, 500)
|