---
name: s28-purchase-orders-payable-id
scenario: S28
description: Analyser les purchase orders — payableId=null ne signifie pas absence de facture, suivi des expirations, POs multi-devises
tags: [purchase-orders, payable-id, expiry, multi-currency, budget]
when_to_use: Quand on veut suivre l'avancement des POs (budget vs facturé), identifier les POs expirant bientôt, ou retrouver les factures liées
risk: P2 — Dans invoices[].payableId : null signifie que la facture ne peut pas être résolue depuis son requestId — pas qu'elle n'existe pas. Toujours vérifier invoiceRequestId comme fallback.
---

# Instructions — Purchase Orders (POs)

## ⚠️ payableId=null ≠ "pas de facture"

Chaque PO peut avoir un tableau `invoices[]` avec des factures liées.
Chaque invoice a `payableId` qui peut être :
- **UUID** → lien direct vers la payable (utiliser `get_payable_by_id` pour les détails)
- **null** → la facture existe mais ne peut pas être résolue depuis son `requestId`

```json
// Invoice avec payableId null — la facture existe mais n'est pas liée :
{
  "invoiceRequestId": "abc123",
  "invoiceNumber": "INV-001",
  "amount": { "amount": 150000, "currency": "EUR", "precision": 2 },
  "status": "paid",
  "payableId": null   ← NE PAS conclure "pas de facture"
}
```

→ En cas de `payableId=null`, utiliser `invoiceRequestId` ou `invoiceNumber` pour la réconciliation manuelle.

## ⚠️ POs multi-devises : ne pas sommer cross-devises

Les POs peuvent être libellés en EUR, USD, GBP, etc. Les champs `amount`, `billedAmount`, `deliveredAmount` sont dans la devise du PO.
→ Ne jamais additionner des POs en devises différentes sans conversion.
→ Afficher séparément par devise ou convertir explicitement.

Exemple live : PO FullStory en USD ($21,267.89), PO Oracle en EUR (30,000 EUR).

## Étape 1 — POs actifs (status=open)

```json
{ "companyId": "...", "status": ["open"], "pageSize": 100 }
```

→ Retourne les POs en cours qui acceptent encore des factures.
→ `billingStatus: "lateInvoice"` → PO ouvert mais aucune facture reçue récemment.
→ `billingStatus: "partiallyBilled"` → factures reçues mais budget non épuisé.

## Étape 2 — POs expirant bientôt

```json
{
  "companyId": "...",
  "endDateFrom": "YYYY-MM-DD",
  "endDateTo": "YYYY-MM-DD",
  "status": ["open"]
}
```

Exemple : POs expirant dans les 30 prochains jours = endDateFrom=aujourd'hui, endDateTo=aujourd'hui+30j.

## Étape 3 — Calcul du budget consommé

Pour chaque PO :
```
budget_approuvé = amount (en devise du PO)
facturé = billedAmount
livré = deliveredAmount
net = billedAmount - creditNotes (= netAmount field)

taux_consommation = billedAmount / amount × 100
reste_budget = amount - billedAmount
```

⚠️ Un PO avec `billedAmount > amount` est en dépassement de budget.

## Étape 4 — Accéder au détail des factures

Pour chaque PO avec `invoices[]` :
1. Si `payableId != null` → appeler `get_payable_by_id(payableId)` pour les détails complets
2. Si `payableId == null` → noter `invoiceNumber` + `invoiceRequestId` pour réconciliation manuelle

## Exemple live (données réelles)

| PO | Description | Status | Amount | Facturé |
|----|-------------|--------|--------|---------|
| Coworking Lyon | Espace coworking | open | 26 026 EUR | 20 185 EUR (78%) |
| FullStory | Subscription SaaS | open | $21 267 USD | $10 633 USD (50%) |
| Oracle | Abonnement annuel | open | 30 000 EUR | 0 EUR (0%) — lateInvoice |
| Coworking Lyon 2 | Extension | open | 35 424 EUR | 5 904 EUR (17%) |

Note : `payableId=null` non visible dans ce jeu de données démo — le risque est documenté dans la spec MCP mais non testable ici.

## Exemple de prompt

- "Quels POs expirent dans les 30 prochains jours ?" → endDateFrom/endDateTo + status=open
- "Quel est le taux de consommation de nos POs actifs ?" → status=open + calcul billedAmount/amount
- "Trouve les factures liées au PO Oracle" → invoices[].payableId → get_payable_by_id
