---
name: measures
description: "Ontwerp en genereer DAX measures voor Power BI, schrijf naar _Berekeningen tabel in TMDL-formaat"
allowed-tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"]
---

# Measures Skill

Ontwerpt en schrijft DAX measures direct naar TMDL-bestanden. Alle measures komen in de centrale `_Berekeningen` tabel. Valideert dependencies, formatting en best practices. Werkt bij voorkeur vanuit de analyze skill output.

## Referenties

Lees bij eerste gebruik:
- `reference/dax-patterns.md` — standaard DAX patronen per categorie (70+ patronen)
- `reference/tmdl-measures.md` — TMDL measure syntax, _Berekeningen tabel en plaatsing
- `reference/formatting-dax.md` — formatString conventies en conditional formatting
- `reference/6g-conventions.md` — 6gorillas standaard measures, tekenconventies en naamgeving

## Workflow

### Stap 1: Inventariseer bestaande measures
- Grep naar `measure` in alle `.tmdl` bestanden
- Noteer: naam, tabel, expressie, formatString
- Voorkom duplicaten bij het toevoegen van nieuwe measures

### Stap 2: Controleer _Berekeningen tabel
- Zoek naar `_Berekeningen.tmdl` in de tables map
- **Als het bestand NIET bestaat**: maak het aan (zie _Berekeningen Tabel sectie)
- **Als het bestand WEL bestaat**: lees het en noteer bestaande measures
- **Als er measures in ANDERE tabellen staan**: plan migratie naar _Berekeningen (zie Migratie)

### Stap 3: Ontvang of stel measures voor
Vanuit een dashboard blueprint, of stel zelf voor op basis van het datamodel:
- **Base measures**: directe aggregaties op fact-tabellen
- **Derived measures**: berekeningen op base measures
- **Time intelligence**: periodevergelijkingen met officiële DAX time functies
- **Budget & variantie**: budget versus realisatie analyses
- **Ranking**: RANKX, Top N, Pareto
- **Formatting**: kleurcodes, KPI-indicatoren, labels

**6G Standaard Financieel Measures Set**

Bij een nieuw financieel dashboard, bied de volgende standaard set aan (zie `reference/6g-conventions.md` voor volledige definities):

| Categorie | Measures | Afhankelijk van |
|-----------|----------|-----------------|
| **Base** | Realisatie, Begroot, Saldi | — |
| **Filters** | Omzet, Kosten, Netto resultaat, Pers.kn, Uitbesteed Werk | Realisatie |
| **Budget filters** | Omzet begroot, Kosten begroot, Netto Begroot, Pers.kn begroot | Begroot |
| **Time: LY** | Realisatie LY | Base measures |
| **Time: YTD** | Realisatie YTD, Begroot YTD, Omzet YTD, Kosten YTD, etc. | Alle filter measures |
| **Variance** | Realisatie vs begroot, Realisatie vs begroot%, Afw.* measures | YTD measures |
| **Tekst/Utility** | Verversing, Leeg, Transparant, Omzet tekst | Diverse |

**Tekenconventie:**
- `Mutatie` kolom: negatief = opbrengst, positief = kost (AFAS conventie)
- `Realisatie` = SUM([Mutatie]) * -1 (omgekeerd voor weergave)
- Kostenmeasures: extra * -1 zodat kosten als positief getal tonen
- `Saldo` kolom (balans): geen tekenwisseling nodig

### Stap 4: Toon als datatable
```
| Naam | Formule | Categorie | displayFolder | Afhankelijk van | formatString |
|------|---------|-----------|---------------|-----------------|--------------|
| Realisatie | SUM(FactRealisatie[Mutatie]) * -1 | Base | Realisatie | - | #,0 |
| Realisatie YTD | TOTALYTD(...) | Time Intel | YTD | Realisatie | #,0 |
| Realisatie vs begroot% | DIVIDE(...) | Variance | Afwijking | YTD measures | 0.0%;-0.0%;0.0% |
```

Wijs elke measure een `displayFolder` toe:

| Display Folder | Inhoud |
|----------------|--------|
| Realisatie | Base actuals, gefilterde actuals, LY |
| Begroot | Budget measures |
| YTD | TOTALYTD varianten |
| Afwijking | Variance (absoluut + %) |
| Tekst | FORMAT, BLANK(), utility |

### Stap 5: Valideer dependency-keten
Measures moeten in dependency-volgorde aangemaakt worden — 6 lagen:

```
Layer 1 — Base (geen afhankelijkheden):
  Realisatie, Begroot, Saldi

Layer 2 — Filters (afhankelijk van base):
  Omzet, Kosten, Netto resultaat, Pers.kn, Uitbesteed Werk,
  Omzet begroot, Kosten begroot, Netto Begroot, Pers.kn begroot

Layer 3 — Time Intelligence (afhankelijk van layer 1 + 2):
  Realisatie LY, Realisatie YTD, Begroot YTD,
  Omzet YTD, Kosten YTD, Netto resultaat YTD,
  Pers.kn YTD, Uitbesteed Werk YTD,
  Omzet begroot YTD, Kosten begroot YTD, Netto Begroot YTD

Layer 4 — Variance (afhankelijk van layer 3):
  Realisatie vs begroot, Realisatie vs begroot%,
  Afw. Omzet, Afw. Kosten, Afw. Netto resultaat,
  Afw. Pers.kn, Afw. Uitbesteed Werk,
  Afw. Omzet%, Afw. Kosten%, Afw. Netto resultaat%

Layer 5 — Ranking (afhankelijk van layer 4):
  Rang Kostenplaats, Is Top N, Is Bottom N, Cumulatief %

Layer 6 — Formatting & Utility (afhankelijk van layer 5):
  Verversing, Leeg, Transparant, Omzet tekst,
  KPI Pijl, Data Labels, Actieve Filters
```

- Detecteer en meld circulaire dependencies
- Base measures eerst, formatting measures als laatste

### Stap 6: Genereer UUIDs
Elke measure en de _Berekeningen tabel zelf heeft een unieke lineageTag:
```bash
python3 -c "import uuid; print(uuid.uuid4())"
```

### Stap 7: Schrijf naar TMDL
Plaats ALLE measures in het `_Berekeningen.tmdl` bestand. Dit is de ENIGE doeltabel voor measures.

**Plaatsing in bestand**: na de `lineageTag` van de tabel, VOOR de partition.

Wijs elke measure een `displayFolder` toe (zie Stap 4 voor de folder-indeling). Groepeer measures per displayFolder in het bestand.

### Stap 8: Verifieer
- Lees gewijzigde bestanden terug
- Controleer TMDL syntax (indentatie, lineageTag aanwezig)
- Controleer dat elke measure een `displayFolder` heeft
- Controleer dat bestaande measures niet verstoord zijn
- Controleer dat de partition onderaan het bestand staat

## _Berekeningen Tabel

### Aanmaken (als het niet bestaat)
```tmdl
table '_Berekeningen'
	lineageTag: {genereer UUID}

	partition '_Berekeningen' = m
		mode: import
		source = let Source = #table({}, {}) in Source
```

### Met measures (volledig voorbeeld)
```tmdl
table '_Berekeningen'
	lineageTag: {genereer UUID}

	measure 'Realisatie' = SUM(FactRealisatie[Mutatie]) * -1
		formatString: #,0
		lineageTag: {genereer UUID}
		displayFolder: Realisatie

	measure 'Kosten' =
			CALCULATE(
				[Realisatie],
				KEEPFILTERS(DimGrootboek[Categorie] = "Kosten")
			) * -1
		formatString: #,0
		lineageTag: {genereer UUID}
		displayFolder: Realisatie

	measure 'Realisatie YTD' = TOTALYTD([Realisatie], DimDatum[Datum])
		formatString: #,0
		lineageTag: {genereer UUID}
		displayFolder: YTD

	measure 'Realisatie vs begroot%' =
			VAR _realisatie = [Realisatie YTD]
			VAR _begroot = [Begroot YTD]
			RETURN
				IF(
					NOT ISBLANK(_begroot) && _begroot <> 0,
					DIVIDE(_realisatie - _begroot, ABS(_begroot)),
					BLANK()
				)
		formatString: 0.0%;-0.0%;0.0%
		lineageTag: {genereer UUID}
		displayFolder: Afwijking

	measure 'Verversing' = FORMAT(NOW(), "DD-MM-YYYY HH:mm")
		lineageTag: {genereer UUID}
		displayFolder: Tekst

	partition '_Berekeningen' = m
		mode: import
		source = let Source = #table({}, {}) in Source
```

### Migratie van bestaande measures
Wanneer een model measures verspreid over fact/dim tabellen heeft:

1. **Identificeer**: grep naar `measure` in alle `.tmdl` bestanden buiten `_Berekeningen.tmdl`
2. **Verplaats**: knip elke `measure`-blok uit de brontabel en plak in `_Berekeningen.tmdl`
   - Behoud de exacte `lineageTag` van elke measure (WIJZIG DEZE NIET)
   - Voeg `displayFolder` toe per categorie
   - Pas formatting strings niet aan (behoud bestaande)
3. **Verwijder**: verwijder de lege measure-secties uit de oorspronkelijke tabelbestanden
4. **Valideer**: controleer dat er geen broken references zijn

**BELANGRIJK**: De `lineageTag` van een measure mag NOOIT veranderen bij verplaatsing — Power BI gebruikt deze om measures te identificeren.

## DAX Best Practices

### DIVIDE() vs /
- **DIVIDE()** gebruiken wanneer de noemer een expressie is (kan nul of BLANK zijn)
- **/** operator gebruiken wanneer de noemer een constante is (bijv. `/ 100`, `/ 12`)
- Laat DIVIDE() standaard BLANK retourneren — niet 0

```dax
-- GOED: noemer is expressie
Marge = DIVIDE([Winst], [Omzet])

-- GOED: noemer is constante
Maandbedrag = [JaarBedrag] / 12

-- FOUT: handmatige zero-check
Marge = IF([Omzet] = 0, BLANK(), [Winst] / [Omzet])
```

### VAR/RETURN
- Gebruik altijd VAR/RETURN wanneer een subexpressie meerdere keren voorkomt
- Gebruik altijd VAR/RETURN bij measures van meer dan 1 regel
- Prefix lokale variabelen met `_`: `_huidig`, `_vj`, `_budget`
- Beschrijvende namen: `_aantalMaanden`, `_realisatieYTD`
- Geen generieke namen als `_x`, `_temp`, `_val`

```dax
-- GOED
Groei YoY % =
VAR _huidig = [Realisatie]
VAR _vj = CALCULATE([Realisatie], SAMEPERIODLASTYEAR(DimDatum[Datum]))
RETURN
    DIVIDE(_huidig - _vj, ABS(_vj))

-- FOUT: subexpressie herhaald
Groei YoY % =
DIVIDE(
    [Realisatie] - CALCULATE([Realisatie], SAMEPERIODLASTYEAR(DimDatum[Datum])),
    CALCULATE([Realisatie], SAMEPERIODLASTYEAR(DimDatum[Datum]))
)
```

### BLANK() Handling
- Retourneer BLANK() wanneer er geen zinvolle waarde is
- Converteer BLANK NOOIT naar 0 — dit veroorzaakt trage visuals met lege groepen
- Uitzondering: wanneer 0 een zinvolle businesswaarde is (bijv. "0 orders")

```dax
-- FOUT: converteert BLANK naar 0
Omzet Veilig = IF(ISBLANK([Omzet]), 0, [Omzet])

-- FOUT: DIVIDE retourneert 0 i.p.v. BLANK
Marge = DIVIDE([Winst], [Omzet], 0)

-- GOED: laat BLANK doorlopen
Marge = DIVIDE([Winst], [Omzet])
```

### COUNTROWS vs COUNT
- Gebruik ALTIJD COUNTROWS i.p.v. COUNT
- COUNTROWS is efficiënter, negeert geen BLANK-waarden, en is duidelijker
- Uitzondering: DISTINCTCOUNT voor unieke waarden (eigen functie)

```dax
-- FOUT
Aantal Orders = COUNT(FactVerkoop[OrderDatum])

-- GOED
Aantal Orders = COUNTROWS(FactVerkoop)
```

### REMOVEFILTERS vs ALL
- Gebruik REMOVEFILTERS als filter modifier in CALCULATE (niet ALL)
- ALL als filter modifier en ALL als table-functie zijn conceptueel verschillend
- REMOVEFILTERS maakt de intentie duidelijker

```dax
-- VOORKEUR
Totaal Ongeacht Filter = CALCULATE([Realisatie], REMOVEFILTERS(DimKostenplaats))

-- ALTERNATIEF (werkt ook, maar dubbelzinnig)
Totaal Ongeacht Filter = CALCULATE([Realisatie], ALL(DimKostenplaats))
```

### Time Intelligence functies
- **TOTALYTD/TOTALQTD/TOTALMTD** voor period-to-date i.p.v. handmatige FILTER/ALL
- **SAMEPERIODLASTYEAR** voor vorig jaar vergelijkingen
- **DATEADD** voor flexibele periodeverschuivingen (vorige maand, kwartaal)
- **PARALLELPERIOD** voor volledige perioden
- **DATESINPERIOD** voor rolling windows (MA3, MA6, MA12)

```dax
-- FOUT: handmatig filter
Realisatie YTD =
CALCULATE([Realisatie], FILTER(ALL(DimDatum), DimDatum[Jaar] = MAX(DimDatum[Jaar]) && DimDatum[MaandNr] <= MAX(DimDatum[MaandNr])))

-- GOED: officiële functie
Realisatie YTD = TOTALYTD([Realisatie], DimDatum[Datum])
```

### Filter Arguments in CALCULATE
- Gebruik Boolean expressies als filter i.p.v. FILTER() wanneer mogelijk
- FILTER() alleen bij complexe condities met measures of multi-kolom vergelijkingen

```dax
-- FOUT: onnodig FILTER (scant hele tabel)
CALCULATE([Realisatie], FILTER('DimGrootboek', DimGrootboek[Categorie] = "Kosten"))

-- GOED: Boolean expressie (gebruikt kolomindex)
CALCULATE([Realisatie], KEEPFILTERS(DimGrootboek[Categorie] = "Kosten"))
```

### Error Handling
Prioriteitsvolgorde:
1. Data cleansen in Power Query (preventief)
2. DIVIDE() gebruiken i.p.v. handmatige deling-checks
3. IF() met preventieve check wanneer nodig
4. IFERROR() alleen als allerlaatste redmiddel (performance-impact)

```dax
-- FOUT: IFERROR als eerste reflex
Marge = IFERROR([Winst] / [Omzet], BLANK())

-- GOED: DIVIDE handelt het af
Marge = DIVIDE([Winst], [Omzet])
```

## Format Strings

| Type | formatString | Voorbeeld |
|------|-------------|-----------|
| Bedrag (geheel) | `#,0` | 1.235 |
| Percentage | `0.0%;-0.0%;0.0%` | 12,3% |
| Percentage (precies) | `0.00%;-0.00%;0.00%` | 12,34% |
| Geheel getal | `#,##0` | 1.235 |
| Decimaal | `#,##0.00` | 1.234,56 |

## Indentatie

- Tabel properties: 1 tab
- Measure definitie: 1 tab
- Properties (`formatString`, `lineageTag`, `displayFolder`): 2 tabs
- Multi-line DAX continuatie: 3 tabs
- Partition: 1 tab
- Partition properties: 2 tabs
