Sistema de Numeracion¶
El sistema genera numeros oficiales unicos para todos los documentos del sistema. Usa advisory locks de PostgreSQL para prevenir race conditions en entornos concurrentes.
Formato del Numero Oficial¶
| Componente | Ejemplo | Origen |
|---|---|---|
| TIPO | IF, CAEX, PV |
Acronimo del tipo de documento |
| ANIO | 2026 |
Anio actual |
| SECUENCIA | 00000001 |
Secuencia global (8 digitos, zero-padded) |
| MUNICIPIO | TXST |
Acronimo de la organizacion (de public.municipalities) |
| DEPARTAMENTO | INTE |
Acronimo del departamento del numerador |
Ejemplos:
IF-2026-00000001-TXST-INTE -- Primer informe del anio
NOTA-2026-00000002-TXST-LEGAL -- Segunda nota (secuencia global)
CAEX-2026-00000003-TXST-INNO -- Caratula de expediente
PV-2026-00000004-TXST-INNO -- Pase de expediente
RESOL-2026-00000005-TXST-HAC -- Resolucion
Secuencia Global¶
La secuencia es global por anio: todos los tipos de documentos comparten la misma secuencia. Esto garantiza:
- No hay numeros duplicados entre tipos de documento
- Orden cronologico unico entre todos los documentos de la organizacion
- Trazabilidad simple (el numero mas alto = ultimo documento numerado)
La secuencia se resetea a 1 al cambiar de anio.
Advisory Lock¶
Por que Advisory Lock¶
PostgreSQL sequences (nextval) no sirven porque:
- No se resetean por anio automaticamente
- Si una transaccion hace rollback, el numero se pierde (huecos permanentes)
El advisory lock permite:
- Serializar el acceso al SELECT MAX + INSERT
- Reutilizar numeros en caso de rollback (no hay huecos)
- Lock ultra corto (10-20ms)
Lock IDs¶
| Lock ID | Uso | Donde se usa |
|---|---|---|
888888 |
Numeracion de documentos oficiales | shared/numbering.py |
999999 |
Numeracion de expedientes | database.py |
Lock IDs fijos
Los IDs 888888 y 999999 son arbitrarios pero deben ser unicos en todo el sistema. No cambiar sin verificar que no hay conflictos.
Flujo de Numeracion¶
sequenceDiagram
participant S as Service (Backend)
participant PG as PostgreSQL
participant T as Tabla official_documents
S->>PG: SELECT pg_advisory_xact_lock(888888)
Note over PG: Lock adquirido (bloquea otros)
S->>T: SELECT COALESCE(MAX(global_sequence), 0) + 1<br/>FROM official_documents<br/>WHERE year = 2026
T-->>S: next_number = 42
S->>S: Formatear: IF-2026-00000042-TXST-INTE
Note over PG: Lock se libera al commit/rollback
S->>T: INSERT INTO official_documents<br/>(..., global_sequence=42, ...)
S->>PG: COMMIT
Note over PG: Lock liberado
Codigo (Python)¶
El modulo shared/numbering.py del Backend centraliza toda la logica:
# Lock ID unico para numeracion de documentos oficiales
OFFICIAL_DOCUMENTS_LOCK_ID = 888888
async def generate_official_number(
document_type_acronym: str,
user_id: str,
year: int,
connection=None,
*,
schema_name: str # SIEMPRE keyword-only
) -> Tuple[str, str, int]:
"""
Genera un numero oficial unico.
Returns:
(official_number, department_id, global_sequence)
"""
cursor = connection.cursor()
# Paso 1: Obtener datos del usuario
# - city_acronym de public.municipalities
# - dept_acronym del departamento del usuario
cursor.execute("""
SELECT acronym as city_acronym
FROM public.municipalities
WHERE schema_name = %s
""", (schema_name,))
city_acronym = cursor.fetchone()['city_acronym']
cursor.execute("""
SELECT d.acronym as dept_acronym, d.id as department_id
FROM users u
LEFT JOIN sectors s ON u.sector_id = s.id
LEFT JOIN departments d ON s.department_id = d.id
WHERE u.id = %s
""", (user_id,))
user_info = cursor.fetchone()
# Paso 2: Advisory lock + generar numero
cursor.execute(f"SELECT pg_advisory_xact_lock({OFFICIAL_DOCUMENTS_LOCK_ID})")
cursor.execute("""
SELECT COALESCE(MAX(global_sequence), 0) + 1 as next_number
FROM official_documents
WHERE year = %s AND global_sequence IS NOT NULL
""", (year,))
next_number = cursor.fetchone()['next_number']
# Paso 3: Formatear
official_number = (
f"{document_type_acronym}-{year}-{next_number:08d}"
f"-{city_acronym}-{dept_acronym}"
)
return official_number, department_id, next_number
schema_name keyword-only
La funcion generate_official_number recibe schema_name despues de *, lo que lo hace keyword-only. Siempre debe llamarse como schema_name=schema_name.
Numeracion de Expedientes¶
Los expedientes usan un sistema similar con el lock ID 999999:
Ejemplo: EE-2026-000001-TXST-INTE
La secuencia de expedientes es independiente de la de documentos oficiales.
def get_next_case_sequence(year: int = None, *, schema_name: str) -> int:
"""Obtener siguiente numero secuencial para expedientes."""
with get_db_connection(schema_name) as conn:
with conn.cursor() as cursor:
# Lock exclusivo para expedientes (ID diferente)
cursor.execute("SELECT pg_advisory_xact_lock(999999)")
cursor.execute("""
SELECT COALESCE(MAX(
CAST(SUBSTRING(case_number FROM '\\d{4}-(\\d+)-') AS INTEGER)
), 0) + 1 as next_sequence
FROM cases
WHERE EXTRACT(YEAR FROM created_at) = %s
""", (year,))
result = cursor.fetchone()
conn.commit()
return result['next_sequence']
Documentos Automaticos¶
Dos tipos de documentos se generan automaticamente y usan el mismo sistema de numeracion:
| Tipo | Acronimo | Cuando se genera | Generador |
|---|---|---|---|
| Caratula | CAEX | Al crear un expediente | services/cases/cover_creator.py |
| Pase | PV | Al transferir un expediente | services/cases/transfer_document_creator.py |
Ambos llaman a generate_official_number() del modulo shared/numbering.py.
Tabla Resumen¶
| Aspecto | Documentos Oficiales | Expedientes |
|---|---|---|
| Lock ID | 888888 | 999999 |
| Formato | {TIPO}-{ANIO}-{SEQ:08d}-{MUNI}-{DEPT} |
EE-{ANIO}-{SEQ:06d}-{MUNI}-{DEPT} |
| Secuencia | Global por anio (8 digitos) | Global por anio (6 digitos) |
| Columna | official_documents.global_sequence |
Extraida de cases.case_number |
| Modulo | shared/numbering.py |
database.py |
| Tipo lock | pg_advisory_xact_lock |
pg_advisory_xact_lock |
| Duracion lock | ~10-20ms | ~10-20ms |