Customer Profitability Analysis in SAP Business One with SQL Queries - featured image

Publications

Profitability Analysis by Customer in SAP Business One with SQL Queries

Hector Pincheira

Hector Pincheira

CIO | CTO
Digital Strategy & Technology Innovation

items

The customer who sells the most isn't always the one who leaves you the most money

In most companies that use SAP Business One, the sales team celebrates customers for their billing volume. However, when you dig into the numbers, you discover something uncomfortable: some of your «best» customers are the least profitable. Aggressive discounts, frequent returns, low-margin products, and extended payment conditions can turn a star account into a disguised financial drag.

The problem is that SAP Business One does not include a native report that cross-references sales, costs, margins, and payment behavior by customer in a consolidated way. Consequently, you need to build SQL queries for customer profitability that they unify all that information to give you a real value map. Below, I show you how to do it step by step with versions for SQL Server and SAP HANA.

What does customer profitability actually measure in SAP Business One?

Before running any query, it is important to understand what variables make up the actual profitability. Therefore, you must consider these five key indicators:

Gross margin: The difference between what you invoice and what the product costs you. It is the starting point, but it is not enough on its own.

Gross profit percentage: A customer who buys $100,000 with a 10% margin is not the same as one who buys $30,000 with a 40% margin. The percentage reveals the quality of the sale.

Purchase frequency: A customer who buys once a year is more costly to maintain than one who buys monthly. Furthermore, recurrence indicates stability.

Payment behavior: If a customer takes 90 days to pay when the terms are 30 days, you are financing their operations with your working capital.

Credit notes and returns: A customer with a high volume of returns destroys operational profitability. As a result, what looks like a successful sale turns into a loss.

The tables involved in SAP Business One

To build these profitability queries, you need to master the following tables:

OCRD (Business Partners): Business partner master with name, group, assigned sales representative, and payment terms. OINV / INV1 (Invoices): Sales invoices where INV1 has the line-by-line detail with prices, costs, and the GrssProfit field. ORIN / RIN1 (Credit Notes): Credit notes representing post-sale returns or discounts. ORCT / RCT2 (Receipts): Received payments that allow calculating the average actual payment days. OSLP (Sales Persons): Assigned sales representatives. OCTG (Payment Terms): Agreed payment terms.

Query 1: Customer ranking by profitability in SAP Business One

This query consolidates net sales, cost, gross margin, and margin percentage by customer over a given period. It is the starting point for any profitability analysis.

SQL Server version

DECLARE @StartDate DATE = '2025-01-01'
DECLARE @EndDate DATE = '2026-03-31'
SELECT
    T0.CardCode            AS 'Customer Code',
    T0.CardName            AS 'Customer Name',
    T3.SlpName             AS 'Salesperson',
    COUNT(DISTINCT T1.DocNum) AS 'Number of Invoices',
    CAST(SUM(T2.LineTotal) AS DECIMAL(15,2))  AS 'Net Sales',
    CAST(SUM(T2.StockValue) AS DECIMAL(15,2)) AS 'Total Cost',
    CAST(SUM(T2.GrssProfit) AS DECIMAL(15,2)) AS 'Gross Profit',
    CAST(CASE WHEN SUM(T2.LineTotal) = 0 THEN 0
        ELSE SUM(T2.GrssProfit) * 100.0 / SUM(T2.LineTotal)
    END AS DECIMAL(5,2))   AS '% Margin',
    CAST(SUM(T2.LineTotal) / COUNT(DISTINCT T1.DocNum) AS DECIMAL(15,2)) AS 'Average Receipt'
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 @StartDate AND @EndDate
  AND T1.CANCELED = 'N'
GROUP BY T0.CardCode, T0.CardName, T3.SlpName
ORDER BY SUM(T2.GrssProfit) DESC

SAP HANA version

DO BEGIN
    DECLARE StartDate DATE = '2025-01-01';
    DECLARE EndDate DATE = '2026-03-31';
    SELECT
        T0."CardCode"            AS "Customer Code",
        T0."CardName"            AS "Customer Name",
        T3."SlpName"             AS "Salesperson",
        COUNT(DISTINCT T1."DocNum") AS "Number of Invoices",
        CAST(SUM(T2."LineTotal") AS DECIMAL(15,2))  AS "Net Sales",
        CAST(SUM(T2."StockValue") AS DECIMAL(15,2)) AS "Total Cost",
        CAST(SUM(T2."GrssProfit") AS DECIMAL(15,2)) AS "Gross Profit",
        CAST(CASE WHEN SUM(T2."LineTotal") = 0 THEN 0
            ELSE SUM(T2."GrossProfit") * 100.0 / SUM(T2."LineTotal")
        END AS DECIMAL(5,2))   AS "% Margin",
        CAST(SUM(T2."LineTotal") / COUNT(DISTINCT T1."DocNum") AS DECIMAL(15,2)) AS "Average Ticket"
    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 :StartDate AND :EndDate
      AND T1."CANCELED" = 'N'
    GROUP BY T0."CardCode", T0."CardName", T3."SlpName"
    ORDER BY SUM(T2."GrssProfit") DESC;
END;

What does it reveal? Order all your customers by actual gross profit, not by volume. The GrssProfit field in INV1 automatically calculates the difference between the selling price and the item cost. Therefore, you can quickly identify who generates value and who only generates movement.

Query 2: Impact of credit notes by customer

Credit notes are a silent indicator of problems. This query cross-references sales against returns to reveal which customers are eroding your margin.

SQL Server version

DECLARE @StartDate DATE = '2025-01-01'
DECLARE @EndDate DATE = '2026-03-31'
SELECT
    Sales.CardCode, Sales.CardName,
    Sales.NetSales,
    ISNULL(NC.TotalNC, 0)  AS 'Credit Memos',
    Sales.NetSales - ISNULL(NC.TotalNC, 0)  AS 'Actual Sales',
    CAST(CASE WHEN Sales.NetSales = 0 THEN 0
        ELSE ISNULL(NC.TotalNC, 0) * 100.0 / Sales.NetSales
    END AS DECIMAL(5,2))   AS '% Return'
FROM (
    SELECT T1.CardCode, T0.CardName,
           SUM(T2.LineTotal) AS NetSales
    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 @StartDate AND @EndDate
      AND T1.CANCELED = 'N'
    GROUP BY T1.CardCode, T0.CardName
) Sales
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 @StartDate AND @EndDate
      AND T1.CANCELED = 'N'
    GROUP BY T1.CardCode
) NC ON NC.CardCode = Sales.CardCode
ORDER BY ISNULL(NC.TotalNC, 0) DESC

SAP HANA version

DO BEGIN
    DECLARE StartDate DATE = '2025-01-01';
    DECLARE EndDate DATE = '2026-03-31';
    SELECT
        "Sales"."CardCode", "Sales"."CardName",
        "Sales"."NetSales",
        IFNULL("NC"."TotalNC", 0)  AS "Credit Memo",
        "Sales"."NetSales" - IFNULL("NC"."TotalNC", 0)  AS "Actual Sales",
        CAST(CASE WHEN "Sales"."NetSales" = 0 THEN 0
            ELSE IFNULL("NC"."TotalNC", 0) * 100.0 / "Sales"."NetSales"
        END AS DECIMAL(5,2))   AS "% Return"
    FROM (
        SELECT T1."CardCode", T0."CardName",
               SUM(T2."LineTotal") AS "Net Sales"
        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 :StartDate AND :EndDate
          AND T1."CANCELED" = 'N'
        GROUP BY T1."CardCode", T0."CardName"
    ) "Sales"
    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 :StartDate AND :EndDate
          AND T1."CANCELED" = 'N'
        GROUP BY T1."CardCode"
    ) "NC" ON "NC"."CardCode" = "Sales"."CardCode"
    ORDER BY IFNULL("NC"."TotalNC", 0) DESC;
END;

If a customer has a return rate higher than 10%, you need to investigate. Is it a quality issue, a logistics issue, or is it a strategy the customer is using to obtain hidden discounts? However, without this query, you would never know because credit memos are processed in isolation.

Query 3: Actual payment behavior vs. agreed-upon condition

This query compares actual payment days against the agreed commercial term. It is essential for detecting customers who finance their operations with your working capital.

SQL Server version

SELECT
    T0.CardCode            AS 'Customer Code',
    T0.CardName            AS 'Customer Name',
    T3.PymntGroup          AS 'Payment Terms',
    T3.ExtraDays           AS 'Agreed Days',
    COUNT(T1.DocNum)       AS 'Paid Invoices',
    CAST(AVG(DATEDIFF(DAY, T1.DocDate, T2.DocDate)) AS DECIMAL(5,1))
                           AS 'Average Days to Pay',
    CAST(AVG(DATEDIFF(DAY, T1.DocDate, T2.DocDate)) - T3.ExtraDays
         AS DECIMAL(5,1))  AS 'Average Days Past Due'
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 DESC

SAP HANA version

SELECT
    T0."CardCode"            AS "Customer Code",
    T0."CardName"            AS "Customer Name",
    T3."PymntGroup"          AS "Payment Terms",
    T3."ExtraDays"           AS "Agreed Days",
    COUNT(T1."DocNum")       AS "Paid Invoices",
    CAST(AVG(DAYS_BETWEEN(T1."DocDate", T2."DocDate")) AS DECIMAL(5,1))
                             AS "Average Payment Days",
    CAST(AVG(DAYS_BETWEEN(T1."DocDate", T2."DocDate")) - T3."ExtraDays"
         AS DECIMAL(5,1))    AS "Average Days Past Due"
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" DESC

The «Average Days Overdue» field is the key indicator. A high positive value indicates a customer who systematically finances their operation with your money. Furthermore, it is important to note that in SAP HANA, it is used DAYS_BETWEEN instead of DATEDIFF and ADD_YEARS instead of DATEADD.

Query 4: Customer profitability balanced scorecard

Finally, this query automatically classifies each customer into an A-B-C-D category based on their actual margin. It is the ultimate tool for segmenting your portfolio.

SQL Server version

SELECT
    T0.CardCode, T0.CardName,
    CAST(SUM(T2.GrssProfit) AS DECIMAL(15,2)) AS 'Gross Profit',
    CAST(CASE WHEN SUM(T2.LineTotal) = 0 THEN 0
        ELSE SUM(T2.GrssProfit) * 100.0 / SUM(T2.LineTotal)
    END AS DECIMAL(5,2))   AS '% Margin',
    COUNT(DISTINCT T1.DocNum) AS 'Invoices',
    CASE
        WHEN SUM(T2.GrossProfit) * 100.0 / NULLIF(SUM(T2.LineTotal), 0) >= 25 THEN 'A - Premium'
        WHEN SUM(T2.GrossProfit) * 100.0 / NULLIF(SUM(T2.LineTotal), 0) >= 15 THEN 'B - Profitable'
        WHEN SUM(T2.GrssProfit) * 100.0 / NULLIF(SUM(T2.LineTotal), 0) >= 5 THEN 'C - Marginal'
        ELSE 'D - Unprofitable'
    END AS 'Classification'
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) DESC

SAP HANA version

SELECT
    T0."CardCode", T0."CardName",
    CAST(SUM(T2."GrssProfit") AS DECIMAL(15,2)) AS "Gross Profit",
    CAST(CASE WHEN SUM(T2."LineTotal") = 0 THEN 0
        ELSE SUM(T2."GrssProfit") * 100.0 / SUM(T2."LineTotal")
    END AS DECIMAL(5,2))   AS "% Margin",
    COUNT(DISTINCT T1."DocNum") AS "Invoices",
    CASE
        WHEN SUM(T2."GrossProfit") * 100.0 / NULLIF(SUM(T2."LineTotal"), 0) >= 25 THEN 'A - Premium'
        WHEN SUM(T2."GrossProfit") * 100.0 / NULLIF(SUM(T2."LineTotal"), 0) >= 15 THEN 'B - Profitable'
        WHEN SUM(T2."GrssProfit") * 100.0 / NULLIF(SUM(T2."LineTotal"), 0) >= 5 THEN 'C - Marginal'
        ELSE 'D - Unprofitable'
    END AS "Classification"
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") DESC

This A-B-C-D classification allows you to segment your portfolio by actual profitability. Consequently, you can protect the Premium ones, develop the Profitable ones, renegotiate with the Marginal ones, and evaluate whether it is worth keeping the Unprofitable ones. Furthermore, by having versions for SQL Server and HANA, you can run this analysis regardless of the version of SAP Business One you use.

Conclusion: profitability is not assumed, it is measured

Having these queries in your Query Manager transforms your company's sales conversation. It is no longer about «who sells the most» but about «who generates the most value.» A sales team that understands the customer profitability in SAP Business One make completely different decisions: negotiate prices better, prioritize the right clients, and stop subsidizing resource-draining accounts.

Furthermore, with SQL Server and SAP HANA versions included, you can implement this analysis regardless of your database infrastructure. Therefore, there are no excuses for continuing to operate blindly.

When was the last time you reviewed the actual profitability of your client portfolio? If the answer is «never,» these queries are your starting point.

Most Read

Pasajero mira un tablero de salidas en un aeropuerto, metafora de la dependencia de proveedores de IA
El Calendario Ajeno: la Dependencia de Proveedores de IA que Nadie Negocia
SAP Business One Procedimientos Almacenados: Reportes Reutilizables - imagen destacada del tutorial
SAP Business One Stored Procedures: Reusable Reports
SAP Business One Pending Orders - featured image of the tutorial
SAP Business One Pending Orders: SQL Query to Control Your Backlog
QlikSense yield analysis: breakdown of the loss
QlikSense Yield Analysis: the Waste Nobody Is Watching
QlikSense mobile dashboard: the three screens
QlikSense Mobile Dashboard: the Three Screens People Actually Open
SAP Business One Bank Deposits - featured image of the tutorial
SAP Business One Bank Deposits: Manage Checks and Collections Step by Step

RELATED ARTICLES