Files
MBS/PROYECTO/ipdir/ipprrp014.4gl
T
jpegueroandClaude Sonnet 5 b6085d4140 Mueve ipdir (Productos Terminados) de PROYECTOS a PROYECTO
Instruccion de Johnny: todos los directorios de fuentes de inventario
deben migrarse de la carpeta temporal PROYECTOS a la carpeta real
PROYECTO, ya que PROYECTOS sera eliminada.

Se elimino tambien un archivo suelto sin relacion llamado "ipdir"
que existia en PROYECTO (del commit inicial) y bloqueaba el nombre
de la carpeta.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 09:29:01 -04:00

759 lines
29 KiB
Plaintext

{
-------------------------------------------------------------------------------
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).
AGOSTO 20, 2026 - MARGEN + DETALLADO/RESUMEN:
5. MARGEN DE BENEFICIO: se calcula el precio de venta REAL
promedio del periodo (ponderado por cantidad, neto del
descuento de cabecera) desde vetb00002/vetb00003, y el
COSTO estandar (iptb00004, antes quedaba en 0). Margen %
sobre precio = (precio-costo)/precio*100. Clasificacion
por cuadrante margen x rotacion (ESTRELLA/VOLUMEN/NICHO/
REVISAR/SIN COSTO), umbrales g_margen_min / g_rot_min.
6. El formulario permite elegir DETALLADO (una linea por
producto -> ipprrp014.4rp) o RESUMEN por CLASE cod_n
(subtotales -> ipprrp014r.4rp). La agregacion por clase
se hace en el FOREACH (emite_clase) y el reporte
'resumen' es PLANO (una fila por clase, total al pie).
NOTA: el consumo del 014 es de PRODUCCION
(pcantidadConduce); el margen ponderado de la clase pesa
por ese consumo.
-------------------------------------------------------------------------------
}
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
# Umbrales de clasificacion margen/rotacion (ajustables aqui)
DEFINE g_margen_min DECIMAL(6,2)
DEFINE g_rot_min DECIMAL(6,2)
# Acumulador por CLASE para el reporte Resumen (una fila por cod_n),
# a nivel de modulo para que emite_clase() y la funcion principal lo compartan.
DEFINE rc RECORD
cod_n INTEGER,
nombre_clase CHAR(30),
categoria CHAR(30),
c_prod INTEGER,
c_estrella INTEGER,
c_revisar INTEGER,
c_valor_inv DECIMAL(16,2),
c_ventas DECIMAL(16,2),
c_ganancia DECIMAL(16,2),
c_margen DECIMAL(12,2),
c_rot_prom DECIMAL(12,4)
END RECORD
DEFINE rc_rot_sum DECIMAL(16,4)
DEFINE prev_cod_n INTEGER
DEFINE cbtipo ui.ComboBox
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),
# --- Analisis margen (Agosto 2026) ---
rotacion_anual DEC(12,4),
margen DEC(12,2),
valor_inv DEC(14,2),
ventas_valor DEC(14,2),
ganancia DEC(14,2),
clasificacion CHAR(12)
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, tipo_reporte
BEFORE INPUT
LET progreso = 10
LET c_dias = 0
# Umbrales de clasificacion margen/rotacion
LET g_margen_min = 25.00
LET g_rot_min = 2.00
# Combo Detallado / Resumen (llenado por codigo)
LET cbtipo = ui.ComboBox.forName("formonly.tipo_reporte")
IF cbtipo IS NOT NULL THEN
CALL cbtipo.clear()
CALL cbtipo.addItem("D","Detallado (por producto)")
CALL cbtipo.addItem("R","Resumen (por clase)")
END IF
IF tipo_reporte IS NULL OR tipo_reporte = " " THEN
LET tipo_reporte = "D"
END IF
DISPLAY BY NAME c_dias, progreso, tipo_reporte
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 FIELD tipo_reporte
IF tipo_reporte IS NULL OR tipo_reporte = " " THEN
LET tipo_reporte = "D"
DISPLAY BY NAME tipo_reporte
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
# Detallado (D) -> ipprrp014.4rp ; Resumen por clase (R) -> ipprrp014r.4rp
IF tipo_reporte = "R" THEN
LET r_filename = "ipprrp014r.4rp"
ELSE
LET r_filename = "ipprrp014.4rp"
END IF
LET idx = 1
LET prev_cod_n = -999999
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
IF tipo_reporte = "R" THEN
START REPORT resumen TO XML HANDLER handler
ELSE
START REPORT estadistica TO XML HANDLER handler
END IF
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
# ---- COSTO ESTANDAR (iptb00004). Antes quedaba en 0; ahora se calcula
# para poder obtener el margen y el valor de exceso reales. ----
LET estadis.costo = 0
SELECT AVG(a.material+a.labor+a.gastos) INTO estadis.costo FROM iptb00004 a
WHERE a.ano = YEAR(TODAY) AND a.mes_fin = 12
AND a.cod_n = estadis.cod_n AND a.cod_grupo = estadis.cod_grupo
AND a.cod_tipo = estadis.cod_tipo AND a.cod_sec = estadis.cod_sec
AND a.status_t IS NULL
IF estadis.costo IS NULL THEN LET estadis.costo = 0 END IF
# ---- PRECIO de venta REAL promedio del periodo (ponderado por cantidad,
# neto del descuento de cabecera). Fuente vetb00002 + vetb00003. ----
LET estadis.precio = 0
SELECT SUM(d.cantidad * d.precio * (1 - (ISNULL(h.porc_desc,0)/100.0)))
/ NULLIF(SUM(d.cantidad),0)
INTO estadis.precio
FROM vetb00003 d, vetb00002 h
WHERE d.factura = h.factura
AND d.cod_n = estadis.cod_n AND d.cod_grupo = estadis.cod_grupo
AND d.cod_tipo = estadis.cod_tipo AND d.cod_sec = estadis.cod_sec
AND h.status_t IS NULL AND d.status_t IS NULL
AND h.fecha_factura BETWEEN fecha1 AND fecha2
IF estadis.precio IS NULL THEN LET estadis.precio = 0 END IF
# 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
# indice de rotacion = consumo / existencia (veces en el periodo)
LET estadis.rotacion = estadis.consumo / estadis.existe
# anualizada: 365.0 (decimal) evita la division entera
LET estadis.rotacion_anual = estadis.rotacion * (365.0 / dias1)
ELSE
LET estadis.rotacion = 0
LET estadis.rotacion_anual = 0
END IF
ELSE
LET estadis.dias = 0
LET estadis.rotacion = 0
LET estadis.rotacion_anual = 0
END IF
LET rotacion_anual = estadis.rotacion_anual
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
# ---- Margen % sobre precio, valores y clasificacion por cuadrante ----
IF estadis.precio > 0 AND estadis.costo > 0 THEN
LET estadis.margen = ((estadis.precio - estadis.costo) / estadis.precio) * 100
ELSE
LET estadis.margen = 0
END IF
LET estadis.valor_inv = estadis.existe * estadis.costo
LET estadis.ventas_valor = estadis.consumo * estadis.precio
LET estadis.ganancia = estadis.consumo * (estadis.precio - estadis.costo)
CASE
WHEN estadis.costo <= 0
LET estadis.clasificacion = "SIN COSTO"
WHEN estadis.margen >= g_margen_min AND estadis.rotacion_anual >= g_rot_min
LET estadis.clasificacion = "ESTRELLA"
WHEN estadis.margen < g_margen_min AND estadis.rotacion_anual >= g_rot_min
LET estadis.clasificacion = "VOLUMEN"
WHEN estadis.margen >= g_margen_min AND estadis.rotacion_anual < g_rot_min
LET estadis.clasificacion = "NICHO"
OTHERWISE
LET estadis.clasificacion = "REVISAR"
END CASE
IF tipo_reporte = "R" THEN
# Agregacion por CLASE: al cambiar cod_n, emite la clase anterior
IF prev_cod_n <> estadis.cod_n THEN
IF prev_cod_n <> -999999 THEN
CALL emite_clase()
END IF
LET prev_cod_n = estadis.cod_n
LET rc.cod_n = estadis.cod_n
LET rc.categoria = estadis.categoria
LET rc.nombre_clase = NULL
SELECT a.producto INTO rc.nombre_clase
FROM iptb00020 a WHERE a.cod_n = estadis.cod_n
IF rc.nombre_clase IS NULL THEN LET rc.nombre_clase = " " END IF
LET rc.c_prod = 0
LET rc.c_estrella = 0
LET rc.c_revisar = 0
LET rc.c_valor_inv = 0
LET rc.c_ventas = 0
LET rc.c_ganancia = 0
LET rc_rot_sum = 0
END IF
LET rc.c_prod = rc.c_prod + 1
LET rc.c_valor_inv = rc.c_valor_inv + estadis.valor_inv
LET rc.c_ventas = rc.c_ventas + estadis.ventas_valor
LET rc.c_ganancia = rc.c_ganancia + estadis.ganancia
LET rc_rot_sum = rc_rot_sum + estadis.rotacion_anual
IF estadis.clasificacion = "REVISAR" THEN LET rc.c_revisar = rc.c_revisar + 1 END IF
IF estadis.clasificacion = "ESTRELLA" THEN LET rc.c_estrella = rc.c_estrella + 1 END IF
ELSE
OUTPUT TO REPORT estadistica(estadis.*, dias1, c_dias, estadis.rotacion_anual)
END IF
LET idx = idx + 1
LET estadis.consumo = 0
LET estadis.existe = 0
LET estadis.dias = 0
LET estadis.rotacion = 0
LET estadis.rotacion_anual = 0
LET estadis.precio = 0
LET estadis.margen = 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
# Emite la ultima clase acumulada (modo Resumen)
IF tipo_reporte = "R" AND prev_cod_n <> -999999 THEN
CALL emite_clase()
END IF
LET progreso = 100
DISPLAY progreso TO progreso
IF tipo_reporte = "R" THEN
FINISH REPORT resumen
ELSE
FINISH REPORT estadistica
END IF
END IF
END FUNCTION
# ------------------------------------------------------------------------------
# Calcula margen ponderado y rotacion promedio de la clase acumulada en 'rc' y la
# envia al reporte 'resumen'. Usada al cambiar de CLASE y al terminar el FOREACH.
# ------------------------------------------------------------------------------
FUNCTION emite_clase()
IF rc.c_ventas > 0 THEN
LET rc.c_margen = (rc.c_ganancia / rc.c_ventas) * 100
ELSE
LET rc.c_margen = 0
END IF
IF rc.c_prod > 0 THEN
LET rc.c_rot_prom = rc_rot_sum / rc.c_prod
ELSE
LET rc.c_rot_prom = 0
END IF
OUTPUT TO REPORT resumen(rc.*)
END FUNCTION
# ==============================================================================
# REPORTE RESUMEN POR CLASE (cod_n) -- version PLANA (una fila por clase).
# La agregacion la hace el FOREACH (emite_clase); aqui solo se imprime y se
# acumula el gran total para el pie. Alimenta ipprrp014r.4rp.
# ==============================================================================
REPORT resumen(r)
DEFINE r RECORD
cod_n INTEGER,
nombre_clase CHAR(30),
categoria CHAR(30),
c_prod INTEGER,
c_estrella INTEGER,
c_revisar INTEGER,
c_valor_inv DECIMAL(16,2),
c_ventas DECIMAL(16,2),
c_ganancia DECIMAL(16,2),
c_margen DECIMAL(12,2),
c_rot_prom DECIMAL(12,4)
END RECORD
DEFINE hora CHAR(5),
fecha DATE,
g_prod INTEGER,
g_valor_inv, g_ventas, g_ganancia DECIMAL(16,2),
g_margen DECIMAL(12,2)
FORMAT
FIRST PAGE HEADER
LET hora = TIME
LET fecha = TODAY
LET lj = (120 - LENGTH(p_companias.nombre CLIPPED)) / 2
LET g_prod = 0
LET g_valor_inv = 0
LET g_ventas = 0
LET g_ganancia = 0
ON EVERY ROW
LET g_prod = g_prod + r.c_prod
LET g_valor_inv = g_valor_inv + r.c_valor_inv
LET g_ventas = g_ventas + r.c_ventas
LET g_ganancia = g_ganancia + r.c_ganancia
IF g_ventas > 0 THEN
LET g_margen = (g_ganancia / g_ventas) * 100
ELSE
LET g_margen = 0
END IF
PRINTX r.*,
g_prod, g_valor_inv, g_ventas, g_ganancia, g_margen,
p_companias.*, fecha, hora, fecha1, fecha2
END REPORT
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),
# --- Analisis margen (Agosto 2026) - debe reflejar estadis ---
rotacion_anual DEC(12,4),
margen DEC(12,2),
valor_inv DEC(14,2),
ventas_valor DEC(14,2),
ganancia DEC(14,2),
clasificacion CHAR(12)
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.margen,
x.clasificacion,
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.
================================================================================
}