{
  "meta": {
    "brand": "Amouage",
    "title": "Y2 → D365 POS export",
    "subtitle": "Source-extraction data contract · Cegid Retail Y2 v26",
    "eyebrow": "Integration data contract",
    "headline": "POS sales transactions, Cegid Y2 to D365 F&O Retail",
    "intro": "This query extracts point-of-sale transactions from Cegid Y2 and emits one pipe-delimited file with five record types. The middleware reads that file and assembles five D365 OData payloads — Header, Sales, Tax, Payment, Discount — in that fixed order. The query owns only the source extraction: correct Cegid columns, signs, consolidation, and references.",
    "flow": [
      "Cegid Y2 v26",
      "H · L · T · P · D file",
      "RFE blob",
      "Azure middleware",
      "D365 F&O Retail"
    ],
    "scope_note": "A document qualifies only when all of these hold. Every record-type branch applies the same gate so the five sets stay consistent. Any transaction paid by gift card is dropped entirely.",
    "idempotent_note": "The run reads only documents where GP_ETATEXPORT <> 'EXP' within the business-date / EOD window, so re-running the same window yields the same rows and never writes back to Y2.",
    "price_note": "S_PRICE (unit price) is always emitted positive. A negative unit price is never valid, regardless of sale, return or exchange — D365 rejects it with HTTP 400.",
    "batch_note": "Each resolved ZBA_BATCHNUMBER must already exist in the D365 batch master or D365 rejects the line — a continuous data-sync discipline outside this query.",
    "footer": "Reference for the AMOUAGE Cegid Y2 → D365 F&O Retail POS export. Field map reconciled against the 551-row production export and against the middleware parser that consumes it.",
    "consolidation_note": "The exported L row is not the Cegid line: its grain is (Cegid line, batch). One QR is one unit, so COUNT(DISTINCT ZLQ_QRCODE) is the exact unit count of the batch on that line, and every line amount is prorated by nScans / qty. Sub-lines re-sum to the line, so the header stays reconciled. See the Batch & SKU logic section for worked examples.",
    "consumer_note": "The consumer is the AMOUAGE Azure Functions middleware: a 5-minute timer pulls the newest SALES_For_D365* blob from Cegid RFE, parses it positionally, persists every line in PostgreSQL keyed by sha256 of the raw line, then fans out one durable sub-orchestration per ticket and POSTs each record to its D365 OData entity. Header failure dead-letters the whole ticket."
  },
  "kinds": [
    {
      "id": "source",
      "label": "source"
    },
    {
      "id": "derived",
      "label": "computed"
    },
    {
      "id": "fixed",
      "label": "fixed"
    },
    {
      "id": "sign",
      "label": "signed"
    },
    {
      "id": "ref",
      "label": "mapped"
    },
    {
      "id": "custom",
      "label": "custom/QR"
    }
  ],
  "records": [
    {
      "code": "H",
      "name": "Header",
      "cardinality": "1 per transaction",
      "desc": "One row that identifies the receipt: entity, store, register, customer, currency, control totals.",
      "fields": [
        {
          "d365": "DATAAREAID",
          "source": "ET_FILIALE",
          "transform": "direct",
          "note": "D365 legal entity (Oman = as)",
          "kind": "ref",
          "sql": "CAST(ET_FILIALE AS VARCHAR(50))"
        },
        {
          "d365": "TRANSACTIONNUMBER",
          "source": "GP_REFINTERNE",
          "transform": "direct",
          "note": "Receipt internal reference — the transaction key",
          "kind": "source",
          "sql": "CAST(GP_REFINTERNE AS VARCHAR(50))"
        },
        {
          "d365": "OPERATINGUNITNUMBER",
          "source": "ET_CHARLIBRE1",
          "transform": "direct",
          "note": "Store free-field → D365 operating unit",
          "kind": "ref",
          "sql": "CAST(ET_CHARLIBRE1 AS VARCHAR(50))"
        },
        {
          "d365": "TERMINAL",
          "source": "GP_CAISSE",
          "transform": "mapped register code",
          "note": "Cash desk / register → D365 terminal (part 2 §2 overrides GP_ETABLISSEMENT)",
          "kind": "ref",
          "sql": "CAST(GP_CAISSE AS VARCHAR(50))"
        },
        {
          "d365": "TRANSACTIONDATE",
          "source": "GP_DTSVCREATE",
          "transform": "direct",
          "note": "POS creation datetime (server, UTC)",
          "kind": "source",
          "sql": "CAST(GP_DTSVCREATE AS VARCHAR(50))"
        },
        {
          "d365": "H_CUSTOMERACCOUNT",
          "source": "GP_TIERS",
          "transform": "conditional",
          "note": "Only when customer is identified (TIERSCOMPL.YTC_TABLELIBRETIERS2 filled); else blank",
          "kind": "derived",
          "sql": "CAST(CASE WHEN ISNULL(YTC_TABLELIBRETIERS2, '') <> '' THEN GP_TIERS ELSE '' END AS VARCHAR(50))"
        },
        {
          "d365": "H_RRECEIPTID",
          "source": "GP_NUMERO",
          "transform": "direct",
          "note": "Document number",
          "kind": "source",
          "sql": "CAST(GP_NUMERO AS VARCHAR(50))"
        },
        {
          "d365": "H_BUSINESSDATE",
          "source": "GP_HEURECREATION",
          "transform": "direct",
          "note": "Store-local business date/time",
          "kind": "source",
          "sql": "CAST(GP_HEURECREATION AS VARCHAR(50))"
        },
        {
          "d365": "H_TRANSACTIONORDERTYPE",
          "source": "—",
          "transform": "fixed = SALESORDER",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'SALESORDER'"
        },
        {
          "d365": "H_TRANSACTIONTYPE",
          "source": "—",
          "transform": "fixed = SALES",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'SALES'"
        },
        {
          "d365": "H_TAXCALCULATIONTYPE",
          "source": "—",
          "transform": "fixed = Regular",
          "note": "Constant (part 2 §2 overrides the CHOIXEXT EL4 lookup)",
          "kind": "fixed",
          "sql": "'Regular'"
        },
        {
          "d365": "H_SALEISRETURNSALE",
          "source": "LIGNE.GL_QTEFACT",
          "transform": "EXISTS(qty < 0) ? YES : NO",
          "note": "Sale = No · Return = Yes · Exchange = Yes",
          "kind": "derived",
          "sql": "CASE WHEN EXISTS (SELECT 1 FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT < 0) THEN 'YES' ELSE 'NO' END"
        },
        {
          "d365": "H_TOACCOUNT",
          "source": "—",
          "transform": "fixed = Yes",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'YES'"
        },
        {
          "d365": "H_STAFF",
          "source": "GP_REPRESENTANT",
          "transform": "direct",
          "note": "Salesperson",
          "kind": "source",
          "sql": "CAST(GP_REPRESENTANT AS VARCHAR(50))"
        },
        {
          "d365": "H_LANGUAGEID",
          "source": "—",
          "transform": "fixed = EN-US",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'EN-US'"
        },
        {
          "d365": "H_CURRENCY",
          "source": "GP_DEVISE",
          "transform": "direct",
          "note": "Document currency",
          "kind": "source",
          "sql": "CAST(GP_DEVISE AS VARCHAR(50))"
        },
        {
          "d365": "H_EXCHANGERATE",
          "source": "—",
          "transform": "fixed = 100",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'100'"
        },
        {
          "d365": "H_WAREHOUSE",
          "source": "GDE_CHARLIBRE1",
          "transform": "of GP_DEPOT via DEPOTS",
          "note": "Warehouse free-field → D365 warehouse",
          "kind": "ref",
          "sql": "CAST(GDE_CHARLIBRE1 AS VARCHAR(50))"
        },
        {
          "d365": "H_NUMBEROFITEMLINES",
          "source": "consolidated S lines",
          "transform": "COUNT of SKU+batch groups",
          "note": "Control total — counts consolidated L lines, not raw Cegid lines (revalidated by middleware)",
          "kind": "derived",
          "sql": "CAST((SELECT COUNT(DISTINCT ISNULL(HL.GL_CODEARTICLE, '') + '~' + ISNULL(HB.ZBA_BATCHNUMBER, '') + '~' + CAST(SIGN(HL.GL_QTEFACT) AS VARCHAR(2))) FROM LIGNE HL LEFT JOIN ZLIGNESQRCODE HQ ON HQ.ZLQ_NATUREPIECEG = HL.GL_NATUREPIECEG AND HQ.ZLQ_SOUCHE = HL.GL_SOUCHE AND HQ.ZLQ_NUMERO = HL.GL_NUMERO AND HQ.ZLQ_INDICEG = HL.GL_INDICEG AND HQ.ZLQ_NUMLIGNE = HL.GL_NUMLIGNE LEFT JOIN ZQRCODEITEMS HB ON HB.ZBA_QRCODE = HQ.ZLQ_QRCODE WHERE HL.GL_NATUREPIECEG = GP_NATUREPIECEG AND HL.GL_SOUCHE = GP_SOUCHE AND HL.GL_NUMERO = GP_NUMERO AND HL.GL_INDICEG = GP_INDICEG AND HL.GL_TYPELIGNE = 'ART' AND HL.GL_TYPEARTICLE = 'MAR') AS VARCHAR(50))"
        },
        {
          "d365": "H_NUMBEROFITEMS",
          "source": "LIGNE.GL_QTEFACT",
          "transform": "SUM(ABS()) ART/MAR",
          "note": "Control total — clean exchange counts both legs (+1 and −1 → 2)",
          "kind": "derived",
          "sql": "CAST((SELECT SUM(ABS(ISNULL(HL.GL_QTEFACT, 0))) FROM LIGNE HL WHERE HL.GL_NATUREPIECEG = GP_NATUREPIECEG AND HL.GL_SOUCHE = GP_SOUCHE AND HL.GL_NUMERO = GP_NUMERO AND HL.GL_INDICEG = GP_INDICEG AND HL.GL_TYPELIGNE = 'ART' AND HL.GL_TYPEARTICLE = 'MAR') AS VARCHAR(50))"
        },
        {
          "d365": "H_NUMBEROFPAYMENTLINES",
          "source": "PIEDECHE.GPE_MODEPAIE",
          "transform": "COUNT(DISTINCT) ≠ 004",
          "note": "Control total, gift card excluded",
          "kind": "derived",
          "sql": "CAST((SELECT COUNT(DISTINCT GPE_MODEPAIE) FROM PIEDECHE WHERE GPE_NATUREPIECEG = GP_NATUREPIECEG AND GPE_SOUCHE = GP_SOUCHE AND GPE_NUMERO = GP_NUMERO AND GPE_INDICEG = GP_INDICEG AND GPE_MODEPAIE <> '004') AS VARCHAR(50))"
        },
        {
          "d365": "H_NETPRICE",
          "source": "LIGNE.GL_MONTANTHT",
          "transform": "SUM(−1 × …)",
          "note": "Pre-discount, VAT-excl",
          "kind": "sign",
          "sql": "CAST((SELECT SUM(-1 * HL.GL_MONTANTHT) FROM LIGNE HL WHERE HL.GL_NATUREPIECEG = GP_NATUREPIECEG AND HL.GL_SOUCHE = GP_SOUCHE AND HL.GL_NUMERO = GP_NUMERO AND HL.GL_INDICEG = GP_INDICEG AND HL.GL_TYPELIGNE = 'ART' AND HL.GL_TYPEARTICLE = 'MAR') AS VARCHAR(50))"
        },
        {
          "d365": "H_NETAMOUNT",
          "source": "GP_TOTALHT",
          "transform": "−1 ×",
          "note": "Post-discount, VAT-excl",
          "kind": "sign",
          "sql": "CAST(-1 * GP_TOTALHT AS VARCHAR(50))"
        },
        {
          "d365": "H_GROSSAMOUNT",
          "source": "GP_TOTALTTC",
          "transform": "−1 ×",
          "note": "VAT-incl total",
          "kind": "sign",
          "sql": "CAST(-1 * GP_TOTALTTC AS VARCHAR(50))"
        },
        {
          "d365": "H_DISCOUNTAMOUNT",
          "source": "LIGNEREMISE.MLR_MONTANTHT",
          "transform": "SUM(ABS())",
          "note": "VAT-excl discount total",
          "kind": "derived",
          "sql": "CAST((SELECT SUM(ABS(MLR_MONTANTHT)) FROM LIGNEREMISE WHERE MLR_NATUREPIECEG = GP_NATUREPIECEG AND MLR_SOUCHE = GP_SOUCHE AND MLR_NUMERO = GP_NUMERO AND MLR_INDICEG = GP_INDICEG) AS VARCHAR(50))"
        },
        {
          "d365": "H_DISCOUNTAMOUNTWITHOUTTAX",
          "source": "LIGNEREMISE.MLR_MONTANTHT",
          "transform": "SUM(ABS())",
          "note": "Same basis",
          "kind": "derived",
          "sql": "CAST((SELECT SUM(ABS(MLR_MONTANTHT)) FROM LIGNEREMISE WHERE MLR_NATUREPIECEG = GP_NATUREPIECEG AND MLR_SOUCHE = GP_SOUCHE AND MLR_NUMERO = GP_NUMERO AND MLR_INDICEG = GP_INDICEG) AS VARCHAR(50))"
        },
        {
          "d365": "H_TOTALDISCOUNTAMOUNT",
          "source": "LIGNEREMISE.MLR_MONTANTHT",
          "transform": "SUM(ABS())",
          "note": "Same basis",
          "kind": "derived",
          "sql": "CAST((SELECT SUM(ABS(MLR_MONTANTHT)) FROM LIGNEREMISE WHERE MLR_NATUREPIECEG = GP_NATUREPIECEG AND MLR_SOUCHE = GP_SOUCHE AND MLR_NUMERO = GP_NUMERO AND MLR_INDICEG = GP_INDICEG) AS VARCHAR(50))"
        },
        {
          "d365": "H_TOTALMANUALDISCOUNTAMOUNT",
          "source": "—",
          "transform": "fixed = 0.00",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'0.00'"
        },
        {
          "d365": "H_TOTALMANUALDISCOUNTPERCENTAGE",
          "source": "LIGNEREMISE.MLR_REMISE",
          "transform": "ABS() TOP 1",
          "note": "Manual discount %",
          "kind": "derived",
          "sql": "CAST((SELECT TOP 1 ABS(MLR_REMISE) FROM LIGNEREMISE WHERE MLR_NATUREPIECEG = GP_NATUREPIECEG AND MLR_SOUCHE = GP_SOUCHE AND MLR_NUMERO = GP_NUMERO AND MLR_INDICEG = GP_INDICEG) AS VARCHAR(50))"
        },
        {
          "d365": "H_PAYMENTAMOUNT",
          "source": "PIEDECHE.GPE_MONTANTECHE",
          "transform": "SUM()",
          "note": "Sum of tenders (≠ header net when multi-tender)",
          "kind": "derived",
          "sql": "CAST((SELECT SUM(GPE_MONTANTECHE) FROM PIEDECHE WHERE GPE_NATUREPIECEG = GP_NATUREPIECEG AND GPE_SOUCHE = GP_SOUCHE AND GPE_NUMERO = GP_NUMERO AND GPE_INDICEG = GP_INDICEG AND GPE_MODEPAIE <> '004') AS VARCHAR(50))"
        },
        {
          "d365": "H_AMOUNTPOSTEDTOACCOUNT",
          "source": "GP_TOTALTTC",
          "transform": "direct",
          "note": "Amount posted to account",
          "kind": "source",
          "sql": "CAST(GP_TOTALTTC AS VARCHAR(50))"
        },
        {
          "d365": "TYPE_VENTE",
          "source": "LIGNE.GL_QTEFACT + LIGNEREMISE",
          "transform": "CASE classification",
          "note": "Diagnostic: NORMAL/DISCOUNT × SALE/RETURN/EXCHANGE",
          "kind": "derived",
          "sql": "CASE WHEN EXISTS (SELECT 1 FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT < 0) AND (SELECT COUNT(GL_NUMLIGNE) FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT > 0) = 0 AND (SELECT COUNT(MLR_REMISE) FROM LIGNEREMISE WHERE MLR_NATUREPIECEG = GP_NATUREPIECEG AND MLR_SOUCHE = GP_SOUCHE AND MLR_NUMERO = GP_NUMERO AND MLR_INDICEG = GP_INDICEG) = 0 THEN 'NORMAL RETURN' WHEN EXISTS (SELECT 1 FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT < 0) AND (SELECT COUNT(GL_NUMLIGNE) FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT > 0) = 0 AND (SELECT COUNT(MLR_REMISE) FROM LIGNEREMISE WHERE MLR_NATUREPIECEG = GP_NATUREPIECEG AND MLR_SOUCHE = GP_SOUCHE AND MLR_NUMERO = GP_NUMERO AND MLR_INDICEG = GP_INDICEG) <> 0 THEN 'DISCOUNT RETURN' WHEN (SELECT COUNT(GL_NUMLIGNE) FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT < 0) >= 1 AND (SELECT COUNT(GL_NUMLIGNE) FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT > 0) >= 1 AND (SELECT COUNT(MLR_REMISE) FROM LIGNEREMISE WHERE MLR_NATUREPIECEG = GP_NATUREPIECEG AND MLR_SOUCHE = GP_SOUCHE AND MLR_NUMERO = GP_NUMERO AND MLR_INDICEG = GP_INDICEG) <> 0 THEN 'DISCOUNT EXCHANGE' WHEN (SELECT COUNT(GL_NUMLIGNE) FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT < 0) >= 1 AND (SELECT COUNT(GL_NUMLIGNE) FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT > 0) >= 1 AND (SELECT COUNT(MLR_REMISE) FROM LIGNEREMISE WHERE MLR_NATUREPIECEG = GP_NATUREPIECEG AND MLR_SOUCHE = GP_SOUCHE AND MLR_NUMERO = GP_NUMERO AND MLR_INDICEG = GP_INDICEG) = 0 THEN 'NORMAL EXCHANGE' WHEN (SELECT COUNT(GL_NUMLIGNE) FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT < 0) = 0 AND (SELECT COUNT(GL_NUMLIGNE) FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT > 0) >= 1 AND (SELECT COUNT(MLR_REMISE) FROM LIGNEREMISE WHERE MLR_NATUREPIECEG = GP_NATUREPIECEG AND MLR_SOUCHE = GP_SOUCHE AND MLR_NUMERO = GP_NUMERO AND MLR_INDICEG = GP_INDICEG) = 0 THEN 'NORMAL SALE' WHEN (SELECT COUNT(GL_NUMLIGNE) FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT < 0) = 0 AND (SELECT COUNT(GL_NUMLIGNE) FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG AND GL_TYPELIGNE = 'ART' AND GL_TYPEARTICLE = 'MAR' AND GL_QTEFACT > 0) >= 1 AND (SELECT COUNT(MLR_REMISE) FROM LIGNEREMISE WHERE MLR_NATUREPIECEG = GP_NATUREPIECEG AND MLR_SOUCHE = GP_SOUCHE AND MLR_NUMERO = GP_NUMERO AND MLR_INDICEG = GP_INDICEG) <> 0 THEN 'NORMAL DISCOUNT' ELSE 'OTHER' END"
        },
        {
          "d365": "IDENTIFIED_CUSTOMER",
          "source": "YTC_TABLELIBRETIERS2",
          "transform": "non-empty ? YES : NO",
          "note": "Diagnostic: known vs walk-in customer",
          "kind": "derived",
          "sql": "CAST(CASE WHEN ISNULL(YTC_TABLELIBRETIERS2, '') <> '' THEN 'YES' ELSE 'NO' END AS VARCHAR(50))"
        }
      ]
    },
    {
      "code": "L",
      "name": "Sales line",
      "cardinality": "1..N (one per SKU+batch group)",
      "desc": "One row per SKU + batch group. Repeated scans of the same SKU and batch collapse into a single line with summed quantities and amounts; different batches stay separate.",
      "fields": [
        {
          "d365": "L_LINENUMBER",
          "source": "GL_NUMLIGNE + ZLQ_NUMORDRE",
          "transform": "GL_NUMLIGNE × 100 + MIN(ZLQ_NUMORDRE)",
          "note": "Sub-line number. Deterministic and reproduced identically in the T and D branches so the links hold.",
          "kind": "derived",
          "sql": "CAST(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN MAX(L.GL_NUMLIGNE) * 100 + MIN(ZL.ZLQ_NUMORDRE) ELSE MAX(L.GL_NUMLIGNE) * 100 END AS VARCHAR(50))"
        },
        {
          "d365": "L_ITEMID",
          "source": "GL_CODEARTICLE",
          "transform": "direct",
          "note": "SKU — first half of the consolidation key",
          "kind": "source",
          "sql": "CAST(MAX(L.GL_CODEARTICLE) AS VARCHAR(50))"
        },
        {
          "d365": "L_IVENTBATCHID",
          "source": "ZBA_BATCHNUMBER",
          "transform": "grouping key",
          "note": "Second half of the grain; blank for a non-scanned article",
          "kind": "custom",
          "sql": "CAST(ISNULL(ZQ.ZBA_BATCHNUMBER, '') AS VARCHAR(50))"
        },
        {
          "d365": "L_QUANTITY",
          "source": "ZLQ_QRCODE / GL_QTEFACT",
          "transform": "COUNT(DISTINCT QR) × sign × −1",
          "note": "Units of this batch on the line — one scan is one unit. Falls back to GL_QTEFACT when nothing was scanned.",
          "kind": "sign",
          "sql": "CAST(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN COUNT(DISTINCT ZL.ZLQ_QRCODE) * SIGN(MAX(L.GL_QTEFACT)) * -1 ELSE MAX(L.GL_QTEFACT) * -1 END AS VARCHAR(50))"
        },
        {
          "d365": "L_UNITQUANTITY",
          "source": "ZLQ_QRCODE / GL_QTEFACT",
          "transform": "COUNT(DISTINCT QR) × sign × −1",
          "note": "Units of this batch on the line — one scan is one unit. Falls back to GL_QTEFACT when nothing was scanned.",
          "kind": "sign",
          "sql": "CAST(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN COUNT(DISTINCT ZL.ZLQ_QRCODE) * SIGN(MAX(L.GL_QTEFACT)) * -1 ELSE MAX(L.GL_QTEFACT) * -1 END AS VARCHAR(50))"
        },
        {
          "d365": "L_PRICE",
          "source": "GL_PUHT",
          "transform": "ABS()",
          "note": "Unit price — unaffected by proration, always positive",
          "kind": "sign",
          "sql": "CAST(ABS(MAX(L.GL_PUHT)) AS VARCHAR(50))"
        },
        {
          "d365": "L_NETPRICE",
          "source": "GL_QTEFACT × GL_PUHT",
          "transform": "SUM() × −1",
          "note": "Extended pre-discount, summed over the group",
          "kind": "sign",
          "sql": "CAST(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN MAX(L.GL_PUHT) * COUNT(DISTINCT ZL.ZLQ_QRCODE) * SIGN(MAX(L.GL_QTEFACT)) * -1 ELSE MAX(L.GL_QTEFACT) * MAX(L.GL_PUHT) * -1 END AS VARCHAR(50))"
        },
        {
          "d365": "L_NETAMOUNT",
          "source": "GL_TOTALHT",
          "transform": "× nScans / qty × −1",
          "note": "Line amount prorated to the batch share, then sign-flipped",
          "kind": "sign",
          "sql": "CAST(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN MAX(L.GL_TOTALHT) * COUNT(DISTINCT ZL.ZLQ_QRCODE) / NULLIF(ABS(MAX(L.GL_QTEFACT)), 0) ELSE MAX(L.GL_TOTALHT) END * -1 AS VARCHAR(50))"
        },
        {
          "d365": "L_NETAMOUNTINCLUSIVETAX",
          "source": "GL_TOTALHT",
          "transform": "× nScans / qty × −1",
          "note": "Line amount prorated to the batch share, then sign-flipped",
          "kind": "sign",
          "sql": "CAST(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN MAX(L.GL_TOTALHT) * COUNT(DISTINCT ZL.ZLQ_QRCODE) / NULLIF(ABS(MAX(L.GL_QTEFACT)), 0) ELSE MAX(L.GL_TOTALHT) END * -1 AS VARCHAR(50))"
        },
        {
          "d365": "L_SALESTAXAMOUNT",
          "source": "GL_TOTALTAXE1",
          "transform": "× nScans / qty × −1",
          "note": "Line tax prorated to the batch share",
          "kind": "sign",
          "sql": "CAST(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN MAX(L.GL_TOTALTAXE1) * COUNT(DISTINCT ZL.ZLQ_QRCODE) / NULLIF(ABS(MAX(L.GL_QTEFACT)), 0) ELSE MAX(L.GL_TOTALTAXE1) END * -1 AS VARCHAR(50))"
        },
        {
          "d365": "L_CURRENCY",
          "source": "GL_DEVISE",
          "transform": "direct",
          "note": "Line currency",
          "kind": "source",
          "sql": "CAST(MAX(L.GL_DEVISE) AS VARCHAR(50))"
        },
        {
          "d365": "L_SALESTAXGROUP",
          "source": "YX1.YX_LIBELLE",
          "transform": "CHOIXEXT EL1 on ET_LIBREET1",
          "note": "Sales tax group — Oman = CDOM",
          "kind": "ref",
          "sql": "CAST(MAX(YX1.YX_LIBELLE) AS VARCHAR(50))"
        },
        {
          "d365": "L_ORIGINALSALESTAXGROUP",
          "source": "YX1.YX_LIBELLE",
          "transform": "CHOIXEXT EL1",
          "note": "Same — Oman = CDOM",
          "kind": "ref",
          "sql": "CAST(MAX(YX1.YX_LIBELLE) AS VARCHAR(50))"
        },
        {
          "d365": "L_TAXCODE",
          "source": "YX3.YX_LIBELLE",
          "transform": "CHOIXEXT EL3 on ET_LIBREET3",
          "note": "Line tax code — Oman = S5 (part 2 §3)",
          "kind": "ref",
          "sql": "CAST(MAX(YX3.YX_LIBELLE) AS VARCHAR(50))"
        },
        {
          "d365": "L_WAREHOUSE",
          "source": "GDE_CHARLIBRE1",
          "transform": "of GL_DEPOT via DEPOTS",
          "note": "Line warehouse",
          "kind": "ref",
          "sql": "CAST(MAX(GDE_CHARLIBRE1) AS VARCHAR(50))"
        },
        {
          "d365": "L_ORIGINALITEMSALESTAXGROUP",
          "source": "—",
          "transform": "fixed = FULL",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'FULL'"
        },
        {
          "d365": "L_ITEMSALESTAXGROUP",
          "source": "—",
          "transform": "fixed = FULL",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'FULL'"
        },
        {
          "d365": "L_UNIT",
          "source": "—",
          "transform": "fixed = EA",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'EA'"
        },
        {
          "d365": "L_CATEGORYNAME",
          "source": "—",
          "transform": "fixed = Retail Catgorey",
          "note": "Constant (spelling preserved)",
          "kind": "fixed",
          "sql": "'RETAIL CATGOREY'"
        },
        {
          "d365": "L_CATEGORYHIERARCHYNAME",
          "source": "—",
          "transform": "fixed = Retail Catgorey",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'RETAIL CATGOREY'"
        },
        {
          "d365": "L_KEYBOARDPRODUCTENTRY",
          "source": "—",
          "transform": "fixed = YES",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'YES'"
        },
        {
          "d365": "L_BUSINESSDATE",
          "source": "GP_HEURECREATION",
          "transform": "direct",
          "note": "Store-local",
          "kind": "source",
          "sql": "CAST(MAX(GP_HEURECREATION) AS VARCHAR(50))"
        },
        {
          "d365": "L_DISCOUNTAMOUNTWITHOUTTAX",
          "source": "MLR_MONTANTHT (joined)",
          "transform": "ABS(SUM()) / qty when scanned, else ABS(SUM())",
          "note": "LIGNEREMISE is joined, not sub-queried: the scan fan-out inflates SUM by nScans, so dividing by the line quantity yields the batch share exactly.",
          "kind": "derived",
          "sql": "CAST(ISNULL(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN ABS(SUM(R.MLR_MONTANTHT)) / NULLIF(ABS(MAX(L.GL_QTEFACT)), 0) ELSE ABS(SUM(R.MLR_MONTANTHT)) END, 0) AS VARCHAR(50))"
        },
        {
          "d365": "L_TOTALDISCOUNT",
          "source": "MLR_MONTANTHT (joined)",
          "transform": "ABS(SUM()) / qty when scanned, else ABS(SUM())",
          "note": "LIGNEREMISE is joined, not sub-queried: the scan fan-out inflates SUM by nScans, so dividing by the line quantity yields the batch share exactly.",
          "kind": "derived",
          "sql": "CAST(ISNULL(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN ABS(SUM(R.MLR_MONTANTHT)) / NULLIF(ABS(MAX(L.GL_QTEFACT)), 0) ELSE ABS(SUM(R.MLR_MONTANTHT)) END, 0) AS VARCHAR(50))"
        },
        {
          "d365": "L_CASHDISCOUNTAMOUNT",
          "source": "MLR_MONTANTHT (joined)",
          "transform": "ABS(SUM()) / qty when scanned, else ABS(SUM())",
          "note": "LIGNEREMISE is joined, not sub-queried: the scan fan-out inflates SUM by nScans, so dividing by the line quantity yields the batch share exactly.",
          "kind": "derived",
          "sql": "CAST(ISNULL(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN ABS(SUM(R.MLR_MONTANTHT)) / NULLIF(ABS(MAX(L.GL_QTEFACT)), 0) ELSE ABS(SUM(R.MLR_MONTANTHT)) END, 0) AS VARCHAR(50))"
        },
        {
          "d365": "L_TOTALDISCOUNTPERCENTAGE",
          "source": "MLR_REMISE",
          "transform": "ABS() TOP 1",
          "note": "Rate is identical on every sub-line of the same Cegid line",
          "kind": "derived",
          "sql": "CAST(ISNULL(MAX(ABS(R.MLR_REMISE)), 0) AS VARCHAR(50))"
        },
        {
          "d365": "L_LINEDISCOUNT",
          "source": "—",
          "transform": "fixed = 0.00",
          "note": "Line discounts disabled",
          "kind": "fixed",
          "sql": "'0.00'"
        },
        {
          "d365": "L_LINEMANUALDISCOUNTAMOUNT",
          "source": "—",
          "transform": "fixed = 0.00",
          "note": "Line discounts disabled",
          "kind": "fixed",
          "sql": "'0.00'"
        },
        {
          "d365": "L_LINEMANUALDISCOUNTPERCENTAGE",
          "source": "—",
          "transform": "fixed = 0.00",
          "note": "Line discounts disabled",
          "kind": "fixed",
          "sql": "'0.00'"
        },
        {
          "d365": "L_ISLINEDISCOUNTED",
          "source": "—",
          "transform": "fixed = NO",
          "note": "Line discounts disabled",
          "kind": "fixed",
          "sql": "'NO'"
        },
        {
          "d365": "L_ISRETURNNOSALE",
          "source": "GL_QTEFACT",
          "transform": "< 0 ? YES : NO",
          "note": "Sale = No · Return = Yes",
          "kind": "derived",
          "sql": "CASE WHEN MAX(L.GL_QTEFACT) < 0 THEN 'YES' ELSE 'NO' END"
        },
        {
          "d365": "L_RETURNOPERATINGUNITNUMBER",
          "source": "LIAISONPIECE (GLP) → ET_CHARLIBRE1",
          "transform": "chained lookup to original",
          "note": "Original-sale store — equals OPERATINGUNITNUMBER for same-store returns, differs cross-store",
          "kind": "ref",
          "sql": "CAST(ISNULL((SELECT TOP 1 ET2.ET_CHARLIBRE1 FROM ETABLISS ET2 WHERE ET2.ET_ETABLISSEMENT = (SELECT TOP 1 RP.GP_ETABLISSEMENT FROM PIECE RP WHERE RP.GP_NUMERO = (SELECT TOP 1 GLP.GLP_NUMERO FROM LIAISONPIECE GLP WHERE GLP.GLP_SOUCHE = L.GL_SOUCHE AND GLP.GLP_CODESITE = L.GL_SOUCHE AND GLP.GLP_NATUREPIECEG = 'FFO' AND GLP.GLP_INDICEG = L.GL_INDICEG AND GLP.GLP_RANG = 'O' AND GLP.GLP_NUMLIEN = (SELECT TOP 1 GLPP.GLP_NUMLIEN FROM LIAISONPIECE GLPP WHERE GLPP.GLP_NUMERO = L.GL_NUMERO AND GLPP.GLP_SOUCHE = L.GL_SOUCHE AND GLPP.GLP_CODESITE = L.GL_SOUCHE AND GLPP.GLP_NATUREPIECEG = L.GL_NATUREPIECEG AND GLPP.GLP_INDICEG = L.GL_INDICEG ORDER BY GLPP.GLP_NUMLIEN)) AND RP.GP_SOUCHE = L.GL_SOUCHE AND RP.GP_NATUREPIECEG = 'FFO')), '') AS VARCHAR(50))"
        },
        {
          "d365": "L_RETURNTRANSACTIONNUMBER",
          "source": "LIAISONPIECE (GLP) → GP_REFINTERNE",
          "transform": "chained lookup to original",
          "note": "Original sale’s transaction number — return / exchange-return lines only",
          "kind": "ref",
          "sql": "CAST(ISNULL((SELECT TOP 1 RP.GP_REFINTERNE FROM PIECE RP WHERE RP.GP_NUMERO = (SELECT TOP 1 GLP.GLP_NUMERO FROM LIAISONPIECE GLP WHERE GLP.GLP_SOUCHE = L.GL_SOUCHE AND GLP.GLP_CODESITE = L.GL_SOUCHE AND GLP.GLP_NATUREPIECEG = 'FFO' AND GLP.GLP_INDICEG = L.GL_INDICEG AND GLP.GLP_RANG = 'O' AND GLP.GLP_NUMLIEN = (SELECT TOP 1 GLPP.GLP_NUMLIEN FROM LIAISONPIECE GLPP WHERE GLPP.GLP_NUMERO = L.GL_NUMERO AND GLPP.GLP_SOUCHE = L.GL_SOUCHE AND GLPP.GLP_CODESITE = L.GL_SOUCHE AND GLPP.GLP_NATUREPIECEG = L.GL_NATUREPIECEG AND GLPP.GLP_INDICEG = L.GL_INDICEG ORDER BY GLPP.GLP_NUMLIEN)) AND RP.GP_SOUCHE = L.GL_SOUCHE AND RP.GP_NATUREPIECEG = 'FFO'), '') AS VARCHAR(50))"
        },
        {
          "d365": "L_RETURNLINENUMBER",
          "source": "LIAISONPIECE (GLP) → GL_NUMLIGNE",
          "transform": "chained lookup to original",
          "note": "Original sale’s line number as supplied by Cegid — return / exchange-return lines only",
          "kind": "ref",
          "sql": "CAST(ISNULL((SELECT TOP 1 RL.GL_NUMLIGNE FROM LIGNE RL WHERE RL.GL_NUMERO = (SELECT TOP 1 GLP.GLP_NUMERO FROM LIAISONPIECE GLP WHERE GLP.GLP_SOUCHE = L.GL_SOUCHE AND GLP.GLP_CODESITE = L.GL_SOUCHE AND GLP.GLP_NATUREPIECEG = 'FFO' AND GLP.GLP_INDICEG = L.GL_INDICEG AND GLP.GLP_RANG = 'O' AND GLP.GLP_NUMLIEN = (SELECT TOP 1 GLPP.GLP_NUMLIEN FROM LIAISONPIECE GLPP WHERE GLPP.GLP_NUMERO = L.GL_NUMERO AND GLPP.GLP_SOUCHE = L.GL_SOUCHE AND GLPP.GLP_CODESITE = L.GL_SOUCHE AND GLPP.GLP_NATUREPIECEG = L.GL_NATUREPIECEG AND GLPP.GLP_INDICEG = L.GL_INDICEG ORDER BY GLPP.GLP_NUMLIEN)) AND RL.GL_SOUCHE = L.GL_SOUCHE AND RL.GL_NATUREPIECEG = 'FFO' AND RL.GL_CODEARTICLE = MAX(L.GL_CODEARTICLE)), '') AS VARCHAR(50))"
        }
      ]
    },
    {
      "code": "T",
      "name": "Tax line",
      "cardinality": "1 per L line",
      "desc": "The tax breakdown attached to each consolidated sales line.",
      "fields": [
        {
          "d365": "T_SALESLINENUMBER",
          "source": "same expression as L_LINENUMBER",
          "transform": "GL_NUMLIGNE × 100 + MIN(ZLQ_NUMORDRE)",
          "note": "FK to L_LINENUMBER — byte-identical expression",
          "kind": "derived",
          "sql": "CAST(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN MAX(L.GL_NUMLIGNE) * 100 + MIN(ZL.ZLQ_NUMORDRE) ELSE MAX(L.GL_NUMLIGNE) * 100 END AS VARCHAR(50))"
        },
        {
          "d365": "T_TAXCODE",
          "source": "YX3.YX_LIBELLE",
          "transform": "CHOIXEXT EL3 on ET_LIBREET3",
          "note": "Tax code — Oman = S5",
          "kind": "ref",
          "sql": "CAST(MAX(YX3.YX_LIBELLE) AS VARCHAR(50))"
        },
        {
          "d365": "T_TAXPERCENTAGE",
          "source": "GPB_TAUXTAXE",
          "transform": "from PIEDBASE",
          "note": "Tax rate",
          "kind": "source",
          "sql": "CAST(MAX(GPB_TAUXTAXE) AS VARCHAR(50))"
        },
        {
          "d365": "T_TAXAMOUNT",
          "source": "GL_TOTALTAXE1",
          "transform": "× nScans / qty",
          "note": "Prorated to the batch share, native sign — the inverse of L_SALESTAXAMOUNT",
          "kind": "derived",
          "sql": "CAST(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN MAX(L.GL_TOTALTAXE1) * COUNT(DISTINCT ZL.ZLQ_QRCODE) / NULLIF(ABS(MAX(L.GL_QTEFACT)), 0) ELSE MAX(L.GL_TOTALTAXE1) END AS VARCHAR(50))"
        },
        {
          "d365": "T_ISTAXINCLUDEDINPRICE",
          "source": "—",
          "transform": "fixed = No",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'NO'"
        },
        {
          "d365": "T_ISEXEMPT",
          "source": "—",
          "transform": "fixed = No",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'NO'"
        }
      ]
    },
    {
      "code": "P",
      "name": "Payment line",
      "cardinality": "1 per method",
      "desc": "One row per distinct tender used on the receipt.",
      "fields": [
        {
          "d365": "P_LINENUMBER",
          "source": "GPE_NUMECHE",
          "transform": "direct",
          "note": "Installment number — sequential within the transaction",
          "kind": "source",
          "sql": "CAST(GPE_NUMECHE AS VARCHAR(50))"
        },
        {
          "d365": "P_STORE",
          "source": "GPE_ETABLISSEMENT",
          "transform": "direct",
          "note": "Payment store",
          "kind": "source",
          "sql": "CAST(GPE_ETABLISSEMENT AS VARCHAR(50))"
        },
        {
          "d365": "P_RECEIPTID",
          "source": "GPE_NUMERO",
          "transform": "direct",
          "note": "Receipt",
          "kind": "source",
          "sql": "CAST(GPE_NUMERO AS VARCHAR(50))"
        },
        {
          "d365": "P_TENDERTYPE",
          "source": "GPE_MODEPAIE",
          "transform": "RIGHT(…,1)",
          "note": "Last char → tender (1 Cash · 2 Visa · 3 Amex · 5 Cust.Acct). 4 = Gift Card is out of scope",
          "kind": "ref",
          "sql": "CAST(RIGHT(GPE_MODEPAIE, 1) AS VARCHAR(50))"
        },
        {
          "d365": "P_AMOUNTTENDERED",
          "source": "GPE_MONTANTECHE",
          "transform": "direct",
          "note": "Per method",
          "kind": "source",
          "sql": "CAST(GPE_MONTANTECHE AS VARCHAR(50))"
        },
        {
          "d365": "P_REFUNDABLEAMOUNT",
          "source": "GPE_MONTANTECHE",
          "transform": "direct",
          "note": "Per method",
          "kind": "source",
          "sql": "CAST(GPE_MONTANTECHE AS VARCHAR(50))"
        },
        {
          "d365": "P_AMOUNTINTENDEREDCURRENCY",
          "source": "GPE_MONTANTECHE",
          "transform": "direct",
          "note": "Per method (part 2 §5 overrides GPE_MONTANTDEV)",
          "kind": "source",
          "sql": "CAST(GPE_MONTANTECHE AS VARCHAR(50))"
        },
        {
          "d365": "P_AMOUNTINACCOUNTINGCURRENCY",
          "source": "GPE_MONTANTECHE",
          "transform": "direct",
          "note": "Per method (part 2 §5 overrides GPE_MONTANTDEV)",
          "kind": "source",
          "sql": "CAST(GPE_MONTANTECHE AS VARCHAR(50))"
        },
        {
          "d365": "P_QUANTITY",
          "source": "GPE_NUMECHE",
          "transform": "COUNT()",
          "note": "Count per method — directional; 0 for a clean exchange",
          "kind": "derived",
          "sql": "CAST(COUNT(GPE_NUMECHE) AS VARCHAR(50))"
        },
        {
          "d365": "P_CURRENCYCODE",
          "source": "GPE_DEVISE",
          "transform": "direct",
          "note": "Currency",
          "kind": "source",
          "sql": "CAST(GPE_DEVISE AS VARCHAR(50))"
        },
        {
          "d365": "P_EXCHANGERATEINTENDEREDCURRENCY",
          "source": "—",
          "transform": "fixed = 100",
          "note": "Constant — multi-currency is a future phase",
          "kind": "fixed",
          "sql": "'100'"
        },
        {
          "d365": "P_EXCHANGERATEINACCOUNTINGCURRENCY",
          "source": "—",
          "transform": "fixed = 100",
          "note": "Constant — multi-currency is a future phase",
          "kind": "fixed",
          "sql": "'100'"
        },
        {
          "d365": "P_STAFF",
          "source": "GP_REPRESENTANT",
          "transform": "direct",
          "note": "Salesperson",
          "kind": "source",
          "sql": "CAST(GP_REPRESENTANT AS VARCHAR(50))"
        },
        {
          "d365": "P_AMOUNTTENDEREDADJUSTMENT",
          "source": "—",
          "transform": "fixed = 0.00",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'0.00'"
        },
        {
          "d365": "P_LINKEDPAYMENTLINENUMBER",
          "source": "—",
          "transform": "fixed = 0.00",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'0.00'"
        },
        {
          "d365": "P_TRANSACTIONSTATUS",
          "source": "—",
          "transform": "fixed = Posted",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'POSTED'"
        },
        {
          "d365": "P_ISPREPAYMENT",
          "source": "—",
          "transform": "fixed = No",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'NO'"
        },
        {
          "d365": "P_ISCHANGELINE",
          "source": "—",
          "transform": "fixed = No",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'NO'"
        },
        {
          "d365": "P_ISLINKEDREFUND",
          "source": "—",
          "transform": "fixed = No",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'NO'"
        },
        {
          "d365": "P_ISPAYMENTCAPTURED",
          "source": "—",
          "transform": "fixed = Yes",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'YES'"
        }
      ]
    },
    {
      "code": "D",
      "name": "Discount line",
      "cardinality": "0 or 1 per L line",
      "desc": "Exactly one aggregated discount row per discounted sales line — never multiple. All discount components on the same line are summed into it.",
      "fields": [
        {
          "d365": "D_SALESLINENUMBER",
          "source": "same expression as L_LINENUMBER",
          "transform": "GL_NUMLIGNE × 100 + MIN(ZLQ_NUMORDRE)",
          "note": "FK to L_LINENUMBER — byte-identical expression",
          "kind": "derived",
          "sql": "CAST(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN MAX(L.GL_NUMLIGNE) * 100 + MIN(ZL.ZLQ_NUMORDRE) ELSE MAX(L.GL_NUMLIGNE) * 100 END AS VARCHAR(50))"
        },
        {
          "d365": "D_LINENUMBER",
          "source": "MLR_NUMLIGNE",
          "transform": "direct",
          "note": "Discount line number — single line per L line",
          "kind": "source",
          "sql": "CAST(MIN(R.MLR_NUMLIGNE) AS VARCHAR(50))"
        },
        {
          "d365": "D_DISCOUNTCOST",
          "source": "MLR_MONTANTHT",
          "transform": "ABS(SUM()) / qty when scanned, else ABS(SUM())",
          "note": "Same proration as the L line it hangs off — the D rows re-sum to the line discount",
          "kind": "derived",
          "sql": "CAST(ISNULL(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN ABS(SUM(R.MLR_MONTANTHT)) / NULLIF(ABS(MAX(L.GL_QTEFACT)), 0) ELSE ABS(SUM(R.MLR_MONTANTHT)) END, 0) AS VARCHAR(50))"
        },
        {
          "d365": "D_DISCOUNTAMOUNT",
          "source": "MLR_MONTANTHT",
          "transform": "ABS(SUM()) / qty when scanned, else ABS(SUM())",
          "note": "Same proration as the L line it hangs off — the D rows re-sum to the line discount",
          "kind": "derived",
          "sql": "CAST(ISNULL(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN ABS(SUM(R.MLR_MONTANTHT)) / NULLIF(ABS(MAX(L.GL_QTEFACT)), 0) ELSE ABS(SUM(R.MLR_MONTANTHT)) END, 0) AS VARCHAR(50))"
        },
        {
          "d365": "D_EFFECTIVEAMOUNT",
          "source": "MLR_MONTANTHT",
          "transform": "ABS(SUM()) / qty when scanned, else ABS(SUM())",
          "note": "Same proration as the L line it hangs off — the D rows re-sum to the line discount",
          "kind": "derived",
          "sql": "CAST(ISNULL(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN ABS(SUM(R.MLR_MONTANTHT)) / NULLIF(ABS(MAX(L.GL_QTEFACT)), 0) ELSE ABS(SUM(R.MLR_MONTANTHT)) END, 0) AS VARCHAR(50))"
        },
        {
          "d365": "D_DISCOUNTORIGINTYPE",
          "source": "MLR_ORGREMISE",
          "transform": "003 → Manual · 006/007 → Customer",
          "note": "Manual = manual/global · Customer = category (family/employee/board). One origin per transaction, never both. No Periodic / Loyalty",
          "kind": "ref",
          "sql": "CAST(CASE WHEN MIN(R.MLR_ORGREMISE) = '003' THEN 'MANUAL' WHEN MIN(R.MLR_ORGREMISE) IN ('006', '007') THEN 'CUSTOMER' ELSE '' END AS VARCHAR(50))"
        },
        {
          "d365": "D_DISCOUNTPERCENTAGE",
          "source": "MLR_MONTANTHT ÷ GL_MONTANTHT",
          "transform": "ABS(SUM(MLR_MONTANTHT)) / NULLIF(ABS(SUM(GL_MONTANTHT)),0) × 100",
          "note": "Rate is share-independent — identical on every sub-line of the same Cegid line",
          "kind": "derived",
          "sql": "CAST(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0 THEN ABS(SUM(R.MLR_MONTANTHT)) / NULLIF(COUNT(DISTINCT ZL.ZLQ_QRCODE), 0) / NULLIF(ABS(MAX(L.GL_MONTANTHT)), 0) * 100 ELSE ABS(SUM(R.MLR_MONTANTHT)) / NULLIF(ABS(MAX(L.GL_MONTANTHT)), 0) * 100 END AS VARCHAR(50))"
        },
        {
          "d365": "D_MANUALDISCOUNTTYPE",
          "source": "—",
          "transform": "fixed = TotalDiscountPercent",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'TOTALDISCOUNTPERCENT'"
        },
        {
          "d365": "D_CUSTOMERDISCOUNTTYPE",
          "source": "—",
          "transform": "fixed = None",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'NONE'"
        },
        {
          "d365": "D_DEALPRICE",
          "source": "—",
          "transform": "fixed = 0.00",
          "note": "Constant",
          "kind": "fixed",
          "sql": "'0.00'"
        }
      ]
    }
  ],
  "filters": [
    {
      "cond": "GP_NATUREPIECEG = 'FFO'",
      "why": "Only POS receipts."
    },
    {
      "cond": "GP_TICKETANNULE = '-'",
      "why": "Exclude cancelled tickets."
    },
    {
      "cond": "GP_VIVANTE = 'X'",
      "why": "Only live documents."
    },
    {
      "cond": "GP_SUPPRIME <> 'X'",
      "why": "Exclude deleted documents."
    },
    {
      "cond": "line is ART / MAR",
      "why": "Must contain a real merchandise line (GL_TYPELIGNE='ART', GL_TYPEARTICLE='MAR')."
    },
    {
      "cond": "'004' NOT IN (SELECT GPE_MODEPAIE …)",
      "why": "Drop the whole transaction when any tender is a gift card — H / S / T / D branches."
    },
    {
      "cond": "GPE_MODEPAIE <> '004'",
      "why": "Gift-card tender excluded in the P branch."
    },
    {
      "cond": "GP_ETATEXPORT <> 'EXP'",
      "why": "Not already exported — makes the run idempotent."
    },
    {
      "cond": "GP_REFINTERNE in window",
      "why": "Business-date / EOD batch selection."
    },
    {
      "cond": "scan count = ABS(GL_QTEFACT) on every batch line",
      "why": "Guard rail: a ticket where a batch line is under- or over-scanned is excluded whole, so the proration can never be wrong. GP_ETATEXPORT stays <> 'EXP', so it is picked up automatically on the next run once the scans are corrected."
    },
    {
      "cond": "GROUP BY GL_NUMLIGNE + ZBA_BATCHNUMBER",
      "why": "One L row per (Cegid line, batch). T and D repeat the same grain and the same line-number expression."
    }
  ],
  "lookups": [
    {
      "purpose": "Legal entity",
      "source": "ET_FILIALE",
      "resolves": "D365 dataAreaId (Oman = as)"
    },
    {
      "purpose": "Operating unit (store)",
      "source": "ET_CHARLIBRE1",
      "resolves": "D365 store code"
    },
    {
      "purpose": "Warehouse",
      "source": "GDE_CHARLIBRE1",
      "resolves": "D365 warehouse code"
    },
    {
      "purpose": "Terminal / register",
      "source": "GP_CAISSE",
      "resolves": "D365 register code"
    },
    {
      "purpose": "Header tax calc type",
      "source": "—",
      "resolves": "fixed 'Regular' (part 2 §2 overrides the CHOIXEXT EL4 lookup)"
    },
    {
      "purpose": "Line sales-tax group",
      "source": "CHOIXEXT EL1 on ET_LIBREET1",
      "resolves": "YX1.YX_LIBELLE — Oman = CDOM"
    },
    {
      "purpose": "Tax code",
      "source": "CHOIXEXT EL3 on ET_LIBREET3",
      "resolves": "YX3.YX_LIBELLE — Oman = S5"
    },
    {
      "purpose": "Tender type",
      "source": "RIGHT(GPE_MODEPAIE,1)",
      "resolves": "1 Cash · 2 Visa · 3 Amex · 4 Gift Card (transaction dropped) · 5 Customer Account"
    },
    {
      "purpose": "Discount origin",
      "source": "MLR_ORGREMISE",
      "resolves": "003 → Manual · 006 / 007 → Customer"
    },
    {
      "purpose": "Identified customer",
      "source": "TIERSCOMPL.YTC_TABLELIBRETIERS2",
      "resolves": "non-empty ⇒ export GP_TIERS"
    },
    {
      "purpose": "Return origin store",
      "source": "LIAISONPIECE chain",
      "resolves": "original receipt's ET_CHARLIBRE1"
    },
    {
      "purpose": "Batch",
      "source": "ZLIGNESQRCODE → ZQRCODEITEMS",
      "resolves": "resolved ZBA_BATCHNUMBER (never the raw QR)"
    },
    {
      "purpose": "S-line consolidation key",
      "source": "GL_CODEARTICLE + ZBA_BATCHNUMBER",
      "resolves": "one S line per SKU+batch; S_LINENUMBER is that group sequence"
    },
    {
      "purpose": "Return references",
      "source": "LIAISONPIECE chain",
      "resolves": "original receipt’s GP_REFINTERNE and GL_NUMLIGNE"
    }
  ],
  "signs": [
    {
      "title": "Sale",
      "desc": "S quantity / net / tax negative; T tax positive; P amounts positive.",
      "tokens": [
        "S qty −",
        "S net −",
        "T tax +",
        "P pay +"
      ]
    },
    {
      "title": "Return",
      "desc": "Mirror of a sale — S quantity / net / tax positive; T tax negative; P amounts and quantity negative.",
      "tokens": [
        "S qty +",
        "S net +",
        "T tax −",
        "P pay −",
        "P qty −"
      ]
    },
    {
      "title": "Clean 1:1 exchange",
      "desc": "One transaction, SALEISRETURNSALE = Yes. Header net / gross / payment all net to 0; one return S line (+) and one sale S line (−); P amounts and quantity 0; NUMBEROFITEMS counts both legs.",
      "tokens": [
        "return S +",
        "sale S −",
        "header 0",
        "P 0",
        "items 2"
      ]
    },
    {
      "title": "Price-difference exchange",
      "desc": "S lines keep their return / sale signs; P reflects only the difference collected or refunded.",
      "tokens": [
        "return S +",
        "sale S −",
        "P = Δ"
      ]
    }
  ],
  "tables": [
    {
      "table": "PIECE",
      "prefix": "GP",
      "role": "Receipt headers",
      "origin": "std"
    },
    {
      "table": "LIGNE",
      "prefix": "GL",
      "role": "Receipt lines",
      "origin": "std"
    },
    {
      "table": "PIEDECHE",
      "prefix": "GPE",
      "role": "Payments / installments",
      "origin": "std"
    },
    {
      "table": "LIGNEREMISE",
      "prefix": "MLR",
      "role": "Discount detail",
      "origin": "std"
    },
    {
      "table": "PIEDBASE",
      "prefix": "GPB",
      "role": "VAT bases and rates",
      "origin": "std"
    },
    {
      "table": "ETABLISS",
      "prefix": "ET",
      "role": "Stores (free codes drive mappings)",
      "origin": "std"
    },
    {
      "table": "DEPOTS",
      "prefix": "GDE",
      "role": "Warehouses",
      "origin": "std"
    },
    {
      "table": "TIERS",
      "prefix": "T",
      "role": "Customers",
      "origin": "std"
    },
    {
      "table": "TIERSCOMPL",
      "prefix": "YTC",
      "role": "Customer supplement (identified flag)",
      "origin": "std"
    },
    {
      "table": "CHOIXEXT",
      "prefix": "YX",
      "role": "Extended code tables (tax groups/codes)",
      "origin": "std"
    },
    {
      "table": "LIAISONPIECE",
      "prefix": "GLP",
      "role": "Document links (return origin)",
      "origin": "std"
    },
    {
      "table": "ZLIGNESQRCODE",
      "prefix": "ZLQ",
      "role": "Line ↔ scanned QR (custom)",
      "origin": "custom"
    },
    {
      "table": "ZQRCODEITEMS",
      "prefix": "ZBA",
      "role": "QR ↔ resolved batch (custom)",
      "origin": "custom"
    }
  ],
  "version": 6,
  "sqlmodel": {
    "note": "Generated draft. Each field carries its own SQL expression; the branch skeletons below supply FROM / JOIN / WHERE / GROUP BY. {{WINDOW}} is substituted from the generator UI.",
    "castAs": "VARCHAR(50)",
    "windowDefault": "'0050001610001', '0050001610002'",
    "prelude": [
      {
        "col": "C0",
        "by": {
          "H": "'1'",
          "L": "'2'",
          "T": "'3'",
          "P": "'4'",
          "D": "'5'"
        }
      },
      {
        "col": "C1",
        "by": {
          "H": "'H'",
          "L": "'L'",
          "T": "'T'",
          "P": "'P'",
          "D": "'D'"
        }
      },
      {
        "col": "ORDERBY",
        "by": {
          "H": 1,
          "L": 2,
          "T": 3,
          "P": 4,
          "D": 5
        }
      }
    ],
    "repeat": [
      "DATAAREAID",
      "TRANSACTIONNUMBER",
      "OPERATINGUNITNUMBER",
      "TERMINAL"
    ],
    "trailer": [
      {
        "col": "C38",
        "sql": "CAST(GP_NATUREPIECEG AS VARCHAR(50))"
      },
      {
        "col": "C39",
        "sql": "CAST(GP_SOUCHE AS VARCHAR(50))"
      },
      {
        "col": "C40",
        "sql": "CAST(GP_NUMERO AS VARCHAR(50))"
      },
      {
        "col": "C41",
        "sql": "CAST(GP_INDICEG AS VARCHAR(50))"
      },
      {
        "col": "C42",
        "sql": "CAST(GP_ETATEXPORT AS VARCHAR(50))"
      },
      {
        "col": "C44",
        "sql": "CAST(GP_REFINTERNE AS VARCHAR(50))"
      }
    ],
    "orderBy": "TRANSACTIONNUMBER, ORDERBY",
    "branches": {
      "H": {
        "from": "PIECE",
        "joins": [
          "LEFT JOIN ETABLISS ON GP_ETABLISSEMENT = ET_ETABLISSEMENT",
          "LEFT JOIN DEPOTS ON GDE_DEPOT = GP_DEPOT",
          "LEFT JOIN TIERS ON T_TIERS = GP_TIERS AND T_NATUREAUXI = 'CLI'",
          "LEFT JOIN TIERSCOMPL ON YTC_AUXILIAIRE = T_AUXILIAIRE AND YTC_TIERS = GP_TIERS"
        ],
        "where": [
          "GP_NATUREPIECEG = 'FFO'",
          "GP_TICKETANNULE = '-'",
          "GP_VIVANTE = 'X'",
          "GP_SUPPRIME <> 'X'",
          "'MAR' IN (SELECT GL_TYPEARTICLE FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG)",
          "'ART' IN (SELECT GL_TYPELIGNE FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG)",
          "'004' NOT IN (SELECT GPE_MODEPAIE FROM PIEDECHE WHERE GPE_NATUREPIECEG = GP_NATUREPIECEG AND GPE_SOUCHE = GP_SOUCHE AND GPE_NUMERO = GP_NUMERO AND GPE_INDICEG = GP_INDICEG)",
          "GP_ETATEXPORT <> 'EXP'",
          "GP_REFINTERNE IN ({{WINDOW}})",
          "NOT EXISTS (SELECT 1 FROM LIGNE L2 WHERE L2.GL_NATUREPIECEG = GP_NATUREPIECEG AND L2.GL_SOUCHE = GP_SOUCHE AND L2.GL_NUMERO = GP_NUMERO AND L2.GL_INDICEG = GP_INDICEG AND L2.GL_TYPELIGNE = 'ART' AND L2.GL_TYPEARTICLE = 'MAR' AND (SELECT COUNT(DISTINCT Z2.ZLQ_QRCODE) FROM ZLIGNESQRCODE Z2 WHERE Z2.ZLQ_NATUREPIECEG = L2.GL_NATUREPIECEG AND Z2.ZLQ_SOUCHE = L2.GL_SOUCHE AND Z2.ZLQ_NUMERO = L2.GL_NUMERO AND Z2.ZLQ_INDICEG = L2.GL_INDICEG AND Z2.ZLQ_NUMLIGNE = L2.GL_NUMLIGNE) > 0 AND (SELECT COUNT(DISTINCT Z2.ZLQ_QRCODE) FROM ZLIGNESQRCODE Z2 WHERE Z2.ZLQ_NATUREPIECEG = L2.GL_NATUREPIECEG AND Z2.ZLQ_SOUCHE = L2.GL_SOUCHE AND Z2.ZLQ_NUMERO = L2.GL_NUMERO AND Z2.ZLQ_INDICEG = L2.GL_INDICEG AND Z2.ZLQ_NUMLIGNE = L2.GL_NUMLIGNE) <> ABS(L2.GL_QTEFACT))"
        ],
        "groupBy": []
      },
      "T": {
        "from": "LIGNE L",
        "joins": [
          "LEFT JOIN PIECE ON GP_NATUREPIECEG = L.GL_NATUREPIECEG AND GP_SOUCHE = L.GL_SOUCHE AND GP_NUMERO = L.GL_NUMERO AND GP_INDICEG = L.GL_INDICEG",
          "LEFT JOIN ZLIGNESQRCODE ZL ON ZL.ZLQ_NATUREPIECEG = L.GL_NATUREPIECEG AND ZL.ZLQ_SOUCHE = L.GL_SOUCHE AND ZL.ZLQ_NUMERO = L.GL_NUMERO AND ZL.ZLQ_INDICEG = L.GL_INDICEG AND ZL.ZLQ_NUMLIGNE = L.GL_NUMLIGNE",
          "LEFT JOIN ZQRCODEITEMS ZQ ON ZQ.ZBA_QRCODE = ZL.ZLQ_QRCODE",
          "LEFT JOIN ETABLISS ON GP_ETABLISSEMENT = ET_ETABLISSEMENT",
          "LEFT JOIN CHOIXEXT YX3 ON ET_LIBREET3 = YX3.YX_CODE AND YX3.YX_TYPE = 'EL3'",
          "LEFT JOIN PIEDBASE ON GP_NATUREPIECEG = GPB_NATUREPIECEG AND GP_SOUCHE = GPB_SOUCHE AND GP_NUMERO = GPB_NUMERO AND GP_INDICEG = GPB_INDICEG"
        ],
        "where": [
          "GP_NATUREPIECEG = 'FFO'",
          "GP_TICKETANNULE = '-'",
          "GP_VIVANTE = 'X'",
          "GP_SUPPRIME <> 'X'",
          "'004' NOT IN (SELECT GPE_MODEPAIE FROM PIEDECHE WHERE GPE_NATUREPIECEG = GP_NATUREPIECEG AND GPE_SOUCHE = GP_SOUCHE AND GPE_NUMERO = GP_NUMERO AND GPE_INDICEG = GP_INDICEG)",
          "GP_ETATEXPORT <> 'EXP'",
          "L.GL_TYPELIGNE = 'ART'",
          "L.GL_TYPEARTICLE = 'MAR'",
          "NOT EXISTS (SELECT 1 FROM LIGNE L2 WHERE L2.GL_NATUREPIECEG = GP_NATUREPIECEG AND L2.GL_SOUCHE = GP_SOUCHE AND L2.GL_NUMERO = GP_NUMERO AND L2.GL_INDICEG = GP_INDICEG AND L2.GL_TYPELIGNE = 'ART' AND L2.GL_TYPEARTICLE = 'MAR' AND (SELECT COUNT(DISTINCT Z2.ZLQ_QRCODE) FROM ZLIGNESQRCODE Z2 WHERE Z2.ZLQ_NATUREPIECEG = L2.GL_NATUREPIECEG AND Z2.ZLQ_SOUCHE = L2.GL_SOUCHE AND Z2.ZLQ_NUMERO = L2.GL_NUMERO AND Z2.ZLQ_INDICEG = L2.GL_INDICEG AND Z2.ZLQ_NUMLIGNE = L2.GL_NUMLIGNE) > 0 AND (SELECT COUNT(DISTINCT Z2.ZLQ_QRCODE) FROM ZLIGNESQRCODE Z2 WHERE Z2.ZLQ_NATUREPIECEG = L2.GL_NATUREPIECEG AND Z2.ZLQ_SOUCHE = L2.GL_SOUCHE AND Z2.ZLQ_NUMERO = L2.GL_NUMERO AND Z2.ZLQ_INDICEG = L2.GL_INDICEG AND Z2.ZLQ_NUMLIGNE = L2.GL_NUMLIGNE) <> ABS(L2.GL_QTEFACT))",
          "GP_REFINTERNE IN ({{WINDOW}})"
        ],
        "groupBy": [
          "ET_FILIALE",
          "GP_REFINTERNE",
          "ET_CHARLIBRE1",
          "GP_CAISSE",
          "L.GL_NATUREPIECEG",
          "L.GL_SOUCHE",
          "L.GL_NUMERO",
          "L.GL_INDICEG",
          "L.GL_NUMLIGNE",
          "ZQ.ZBA_BATCHNUMBER",
          "GP_NATUREPIECEG",
          "GP_SOUCHE",
          "GP_NUMERO",
          "GP_INDICEG",
          "GP_ETATEXPORT"
        ]
      },
      "P": {
        "from": "PIEDECHE",
        "joins": [
          "LEFT JOIN PIECE ON GP_NATUREPIECEG = GPE_NATUREPIECEG AND GP_SOUCHE = GPE_SOUCHE AND GP_NUMERO = GPE_NUMERO AND GP_INDICEG = GPE_INDICEG",
          "LEFT JOIN ETABLISS ON GP_ETABLISSEMENT = ET_ETABLISSEMENT",
          "LEFT JOIN TIERS ON T_TIERS = GP_TIERS AND T_NATUREAUXI = 'CLI'",
          "LEFT JOIN TIERSCOMPL ON YTC_AUXILIAIRE = T_AUXILIAIRE AND YTC_TIERS = GP_TIERS"
        ],
        "where": [
          "GP_NATUREPIECEG = 'FFO'",
          "GP_TICKETANNULE = '-'",
          "GP_VIVANTE = 'X'",
          "GP_SUPPRIME <> 'X'",
          "'MAR' IN (SELECT GL_TYPEARTICLE FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG)",
          "'ART' IN (SELECT GL_TYPELIGNE FROM LIGNE WHERE GL_NATUREPIECEG = GP_NATUREPIECEG AND GL_SOUCHE = GP_SOUCHE AND GL_NUMERO = GP_NUMERO AND GL_INDICEG = GP_INDICEG)",
          "GPE_MODEPAIE <> '004'",
          "GP_ETATEXPORT <> 'EXP'",
          "GP_REFINTERNE IN ({{WINDOW}})",
          "NOT EXISTS (SELECT 1 FROM LIGNE L2 WHERE L2.GL_NATUREPIECEG = GP_NATUREPIECEG AND L2.GL_SOUCHE = GP_SOUCHE AND L2.GL_NUMERO = GP_NUMERO AND L2.GL_INDICEG = GP_INDICEG AND L2.GL_TYPELIGNE = 'ART' AND L2.GL_TYPEARTICLE = 'MAR' AND (SELECT COUNT(DISTINCT Z2.ZLQ_QRCODE) FROM ZLIGNESQRCODE Z2 WHERE Z2.ZLQ_NATUREPIECEG = L2.GL_NATUREPIECEG AND Z2.ZLQ_SOUCHE = L2.GL_SOUCHE AND Z2.ZLQ_NUMERO = L2.GL_NUMERO AND Z2.ZLQ_INDICEG = L2.GL_INDICEG AND Z2.ZLQ_NUMLIGNE = L2.GL_NUMLIGNE) > 0 AND (SELECT COUNT(DISTINCT Z2.ZLQ_QRCODE) FROM ZLIGNESQRCODE Z2 WHERE Z2.ZLQ_NATUREPIECEG = L2.GL_NATUREPIECEG AND Z2.ZLQ_SOUCHE = L2.GL_SOUCHE AND Z2.ZLQ_NUMERO = L2.GL_NUMERO AND Z2.ZLQ_INDICEG = L2.GL_INDICEG AND Z2.ZLQ_NUMLIGNE = L2.GL_NUMLIGNE) <> ABS(L2.GL_QTEFACT))"
        ],
        "groupBy": [
          "ET_FILIALE",
          "GP_REFINTERNE",
          "ET_CHARLIBRE1",
          "GP_CAISSE",
          "GPE_NUMERO",
          "GPE_NUMECHE",
          "GPE_ETABLISSEMENT",
          "RIGHT(GPE_MODEPAIE, 1)",
          "GPE_MONTANTECHE",
          "GPE_DEVISE",
          "GP_REPRESENTANT",
          "GP_NATUREPIECEG",
          "GP_SOUCHE",
          "GP_NUMERO",
          "GP_INDICEG",
          "GP_ETATEXPORT"
        ]
      },
      "D": {
        "from": "LIGNEREMISE R",
        "joins": [
          "INNER JOIN LIGNE L ON L.GL_NATUREPIECEG = R.MLR_NATUREPIECEG AND L.GL_SOUCHE = R.MLR_SOUCHE AND L.GL_NUMERO = R.MLR_NUMERO AND L.GL_INDICEG = R.MLR_INDICEG AND L.GL_NUMLIGNE = R.MLR_NUMLIGNE",
          "LEFT JOIN PIECE ON GP_NATUREPIECEG = L.GL_NATUREPIECEG AND GP_SOUCHE = L.GL_SOUCHE AND GP_NUMERO = L.GL_NUMERO AND GP_INDICEG = L.GL_INDICEG",
          "LEFT JOIN ZLIGNESQRCODE ZL ON ZL.ZLQ_NATUREPIECEG = L.GL_NATUREPIECEG AND ZL.ZLQ_SOUCHE = L.GL_SOUCHE AND ZL.ZLQ_NUMERO = L.GL_NUMERO AND ZL.ZLQ_INDICEG = L.GL_INDICEG AND ZL.ZLQ_NUMLIGNE = L.GL_NUMLIGNE",
          "LEFT JOIN ZQRCODEITEMS ZQ ON ZQ.ZBA_QRCODE = ZL.ZLQ_QRCODE",
          "LEFT JOIN ETABLISS ON GP_ETABLISSEMENT = ET_ETABLISSEMENT"
        ],
        "where": [
          "GP_NATUREPIECEG = 'FFO'",
          "GP_TICKETANNULE = '-'",
          "GP_VIVANTE = 'X'",
          "GP_SUPPRIME <> 'X'",
          "'004' NOT IN (SELECT GPE_MODEPAIE FROM PIEDECHE WHERE GPE_NATUREPIECEG = GP_NATUREPIECEG AND GPE_SOUCHE = GP_SOUCHE AND GPE_NUMERO = GP_NUMERO AND GPE_INDICEG = GP_INDICEG)",
          "GP_ETATEXPORT <> 'EXP'",
          "L.GL_TYPELIGNE = 'ART'",
          "L.GL_TYPEARTICLE = 'MAR'",
          "NOT EXISTS (SELECT 1 FROM LIGNE L2 WHERE L2.GL_NATUREPIECEG = GP_NATUREPIECEG AND L2.GL_SOUCHE = GP_SOUCHE AND L2.GL_NUMERO = GP_NUMERO AND L2.GL_INDICEG = GP_INDICEG AND L2.GL_TYPELIGNE = 'ART' AND L2.GL_TYPEARTICLE = 'MAR' AND (SELECT COUNT(DISTINCT Z2.ZLQ_QRCODE) FROM ZLIGNESQRCODE Z2 WHERE Z2.ZLQ_NATUREPIECEG = L2.GL_NATUREPIECEG AND Z2.ZLQ_SOUCHE = L2.GL_SOUCHE AND Z2.ZLQ_NUMERO = L2.GL_NUMERO AND Z2.ZLQ_INDICEG = L2.GL_INDICEG AND Z2.ZLQ_NUMLIGNE = L2.GL_NUMLIGNE) > 0 AND (SELECT COUNT(DISTINCT Z2.ZLQ_QRCODE) FROM ZLIGNESQRCODE Z2 WHERE Z2.ZLQ_NATUREPIECEG = L2.GL_NATUREPIECEG AND Z2.ZLQ_SOUCHE = L2.GL_SOUCHE AND Z2.ZLQ_NUMERO = L2.GL_NUMERO AND Z2.ZLQ_INDICEG = L2.GL_INDICEG AND Z2.ZLQ_NUMLIGNE = L2.GL_NUMLIGNE) <> ABS(L2.GL_QTEFACT))",
          "GP_REFINTERNE IN ({{WINDOW}})"
        ],
        "groupBy": [
          "ET_FILIALE",
          "GP_REFINTERNE",
          "ET_CHARLIBRE1",
          "GP_CAISSE",
          "L.GL_NATUREPIECEG",
          "L.GL_SOUCHE",
          "L.GL_NUMERO",
          "L.GL_INDICEG",
          "L.GL_NUMLIGNE",
          "ZQ.ZBA_BATCHNUMBER",
          "GP_NATUREPIECEG",
          "GP_SOUCHE",
          "GP_NUMERO",
          "GP_INDICEG",
          "GP_ETATEXPORT"
        ]
      },
      "L": {
        "from": "LIGNE L",
        "joins": [
          "LEFT JOIN PIECE ON GP_NATUREPIECEG = L.GL_NATUREPIECEG AND GP_SOUCHE = L.GL_SOUCHE AND GP_NUMERO = L.GL_NUMERO AND GP_INDICEG = L.GL_INDICEG",
          "LEFT JOIN ZLIGNESQRCODE ZL ON ZL.ZLQ_NATUREPIECEG = L.GL_NATUREPIECEG AND ZL.ZLQ_SOUCHE = L.GL_SOUCHE AND ZL.ZLQ_NUMERO = L.GL_NUMERO AND ZL.ZLQ_INDICEG = L.GL_INDICEG AND ZL.ZLQ_NUMLIGNE = L.GL_NUMLIGNE",
          "LEFT JOIN ZQRCODEITEMS ZQ ON ZQ.ZBA_QRCODE = ZL.ZLQ_QRCODE",
          "LEFT JOIN LIGNEREMISE R ON R.MLR_NATUREPIECEG = L.GL_NATUREPIECEG AND R.MLR_SOUCHE = L.GL_SOUCHE AND R.MLR_NUMERO = L.GL_NUMERO AND R.MLR_INDICEG = L.GL_INDICEG AND R.MLR_NUMLIGNE = L.GL_NUMLIGNE",
          "LEFT JOIN ETABLISS ON GP_ETABLISSEMENT = ET_ETABLISSEMENT",
          "LEFT JOIN DEPOTS ON GDE_DEPOT = L.GL_DEPOT",
          "LEFT JOIN CHOIXEXT YX1 ON ET_LIBREET1 = YX1.YX_CODE AND YX1.YX_TYPE = 'EL1'",
          "LEFT JOIN CHOIXEXT YX3 ON ET_LIBREET3 = YX3.YX_CODE AND YX3.YX_TYPE = 'EL3'"
        ],
        "where": [
          "GP_NATUREPIECEG = 'FFO'",
          "GP_TICKETANNULE = '-'",
          "GP_VIVANTE = 'X'",
          "GP_SUPPRIME <> 'X'",
          "'004' NOT IN (SELECT GPE_MODEPAIE FROM PIEDECHE WHERE GPE_NATUREPIECEG = GP_NATUREPIECEG AND GPE_SOUCHE = GP_SOUCHE AND GPE_NUMERO = GP_NUMERO AND GPE_INDICEG = GP_INDICEG)",
          "GP_ETATEXPORT <> 'EXP'",
          "L.GL_TYPELIGNE = 'ART'",
          "L.GL_TYPEARTICLE = 'MAR'",
          "NOT EXISTS (SELECT 1 FROM LIGNE L2 WHERE L2.GL_NATUREPIECEG = GP_NATUREPIECEG AND L2.GL_SOUCHE = GP_SOUCHE AND L2.GL_NUMERO = GP_NUMERO AND L2.GL_INDICEG = GP_INDICEG AND L2.GL_TYPELIGNE = 'ART' AND L2.GL_TYPEARTICLE = 'MAR' AND (SELECT COUNT(DISTINCT Z2.ZLQ_QRCODE) FROM ZLIGNESQRCODE Z2 WHERE Z2.ZLQ_NATUREPIECEG = L2.GL_NATUREPIECEG AND Z2.ZLQ_SOUCHE = L2.GL_SOUCHE AND Z2.ZLQ_NUMERO = L2.GL_NUMERO AND Z2.ZLQ_INDICEG = L2.GL_INDICEG AND Z2.ZLQ_NUMLIGNE = L2.GL_NUMLIGNE) > 0 AND (SELECT COUNT(DISTINCT Z2.ZLQ_QRCODE) FROM ZLIGNESQRCODE Z2 WHERE Z2.ZLQ_NATUREPIECEG = L2.GL_NATUREPIECEG AND Z2.ZLQ_SOUCHE = L2.GL_SOUCHE AND Z2.ZLQ_NUMERO = L2.GL_NUMERO AND Z2.ZLQ_INDICEG = L2.GL_INDICEG AND Z2.ZLQ_NUMLIGNE = L2.GL_NUMLIGNE) <> ABS(L2.GL_QTEFACT))",
          "GP_REFINTERNE IN ({{WINDOW}})"
        ],
        "groupBy": [
          "ET_FILIALE",
          "GP_REFINTERNE",
          "ET_CHARLIBRE1",
          "GP_CAISSE",
          "L.GL_NATUREPIECEG",
          "L.GL_SOUCHE",
          "L.GL_NUMERO",
          "L.GL_INDICEG",
          "L.GL_NUMLIGNE",
          "ZQ.ZBA_BATCHNUMBER",
          "GP_NATUREPIECEG",
          "GP_SOUCHE",
          "GP_NUMERO",
          "GP_INDICEG",
          "GP_ETATEXPORT"
        ]
      }
    }
  },
  "consumer": {
    "system": "AMOUAGE Azure Functions middleware (Python, Durable Functions) — export_sales flow",
    "path": "export_sales_helpers/csv_parser.py + payload_builders.py",
    "trigger": "timer_export_sales, CRON 0 */5 * * * * (every 5 min) → ExportSalesMainOrchestrator",
    "pickup": "most recent RFE blob whose name starts with SALES_For_D365; decoded utf-8-sig → utf-16 → utf-8 → latin1",
    "format": "Pipe-separated, POSITIONAL, no header row. Field 1 IS the record tag. Each record type has its own ordered field list (ragged), hard-coded in FIELDS_MAP — not a union-aligned wide row.",
    "transactionKey": "DATAAREAID|TRANSACTIONNUMBER",
    "idempotency": "line_hash = sha256(raw stripped line); export_file_tracking + upsert_line never downgrade SUCCESS",
    "skipRule": "unknown first field, or empty DATAAREAID / TRANSACTIONNUMBER → line skipped with a warning, not an error",
    "hardErrors": [
      "duplicate H for the same transaction",
      "a transaction with no H line"
    ],
    "blocking": "H fails → L/T/P/D dead-lettered; any L fails → T and D blocked, P still attempted",
    "endpoints": {
      "H": "/data/RetailTransactions",
      "L": "/data/RetailTransactionSalesLinesV2",
      "T": "/data/RetailTransactionTaxLines",
      "P": "/data/RetailTransactionPaymentLinesV2",
      "D": "/data/RetailTransactionDiscountTransEntity"
    },
    "fields": {
      "H": [
        "DATAAREAID",
        "TRANSACTIONNUMBER",
        "OPERATINGUNITNUMBER",
        "TERMINAL",
        "RRECEIPTID",
        "BATCHID",
        "BATCHTERMINALID",
        "CREATEDONPOSTERMINAL",
        "BEGINDATETIME",
        "TRANSACTIONDATE",
        "BUSINESSDATE",
        "TRANSACTIONORDERTYPE",
        "TRANSACTIONTYPE",
        "TRANSACTIONSTATUS",
        "TRANSACTIONTIME",
        "TAXCALCULATIONTYPE",
        "SALEISRETURNSALE",
        "TOACCOUNT",
        "POSTASSHIPMENT",
        "CREATEDOFFLINE",
        "ISTAXINCLUDEDINPRICE",
        "ISTAXEXEMPTEDFORPRICEINCLUSIVE",
        "ITEMSPOSTED",
        "STAFF",
        "LANGUAGEID",
        "CURRENCY",
        "EXCHANGERATE",
        "WAREHOUSE",
        "NUMBEROFITEMLINES",
        "NUMBEROFITEMS",
        "NUMBEROFPAYMENTLINES",
        "NETPRICE",
        "NETAMOUNT",
        "GROSSAMOUNT",
        "DISCOUNTAMOUNT",
        "DISCOUNTAMOUNTWITHOUTTAX",
        "TOTALDISCOUNTAMOUNT",
        "TOTALMANUALDISCOUNTAMOUNT",
        "TOTALMANUALDISCOUNTPERCENTAGE",
        "PAYMENTAMOUNT",
        "AMOUNTPOSTEDTOACCOUNT",
        "CUSTOMERACCOUNT"
      ],
      "L": [
        "DATAAREAID",
        "TRANSACTIONNUMBER",
        "OPERATINGUNITNUMBER",
        "TERMINAL",
        "LINENUMBER",
        "RECEIPTNUMBER",
        "BARCODE",
        "ITEMID",
        "CATEGORYNAME",
        "CATEGORYHIERARCHYNAME",
        "CHANNELLISTINGID",
        "QUANTITY",
        "UNITQUANTITY",
        "UNIT",
        "PRICE",
        "NETPRICE",
        "NETAMOUNT",
        "NETAMOUNTINCLUSIVETAX",
        "SALESTAXAMOUNT",
        "SALESTAXGROUP",
        "ORIGINALSALESTAXGROUP",
        "ITEMSALESTAXGROUP",
        "ORIGINALITEMSALESTAXGROUP",
        "ORIGINALPRICE",
        "COSTAMOUNT",
        "DISCOUNTAMOUNTWITHOUTTAX",
        "TOTALDISCOUNT",
        "TOTALDISCOUNTPERCENTAGE",
        "TRANSACTIONDATE",
        "BUSINESSDATE",
        "TRANSACTIONSTATUS",
        "TRANSACTIONCODE",
        "INVENTORYSTATUS",
        "CURRENCY",
        "WAREHOUSE",
        "INVENTBATCHID",
        "RETURNOPERATINGUNITNUMBER",
        "RETURNTRACKINGSTATUS",
        "ISRETURNNOSALE",
        "RETURNTRANSACTIONNUMBER",
        "RETURNTERMINAL",
        "RETURNLINENUMBER",
        "KEYBOARDPRODUCTENTRY",
        "CUSTOMERACCOUNT"
      ],
      "T": [
        "DATAAREAID",
        "TRANSACTIONNUMBER",
        "OPERATINGUNITNUMBER",
        "TERMINAL",
        "SALESLINENUMBER",
        "TAXCODE",
        "TAXPERCENTAGE",
        "TAXAMOUNT",
        "ISTAXINCLUDEDINPRICE",
        "ISEXEMPT",
        "SOURCEKEY"
      ],
      "P": [
        "DATAAREAID",
        "TRANSACTIONNUMBER",
        "OPERATINGUNITNUMBER",
        "TERMINAL",
        "LINENUMBER",
        "STORE",
        "RECEIPTID",
        "TENDERTYPE",
        "FUNCTION",
        "AMOUNTTENDERED",
        "AMOUNTINTENDEREDCURRENCY",
        "AMOUNTINACCOUNTINGCURRENCY",
        "REFUNDABLEAMOUNT",
        "QUANTITY",
        "CURRENCYCODE",
        "EXCHANGERATEINACCOUNTINGCURRENCY",
        "EXCHANGERATEINTENDEREDCURRENCY",
        "TRANSACTIONSTATUS",
        "VOIDSTATUS",
        "STAFF",
        "ISPREPAYMENT",
        "ISCHANGELINE",
        "ISLINKEDREFUND",
        "ISPAYMENTCAPTURED",
        "SOURCEKEY",
        "ACCOUNTNUMBER"
      ],
      "D": [
        "DATAAREAID",
        "STORE",
        "TERMINALID",
        "TRANSACTIONNUMBER",
        "SALELINENUM",
        "OMOPERATINGUNIT_PARTYNUMBER",
        "LINENUM",
        "AMOUNT",
        "DISCOUNTCOST",
        "DISCOUNTORIGINTYPE",
        "MANUALDISCOUNTTYPE",
        "PERCENTAGE",
        "CUSTOMERDISCOUNTTYPE",
        "APPLIEDDISCOUNTLINENUM",
        "BUNDLEID",
        "EFFECTIVEPERCENTAGE",
        "REPLICATIONCOUNTERFROMORIGIN"
      ]
    },
    "mismatch": "The query emits a union-aligned wide row starting with C0 ('1'..'5'); the middleware reads field 1 as the record tag. Every line would be skipped, no ticket would be built, and the file would still be marked DONE — silent data loss. Either the query emits the ragged per-record layout above, or FIELDS_MAP is rewritten for the wide layout."
  },
  "observed": {
    "file": "EXPORT ALL SALES AMOUGE (1).xlsx",
    "shape": "104 columns × 551 data rows · 115 transactions",
    "trailer": "single C38 column (value FFO) — not the C38..C44 set in the example query",
    "counts": {
      "H": 114,
      "L": 139,
      "T": 139,
      "P": 127,
      "D": 32
    },
    "values": [
      {
        "field": "DATAAREAID",
        "seen": "'AS ' (uppercase, trailing space)",
        "note": "Contract says Oman = as. Trailing space will not match a D365 dataAreaId — trim at source."
      },
      {
        "field": "TERMINAL",
        "seen": "000001",
        "note": "Zero-padded register code — consistent with GP_CAISSE."
      },
      {
        "field": "OPERATINGUNITNUMBER",
        "seen": "00000021",
        "note": "Zero-padded store code."
      },
      {
        "field": "H_TAXCALCULATIONTYPE",
        "seen": "Regular",
        "note": "Confirms the part-2 fixed value."
      },
      {
        "field": "L_SALESTAXGROUP",
        "seen": "CDOM",
        "note": "Confirms Oman = CDOM."
      },
      {
        "field": "T_TAXCODE",
        "seen": "S5",
        "note": "Confirms Oman = S5."
      },
      {
        "field": "L_PRICE",
        "seen": "all positive",
        "note": "Price invariant holds in production."
      },
      {
        "field": "L_LINENUMBER",
        "seen": "1 / 2 / 3",
        "note": "Raw GL_NUMLIGNE — no SKU+batch consolidation applied yet."
      }
    ],
    "anomalies": [
      {
        "what": "P_TENDERTYPE = 'D'",
        "detail": "Non-numeric tender in production; the documented map is 1/2/3/4/5. RIGHT(GPE_MODEPAIE,1) is returning a letter for some payment modes."
      },
      {
        "what": "P_EXCHANGERATEINTENDEREDCURRENCY = 0.000000000",
        "detail": "Some payment lines carry 0, not 100 — GPE_COTATIONESP is null/zero. Contradicts the fixed-100 rule."
      },
      {
        "what": "D_DISCOUNTORIGINTYPE empty on 6 of 32 D rows",
        "detail": "MLR_ORGREMISE outside 003/006/007 falls through the CASE to ''. D365 will receive a blank origin type."
      },
      {
        "what": "115 transactions but only 114 H rows",
        "detail": "One transaction has no header — the middleware treats that as a HARD ERROR and marks the whole file INVALID."
      },
      {
        "what": "T_TAXPERCENTAGE missing on 2 of 139 T rows",
        "detail": "PIEDBASE join produced no rate for those lines."
      },
      {
        "what": "P_QUANTITY always 1",
        "detail": "Never directional and never 0 — the clean-exchange rule in the spec is not implemented."
      },
      {
        "what": "No NB_DISCOUNT column",
        "detail": "Present in the example query, absent from the production layout."
      }
    ]
  },
  "batchmodel": {
    "premise": "One QR code is one unit. A scanned bottle is one row in ZLIGNESQRCODE; a single batch can cover several articles and several units. That is what makes the whole model work.",
    "grain": "The exported L row is NOT the Cegid line. Its grain is (Cegid line, batch).",
    "groupBy": "GROUP BY … L.GL_NUMLIGNE, ZQ.ZBA_BATCHNUMBER …",
    "counting": "Because every scan is unique, COUNT(DISTINCT ZL.ZLQ_QRCODE) over the group is the exact number of units of that batch on that line. That count drives everything: the exported quantity and the proration of every amount.",
    "formula": "amount_batch = amount_line × nScans_batch / qty_line",
    "invariant": "The sub-lines re-sum to the Cegid line exactly, so the header stays reconciled.",
    "linenumber": "L_LINENUMBER = GL_NUMLIGNE × 100 + MIN(ZLQ_NUMORDRE). Every scan carries its own unique ZLQ_NUMORDRE, so each group’s MIN differs — 101, 102, … Deterministic, and the identical expression is used in the T and D branches, so the links hold.",
    "simple": "A non-scanned article (sample, tester) has no ZLIGNESQRCODE row → COUNT = 0 → the ELSE branch applies: blank L_IVENTBATCHID, quantity straight from GL_QTEFACT, no proration. One expression covers both worlds.",
    "simpleSql": "CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0\n     THEN  … value prorated by batch …      -- batch-managed article\n     ELSE  … GL_QTEFACT / GL_TOTALHT direct …  -- simple article\nEND",
    "whySku": "The SKU alone is never enough: two batches of the same article are two distinct stocks in D365. Merging them would destroy stock traceability. Hence the grain is the pair, never the SKU on its own.",
    "guard": "ABS(GL_QTEFACT) <> (scan count of the line)  →  whole ticket excluded",
    "guardWhy": "The mechanism assumes nScans = quantity. A line sold at 3 but scanned twice would prorate wrongly, so the entire ticket is held back until the scans are corrected.",
    "sqlFix": {
      "problem": "The TOB typer cannot type a correlated subquery multiplied by an aggregate expression — (SELECT SUM…) × CASE WHEN COUNT(…). Each half types on its own; the product does not.",
      "cure": "Join LIGNEREMISE in the FROM and let aggregates alone carry the proration. The scan fan-out multiplies SUM(MLR_MONTANTHT) by nScans, so dividing by the line quantity gives the batch share with no subquery at all — the fan-out cancels algebraically.",
      "before": "CAST((SELECT ABS(SUM(R.MLR_MONTANTHT)) FROM LIGNEREMISE R WHERE …)\n     * CASE WHEN COUNT(ZL.ZLQ_QRCODE) > 0\n            THEN COUNT(ZL.ZLQ_QRCODE) * 1.0 / IIF(L.GL_QTEFACT = 0, NULL, ABS(L.GL_QTEFACT))\n            ELSE 1 END AS VARCHAR(50))",
      "after": "CAST(ISNULL(CASE WHEN COUNT(DISTINCT ZL.ZLQ_QRCODE) > 0\n                 THEN ABS(SUM(R.MLR_MONTANTHT)) / NULLIF(ABS(MAX(L.GL_QTEFACT)), 0)\n                 ELSE ABS(SUM(R.MLR_MONTANTHT))\n            END, 0) AS VARCHAR(50))"
    },
    "examples": [
      {
        "n": "1",
        "title": "One batch — the simple case",
        "setup": "Line 1: article 10068, qty 3, HT 90, tax 4.50. All three bottles carry batch J27996 (3 scans).",
        "cols": [
          "L_LINENUMBER",
          "L_ITEMID",
          "L_IVENTBATCHID",
          "L_QUANTITY",
          "L_NETAMOUNT"
        ],
        "rows": [
          [
            "100 + numordre_min",
            "10068",
            "J27996",
            "−3",
            "−90"
          ]
        ],
        "note": "One group (line 1, J27996), COUNT = 3, qty = 3 → share = 3/3 = 1. One L row, nothing split. The × 1 is transparent."
      },
      {
        "n": "2",
        "title": "Same SKU, two batches — the split",
        "setup": "Line 1: article 10068, qty 3, HT 90, tax 4.50. The three bottles scanned are J27996, J27996, K88001 (2 + 1).",
        "cols": [
          "Group",
          "nScans",
          "share",
          "L_QUANTITY",
          "L_NETAMOUNT",
          "L_SALESTAXAMOUNT"
        ],
        "rows": [
          [
            "(L1, J27996)",
            "2",
            "2/3",
            "−2",
            "90 × 2/3 = −60",
            "4.50 × 2/3 = −3.00"
          ],
          [
            "(L1, K88001)",
            "1",
            "1/3",
            "−1",
            "90 × 1/3 = −30",
            "4.50 × 1/3 = −1.50"
          ]
        ],
        "note": "One Cegid line → two L rows, one per batch. Sum: −3, −90, −4.50 = the original line ✓ L_LINENUMBER separates them: 101 and 102."
      },
      {
        "n": "3",
        "title": "The SKU alone is never enough",
        "setup": "Two batches of the same article are two distinct stocks in D365 — they can neither be merged (stock traceability would be lost) nor swapped.",
        "cols": [
          "Article type",
          "ZLIGNESQRCODE rows",
          "COUNT(DISTINCT QR)",
          "Branch taken",
          "L_IVENTBATCHID"
        ],
        "rows": [
          [
            "Batch-managed",
            "one per scanned unit",
            "> 0",
            "THEN — prorated",
            "resolved batch number"
          ],
          [
            "Simple (sample, tester)",
            "none",
            "0",
            "ELSE — direct",
            "(blank)"
          ]
        ],
        "note": "The same expression handles both worlds, so no branch of the query has to know which kind of article it is."
      },
      {
        "n": "4",
        "title": "The discount follows the batch",
        "setup": "Line 1, qty 3, HT 90, discount 9 (10 %), batches J27996 ×2 + K88001 ×1.",
        "cols": [
          "Group (→ D)",
          "D_SALESLINENUMBER",
          "D_DISCOUNTAMOUNT",
          "D_DISCOUNTPERCENTAGE"
        ],
        "rows": [
          [
            "J27996",
            "101",
            "9 × 2/3 = 6.00",
            "10 %"
          ],
          [
            "K88001",
            "102",
            "9 × 1/3 = 3.00",
            "10 %"
          ]
        ],
        "note": "Two D rows, each hanging off its own L sub-line via the same number. The percentage is recomputed (Σdiscount / base × 100) so it is identical on both — it is the same rate. Sum of D = 9 = H_DISCOUNTAMOUNT ✓"
      }
    ]
  },
  "walkthrough": {
    "title": "One receipt, end to end",
    "intro": "A single ticket built to hit every path at once: a batch article that splits in two, a plain article that never gets scanned, a return, a discount that follows the split, and two tenders. Everything below is arithmetic — no SQL. VAT is 5 %, the store is Oman.",
    "ticket": [
      {
        "k": "Transaction",
        "v": "0050001610007"
      },
      {
        "k": "Store · register",
        "v": "00000021 · 000001"
      },
      {
        "k": "Entity",
        "v": "AS (Oman)"
      },
      {
        "k": "Customer",
        "v": "walk-in — no account emitted"
      },
      {
        "k": "Nature",
        "v": "sale with a return line → SALEISRETURNSALE = YES"
      }
    ],
    "inputNote": "What Cegid holds. GL_MONTANTHT is before discount, GL_TOTALHT after — that is why line 1 shows both.",
    "input": {
      "cols": [
        "Cegid line",
        "Article",
        "Qty",
        "Unit HT",
        "Pre-disc. HT",
        "Discount",
        "GL_TOTALHT",
        "Tax 5 %"
      ],
      "rows": [
        [
          "1",
          "10068 · batch-managed",
          "3",
          "30.00",
          "90.00",
          "9.00 (10 %)",
          "81.00",
          "4.05"
        ],
        [
          "2",
          "41128 · tester, never scanned",
          "1",
          "20.00",
          "20.00",
          "—",
          "20.00",
          "1.00"
        ],
        [
          "3",
          "13042 · batch-managed, RETURN",
          "−1",
          "50.00",
          "−50.00",
          "—",
          "−50.00",
          "−2.50"
        ]
      ]
    },
    "scansNote": "One QR is one bottle. Line 1 was scanned three times but across two different batches — that is the only reason it splits. Line 2 has no scan row at all. Line 3 has one.",
    "scans": {
      "cols": [
        "Cegid line",
        "ZLQ_NUMORDRE",
        "Batch",
        "→ group"
      ],
      "rows": [
        [
          "1",
          "1",
          "J27996",
          "(L1, J27996)"
        ],
        [
          "1",
          "2",
          "J27996",
          "(L1, J27996)"
        ],
        [
          "1",
          "3",
          "K88001",
          "(L1, K88001)"
        ],
        [
          "2",
          "—",
          "— (no scan)",
          "(L2, blank)"
        ],
        [
          "3",
          "1",
          "K30071",
          "(L3, K30071)"
        ]
      ]
    },
    "rules": [
      {
        "n": "1",
        "t": "Group by (line, batch)",
        "d": "Line 1 holds two batches, so it becomes two exported lines. Lines 2 and 3 hold one group each. Three Cegid lines → four exported lines."
      },
      {
        "n": "2",
        "t": "Prorate by share of scans",
        "d": "Every line amount is multiplied by nScans / qty. Line 1 splits 2/3 and 1/3. Lines 2 and 3 have a share of 1, so nothing changes for them."
      },
      {
        "n": "3",
        "t": "Number the sub-lines",
        "d": "Cegid line × 100 + the lowest ZLQ_NUMORDRE in the group. Unscanned lines get × 100 with nothing added. Numbers are unique and reproducible — not necessarily contiguous."
      },
      {
        "n": "4",
        "t": "Flip the signs",
        "d": "Sales go out negative, returns positive, tax the other way round. Unit price is the exception — always positive."
      }
    ],
    "output": [
      {
        "code": "H",
        "label": "Header — 1 row",
        "cols": [
          "Field",
          "Value",
          "Where it comes from"
        ],
        "rows": [
          [
            "H_NUMBEROFITEMLINES",
            "4",
            "count of exported L rows, not Cegid lines"
          ],
          [
            "H_NUMBEROFITEMS",
            "5",
            "3 + 1 + 1 — absolute quantities"
          ],
          [
            "H_NETPRICE",
            "−60.00",
            "90 + 20 − 50, pre-discount, flipped"
          ],
          [
            "H_NETAMOUNT",
            "−51.00",
            "81 + 20 − 50, post-discount, flipped"
          ],
          [
            "H_GROSSAMOUNT",
            "−53.55",
            "51.00 + 2.55 tax, flipped"
          ],
          [
            "H_DISCOUNTAMOUNT",
            "9.00",
            "the whole receipt’s discount, positive"
          ],
          [
            "H_PAYMENTAMOUNT",
            "53.55",
            "sum of the two tenders"
          ],
          [
            "H_SALEISRETURNSALE",
            "YES",
            "line 3 carries a negative quantity"
          ],
          [
            "H_NUMBEROFPAYMENTLINES",
            "2",
            "cash and Visa"
          ]
        ]
      },
      {
        "code": "L",
        "label": "Sales lines — 4 rows (from 3 Cegid lines)",
        "cols": [
          "L_LINENUMBER",
          "Batch",
          "Share",
          "Qty",
          "Unit",
          "Net amount",
          "Tax",
          "Discount",
          "Return?"
        ],
        "rows": [
          [
            "101",
            "J27996",
            "2/3",
            "−2",
            "30.00",
            "−54.00",
            "−2.70",
            "6.00",
            "NO"
          ],
          [
            "103",
            "K88001",
            "1/3",
            "−1",
            "30.00",
            "−27.00",
            "−1.35",
            "3.00",
            "NO"
          ],
          [
            "200",
            "(blank)",
            "1",
            "−1",
            "20.00",
            "−20.00",
            "−1.00",
            "0.00",
            "NO"
          ],
          [
            "301",
            "K30071",
            "1",
            "+1",
            "50.00",
            "+50.00",
            "+2.50",
            "0.00",
            "YES"
          ]
        ]
      },
      {
        "code": "T",
        "label": "Tax lines — 4 rows, one per sales line",
        "cols": [
          "T_SALESLINENUMBER",
          "T_TAXCODE",
          "T_TAXPERCENTAGE",
          "T_TAXAMOUNT"
        ],
        "rows": [
          [
            "101",
            "S5",
            "5.0000",
            "+2.70"
          ],
          [
            "103",
            "S5",
            "5.0000",
            "+1.35"
          ],
          [
            "200",
            "S5",
            "5.0000",
            "+1.00"
          ],
          [
            "301",
            "S5",
            "5.0000",
            "−2.50"
          ]
        ]
      },
      {
        "code": "P",
        "label": "Payment lines — 2 rows, one per tender",
        "cols": [
          "P_LINENUMBER",
          "P_TENDERTYPE",
          "P_AMOUNTTENDERED",
          "P_CURRENCYCODE"
        ],
        "rows": [
          [
            "1",
            "1 · Cash",
            "30.00",
            "OMR"
          ],
          [
            "2",
            "2 · Visa",
            "23.55",
            "OMR"
          ]
        ]
      },
      {
        "code": "D",
        "label": "Discount lines — 2 rows, only line 1 was discounted",
        "cols": [
          "D_SALESLINENUMBER",
          "D_DISCOUNTAMOUNT",
          "D_DISCOUNTPERCENTAGE",
          "D_DISCOUNTORIGINTYPE"
        ],
        "rows": [
          [
            "101",
            "6.00",
            "10.00",
            "MANUAL"
          ],
          [
            "103",
            "3.00",
            "10.00",
            "MANUAL"
          ]
        ]
      }
    ],
    "checksNote": "Nothing is lost in the split. Every header total is the sum of its detail rows — this is what makes the proration safe.",
    "checks": [
      {
        "what": "Sales lines re-sum to the receipt",
        "calc": "−54.00 − 27.00 − 20.00 + 50.00",
        "eq": "−51.00 = H_NETAMOUNT"
      },
      {
        "what": "Line 1’s two halves re-sum to line 1",
        "calc": "−54.00 − 27.00",
        "eq": "−81.00 = its GL_TOTALHT, flipped"
      },
      {
        "what": "Quantities re-sum",
        "calc": "2 + 1 + 1 + 1",
        "eq": "5 = H_NUMBEROFITEMS"
      },
      {
        "what": "Discount rows re-sum",
        "calc": "6.00 + 3.00",
        "eq": "9.00 = H_DISCOUNTAMOUNT"
      },
      {
        "what": "Tax mirrors the sales lines",
        "calc": "Σ L tax = −2.55 · Σ T tax = +2.55",
        "eq": "exact inverse"
      },
      {
        "what": "Tenders cover the gross",
        "calc": "30.00 + 23.55",
        "eq": "53.55 = |H_GROSSAMOUNT|"
      }
    ],
    "notesTitle": "Three things worth noticing",
    "notes": [
      "The sub-line numbers are 101, 103, 200, 301 — not 1, 2, 3, 4. They are unique and reproducible, which is all that matters, but they are not contiguous and they no longer match the Cegid line numbers. Anything downstream keying on them must be re-pointed.",
      "Line 2 never touches the batch machinery. No scan row means the plain path: quantity straight from Cegid, blank batch, no proration. The same rule covers testers, samples and any article the store does not scan.",
      "The return keeps its own signs and carries the original store and receipt back with it, so D365 can match the refund to the sale it came from — even when it was sold in a different shop."
    ],
    "excludedTitle": "Deliberately not in this example",
    "excluded": [
      "Gift cards — any receipt touching tender 004 is dropped whole, so there is nothing to show.",
      "A clean 1:1 exchange — same shape as the return here, but the header and payment net to zero.",
      "An under-scanned batch line — the guard rail holds the whole ticket back until the scans are fixed."
    ]
  }
}
