diff --git a/modules/descargas/informe-rup/elementos-rup/receta-medica.component.ts b/modules/descargas/informe-rup/elementos-rup/receta-medica.component.ts
index eb385e1806..b32ca16f7a 100644
--- a/modules/descargas/informe-rup/elementos-rup/receta-medica.component.ts
+++ b/modules/descargas/informe-rup/elementos-rup/receta-medica.component.ts
@@ -154,7 +154,7 @@ del Ministerio de Salud de la Nación - RL-2025-24026558-APN-SSVEIYES#MS
this.data = {
registro: this.registro,
- esReceta: this.depth ? 1 : 0, // Si es 0 no muestra el código de barras
+ esReceta: 0,
idReceta: finalIdReceta,
estadoReceta,
barcodeSVG: generateBarcodeSVG(finalIdReceta, 'code128')
diff --git a/modules/descargas/recetas/recetas-body.ts b/modules/descargas/recetas/recetas-body.ts
new file mode 100644
index 0000000000..241266f9c5
--- /dev/null
+++ b/modules/descargas/recetas/recetas-body.ts
@@ -0,0 +1,284 @@
+import { HTMLComponent } from '../model/html-component.class';
+import * as moment from 'moment';
+import { generateBarcodeSVG } from '../model/barcode';
+import { makeFsFirma } from '../../../core/tm/schemas/firmaProf';
+import { streamToBase64 } from '../../../core/tm/controller/file-storage';
+import { searchMatriculas } from '../../../core/tm/controller/profesional';
+import { Profesional } from '../../../core/tm/schemas/profesional';
+
+export class RecetasBody extends HTMLComponent {
+ template = `
+
+ {{#each recetas}}
+
+
+
+
+
Vigente Desde
+ {{fechaEmision}}
+
+
+
Fecha de Vencimiento
+ {{fechaVencimiento}}
+
+
+
+
+
+
+
+
+
+ RECETA MÉDICA
+
+ ESTADO RECETA.
+ {{estadoReceta}}
+
+ |
+
+
+ {{{barcodeSVG}}}
+
+ |
+
+
+
+
+
+
+
+ |
+ {{nombre}}
+ {{#if esMagistral}}FÓRMULA MAGISTRAL {{/if}}
+ {{#if tratamientoProlongado}}TRATAMIENTO PROLONGADO — Mes {{ordenTratamiento}} de {{totalTratamiento}} {{/if}}
+ |
+
+
+
+
+ | Cantidad |
+ {{cantidad}} |
+
+
+ | Dosis diaria |
+ {{dosisDiaria}} |
+
+
+ | Diagnóstico |
+ {{diagnostico}} |
+
+
+ | Obra Social |
+ {{obraSocial}} |
+
+
+
+
+ {{#if notaMedica}}
+
+
NOTA MÉDICA PARA EL PACIENTE
+
{{notaMedica}}
+
+ {{/if}}
+ {{#if firma}}
+
+
Este documento ha sido firmado electronicamente por:
+ {{#if firma}}

{{/if}}
+
+
{{detalle}}
+ {{#if matriculas}}
{{{matriculas}}}
{{/if}}
+
+ {{/if}}
+
+
+ {{/each}}
+
+ `;
+
+ constructor(
+ public prestacion,
+ public paciente,
+ public organizacion,
+ public recetasData: any[],
+ public registroId: string | null
+ ) {
+ super();
+ }
+
+ public async process() {
+
+ const recetas = [];
+ for (const receta of this.recetasData) {
+ const esInsumo = !!receta.insumo;
+ const fechaEmision = moment(receta.fechaRegistro).format('DD/MM/YYYY');
+ const fechaVencimiento = moment(receta.fechaRegistro).add(30, 'days').format('DD/MM/YYYY');
+
+ let nombre = '';
+ if (esInsumo) {
+ nombre = receta.insumo?.nombre || receta.insumo?.especificacion || '';
+ } else {
+ if (receta.medicamento?.esMagistral) {
+ nombre = receta.medicamento?.magistral?.nombre || receta.medicamento?.nombre || '';
+ } else {
+ nombre = receta.medicamento?.nombre || receta.medicamento?.concepto?.term || '';
+ }
+ }
+
+ let presentacion = '';
+ if (esInsumo) {
+ presentacion = receta.insumo?.tipo || receta.insumo?.unidades || '';
+ if (receta.insumo?.especificacion) {
+ presentacion = receta.insumo.especificacion;
+ }
+ } else {
+ const pres = receta.medicamento?.presentacion || '';
+ const unidades = receta.medicamento?.unidades ? `${receta.medicamento.unidades} ` : '';
+ presentacion = `${unidades}${pres}`.trim() || (pres || '');
+ if (!presentacion && receta.medicamento?.presentacion) {
+ presentacion = receta.medicamento.presentacion;
+ }
+ }
+ if (!presentacion) {
+ presentacion = esInsumo ? (receta.insumo?.tipo || '-') : (receta.medicamento?.presentacion || '-');
+ }
+
+ let cantidad = '';
+ if (esInsumo) {
+ const cant = receta.insumo?.cantidad;
+ const tipo = receta.insumo?.tipo || '';
+ cantidad = cant ? `${cant} ${tipo}`.trim() : `${receta.insumo?.cantidad || ''}`.trim();
+ if (!cantidad) { cantidad = '-'; }
+ if (receta.insumo?.cantidad) {
+ cantidad = `${receta.insumo.cantidad} envase(s)`;
+ if (receta.insumo?.unidades) { cantidad += ` de ${receta.insumo.unidades}`; }
+ }
+ } else {
+ const cantEnvases = receta.medicamento?.cantEnvases;
+ const cantidadUni = receta.medicamento?.cantidad;
+ const pres = receta.medicamento?.presentacion || 'comp';
+ if (cantEnvases && cantidadUni) {
+ cantidad = `${cantEnvases} envase(s) de ${cantidadUni} ${pres}(s)`;
+ } else if (cantEnvases) {
+ cantidad = `${cantEnvases} envase(s)`;
+ } else {
+ cantidad = `${cantidadUni || ''} ${pres}`.trim() || '-';
+ }
+ }
+
+ // Dosis diaria
+ let dosisDiaria = '-';
+ if (esInsumo) {
+ dosisDiaria = receta.insumo?.especificacion || receta.diagnostico?.term || '-';
+
+ if (receta.insumo?.cantidad) {
+ dosisDiaria = receta.insumo?.especificacion || '-';
+ }
+ } else {
+ const dd = receta.medicamento?.dosisDiaria;
+ if (dd && (dd.dosis || dd.intervalo || dd.dias)) {
+ const parts = [];
+ if (dd.dosis) { parts.push(`${dd.dosis}`); }
+ if (dd.intervalo?.nombre) { parts.push(`cada ${dd.intervalo.nombre}`); }
+ if (dd.dias) { parts.push(`durante ${dd.dias} día(s)`); }
+ dosisDiaria = parts.join(' ') || '-';
+
+ if (dd.dosis && dd.intervalo?.nombre && dd.dias) {
+ dosisDiaria = `${dd.dosis} cada ${dd.intervalo.nombre} durante ${dd.dias} día(s)`;
+ } else if (dd.dosis && dd.intervalo?.nombre) {
+ dosisDiaria = `${dd.dosis} cada ${dd.intervalo.nombre}`;
+ }
+ } else {
+ dosisDiaria = '-';
+ }
+ }
+
+ const diagnostico = receta.diagnostico?.term || receta.diagnostico?.descripcion || '-';
+ const obraSocial = receta.paciente?.obraSocial?.financiador || receta.paciente?.obraSocial?.nombre || this.prestacion?.paciente?.obraSocial?.financiador || 'sin obra social';
+ const notaMedica = !esInsumo ? (receta.medicamento?.dosisDiaria?.notaMedica || null) : (receta.insumo?.especificacion || null);
+ const notaFinal = (!esInsumo && notaMedica) ? notaMedica : (esInsumo ? null : notaMedica);
+ const estadoRaw = receta.estadoActual?.tipo || 'vigente';
+ const estadoReceta = estadoRaw.charAt(0).toUpperCase() + estadoRaw.slice(1).replace('-', ' ');
+
+ // Tratamiento prolongado
+ const tratamientoProlongado = !esInsumo ? !!receta.medicamento?.tratamientoProlongado : !!receta.insumo?.tratamientoProlongado;
+ const ordenTratamiento = !esInsumo ? ((receta.medicamento?.ordenTratamiento ?? 0) + 1) : ((receta.insumo?.ordenTratamiento ?? 0) + 1);
+ const totalTratamiento = !esInsumo ? (receta.medicamento?.tiempoTratamiento?.id || receta.medicamento?.tiempoTratamiento?.nombre || '') : (receta.insumo?.tiempoTratamiento?.id || '');
+
+ const esMagistral = !esInsumo ? !!receta.medicamento?.esMagistral : receta.insumo?.tipo === 'magistral';
+
+ const idReceta = receta.idReceta || receta._id?.toString() || '';
+ const barcodeSVG = idReceta ? generateBarcodeSVG(idReceta, 'code128') : '';
+
+ // Firma y formación del profesional (por receta, igual que informe-rup firma)
+ let firma = null;
+ let detalle = null;
+ let matriculas = null;
+ try {
+ const profData = receta.profesional;
+ const prof = profData?._id || profData?.id ? await Profesional.findOne({ _id: profData.id || profData._id }) as any : await Profesional.findOne({ documento: profData?.documento }) as any;
+ if (prof) {
+ firma = await this.getFirma(prof);
+ const mat = await this.getMatriculas(prof);
+ matriculas = mat;
+ detalle = prof.apellido + ', ' + prof.nombre;
+ } else if (profData) {
+ detalle = (profData.apellido || '') + ', ' + (profData.nombre || '');
+ }
+ } catch (e) {
+ // silencioso, sin firma
+ }
+
+ recetas.push({
+ esInsumo,
+ idReceta,
+ barcodeSVG,
+ fechaEmision,
+ fechaVencimiento,
+ estadoReceta,
+ nombre,
+ presentacion,
+ cantidad,
+ dosisDiaria,
+ diagnostico,
+ obraSocial,
+ notaMedica: notaFinal,
+ esMagistral,
+ tratamientoProlongado,
+ ordenTratamiento,
+ totalTratamiento,
+ firma,
+ detalle,
+ matriculas
+ });
+ }
+
+ this.data = {
+ recetas
+ };
+ }
+
+ private async getFirma(profesional) {
+ try {
+ const FirmaSchema = makeFsFirma();
+ const idProfesional = String(profesional.id || profesional._id);
+ const file = await FirmaSchema.findOne({ 'metadata.idProfesional': idProfesional });
+ if (file && file._id) {
+ const stream = await FirmaSchema.readFile({ _id: file._id });
+ const base64 = await streamToBase64(stream);
+ return base64;
+ }
+ } catch (e) {}
+ return null;
+ }
+
+ private async getMatriculas(profesional) {
+ try {
+ const info = await searchMatriculas(profesional.id || profesional._id);
+ const grado = (info?.formacionGrado || []).map(e => `${e.nombre} MP ${e.numero}`);
+ const posgrado = (info?.formacionPosgrado || []).map(e => `${e.nombre} ME ${e.numero}`);
+ return [...grado, ...posgrado].join('
');
+ } catch (e) {
+ return null;
+ }
+ }
+}
diff --git a/modules/descargas/recetas/recetas-header.ts b/modules/descargas/recetas/recetas-header.ts
new file mode 100644
index 0000000000..4adcb51f15
--- /dev/null
+++ b/modules/descargas/recetas/recetas-header.ts
@@ -0,0 +1,145 @@
+import { InformeRupHeader } from '../informe-rup/informe-header';
+import { loadImage } from '../model/informe.class';
+
+export class RecetasHeader extends InformeRupHeader {
+ template = `
+
+
+
+
+
+
Datos del paciente
+
+ {{ paciente.apellido }},
+ {{#if paciente.alias}}
+ {{paciente.alias}}
+ {{else}}
+ {{paciente.nombre}}
+ {{/if}}
+
+
+ {{ paciente.genero }} |
+ {{#if paciente.edad }}
+ {{ paciente.edad }} años |
+ {{/if}}
+ {{ paciente.documento }}
+
+
+
+
+
+ Obra Social:
+
+ {{#if paciente.obraSocial}}
+ {{paciente.obraSocial}}
+ {{else}}
+ sin obra social
+ {{/if}}
+
+
+ Nro. Afiliado:
+ {{#if paciente.numeroAfiliado}}
+ {{ paciente.numeroAfiliado }}
+ {{else}}
+ Sin datos
+ {{/if}}
+
+
+
+
+
+
+ Fecha de Nac.
+
+
+ {{ paciente.fechaNacimiento }}
+
+
+
+
+ Nro. de carpeta
+
+
+ {{#if paciente.numeroCarpeta }}
+ {{ paciente.numeroCarpeta }}
+ {{else}}
+ sin número de carpeta
+ {{/if}}
+
+
+
+
+ {{#if ubicacion}}
+
+
+
+ Internación
+
+ {{ubicacion}}
+
+
+ {{/if}}
+
+
+
+
+ {{#if origenTop}}
+
+
DATOS DE ORIGEN DE SOLICITUD
+
+ {{{ origen.efectorOrigen }}}
+
+
+
+
+
+
Profesional
+
+ {{ origen.profesionalOrigenApellido }}, {{ origen.profesionalOrigenNombre }}
+
+
+
+
+
+ Fecha Solicitud
+
+
+ {{ origen.fechaSolicitud }}hs
+
+
+ {{else}}
+
+
Datos de la prestación
+
+ {{{ organizacion.nombre }}}
+
+
+ {{ organizacion.direccion }}
+
+
+
+
+
+
Profesional
+
+ {{ profesional.apellido }}, {{ profesional.nombre }}
+
+
+
+ {{/if}}
+
+
+ {{#unless consultaValidada }}
+
+ Prestación no validada por profesional
+
+ {{/unless}}
+ `;
+
+ constructor(prestacion, paciente, organizacion, cama) {
+ super(prestacion, paciente, organizacion, cama);
+ this.data.logoHeader = loadImage('templates/matriculaciones/img/header-matriculaciones.png');
+ }
+}
diff --git a/modules/descargas/recetas/recetas.ts b/modules/descargas/recetas/recetas.ts
new file mode 100644
index 0000000000..20744fad17
--- /dev/null
+++ b/modules/descargas/recetas/recetas.ts
@@ -0,0 +1,121 @@
+import { Types } from 'mongoose';
+import { InformePDF, getAssetsURL } from '../model/informe.class';
+import { Prestacion } from '../../rup/schemas/prestacion';
+import { Organizacion } from '../../../core/tm/schemas/organizacion';
+import { RecetasHeader } from './recetas-header';
+import { InformeRupFooter } from '../informe-rup/informe-footer';
+import { RecetasBody } from './recetas-body';
+import { elementosRUPAsSet, fulfillPrestacion } from '../../rup/controllers/elementos-rup.controller';
+import { findByPaciente } from '../../rup/internacion/camas.controller';
+import { findById } from '../../../core-v2/mpi/paciente/paciente.controller';
+import { Receta } from '../../recetas/receta-schema';
+import { RecetaInsumo } from '../../recetas/recetasInsumos/receta-insumo.schema';
+
+export class InformeRecetas extends InformePDF {
+
+ constructor(private prestacionId: string | Types.ObjectId, private registroId: string | Types.ObjectId = null, private usuario: any, private snapshots: any = null, private recetasIds: string[] = null) {
+ super();
+ }
+
+ stylesUrl = [
+ getAssetsURL('templates/rup/informes/sass/main.scss')
+ ];
+
+ public async process() {
+ const prestacion: any = await Prestacion.findById(this.prestacionId);
+
+ if (!prestacion) {
+ throw new Error('Prestación no encontrada');
+ }
+ if (this.snapshots) {
+ prestacion.ejecucion.registros.forEach(r => {
+ if (this.snapshots[r.id]) {
+ r.valor = r.valor || {};
+ r.valor.snapshot = this.snapshots[r.id];
+ }
+ });
+ }
+
+ const paciente = await findById(prestacion.paciente.id);
+ const organizacion = await Organizacion.findById(prestacion.ejecucion.organizacion.id);
+ const elementosRUPSet = await elementosRUPAsSet();
+
+ await fulfillPrestacion(prestacion, elementosRUPSet);
+
+ const cama = await this.getCamaInternacion(prestacion);
+
+ // Obtener recetas asociadas
+ const recetas = await this.getRecetas(prestacion);
+
+ this.header = new RecetasHeader(prestacion, paciente, organizacion, cama);
+ this.body = new RecetasBody(prestacion, paciente, organizacion, recetas, this.registroId ? String(this.registroId) : null);
+ this.footer = new InformeRupFooter(prestacion, paciente, organizacion, this.usuario);
+
+ await super.process();
+ }
+
+ private async getRecetas(prestacion: any) {
+ if (this.recetasIds && this.recetasIds.length) {
+ const ids = this.recetasIds.map(id => Types.ObjectId(id));
+ const [medicamentos, insumos] = await Promise.all([
+ Receta.find({ _id: { $in: ids } }).sort({ fechaRegistro: 1 }),
+ RecetaInsumo.find({ _id: { $in: ids } }).sort({ fechaRegistro: 1 })
+ ]);
+ const all: any[] = [...medicamentos, ...insumos].sort((a: any, b: any) => new Date(a.fechaRegistro).getTime() - new Date(b.fechaRegistro).getTime());
+ return all;
+ }
+
+ const queryBase: any = {
+ idPrestacion: prestacion._id.toString()
+ };
+ if (this.registroId) {
+ queryBase.idRegistro = String(this.registroId);
+ }
+
+ const [recetasMed, recetasInsumo] = await Promise.all([
+ Receta.find(queryBase).sort({ fechaRegistro: 1 }),
+ RecetaInsumo.find(queryBase).sort({ fechaRegistro: 1 })
+ ]);
+
+ let todas: any[] = [...recetasMed, ...recetasInsumo];
+
+ if (!this.registroId && todas.length > 1) {
+ const vigentesPendientes = todas.filter(receta =>
+ ['vigente', 'pendiente'].includes(receta.estadoActual?.tipo)
+ );
+ // Si hay al menos una vigente/pendiente, mostramos solo esas; sino mostramos todas (ej vencidas)
+ if (vigentesPendientes.length > 0) {
+ todas = vigentesPendientes;
+ }
+ }
+
+ // Ordenar por fechaRegistro y por ordenTratamiento si existe
+ todas.sort((a: any, b: any) => {
+ const fechaA = new Date(a.fechaRegistro).getTime();
+ const fechaB = new Date(b.fechaRegistro).getTime();
+ if (fechaA !== fechaB) { return fechaA - fechaB; }
+ const ordenA = a.medicamento?.ordenTratamiento ?? a.insumo?.ordenTratamiento ?? 0;
+ const ordenB = b.medicamento?.ordenTratamiento ?? b.insumo?.ordenTratamiento ?? 0;
+ return ordenA - ordenB;
+ });
+
+ return todas;
+ }
+
+ async getCamaInternacion(prestacion) {
+ if (prestacion.solicitud.ambitoOrigen === 'internacion') {
+ const org = prestacion.solicitud.organizacion;
+ const cama: any = await findByPaciente(
+ { organizacion: org, capa: 'medica', ambito: 'internacion' },
+ prestacion.paciente.id,
+ prestacion.ejecucion.fecha
+ );
+ if (cama) {
+ const sectores = cama.sectores || [];
+ cama.sectorName = [...sectores].reverse().map(s => s.nombre).join(', ');
+ return cama;
+ }
+ }
+ return null;
+ }
+}
diff --git a/modules/descargas/routes/descargas.ts b/modules/descargas/routes/descargas.ts
index 71e1b3c779..e53cf8ca77 100644
--- a/modules/descargas/routes/descargas.ts
+++ b/modules/descargas/routes/descargas.ts
@@ -22,6 +22,7 @@ import { laboratorioLog } from './../../rup/laboratorio.log';
import { Paciente } from './../../../core-v2/mpi';
import { FichaEpidemiologica } from '../ficha-epidemiologica/ficha';
import { Forms } from '../../forms/forms.schema';
+import { InformeRecetas } from '../recetas/recetas';
const router = express.Router();
@@ -278,6 +279,41 @@ router.post('/listadoTurnos', Auth.authenticate(), async (req: any, res, next) =
});
+/**
+ * Descarga de recetas por prestación/registro o por ids de recetas.
+ * Genera una hoja por cada medicamento/insumo con barcode propio,
+ * obra social, nota médica, fórmula magistral y fechas emisión/vencimiento.
+ */
+router.get('/recetas/:idPrestacion/(:idRegistro)?', Auth.authenticate(), async (req: any, res, next) => {
+ try {
+ const idPrestacion = req.params.idPrestacion;
+ const idRegistro = req.params.idRegistro;
+ const recetasIds = req.query.recetasIds ? String(req.query.recetasIds).split(',').filter(Boolean) : null;
+ const informe = new InformeRecetas(idPrestacion, idRegistro, req.user, null, recetasIds);
+ const fileName = await informe.informe({ header: { height: '6.0cm' } });
+ return res.download(fileName);
+ } catch (err) {
+ return next(err);
+ }
+});
+
+router.post('/recetas', Auth.authenticate(), async (req: any, res, next) => {
+ try {
+ const idPrestacion = req.body.idPrestacion;
+ const idRegistro = req.body.idRegistro;
+ const recetasIds = req.body.recetasIds || req.body.recetas || null;
+ const snapshots = req.body.snapshots || null;
+ if (!idPrestacion && (!recetasIds || !recetasIds.length)) {
+ return next(new Error('Se requiere idPrestacion o recetasIds'));
+ }
+ const informe = new InformeRecetas(idPrestacion, idRegistro, req.user, snapshots, recetasIds);
+ const fileName = await informe.informe({ header: { height: '6.0cm' } });
+ return res.download(fileName);
+ } catch (err) {
+ return next(err);
+ }
+});
+
/**
* Se usa POST para generar la descarga porque se envían datos
* que van a ser parte del archivo
diff --git a/modules/recetas/receta-schema.ts b/modules/recetas/receta-schema.ts
index bcdee23ec0..3d181d4734 100644
--- a/modules/recetas/receta-schema.ts
+++ b/modules/recetas/receta-schema.ts
@@ -223,6 +223,18 @@ recetaSchema.virtual('nombre').get(function (this: any) {
return this.medicamento?.nombre || (this.medicamento?.esMagistral ? (this.medicamento?.magistral?.nombre || '') : (this.medicamento?.concepto?.term || ''));
});
+recetaSchema.virtual('fechaVencimiento').get(function (this: any) {
+ if (!this.fechaRegistro) { return null; }
+ const fecha = new Date(this.fechaRegistro);
+ fecha.setDate(fecha.getDate() + 30);
+ return fecha;
+});
+
+recetaSchema.virtual('fechaEmision').get(function (this: any) {
+ return this.fechaRegistro;
+});
+
+
recetaSchema.pre('save', function (next) {
const receta: any = this;
diff --git a/modules/recetas/recetasInsumos/receta-insumo.schema.ts b/modules/recetas/recetasInsumos/receta-insumo.schema.ts
index db7d5ea3ef..225be5b797 100644
--- a/modules/recetas/recetasInsumos/receta-insumo.schema.ts
+++ b/modules/recetas/recetasInsumos/receta-insumo.schema.ts
@@ -172,6 +172,17 @@ recetaInsumoSchema.post('save', async (prescription: any) => {
}
});
+recetaInsumoSchema.virtual('fechaVencimiento').get(function (this: any) {
+ if (!this.fechaRegistro) { return null; }
+ const fecha = new Date(this.fechaRegistro);
+ fecha.setDate(fecha.getDate() + 30);
+ return fecha;
+});
+
+recetaInsumoSchema.virtual('fechaEmision').get(function (this: any) {
+ return this.fechaRegistro;
+});
+
recetaInsumoSchema.plugin(AuditPlugin);
recetaInsumoSchema.index({