# -*- 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)