{ ------------------------------------------------------------------------------- PROGRAMA : IPPRRP014 (OPTIMIZADO) OBJETIVO : REPORTE DIAS DE INVENTARIOS - GERENTE DE MARCAS PROGRAMADOR : Ing. Juan Francisco Soto FECHA REALIZACION : Agosto 19, 1992 FECHA ULT. MODIFICA : Julio 07, 2026 DESCRIPCION : OPTIMIZACION DE RENDIMIENTO: 1. Ultima venta, ultima compra y cantidad comprada se calculan en UN SOLO query (LEFT JOIN a subconsultas agregadas) en lugar de 3 SELECTs por producto dentro del REPORT. Se elimino la tautologia d.numero_orden = d.numero_orden. 2. Se elimino DISPLAY por fila y el refresh de progreso se hace cada 50 filas (cada refresh es un round-trip al GDC). 3. Se elimino el SELECT a companias dentro de BEFORE GROUP OF (ya esta cargado en MAIN). 4. CORRECCION FORMULA ROTACION: Antes: rotacion = existencia / consumo (cobertura) Ahora: rotacion = consumo / existencia (indice de rotacion estandar: veces que el inventario rota en el periodo). Se agrega ademas la rotacion anualizada = rotacion * (365/dias). DIAS INV se mantiene igual (formula correcta). ------------------------------------------------------------------------------- } GLOBALS "ipprgb000.4gl" DEFINE sale CHAR(1) DEFINE p_ano CHAR(4) DEFINE fecha2 DATE DEFINE dias1 SMALLINT, m_meses SMALLINT, conmes, valor1 DECIMAL(12, 2), representa VARCHAR(100), num_representa INT, chexistencia CHAR(2), progreso, cuenta_reg, cuenta INT MAIN DEFER INTERRUPT CALL ARG_VAL(1) RETURNING usuarios CALL ARG_VAL(2) RETURNING clave CONNECT TO "smarmotech" USER usuarios USING clave SELECT a.* INTO p_companias.* FROM companias a CALL ipprrp013() END MAIN FUNCTION ipprrp013() # Variable que toma el valor de los dias que se va a valorar el reporte DEFINE c_dias INTEGER # Variables varias para el control de busqueda de las informaciones DEFINE selec1, select_bal STRING # Datos precalculados por SQL (antes se buscaban con 3 SELECTs por fila) DEFINE w_fecha_v DATE, w_fecha_oc DATE, w_tcantidad DEC(12, 4) DEFINE rotacion_anual DEC(12, 2) # Record para la impresion del reporte DEFINE estadis RECORD cod_n LIKE intb00001.cod_n, cod_grupo LIKE intb00001.cod_grupo, cod_tipo LIKE intb00001.cod_tipo, cod_sec LIKE intb00001.cod_sec, descrip_esp LIKE intb00001.descrip_esp, unidad_med LIKE iptb00002.unidad_med, consumo DEC(12, 4), grupo CHAR(30), tipo CHAR(30), codCategoria INT, categoria CHAR(30), fecha_v, fecha_oc DATE, tcantidad DEC(12, 4), existe DEC(12, 4), valor DECIMAL(12, 4), costo DECIMAL(12, 4), dias INTEGER, pcodigo CHAR(25), cantidad_orden, pcantidadConduce, transito, existencia, reservado, disponible, precio, rotacion DEC(12, 2) END RECORD DEFINE handler om.SaxDocumentHandler, r_filename STRING, r_output STRING, preview INTEGER OPTIONS FORM LINE 8, ERROR LINE 23, COMMENT LINE 21 OPEN FORM ipfmrp014 FROM "ipfmrp014" DISPLAY FORM ipfmrp014 DISPLAY "ipprrp014" AT 4, 3 ATTRIBUTE(RED) DISPLAY "Dias de Inventarios Gerente Marcas" AT 6, 33 ATTRIBUTE(BLACK) INPUT BY NAME fecha1, fecha2, c_dias, m_meses, chexistencia BEFORE INPUT LET progreso = 10 LET c_dias = 0 DISPLAY BY NAME c_dias, progreso SELECT DISTINCT RTRIM(a.nom1_emp) + ' ' + RTRIM(a.apell1_emp), a.num_emp INTO representa, num_representa FROM adtb00003 a, seg0001 b WHERE a.num_emp = b.num_emp AND b.usuario = usuarios AND a.gerentemarcas = 'SI' IF STATUS = NOTFOUND THEN CALL fgl_winmessage( "INFO", "USTED NO ES GERENTE DE MARCAS, NO PUEDE GENERAR ESTE INFORME", "INFO") EXIT PROGRAM END IF DISPLAY BY NAME representa AFTER FIELD c_dias IF c_dias IS NULL THEN LET numero_msg = 16 CALL msg(numero_msg) NEXT FIELD c_dias END IF AFTER FIELD fecha1 IF fecha1 IS NULL THEN LET numero_msg = 16 CALL msg(numero_msg) NEXT FIELD fecha1 END IF AFTER FIELD fecha2 IF fecha2 IS NULL THEN LET numero_msg = 16 CALL msg(numero_msg) NEXT FIELD fecha2 END IF AFTER FIELD m_meses IF m_meses IS NULL OR m_meses = 0 THEN LET numero_msg = 16 CALL msg(numero_msg) NEXT FIELD m_meses END IF AFTER INPUT IF int_flag THEN LET numero_msg = 2 CALL msg(numero_msg) LET int_flag = FALSE RETURN END IF IF chexistencia IS NULL THEN NEXT FIELD chexistencia END IF END INPUT LET dias1 = fecha2 - fecha1 LET dias1 = dias1 + 1 DISPLAY BY NAME dias1 SELECT MAX(a.ano) INTO p_ano FROM prdtable a WHERE a.fecha_corte <= fecha2 CONSTRUCT criterio ON x.cod_categoria, a.cod_n, a.cod_grupo, a.cod_tipo, a.cod_sec FROM cod_cat, cod_n, cod_grupo, cod_tipo, cod_sec BEFORE CONSTRUCT CALL categorias() AFTER CONSTRUCT IF int_flag THEN LET numero_msg = 2 CALL msg(numero_msg) LET int_flag = FALSE RETURN END IF EXIT CONSTRUCT END CONSTRUCT # Conteo para la barra de progreso (sin ORDER BY, innecesario en un COUNT) LET select_bal = "SELECT COUNT(*) FROM iptb00002 a INNER JOIN iptb00035 x1 ON a.cod_n = x1.cod_n INNER JOIN iptb00029 d ON a.cod_grupo = d.cod_grupo INNER JOIN iptb00034 x ON x.cod_categoria = x1.cod_cat where ", criterio CLIPPED, " and a.num_emp = ", num_representa PREPARE comando1 FROM select_bal EXECUTE comando1 INTO cuenta_reg # ============================================================================== # QUERY PRINCIPAL OPTIMIZADO (CONJUNTO, NO FILA POR FILA) # ------------------------------------------------------------------------------ # Las subconsultas uv (ultima venta) y uc (ultima compra + cantidad) se agregan # UNA sola vez por GROUP BY y se unen por LEFT JOIN. SQL Server recorre las # tablas de historico una vez en total, en lugar de una vez por cada producto. # Se corrigio la condicion muerta "d.numero_orden = d.numero_orden". # ============================================================================== LET select_bal = "SELECT a.cod_n, a.cod_grupo, a.cod_tipo, a.cod_sec, a.descrip_esp,", " a.unidad_med, '0', d.descripcion, ' ', x.cod_categoria, x.descripcion,", " convert(char(10), uv.fecha_v, 103),", " convert(char(10), uc.fecha_oc, 103),", " ISNULL(uc.tcantidad, 0)", " FROM iptb00002 a", " INNER JOIN iptb00035 x1 ON a.cod_n = x1.cod_n", " INNER JOIN iptb00029 d ON a.cod_grupo = d.cod_grupo", " INNER JOIN iptb00034 x ON x.cod_categoria = x1.cod_cat", " LEFT JOIN (", " SELECT dv.cod_n, dv.cod_grupo, dv.cod_tipo, dv.cod_sec,", " MAX(fv.fecha_factura) AS fecha_v", " FROM vetb00003 dv", " INNER JOIN vetb00002 fv ON dv.factura = fv.factura", " WHERE fv.status_t IS NULL", " GROUP BY dv.cod_n, dv.cod_grupo, dv.cod_tipo, dv.cod_sec", " ) uv ON uv.cod_n = a.cod_n AND uv.cod_grupo = a.cod_grupo", " AND uv.cod_tipo = a.cod_tipo AND uv.cod_sec = a.cod_sec", " LEFT JOIN (", " SELECT dc.cod_n, dc.cod_grupo, dc.cod_tipo, dc.cod_sec,", " MAX(oc.fech_oc) AS fecha_oc,", " SUM(dc.cantidad) AS tcantidad", " FROM cotb00015 dc", " INNER JOIN cotb00014 oc ON dc.num_oc = oc.num_oc", " WHERE dc.status_t IS NULL", " GROUP BY dc.cod_n, dc.cod_grupo, dc.cod_tipo, dc.cod_sec", " ) uc ON uc.cod_n = a.cod_n AND uc.cod_grupo = a.cod_grupo", " AND uc.cod_tipo = a.cod_tipo AND uc.cod_sec = a.cod_sec", " WHERE ", criterio CLIPPED, " AND a.num_emp = ", num_representa, " ORDER BY x.cod_categoria, a.cod_n, a.cod_grupo, a.cod_tipo, a.cod_sec" PREPARE comando2 FROM select_bal DECLARE accion CURSOR FOR comando2 LET r_filename = "ipprrp014.4rp" LET idx = 1 DISPLAY progreso TO progreso CALL ui.Interface.refresh() FOREACH accion INTO estadis.* LET estadis.pcodigo = estadis.cod_n USING "&&&&", "-", estadis.cod_grupo USING "&&&&", "-", estadis.cod_tipo USING "&&&&", "-", estadis.cod_sec USING "&&&&&&" IF int_flag THEN LET numero_msg = 2 CALL msg(numero_msg) LET int_flag = FALSE LET estadis.consumo = 0 LET estadis.existe = 0 LET sale = "N" RETURN END IF IF idx = 1 THEN IF fgl_report_loadCurrentSettings(r_filename) THEN CALL seleccionarSalida() RETURNING r_output LET preview = 1 CALL fgl_report_selectDevice(r_output) CALL fgl_report_selectPreview(preview) CALL fgl_report_configurexlsxdevice(null,null,null,false,false,null,1) LET handler = fgl_report_commitCurrentSettings() ELSE EXIT PROGRAM END IF IF handler IS NOT NULL THEN START REPORT estadistica TO XML HANDLER handler ELSE EXIT FOREACH END IF END IF # Busqueda de existencias / consumo (pendiente de convertir a conjunto, # ver nota al final del programa) CALL disponibles( estadis.cod_n, estadis.cod_grupo, estadis.cod_tipo, estadis.cod_sec, fecha2) RETURNING estadis.cantidad_orden, estadis.pcantidadConduce, estadis.transito, estadis.existencia, estadis.reservado, valororden IF chexistencia = 'NO' AND estadis.existencia <= 0 THEN LET idx = idx + 1 CONTINUE FOREACH END IF LET estadis.consumo = estadis.pcantidadConduce IF estadis.consumo < 0 THEN LET estadis.consumo = estadis.consumo * -1 END IF LET estadis.existe = estadis.existencia # Calculo para los dias de produccion IF estadis.existe > 0 THEN LET estadis.cantidad_orden = estadis.cantidad_orden - estadis.pcantidadConduce LET estadis.disponible = estadis.existe - estadis.cantidad_orden - estadis.reservado IF estadis.consumo > 0 THEN LET estadis.dias = (estadis.existe / estadis.consumo) * dias1 # ============================================================== # FORMULA CORREGIDA: indice de rotacion = consumo / existencia # (veces que rota el inventario en el periodo). # La formula anterior (existe/consumo) era la COBERTURA. # Nota: idealmente usar existencia PROMEDIO del periodo: # (existencia a fecha1 + existencia a fecha2) / 2 # ============================================================== LET estadis.rotacion = estadis.consumo / estadis.existe LET rotacion_anual = estadis.rotacion * (365 / dias1) ELSE LET estadis.rotacion = 0 LET rotacion_anual = 0 END IF ELSE LET estadis.dias = 0 LET estadis.rotacion = 0 LET rotacion_anual = 0 END IF LET estadis.valor = 0 IF estadis.dias >= c_dias THEN IF estadis.dias > 0 THEN LET valor1 = (estadis.existe * c_dias) / estadis.dias LET estadis.valor = (estadis.existe - valor1) * estadis.costo ELSE LET estadis.valor = 0 END IF END IF LET estadis.costo = 0 OUTPUT TO REPORT estadistica(estadis.*, dias1, c_dias, rotacion_anual) LET idx = idx + 1 LET estadis.consumo = 0 LET estadis.existe = 0 LET estadis.dias = 0 LET estadis.rotacion = 0 # Refrescar progreso solo cada 50 filas: cada refresh es un # round-trip al GDC y hacerlo por fila cuesta minutos. IF idx MOD 50 = 0 THEN LET progreso = (idx / cuenta_reg) * 100 DISPLAY progreso TO progreso CALL ui.Interface.refresh() END IF END FOREACH IF idx = 1 THEN CALL fgl_winmessage( "INFO", "NO EXISTEN INFORMACIONES CON ESTOS PARAMETROS", "INFO") ELSE LET progreso = 100 DISPLAY progreso TO progreso FINISH REPORT estadistica END IF END FUNCTION REPORT estadistica(x, dias2, c_dias, rot_anual) DEFINE x RECORD cod_n LIKE intb00001.cod_n, cod_grupo LIKE intb00001.cod_grupo, cod_tipo LIKE intb00001.cod_tipo, cod_sec LIKE intb00001.cod_sec, descrip_esp LIKE intb00001.descrip_esp, unidad_med LIKE iptb00002.unidad_med, consumo LIKE iptb00006.cantidad_1, grupo CHAR(30), tipo CHAR(30), codCategoria INT, categoria CHAR(30), fecha_v, fecha_oc DATE, tcantidad DEC(12, 4), existe DEC(12, 4), valor DECIMAL(12, 3), costo DECIMAL(12, 4), dias INTEGER, pcodigo CHAR(25), cantidad_orden, pcantidadConduce, transito, existencia, reservado, disponible, precio, rotacion DEC(12, 2) END RECORD, tcantidad DEC(12, 4), rot_anual DEC(12, 2) DEFINE c_dias SMALLINT, dias_oc INTEGER DEFINE m_total DECIMAL(10, 2) DEFINE dias2 SMALLINT DEFINE hora CHAR(5) DEFINE t_total, t_valor DECIMAL(12, 2), chorden STRING, porden INTEGER, fecha DATE, pcosto DEC(12, 4), total_registros SMALLINT, total_grupo SMALLINT, nombre CHAR(50), cantidad_or DEC(12, 4) FORMAT FIRST PAGE HEADER LET hora = TIME LET fecha = TODAY LET total_registros = 0 LET lj = (120 - LENGTH(p_companias.nombre CLIPPED)) / 2 PRINTX dias1 BEFORE GROUP OF x.cod_grupo LET total_grupo = 0 LET hora = TIME # OPTIMIZACION: se elimino el SELECT a companias por cada grupo; # p_companias ya se carga una sola vez en MAIN. ON EVERY ROW LET total_registros = total_registros + 1 LET total_grupo = total_grupo + 1 LET nombre_ant = nombre # OPTIMIZACION: fecha_v, fecha_oc y tcantidad ya vienen # calculadas desde el query principal (LEFT JOIN agregado). # Antes aqui se ejecutaban 3 SELECTs por cada producto. LET conmes = 0 IF t_total IS NULL THEN LET t_total = 0 END IF IF t_valor IS NULL THEN LET t_valor = 0 END IF LET m_total = x.existe * x.costo LET cantidad_or = tcantidad + x.existe IF x.consumo > 0 THEN LET dias_oc = (cantidad_or / x.consumo) * dias1 END IF IF m_meses > 0 THEN LET conmes = x.consumo / m_meses END IF PRINTX x.cod_n, x.cod_grupo, x.cod_tipo, x.cod_sec, x.descrip_esp, x.consumo, x.existe, x.dias, x.rotacion, rot_anual, x.unidad_med, fecha, hora, fecha1, fecha2, x.fecha_oc, chorden, x.tcantidad, p_companias.*, pcosto, total_registros, total_grupo, x.fecha_v END REPORT { ================================================================================ NOTA DE OPTIMIZACION PENDIENTE - disponibles() -------------------------------------------------------------------------------- El unico trabajo por-fila que queda es CALL disponibles() (en ipprgb000.4gl), que probablemente ejecuta varios SELECT SUM sobre las tablas de movimientos por cada producto. Para llevar el reporte de minutos a segundos, ese calculo tambien debe convertirse a conjunto: un solo query con GROUP BY por cod_n/cod_grupo/cod_tipo/cod_sec (o una tabla temporal #existencias) unido por LEFT JOIN al query principal, igual que se hizo con ultima venta/compra. ================================================================================ }