El cliente que más vende no siempre es el que más dinero te deja
En la mayoría de las empresas que usan SAP Business One, el equipo comercial celebra a los clientes por su volumen de facturación. Sin embargo, cuando profundizas en los números, descubres algo incómodo: algunos de tus «mejores» clientes son los menos rentables. Descuentos agresivos, devoluciones frecuentes, productos de bajo margen y condiciones de pago extendidas pueden convertir una cuenta estrella en un lastre financiero disfrazado.
El problema es que SAP Business One no trae un reporte nativo que cruce ventas, costos, márgenes y comportamiento de pago por cliente de forma consolidada. En consecuencia, necesitas construir queries SQL de rentabilidad por cliente que unifiquen toda esa información para darte un mapa real de valor. A continuación, te muestro cómo hacerlo paso a paso con versiones para SQL Server y SAP HANA.
¿Qué mide realmente la rentabilidad de un cliente en SAP Business One?
Antes de ejecutar cualquier query, es importante entender qué variables componen la rentabilidad real. Por lo tanto, debes considerar estos cinco indicadores clave:
Margen bruto: La diferencia entre lo que facturas y lo que te cuesta el producto. Es el punto de partida, pero no es suficiente por sí solo.
Margen bruto porcentual: Un cliente que compra $100.000 con 10% de margen no es igual a uno que compra $30.000 con 40% de margen. El porcentaje revela la calidad de la venta.
Frecuencia de compra: Un cliente que compra una vez al año es más costoso de mantener que uno que compra mensualmente. Además, la recurrencia indica estabilidad.
Comportamiento de pago: Si un cliente tarda 90 días en pagar cuando la condición es 30, estás financiando su operación con tu capital de trabajo.
Notas de crédito y devoluciones: Un cliente con alto volumen de devoluciones destruye rentabilidad operacional. Como resultado, lo que parece una venta exitosa se convierte en pérdida.
Las tablas involucradas en SAP Business One
Para construir estas queries de rentabilidad necesitas dominar las siguientes tablas:
OCRD (Business Partners): Maestro de socios de negocio con nombre, grupo, vendedor asignado y condiciones de pago. OINV / INV1 (Invoices): Facturas de venta donde INV1 tiene el detalle por línea con precios, costos y el campo GrssProfit. ORIN / RIN1 (Credit Notes): Notas de crédito que representan devoluciones o descuentos post-venta. ORCT / RCT2 (Receipts): Pagos recibidos que permiten calcular los días promedio de pago real. OSLP (Sales Persons): Vendedores asignados. OCTG (Payment Terms): Condiciones de pago pactadas.
Query 1: Ranking de clientes por rentabilidad en SAP Business One
Esta query consolida ventas netas, costo, margen bruto y porcentaje de margen por cliente en un período determinado. Es el punto de partida para cualquier análisis de rentabilidad.
Versión SQL Server
DECLARE @FechaDesde DATE = '2025-01-01'
DECLARE @FechaHasta DATE = '2026-03-31'
SELECT
T0.CardCode AS 'Código Cliente',
T0.CardName AS 'Nombre Cliente',
T3.SlpName AS 'Vendedor',
COUNT(DISTINCT T1.DocNum) AS 'Nro. Facturas',
CAST(SUM(T2.LineTotal) AS DECIMAL(15,2)) AS 'Venta Neta',
CAST(SUM(T2.StockValue) AS DECIMAL(15,2)) AS 'Costo Total',
CAST(SUM(T2.GrssProfit) AS DECIMAL(15,2)) AS 'Utilidad Bruta',
CAST(CASE WHEN SUM(T2.LineTotal) = 0 THEN 0
ELSE SUM(T2.GrssProfit) * 100.0 / SUM(T2.LineTotal)
END AS DECIMAL(5,2)) AS '% Margen',
CAST(SUM(T2.LineTotal) / COUNT(DISTINCT T1.DocNum) AS DECIMAL(15,2)) AS 'Ticket Promedio'
FROM OCRD T0
INNER JOIN OINV T1 ON T1.CardCode = T0.CardCode
INNER JOIN INV1 T2 ON T2.DocEntry = T1.DocEntry
LEFT JOIN OSLP T3 ON T3.SlpCode = T0.SlpCode
WHERE T1.DocDate BETWEEN @FechaDesde AND @FechaHasta
AND T1.CANCELED = 'N'
GROUP BY T0.CardCode, T0.CardName, T3.SlpName
ORDER BY SUM(T2.GrssProfit) DESCVersión SAP HANA
DO BEGIN
DECLARE FechaDesde DATE = '2025-01-01';
DECLARE FechaHasta DATE = '2026-03-31';
SELECT
T0."CardCode" AS "Código Cliente",
T0."CardName" AS "Nombre Cliente",
T3."SlpName" AS "Vendedor",
COUNT(DISTINCT T1."DocNum") AS "Nro. Facturas",
CAST(SUM(T2."LineTotal") AS DECIMAL(15,2)) AS "Venta Neta",
CAST(SUM(T2."StockValue") AS DECIMAL(15,2)) AS "Costo Total",
CAST(SUM(T2."GrssProfit") AS DECIMAL(15,2)) AS "Utilidad Bruta",
CAST(CASE WHEN SUM(T2."LineTotal") = 0 THEN 0
ELSE SUM(T2."GrssProfit") * 100.0 / SUM(T2."LineTotal")
END AS DECIMAL(5,2)) AS "% Margen",
CAST(SUM(T2."LineTotal") / COUNT(DISTINCT T1."DocNum") AS DECIMAL(15,2)) AS "Ticket Promedio"
FROM "OCRD" T0
INNER JOIN "OINV" T1 ON T1."CardCode" = T0."CardCode"
INNER JOIN "INV1" T2 ON T2."DocEntry" = T1."DocEntry"
LEFT JOIN "OSLP" T3 ON T3."SlpCode" = T0."SlpCode"
WHERE T1."DocDate" BETWEEN :FechaDesde AND :FechaHasta
AND T1."CANCELED" = 'N'
GROUP BY T0."CardCode", T0."CardName", T3."SlpName"
ORDER BY SUM(T2."GrssProfit") DESC;
END;¿Qué revela? Ordena a todos tus clientes por utilidad bruta real, no por volumen. El campo GrssProfit de INV1 calcula automáticamente la diferencia entre precio de venta y costo del artículo. Por lo tanto, puedes identificar rápidamente quién genera valor y quién solo genera movimiento.
Query 2: Impacto de notas de crédito por cliente
Las notas de crédito son un indicador silencioso de problemas. Esta query cruza ventas contra devoluciones para revelar qué clientes están erosionando tu margen.
Versión SQL Server
DECLARE @FechaDesde DATE = '2025-01-01'
DECLARE @FechaHasta DATE = '2026-03-31'
SELECT
Ventas.CardCode, Ventas.CardName,
Ventas.VentaNeta,
ISNULL(NC.TotalNC, 0) AS 'Notas de Crédito',
Ventas.VentaNeta - ISNULL(NC.TotalNC, 0) AS 'Venta Real',
CAST(CASE WHEN Ventas.VentaNeta = 0 THEN 0
ELSE ISNULL(NC.TotalNC, 0) * 100.0 / Ventas.VentaNeta
END AS DECIMAL(5,2)) AS '% Devolución'
FROM (
SELECT T1.CardCode, T0.CardName,
SUM(T2.LineTotal) AS VentaNeta
FROM OCRD T0
INNER JOIN OINV T1 ON T1.CardCode = T0.CardCode
INNER JOIN INV1 T2 ON T2.DocEntry = T1.DocEntry
WHERE T1.DocDate BETWEEN @FechaDesde AND @FechaHasta
AND T1.CANCELED = 'N'
GROUP BY T1.CardCode, T0.CardName
) Ventas
LEFT JOIN (
SELECT T1.CardCode, SUM(T2.LineTotal) AS TotalNC
FROM ORIN T1
INNER JOIN RIN1 T2 ON T2.DocEntry = T1.DocEntry
WHERE T1.DocDate BETWEEN @FechaDesde AND @FechaHasta
AND T1.CANCELED = 'N'
GROUP BY T1.CardCode
) NC ON NC.CardCode = Ventas.CardCode
ORDER BY ISNULL(NC.TotalNC, 0) DESCVersión SAP HANA
DO BEGIN
DECLARE FechaDesde DATE = '2025-01-01';
DECLARE FechaHasta DATE = '2026-03-31';
SELECT
"Ventas"."CardCode", "Ventas"."CardName",
"Ventas"."VentaNeta",
IFNULL("NC"."TotalNC", 0) AS "Notas de Crédito",
"Ventas"."VentaNeta" - IFNULL("NC"."TotalNC", 0) AS "Venta Real",
CAST(CASE WHEN "Ventas"."VentaNeta" = 0 THEN 0
ELSE IFNULL("NC"."TotalNC", 0) * 100.0 / "Ventas"."VentaNeta"
END AS DECIMAL(5,2)) AS "% Devolución"
FROM (
SELECT T1."CardCode", T0."CardName",
SUM(T2."LineTotal") AS "VentaNeta"
FROM "OCRD" T0
INNER JOIN "OINV" T1 ON T1."CardCode" = T0."CardCode"
INNER JOIN "INV1" T2 ON T2."DocEntry" = T1."DocEntry"
WHERE T1."DocDate" BETWEEN :FechaDesde AND :FechaHasta
AND T1."CANCELED" = 'N'
GROUP BY T1."CardCode", T0."CardName"
) "Ventas"
LEFT JOIN (
SELECT T1."CardCode", SUM(T2."LineTotal") AS "TotalNC"
FROM "ORIN" T1
INNER JOIN "RIN1" T2 ON T2."DocEntry" = T1."DocEntry"
WHERE T1."DocDate" BETWEEN :FechaDesde AND :FechaHasta
AND T1."CANCELED" = 'N'
GROUP BY T1."CardCode"
) "NC" ON "NC"."CardCode" = "Ventas"."CardCode"
ORDER BY IFNULL("NC"."TotalNC", 0) DESC;
END;Si un cliente tiene un porcentaje de devolución superior al 10%, necesitas investigar. ¿Es un problema de calidad, de logística, o es una estrategia del cliente para obtener descuentos encubiertos? Sin embargo, sin esta query nunca lo sabrías porque las notas de crédito se procesan de forma aislada.
Query 3: Comportamiento de pago real vs. condición pactada
Esta query compara los días de pago reales contra la condición comercial pactada. Es fundamental para detectar clientes que financian su operación con tu capital de trabajo.
Versión SQL Server
SELECT
T0.CardCode AS 'Código Cliente',
T0.CardName AS 'Nombre Cliente',
T3.PymntGroup AS 'Condición Pago',
T3.ExtraDays AS 'Días Pactados',
COUNT(T1.DocNum) AS 'Facturas Pagadas',
CAST(AVG(DATEDIFF(DAY, T1.DocDate, T2.DocDate)) AS DECIMAL(5,1))
AS 'Días Pago Promedio',
CAST(AVG(DATEDIFF(DAY, T1.DocDate, T2.DocDate)) - T3.ExtraDays
AS DECIMAL(5,1)) AS 'Días Atraso Promedio'
FROM OCRD T0
INNER JOIN OINV T1 ON T1.CardCode = T0.CardCode
INNER JOIN ORCT T2 ON T2.DocNum IN (
SELECT U0.DocNum FROM RCT2 U0
WHERE U0.InvType = 13 AND U0.DocEntry = T1.DocEntry)
INNER JOIN OCTG T3 ON T3.GroupNum = T0.GroupNum
WHERE T1.CANCELED = 'N'
AND T1.PaidToDate >= T1.DocTotal
AND T1.DocDate >= DATEADD(YEAR, -1, GETDATE())
GROUP BY T0.CardCode, T0.CardName, T3.PymntGroup, T3.ExtraDays
HAVING COUNT(T1.DocNum) >= 3
ORDER BY AVG(DATEDIFF(DAY, T1.DocDate, T2.DocDate)) - T3.ExtraDays DESCVersión SAP HANA
SELECT
T0."CardCode" AS "Código Cliente",
T0."CardName" AS "Nombre Cliente",
T3."PymntGroup" AS "Condición Pago",
T3."ExtraDays" AS "Días Pactados",
COUNT(T1."DocNum") AS "Facturas Pagadas",
CAST(AVG(DAYS_BETWEEN(T1."DocDate", T2."DocDate")) AS DECIMAL(5,1))
AS "Días Pago Promedio",
CAST(AVG(DAYS_BETWEEN(T1."DocDate", T2."DocDate")) - T3."ExtraDays"
AS DECIMAL(5,1)) AS "Días Atraso Promedio"
FROM "OCRD" T0
INNER JOIN "OINV" T1 ON T1."CardCode" = T0."CardCode"
INNER JOIN "ORCT" T2 ON T2."DocNum" IN (
SELECT U0."DocNum" FROM "RCT2" U0
WHERE U0."InvType" = 13 AND U0."DocEntry" = T1."DocEntry")
INNER JOIN "OCTG" T3 ON T3."GroupNum" = T0."GroupNum"
WHERE T1."CANCELED" = 'N'
AND T1."PaidToDate" >= T1."DocTotal"
AND T1."DocDate" >= ADD_YEARS(CURRENT_DATE, -1)
GROUP BY T0."CardCode", T0."CardName", T3."PymntGroup", T3."ExtraDays"
HAVING COUNT(T1."DocNum") >= 3
ORDER BY AVG(DAYS_BETWEEN(T1."DocDate", T2."DocDate")) - T3."ExtraDays" DESCEl campo «Días Atraso Promedio» es el indicador clave. Un valor positivo alto indica un cliente que sistemáticamente financia su operación con tu dinero. Además, es importante notar que en SAP HANA se usa DAYS_BETWEEN en lugar de DATEDIFF y ADD_YEARS en lugar de DATEADD.
Query 4: Scorecard integral de rentabilidad por cliente
Finalmente, esta query clasifica automáticamente a cada cliente en una categoría A-B-C-D según su margen real. Es la herramienta definitiva para segmentar tu cartera.
Versión SQL Server
SELECT
T0.CardCode, T0.CardName,
CAST(SUM(T2.GrssProfit) AS DECIMAL(15,2)) AS 'Utilidad Bruta',
CAST(CASE WHEN SUM(T2.LineTotal) = 0 THEN 0
ELSE SUM(T2.GrssProfit) * 100.0 / SUM(T2.LineTotal)
END AS DECIMAL(5,2)) AS '% Margen',
COUNT(DISTINCT T1.DocNum) AS 'Facturas',
CASE
WHEN SUM(T2.GrssProfit) * 100.0 / NULLIF(SUM(T2.LineTotal),0) >= 25 THEN 'A - Premium'
WHEN SUM(T2.GrssProfit) * 100.0 / NULLIF(SUM(T2.LineTotal),0) >= 15 THEN 'B - Rentable'
WHEN SUM(T2.GrssProfit) * 100.0 / NULLIF(SUM(T2.LineTotal),0) >= 5 THEN 'C - Marginal'
ELSE 'D - No Rentable'
END AS 'Clasificación'
FROM OCRD T0
INNER JOIN OINV T1 ON T1.CardCode = T0.CardCode
INNER JOIN INV1 T2 ON T2.DocEntry = T1.DocEntry
WHERE T1.CANCELED = 'N'
AND T1.DocDate >= DATEADD(YEAR, -1, GETDATE())
GROUP BY T0.CardCode, T0.CardName
ORDER BY SUM(T2.GrssProfit) DESCVersión SAP HANA
SELECT
T0."CardCode", T0."CardName",
CAST(SUM(T2."GrssProfit") AS DECIMAL(15,2)) AS "Utilidad Bruta",
CAST(CASE WHEN SUM(T2."LineTotal") = 0 THEN 0
ELSE SUM(T2."GrssProfit") * 100.0 / SUM(T2."LineTotal")
END AS DECIMAL(5,2)) AS "% Margen",
COUNT(DISTINCT T1."DocNum") AS "Facturas",
CASE
WHEN SUM(T2."GrssProfit") * 100.0 / NULLIF(SUM(T2."LineTotal"),0) >= 25 THEN 'A - Premium'
WHEN SUM(T2."GrssProfit") * 100.0 / NULLIF(SUM(T2."LineTotal"),0) >= 15 THEN 'B - Rentable'
WHEN SUM(T2."GrssProfit") * 100.0 / NULLIF(SUM(T2."LineTotal"),0) >= 5 THEN 'C - Marginal'
ELSE 'D - No Rentable'
END AS "Clasificación"
FROM "OCRD" T0
INNER JOIN "OINV" T1 ON T1."CardCode" = T0."CardCode"
INNER JOIN "INV1" T2 ON T2."DocEntry" = T1."DocEntry"
WHERE T1."CANCELED" = 'N'
AND T1."DocDate" >= ADD_YEARS(CURRENT_DATE, -1)
GROUP BY T0."CardCode", T0."CardName"
ORDER BY SUM(T2."GrssProfit") DESCEsta clasificación A-B-C-D te permite segmentar tu cartera por rentabilidad real. En consecuencia, puedes proteger a los Premium, desarrollar a los Rentables, renegociar con los Marginales y evaluar si conviene mantener a los No Rentables. Además, al tener versiones para SQL Server y HANA, puedes ejecutar este análisis sin importar la versión de SAP Business One que utilices.
Conclusión: la rentabilidad no se asume, se mide
Tener estas queries en tu Query Manager transforma la conversación comercial de tu empresa. Ya no se trata de «quién vende más» sino de «quién genera más valor». Un equipo comercial que entiende la rentabilidad por cliente en SAP Business One toma decisiones completamente diferentes: negocia mejor los precios, prioriza los clientes correctos y deja de subsidiar cuentas que drenan recursos.
Además, con las versiones para SQL Server y SAP HANA incluidas, puedes implementar este análisis independientemente de tu infraestructura de base de datos. Por lo tanto, no hay excusas para seguir operando a ciegas.
¿Cuándo fue la última vez que revisaste la rentabilidad real de tu cartera de clientes? Si la respuesta es «nunca», estos queries son tu punto de partida.




