diff --git a/PROYECTO/PRDIR/prfmmt022.4fd b/PROYECTO/PRDIR/prfmmt022.4fd
index 2d5a42c..29c98a7 100644
--- a/PROYECTO/PRDIR/prfmmt022.4fd
+++ b/PROYECTO/PRDIR/prfmmt022.4fd
@@ -40,7 +40,7 @@
-
+
diff --git a/PROYECTO/PRDIR/prfmrp116.4fd b/PROYECTO/PRDIR/prfmrp116.4fd
new file mode 100644
index 0000000..1fe32c7
--- /dev/null
+++ b/PROYECTO/PRDIR/prfmrp116.4fd
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PROYECTO/PRDIR/prfmrp116.per b/PROYECTO/PRDIR/prfmrp116.per
index fa987e1..14f8158 100644
--- a/PROYECTO/PRDIR/prfmrp116.per
+++ b/PROYECTO/PRDIR/prfmrp116.per
@@ -48,7 +48,7 @@ DATEEDIT f1 = formonly.fecha type date,format = "dd/mm/yyyy";
o1 = formonly.num_oc,comments="Digite un numero de orden o deje en blanco para todas";
v1 = formonly.sec_vend,comments="Digite un numero de vendedor o deje en blanco para todos";
COMBOBOX s=formonly.salida,ITEMS=(("prprrp116.4rp","Detallado"),("prprrp016a.4rp","Resumido")),default="Detallado";
-COMBOBOX v=formonly.visualiza,ITEMS=(("SVG","PANTALLA"),("PDF","ACROBAT READER")),default="PANTALLA";
+COMBOBOX v=formonly.visualiza,ITEMS=(("SVG","PANTALLA"),("PDF","ACROBAT READER"),("XLSX","EXCEL")),default="PANTALLA";
instructions
delimiters " "
end
diff --git a/PROYECTO/PRDIR/prprcs001.4gl b/PROYECTO/PRDIR/prprcs001.4gl
index c8a91b2..ef7c4ff 100644
--- a/PROYECTO/PRDIR/prprcs001.4gl
+++ b/PROYECTO/PRDIR/prprcs001.4gl
@@ -71,7 +71,9 @@ DEFINE hoy DATE
END INPUT
CONSTRUCT BY NAME criterio ON a.tipo_cliente,a.sec_cliente,
- a.num_oc,a.fecha
+ a.num_oc,a.fecha_oc
+ FROM tipo_cliente,sec_cliente,
+ num_oc,fecha
AFTER CONSTRUCT
IF tipo_orden = 'N' THEN
diff --git a/PROYECTO/PRDIR/prprrp116.4gl b/PROYECTO/PRDIR/prprrp116.4gl
index 08fd939..c182d39 100644
--- a/PROYECTO/PRDIR/prprrp116.4gl
+++ b/PROYECTO/PRDIR/prprrp116.4gl
@@ -79,6 +79,8 @@ FUNCTION prprrp016()
END RECORD,
salida,visualiza CHAR(20)
+ DEFINE hay_datos SMALLINT
+
OPTIONS
PROMPT LINE 23,
FORM LINE 8,
@@ -201,12 +203,46 @@ FUNCTION prprrp016()
"WHERE ",criterio clipped,
" and a.status_t is null and f.status_t is null "
- END IF
+ END IF
PREPARE comando FROM selec
DECLARE busca CURSOR FOR comando
+ -- Verifica primero si hay registros: si el reporte se inicia sin filas,
+ -- el motor de reportes falla al no poder resolver p_compania.nombre
+ -- (variable "look ahead" del encabezado) porque nunca se llega a imprimir nada.
+ OPEN busca
+ FETCH busca INTO busqueda.cotizacion_no,busqueda.fecha,busqueda.tipo_cliente,
+ busqueda.sec_cliente,busqueda.nombre_proyecto,busqueda.direccion_proyecto,
+ busqueda.telefono_r,busqueda.nombre,busqueda.num_oc,busqueda.fecha_oc,
+ busqueda.fecha_entrega,busqueda.fecha_cierre,busqueda.cod_n,
+ busqueda.cod_grupo,busqueda.cod_tipo,busqueda.cod_sec,
+ busqueda.descrip_esp,busqueda.unidad_med,busqueda.cantidad,
+ busqueda.precio,busqueda.cantidad_2,busqueda.cod_mov,
+ busqueda.fecha_doc,busqueda.documento,busqueda.status_mov,
+ busqueda.simbolo,busqueda.vendedor,busqueda.monto_cotizacion,
+ busqueda.condicion,busqueda.estado_orden,busqueda.telefono,
+ busqueda.celular,busqueda.email,busqueda.contacto
+ IF STATUS = NOTFOUND THEN
+ CALL fgl_winmessage("WARNING","NO EXISTEN REGISTROS CON ESA CONDICION","SOP")
+ LET hay_datos = FALSE
+ ELSE
+ LET hay_datos = TRUE
+ END IF
+
+ # El formulario solo guarda el texto visible del combo (no el valor interno
+ # que tenia el .per original), asi que aqui se traduce de vuelta.
+ CASE salida CLIPPED
+ WHEN "Detallado" LET salida = "prprrp116.4rp"
+ WHEN "Resumido" LET salida = "prprrp016a.4rp"
+ END CASE
+ CASE visualiza CLIPPED
+ WHEN "PANTALLA" LET visualiza = "SVG"
+ WHEN "ACROBAT READER" LET visualiza = "PDF"
+ WHEN "EXCEL" LET visualiza = "XLSX"
+ END CASE
+
LET r_filename = salida CLIPPED
-
+
IF fgl_report_loadCurrentSettings(r_filename) THEN -- load the .4rp file
LET r_output=visualiza CLIPPED
LET preview=1
@@ -216,30 +252,13 @@ FUNCTION prprrp016()
END IF
-IF handler IS NOT NULL THEN -- report engine was configured ok
+IF handler IS NOT NULL AND hay_datos THEN -- report engine was configured ok and there is data to print
START REPORT reporte_16 TO XML HANDLER handler
-
-
- FOREACH busca INTO busqueda.cotizacion_no,busqueda.fecha,busqueda.tipo_cliente,
- busqueda.sec_cliente,busqueda.nombre_proyecto,busqueda.direccion_proyecto,
- busqueda.telefono_r,busqueda.nombre,busqueda.num_oc,busqueda.fecha_oc,
- busqueda.fecha_entrega,busqueda.fecha_cierre,busqueda.cod_n,
- busqueda.cod_grupo,busqueda.cod_tipo,busqueda.cod_sec,
- busqueda.descrip_esp,busqueda.unidad_med,busqueda.cantidad,
- busqueda.precio,busqueda.cantidad_2,busqueda.cod_mov,
- busqueda.fecha_doc,busqueda.documento,busqueda.status_mov,
- busqueda.simbolo,busqueda.vendedor,busqueda.monto_cotizacion,
- busqueda.condicion,busqueda.estado_orden,busqueda.telefono,
- busqueda.celular,busqueda.email,busqueda.contacto
-
-
-
- IF STATUS = NOTFOUND THEN
- CALL fgl_winmessage("WARNING","NO EXISTEN REGISTROS CON ESA CONDICION","SOP")
- EXIT FOREACH
- END IF
+ -- La primera fila ya fue leida arriba (para saber si hay_datos); aqui se
+ -- procesa esa fila y se sigue con FETCH manual hasta agotar el cursor.
+ WHILE hay_datos
IF busqueda.cantidad_2 IS NULL THEN
LET busqueda.cantidad_2 = 0
END IF
@@ -247,7 +266,7 @@ IF handler IS NOT NULL THEN -- report engine was configured ok
LET busqueda.fecha_avance= NULL
LET busqueda.monto_avance = NULL
- SELECT MAX(a.num_doc),MAX(a.fecha_orig)
+ SELECT MAX(a.num_doc),MAX(a.fecha_orig)
INTO busqueda.avance,busqueda.fecha_avance,
busqueda.monto_avance
FROM cctb00001 a
@@ -255,7 +274,7 @@ IF handler IS NOT NULL THEN -- report engine was configured ok
a.tipo_Doc = 'AV' and a.tipo_cliente = busqueda.tipo_cliente and
a.sec_cliente = busqueda.sec_cliente and a.status_t is null and
a.fecha_orig > '08/01/2009'
-
+
LET busqueda.codigo = busqueda.cod_n using "&&&&","-",
busqueda.cod_grupo USING "&&&&","-",
busqueda.cod_tipo USING "&&&&","-",
@@ -266,11 +285,27 @@ IF handler IS NOT NULL THEN -- report engine was configured ok
IF busqueda.estado_orden = "S" THEN
LET busqueda.estado_orden = "CERRADA"
END IF
-
+
OUTPUT TO REPORT reporte_16(busqueda.*)
- END FOREACH
+
+ FETCH busca INTO busqueda.cotizacion_no,busqueda.fecha,busqueda.tipo_cliente,
+ busqueda.sec_cliente,busqueda.nombre_proyecto,busqueda.direccion_proyecto,
+ busqueda.telefono_r,busqueda.nombre,busqueda.num_oc,busqueda.fecha_oc,
+ busqueda.fecha_entrega,busqueda.fecha_cierre,busqueda.cod_n,
+ busqueda.cod_grupo,busqueda.cod_tipo,busqueda.cod_sec,
+ busqueda.descrip_esp,busqueda.unidad_med,busqueda.cantidad,
+ busqueda.precio,busqueda.cantidad_2,busqueda.cod_mov,
+ busqueda.fecha_doc,busqueda.documento,busqueda.status_mov,
+ busqueda.simbolo,busqueda.vendedor,busqueda.monto_cotizacion,
+ busqueda.condicion,busqueda.estado_orden,busqueda.telefono,
+ busqueda.celular,busqueda.email,busqueda.contacto
+ IF STATUS = NOTFOUND THEN
+ LET hay_datos = FALSE
+ END IF
+ END WHILE
FINISH REPORT reporte_16
END IF
+ CLOSE busca
AFTER DIALOG
@@ -349,7 +384,7 @@ BEFORE GROUP OF x.cotizacion_no
PRINTX x.cotizacion_no,x.direccion_proyecto,x.nombre_proyecto,
x.tipo_cliente,x.sec_cliente,x.telefono_r,x.num_oc,x.fecha,x.fecha_cierre,
- x.fecha_entrega,x.fecha_oc
+ x.fecha_entrega,x.fecha_oc,p_compania.nombre
BEFORE GROUP OF x.codigo
diff --git a/PROYECTO/cgdir/cgprmt004.4gl b/PROYECTO/cgdir/cgprmt004.4gl
index 199ccb7..13fa029 100644
--- a/PROYECTO/cgdir/cgprmt004.4gl
+++ b/PROYECTO/cgdir/cgprmt004.4gl
@@ -20,7 +20,7 @@ DEFINE
DEFINE
dias, registros, pcod_sp, numero_entrada SMALLINT,
secuencia_entrada INT,
- xentrada VARCHAR(14),
+ xentrada VARCHAR(25),
nom1_emp, nom2_Emp, apell1_Emp, apell2_emp VARCHAR(30),
num_doccxp, num_doccxc VARCHAR(25)
diff --git a/PROYECTO/codir/coprmt025.4gl b/PROYECTO/codir/coprmt025.4gl
index e0ae066..d0a9226 100644
--- a/PROYECTO/codir/coprmt025.4gl
+++ b/PROYECTO/codir/coprmt025.4gl
@@ -35,7 +35,27 @@ DEFINE xusername,xsmtphost,xsmtpport,xpassword STRING,
toEmail, orden_trabajo, answer_workorder, descrip_depto STRING, depto INT
-DEFINE data RECORD LIKE cotb00001.*
+DEFINE data RECORD
+ area_sp VARCHAR(10),
+ ciu_sp VARCHAR(30),
+ cod_pais VARCHAR(3),
+ cod_sp SMALLINT,
+ cod_sp_sec SMALLINT,
+ cont_sp VARCHAR(30),
+ contactID INTEGER,
+ dir_sp VARCHAR(60),
+ email VARCHAR(60),
+ fax_sp VARCHAR(25),
+ fech_crea DATETIME YEAR TO FRACTION,
+ fech_mod DATETIME YEAR TO FRACTION,
+ id_maintainx INTEGER,
+ nom_sp VARCHAR(41),
+ status_t VARCHAR(1),
+ tel_sp VARCHAR(25),
+ term_sp SMALLINT,
+ us_crea VARCHAR(50),
+ us_mod VARCHAR(50)
+ END RECORD
DEFINE container RECORD
tipo_inv STRING,
diff --git a/PROYECTO/cpdir/cpprmt001.4gl b/PROYECTO/cpdir/cpprmt001.4gl
index 5ce389b..f2b11de 100644
--- a/PROYECTO/cpdir/cpprmt001.4gl
+++ b/PROYECTO/cpdir/cpprmt001.4gl
@@ -1596,7 +1596,7 @@ FUNCTION cppcmd000()
END IF
END FOR
- IF entradas_atadas[0].entrada IS NOT NULL THEN
+ IF entradas_atadas[1].entrada IS NOT NULL THEN
DELETE FROM cptb00010
WHERE num_doc = factura.num_doc
AND cod_sp = factura.cod_sp
diff --git a/PROYECTO/mbsERP.4pw b/PROYECTO/mbsERP.4pw
index 589792c..1af8b6c 100644
--- a/PROYECTO/mbsERP.4pw
+++ b/PROYECTO/mbsERP.4pw
@@ -330,8 +330,65 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -370,7 +427,7 @@
-
+
@@ -387,7 +444,7 @@
-
+
@@ -420,6 +477,7 @@
+
diff --git a/PROYECTOS/ipdir/ipfmrp013.4fd b/PROYECTOS/ipdir/ipfmrp013.4fd
index 00ec223..8b5e8c2 100644
--- a/PROYECTOS/ipdir/ipfmrp013.4fd
+++ b/PROYECTOS/ipdir/ipfmrp013.4fd
@@ -10,6 +10,7 @@
+
@@ -37,6 +38,8 @@
+
+
diff --git a/PROYECTOS/ipdir/ipfmrp013.per b/PROYECTOS/ipdir/ipfmrp013.per
index 12fcd4a..5178ac5 100644
--- a/PROYECTOS/ipdir/ipfmrp013.per
+++ b/PROYECTOS/ipdir/ipfmrp013.per
@@ -7,6 +7,7 @@ screen size 24 by 80
Dias consumo [p1 ]
Categorias [ca1 ]
Codigo del Prod. Terminado [a ]-[b ]-[c ]-[d ]
+ Tipo Reporte [tr ]
}
end
tables
@@ -15,6 +16,7 @@ attributes
ll = formonly.m_meses;
p1 = formonly.dias1;
COMBOBOX ca1=formonly.cod_cat;
+COMBOBOX tr=formonly.tipo_reporte;
DATEEDIT k1 = formonly.fecha1 type date,format = "dd/mm/yy",
comments = "Digite la fecha inicial. Utilice el formato 'DDMMAA'";
DATEEDIT k2 = formonly.fecha2 type date,format = "dd/mm/yy",
diff --git a/PROYECTOS/ipdir/ipfmrp014.4fd b/PROYECTOS/ipdir/ipfmrp014.4fd
index 793b27b..2dca3a8 100644
--- a/PROYECTOS/ipdir/ipfmrp014.4fd
+++ b/PROYECTOS/ipdir/ipfmrp014.4fd
@@ -17,6 +17,7 @@
+
diff --git a/PROYECTOS/ipdir/ipprmt044.4gl b/PROYECTOS/ipdir/ipprmt044.4gl
index e72be4f..394c6d9 100644
--- a/PROYECTOS/ipdir/ipprmt044.4gl
+++ b/PROYECTOS/ipdir/ipprmt044.4gl
@@ -3319,6 +3319,12 @@ FUNCTION ipprad44()
PREPARE smt FROM procedimiento
+ # Guardia sobre el SP: por defecto WHENEVER ERROR STOP
+ # detiene el programa si el SP lanza un error SQL (p.ej.
+ # -3998 "transaccion no-confirmable"), sin llegar a la
+ # validacion limpia de out_result. Lo capturamos para
+ # revertir y avisar en vez de crashear.
+ WHENEVER ERROR CONTINUE
EXECUTE smt
USING ordenx IN,
--idx IN,
@@ -3367,6 +3373,27 @@ FUNCTION ipprad44()
out_cantidad_item OUT,
out_cantidad_ubicacion OUT,
out_msg OUT
+ WHENEVER ERROR STOP
+
+ # Si el EXEC del SP fallo a nivel SQL (STATUS < 0), como el
+ # -3998 que deja la transaccion doomed, revertir y avisar
+ # limpiamente en vez de detener el programa.
+ IF STATUS < 0 THEN
+ DISPLAY "ERROR SQL sp_insert_inventario: ",
+ STATUS, " ", SQLERRMESSAGE
+ DISPLAY "ITEM: ",
+ movi2[idx].cod_n, "-",
+ movi2[idx].cod_grupo, "-",
+ movi2[idx].cod_tipo, "-",
+ movi2[idx].cod_sec
+ DISPLAY "DOCUMENTO: ", otros_mov.num_doc,
+ " MOV: ", otros_mov.cod_mov,
+ " BODEGA: ", otros_mov.bodega
+ ROLLBACK WORK
+ LET numero_msg = 1679
+ CALL msg(numero_msg)
+ NEXT FIELD cod_n
+ END IF
IF out_result <> 0 THEN
@@ -3396,7 +3423,13 @@ FUNCTION ipprad44()
DISPLAY "============================================"
+ # El SP pudo haber revertido ya la transaccion si
+ # quedo condenada (CATCH con XACT_STATE = -1). Hacemos
+ # este ROLLBACK tolerante para no crashear si ya no
+ # hay transaccion activa.
+ WHENEVER ERROR CONTINUE
ROLLBACK WORK
+ WHENEVER ERROR STOP
IF out_msg IS NOT NULL THEN
ERROR out_msg
diff --git a/PROYECTOS/ipdir/ipprrp013.4gl b/PROYECTOS/ipdir/ipprrp013.4gl
index 99d9759..51b932b 100644
--- a/PROYECTOS/ipdir/ipprrp013.4gl
+++ b/PROYECTOS/ipdir/ipprrp013.4gl
@@ -5,9 +5,40 @@ OBJETIVO : REPORTE DIAS DE INVENTARIOS
PROGRAMADOR : Ing. Juan Francisco Soto
FECHA REALIZACION : Agosto 19, 1992
MODIFICADO POR : Ing. Juan Fco. Soto
-FECHA ULT. MODIFICA : Julio 19, 1993.
+FECHA ULT. MODIFICA : Agosto 20, 2026.
DESCRIPCION : Agregar Columna Costo Standard para cantidades mayores a
una cantidad de dias especificada.
+
+ AGOSTO 20, 2026 - ANALISIS DE MARGEN Y ROTACION:
+ 1. 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 margen % sobre precio:
+ margen = (precio - costo) / precio * 100
+ El costo es el costo estandar (iptb00004), igual que
+ antes. Se movio el calculo del costo ANTES del calculo
+ del valor de exceso para que ese valor use el costo
+ real (antes usaba costo = 0).
+ 2. ROTACION: igual que en ipprrp014:
+ rotacion = consumo / existencia
+ rotacion_anual = rotacion * (365 / dias del periodo)
+ En este reporte el consumo son las SALIDAS del periodo
+ (movs 30,31,55,56,40), que es la base correcta para la
+ rotacion de inventario.
+ 3. CLASIFICACION por cuadrante (margen x rotacion):
+ ESTRELLA = alto margen + alta rotacion
+ VOLUMEN = bajo margen + alta rotacion
+ NICHO = alto margen + baja rotacion
+ REVISAR = bajo margen + baja rotacion (candidato
+ a descontinuar / renegociar costo)
+ SIN COSTO = producto sin costo estandar cargado
+ Umbrales configurables (g_margen_min, g_rot_min).
+ 4. Se agregan valor_inv (existe*costo), ventas_valor
+ (consumo*precio) y ganancia (consumo*(precio-costo))
+ para poder totalizar el margen PONDERADO por CLASE en
+ el disenio .4rp (grupo por cod_n).
+ NOTA: el layout .4rp debe actualizarse en GST arrastrando
+ los nuevos campos (ver guia adjunta).
-------------------------------------------------------------------------------
}
GLOBALS "ipprgb000.4gl"
@@ -19,6 +50,31 @@ DEFINE dias1 SMALLINT,
m_meses SMALLINT,
conmes,valor1 DECIMAL(12,2)
+# ------------------------------------------------------------------------------
+# Umbrales de clasificacion (ajustables aqui, o promover a campos del form).
+# g_margen_min : margen % sobre precio a partir del cual se considera "alto"
+# g_rot_min : rotacion ANUALIZADA (veces/anio) a partir de la cual es "alta"
+# ------------------------------------------------------------------------------
+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 ipprrp013() 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)
+
MAIN
DEFER INTERRUPT
CALL ARG_VAL(1) RETURNING usuarios
@@ -33,6 +89,9 @@ FUNCTION ipprrp013()
# Variable que toma el valor de los dias que se va a valorar el reporte
DEFINE c_dias INTEGER
+# Combo para elegir Detallado / Resumen
+DEFINE cbtipo ui.ComboBox
+DEFINE prev_cod_n INTEGER
# Variables varias para el control de busqueda de las informaciones
DEFINE selec1,select_bal string
@@ -66,7 +125,16 @@ DEFINE c_dias INTEGER
costo DECIMAL(12,4),
dias INTEGER,
pcodigo char(25),
- cantidad_orden,pcantidadConduce,transito,existencia,reservado,disponible DEC(12,2)
+ cantidad_orden,pcantidadConduce,transito,existencia,reservado,disponible DEC(12,2),
+ # --- Analisis margen / rotacion (Agosto 2026) ---
+ precio DEC(12,4), -- precio venta real prom. neto del periodo
+ margen DEC(12,2), -- % sobre precio = (precio-costo)/precio*100
+ rotacion DEC(12,4), -- consumo / existencia
+ rotacion_anual DEC(12,4), -- rotacion * (365 / dias del periodo)
+ valor_inv DEC(14,2), -- existencia * costo (valor inventario)
+ ventas_valor DEC(14,2), -- consumo * precio (venta del periodo)
+ ganancia DEC(14,2), -- consumo * (precio - costo)
+ clasificacion CHAR(12) -- ESTRELLA/VOLUMEN/NICHO/REVISAR/SIN COSTO
END RECORD
DEFINE handler om.SaxDocumentHandler, -- return value from fgl_report_commitCurrentSettings()
r_filename STRING, -- filename of Report Design Document including .4rp extension
@@ -86,10 +154,28 @@ DEFINE c_dias INTEGER
DISPLAY "Dias de Inventarios" AT 6,33 ATTRIBUTE(BLACK)
LET tipo_papel = 2
CALL msgrp000(tipo_papel)
-
- INPUT BY NAME fecha1,fecha2,c_dias,m_meses
-
+ # Umbrales de clasificacion margen/rotacion (ver DEFINE arriba)
+ LET g_margen_min = 25.00
+ LET g_rot_min = 2.00
+
+
+ INPUT BY NAME fecha1,fecha2,c_dias,m_meses,tipo_reporte
+
+ BEFORE INPUT
+ # Llena el combo Detallado / Resumen (mismo patron que categorias())
+ 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
+ # Por defecto el reporte DETALLADO
+ IF tipo_reporte IS NULL OR tipo_reporte = " " THEN
+ LET tipo_reporte = "D"
+ END IF
+ DISPLAY BY NAME tipo_reporte
+
AFTER FIELD c_dias
IF c_dias is null THEN
LET numero_msg = 16
@@ -117,6 +203,12 @@ DEFINE c_dias INTEGER
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
END INPUT
IF int_flag THEN
@@ -229,9 +321,17 @@ DEFINE c_dias INTEGER
DECLARE consu SCROLL CURSOR FOR busca_balan
OPEN consu USING fecha2
}
- LET r_filename = "ipprrp013.4rp"
-
- LET idx = 1
+# Elige el disenio .4rp segun la opcion del formulario:
+# D = Detallado (una linea por producto) -> ipprrp013.4rp
+# R = Resumen (subtotales por CLASE cod_n) -> ipprrp013r.4rp
+ IF tipo_reporte = "R" THEN
+ LET r_filename = "ipprrp013r.4rp"
+ ELSE
+ LET r_filename = "ipprrp013.4rp"
+ END IF
+
+ LET idx = 1
+ LET prev_cod_n = -999999
FOREACH accion INTO estadis.*
LET estadis.pcodigo = estadis.cod_n USING "&&&&","-",
@@ -261,10 +361,14 @@ DEFINE c_dias INTEGER
END IF
--run the report
IF handler IS NOT NULL THEN -- report engine was configured ok
- START REPORT estadistica TO XML HANDLER HANDLER
+ IF tipo_reporte = "R" THEN
+ START REPORT resumen TO XML HANDLER HANDLER
ELSE
- EXIT FOREACH
- END IF
+ START REPORT estadistica TO XML HANDLER HANDLER
+ END IF
+ ELSE
+ EXIT FOREACH
+ END IF
END IF
{LET sale = "N"
@@ -299,7 +403,18 @@ END WHILE
valororden
LET estadis.existe = estadis.existencia
- # Calculo para los dias de produccion
+
+ # ---- COSTO ESTANDAR (movido aqui para que el valor de exceso Y el margen
+ # usen el costo real; antes se calculaba despues y quedaba en cero) ----
+ 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
+
+ # 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
@@ -328,30 +443,139 @@ IF estadis.dias >= c_dias THEN
LET estadis.valor = 0
END IF
END IF
-# Costo STD
-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
-DISPLAY "DATA DESPUES ",estadis.existe
+
+# ============================================================================
+# MARGEN DE BENEFICIO Y ROTACION
+# ============================================================================
+# Precio de venta REAL promedio del periodo, ponderado por cantidad y neto del
+# descuento de cabecera (porc_desc). Fuente: facturas vetb00002 + detalle
+# vetb00003 con fecha_factura dentro del rango [fecha1, fecha2].
+ 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
+
+# Margen % sobre precio = (precio - costo) / precio * 100
+ 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
+
+# Rotacion (misma formula que ipprrp014): consumo / existencia
+# anualizada = rotacion * (365 / dias del periodo) -- 365.0 evita div. entera
+ IF estadis.existe > 0 AND estadis.consumo > 0 THEN
+ LET estadis.rotacion = estadis.consumo / estadis.existe
+ LET estadis.rotacion_anual = estadis.rotacion * (365.0 / dias1)
+ ELSE
+ LET estadis.rotacion = 0
+ LET estadis.rotacion_anual = 0
+ END IF
+
+# Valores para totalizar el margen PONDERADO por CLASE en el .4rp
+ 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)
+
+# Clasificacion por cuadrante (margen x rotacion)
+ 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 estadis.existe > 0 THEN
- OUTPUT TO REPORT estadistica(estadis.*,dias1,c_dias)
+ 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
+ # Nueva clase: reinicia acumuladores
+ 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)
+ END IF
END IF
LET idx = idx +1
LET estadis.consumo = 0
LET estadis.existe = 0
-END FOREACH
+ LET estadis.precio = 0
+ LET estadis.margen = 0
+ LET estadis.rotacion = 0
+ LET estadis.rotacion_anual = 0
+END FOREACH
IF idx = 1 THEN
CALL fgl_winmessage("INFO","NO EXISTEN INFORMACIONES CON ESTOS PARAMETROS","INFO")
ELSE
- FINISH REPORT estadistica
+ IF tipo_reporte = "R" THEN
+ # Emite la ultima clase acumulada
+ IF prev_cod_n <> -999999 THEN
+ CALL emite_clase()
+ END IF
+ FINISH REPORT resumen
+ ELSE
+ FINISH REPORT estadistica
+ END IF
END IF
#RUN imprime
-
+
CLEAR SCREEN
-
+
+END FUNCTION
+
+# ------------------------------------------------------------------------------
+# Calcula el margen ponderado y la rotacion promedio de la clase acumulada en
+# 'rc' y la envia al reporte 'resumen'. Usada por ipprrp013() 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
REPORT estadistica(x,dias2,c_dias)
@@ -372,7 +596,16 @@ REPORT estadistica(x,dias2,c_dias)
costo DECIMAL(12,4),
dias INTEGER,
pcodigo char(25),
- cantidad_orden,pcantidadConduce,transito,existencia,reservado,disponible DEC(12,2)
+ cantidad_orden,pcantidadConduce,transito,existencia,reservado,disponible DEC(12,2),
+ # --- Analisis margen / rotacion (Agosto 2026) - debe reflejar estadis ---
+ precio DEC(12,4),
+ margen DEC(12,2),
+ rotacion DEC(12,4),
+ rotacion_anual DEC(12,4),
+ valor_inv DEC(14,2),
+ ventas_valor DEC(14,2),
+ ganancia DEC(14,2),
+ clasificacion CHAR(12)
END RECORD,
cantidad_rec,cantidad_or,tcantidad DEC(12,4),
fecha_v,fecha_oc DATE
@@ -500,3 +733,59 @@ REPORT estadistica(x,dias2,c_dias)
END REPORT
+
+# ==============================================================================
+# REPORTE RESUMEN POR CLASE (cod_n) -- version PLANA (sin grupos)
+# ------------------------------------------------------------------------------
+# Recibe UN registro ya agregado por CLASE (lo arma el FOREACH principal) y emite
+# una linea por clase via ON EVERY ROW. El gran total se acumula aqui y se
+# muestra en el pie del disenio ipprrp013r.4rp (patron identico al footer de
+# 'total_registros' del reporte de detalle, por eso no usa grupos ni triggers).
+# ==============================================================================
+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
+ SELECT a.* INTO p_companias.* FROM companias a WHERE a.cod_comp = 1
+ 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
diff --git a/PROYECTOS/ipdir/ipprrp014.4gl b/PROYECTOS/ipdir/ipprrp014.4gl
index f96f1ee..aaf80b2 100644
--- a/PROYECTOS/ipdir/ipprrp014.4gl
+++ b/PROYECTOS/ipdir/ipprrp014.4gl
@@ -23,6 +23,23 @@ DESCRIPCION : OPTIMIZACION DE RENDIMIENTO:
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"
@@ -39,6 +56,29 @@ DEFINE
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
@@ -95,7 +135,14 @@ FUNCTION ipprrp013()
disponible,
precio,
rotacion
- DEC(12, 2)
+ 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
@@ -111,11 +158,24 @@ FUNCTION ipprrp013()
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
+ INPUT BY NAME fecha1, fecha2, c_dias, m_meses, chexistencia, tipo_reporte
BEFORE INPUT
LET progreso = 10
LET c_dias = 0
- DISPLAY BY NAME c_dias, progreso
+ # 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
@@ -159,6 +219,11 @@ FUNCTION ipprrp013()
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
@@ -252,9 +317,15 @@ FUNCTION ipprrp013()
PREPARE comando2 FROM select_bal
DECLARE accion CURSOR FOR comando2
- LET r_filename = "ipprrp014.4rp"
+ # 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()
@@ -290,7 +361,11 @@ FUNCTION ipprrp013()
EXIT PROGRAM
END IF
IF handler IS NOT NULL THEN
- START REPORT estadistica TO XML HANDLER handler
+ 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
@@ -321,6 +396,31 @@ FUNCTION ipprrp013()
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 =
@@ -329,25 +429,21 @@ FUNCTION ipprrp013()
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
- # ==============================================================
+ # indice de rotacion = consumo / existencia (veces en el periodo)
LET estadis.rotacion = estadis.consumo / estadis.existe
- LET rotacion_anual = estadis.rotacion * (365 / dias1)
+ # anualizada: 365.0 (decimal) evita la division entera
+ LET estadis.rotacion_anual = estadis.rotacion * (365.0 / dias1)
ELSE
LET estadis.rotacion = 0
- LET rotacion_anual = 0
+ LET estadis.rotacion_anual = 0
END IF
ELSE
LET estadis.dias = 0
LET estadis.rotacion = 0
- LET rotacion_anual = 0
+ LET estadis.rotacion_anual = 0
END IF
+ LET rotacion_anual = estadis.rotacion_anual
LET estadis.valor = 0
IF estadis.dias >= c_dias THEN
@@ -358,15 +454,69 @@ FUNCTION ipprrp013()
LET estadis.valor = 0
END IF
END IF
- LET estadis.costo = 0
- OUTPUT TO REPORT estadistica(estadis.*, dias1, c_dias, rotacion_anual)
+ # ---- 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.
@@ -380,13 +530,91 @@ FUNCTION ipprrp013()
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
- FINISH REPORT estadistica
+ 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
@@ -416,7 +644,14 @@ REPORT estadistica(x, dias2, c_dias, rot_anual)
disponible,
precio,
rotacion
- DEC(12, 2)
+ 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),
@@ -491,6 +726,8 @@ REPORT estadistica(x, dias2, c_dias, rot_anual)
x.dias,
x.rotacion,
rot_anual,
+ x.margen,
+ x.clasificacion,
x.unidad_med,
fecha,
hora,
diff --git a/PROYECTOS/irdir/irprmt036.4gl b/PROYECTOS/irdir/irprmt036.4gl
index 8e090d4..24f136f 100644
--- a/PROYECTOS/irdir/irprmt036.4gl
+++ b/PROYECTOS/irdir/irprmt036.4gl
@@ -38,7 +38,32 @@ DEFINE CheckApi RECORD
checkRep BOOLEAN
END RECORD
-DEFINE fakeData RECORD LIKE cotb00001.*
+# fakeData se pasa POR POSICION (fakeData.*) al parametro Data de
+# ApiMaintainC en api_test.4gl, por lo que su estructura (orden y tipos)
+# debe ser IDENTICA a ese record. No usar RECORD LIKE cotb00001.* porque
+# el orden fisico de las columnas de cotb00001 no coincide con ese record
+# y provoca el error -1260 al convertir los tipos.
+DEFINE fakeData RECORD
+ area_sp VARCHAR(10),
+ ciu_sp VARCHAR(30),
+ cod_pais VARCHAR(3),
+ cod_sp SMALLINT,
+ cod_sp_sec SMALLINT,
+ cont_sp VARCHAR(30),
+ contactID INTEGER,
+ dir_sp VARCHAR(60),
+ email VARCHAR(60),
+ fax_sp VARCHAR(25),
+ fech_crea DATETIME YEAR TO FRACTION,
+ fech_mod DATETIME YEAR TO FRACTION,
+ id_maintainx INTEGER,
+ nom_sp VARCHAR(41),
+ status_t VARCHAR(1),
+ tel_sp VARCHAR(25),
+ term_sp SMALLINT,
+ us_crea VARCHAR(50),
+ us_mod VARCHAR(50)
+END RECORD
DEFINE data RECORD
rnumeroDoc,rcodMov,rtipo_cliente,num_oc,tipo_cliente,sec_cliente,bodega,bodega_destino INT,