Discussion

Planificateur de partage de dépenses et de règlement des soldes

De Wikiprompt, l’encyclopédie libre de prompts

Fatih Kadir Akın

31 oct. 2026

Planificateur de partage de dépenses et de règlement des soldes Une compétence/prompt IA complet pour diviser les dépenses partagées à partir d'un registre CSV, avec prise en charge de plusieurs méthodes de répartition, devises et recherche du nombre minimal de transferts de règlement. Comprend des instructions détaillées, des modèles, des exemples et une implémentation Python complète.

Contenu du PromptEnregistrer

🌐
--- name: shared-expense-bill-splitter-and-settle-up-planner description: Divise un registre CSV des coûts partagés pour un voyage, un appartement ou un événement - parts égales, parts, montants exacts, pourcentages et factures détaillées avec taxes et pourboires au prorata, exclusions 'tous sauf', plusieurs payeurs, remboursements, paiements et plusieurs devises à des taux convenus - en centimes, puis trouve le nombre minimal prouvable de virements de règlement (recherche exacte de sous-groupes à somme nulle). Renvoie les soldes, un audit par personne et un message de chat. Inclut un planificateur Python stdlib avec --self-test. --- # Répartiteur de dépenses partagées et planificateur de règlement Vous transformez une liste désordonnée de dépenses partagées en une répartition juste et vérifiable, et en la liste la plus courte possible de paiements. Vous conservez le registre sous forme de CSV que le groupe peut éditer dans n'importe quel tableur, appliquez la règle de répartition dont chaque ligne a réellement besoin (qui était là, qui a commandé quoi, qui est resté moins de nuits), convertissez les devises à des taux convenus par le groupe, et réglez tout le monde avec le moins de virements possible. Chaque centime est comptabilisé, et chacun peut retracer son propre chiffre ligne par ligne. ## Fichiers dans cette compétence - `scripts/settle_up.py` - lit le registre CSV, divise chaque ligne en centimes entiers (arrondi au plus grand reste), calcule payé/part/net par personne, et recherche tous les sous-groupes à somme nulle pour trouver le nombre minimal de virements (exact jusqu'à 18 personnes avec un solde, glouton au-delà) (bibliothèque standard Python 3.8+ uniquement) - `references/split-methods.md` - le format CSV, quelle répartition utiliser pour quel coût, factures détaillées avec taxes et pourboires, exclusions, payeurs, remboursements et paiements - `references/settle-up-and-currency.md` - pourquoi le minimum peut être inférieur à "personnes - 1", comment fonctionne la recherche, arrondis, taux de change et frais de carte, étiquette de paiement - `templates/settle-up-summary.md` - le résumé à remettre et à coller dans le chat du groupe - `examples/lisbon-trip.csv` - 5 amis, 11 lignes : parts par nuits, une personne exclue, un dîner détaillé, une réservation en USD, des sièges premium, deux payeurs, un remboursement, des snacks en GBP et un paiement déjà effectué - `examples/lisbon-settle-output.md` - la sortie réelle du planificateur sur ce registre - `examples/flat-october.csv` - 6 colocataires où le minimum (4 virements) bat le paiement de la plus grande dette à la plus grande créance (5) ## Flux de travail ### 1. Collecter le registre Demandez pour chaque coût partagé : quoi, qui a payé (et combien chacun, si plusieurs personnes ont payé), le montant et la devise, et pour qui c'était. Posez les questions qui changent la répartition : Quelqu'un est-il arrivé en retard ou parti tôt ? Quelqu'un a-t-il sauté un repas ou une activité ? Un dîner était-il partagé ou commandé individuellement ? Quelqu'un a-t-il déjà remboursé quelqu'un d'autre ? Convenez de la devise de base et des taux (un taux par devise pour tout le voyage, sauf si le groupe veut les taux du relevé de carte). Écrivez-le comme le CSV dans `references/split-methods.md`, avec `# people:` pour que les fautes de frappe dans les noms soient détectées. ### 2. Exécuter le planificateur ``` python3 scripts/settle_up.py examples/lisbon-trip.csv python3 scripts/settle_up.py flat.csv --base GBP --rates EUR=0.86 --detail Ben python3 scripts/settle_up.py ledger.csv --json python3 scripts/settle_up.py --self-test ``` Code de sortie 0 = plan imprimé (lisez la section Avertissements), 2 = le registre a une erreur (sommes qui ne correspondent pas, taux manquant, répartition ou nom inconnu) - corrigez la ligne qu'il nomme. Si vous ne pouvez pas exécuter de code, suivez les mêmes étapes à la main : convertissez chaque ligne en centimes de base, répartissez, additionnez payé moins part par personne, puis faites correspondre les débiteurs aux créanciers, en cherchant d'abord les personnes ou petits groupes dont les soldes s'annulent exactement. ### 3. Vérifier le résultat avec le groupe - La ligne de vérification doit indiquer `nets add up to 0.00` ; payé et parts doivent être égaux. - Lisez les soldes à voix haute en termes simples ("Ana a payé 990.00, sa part est de 377.31 et Eli lui a déjà donné 100.00, donc elle récupère 512.69"). - Signalez les lignes qui déterminent le résultat et sont faciles à contester (parts par nuits, repas détaillés, un remboursement, un paiement), et proposez `--detail <nom>` à quiconque veut sa propre ventilation. - Si quelqu'un préfère payer quelqu'un d'autre (ou ne peut pas payer une personne directement), relancez après avoir ajouté le virement convenu comme ligne `payment` ; le plan recalcule le reste. ### 4. Livrer Remplissez `templates/settle-up-summary.md` : totaux, tableau des soldes, virements numérotés, hypothèses (taux, qui a été exclu où, comment taxes/pourboires ont été répartis), et le court message de chat de groupe. Suggérez de marquer chaque virement comme fait en l'ajoutant au registre comme ligne `payment`, afin qu'une seconde exécution montre tout le monde à 0.00. ## Exemple de résultat Sur `examples/lisbon-trip.csv` (5 personnes, 1 583,07 EUR dépensés), Ana a payé l'appartement et les billets de fado et récupère 512,69 EUR ; le plan est de 4 virements : Ben paie Ana 243,17, Dara paie Ana 140,94, Eli paie Ana 128,58 et Eli paie Cem 0,85 (le paiement antérieur de 100,00 d'Eli est déjà compté). Sur `examples/flat-october.csv`, Bo et Finn s'annulent mutuellement, donc 6 colocataires se règlent en 4 virements alors que payer la plus grande dette à la plus grande créance en prendrait 5. ## Règles - N'inventez jamais de montants, de taux ou de participants ; demandez, ou marquez la ligne comme hypothèse dans le résumé. - Utilisez un taux convenu par devise et affichez-le ; ne mélangez pas les taux en direct dans une répartition terminée. - Gardez tous les calculs en centimes et montrez où sont allés les centimes d'arrondi (plus grand reste, égalités à la personne listée en premier). - Ne jugez pas les dépenses ; signalez uniquement les lignes qui ressemblent à des doublons, des fautes de frappe ou des participants manquants. - Un remboursement est une ligne `payment`, jamais une dépense négative ; un remboursement de somme est une dépense négative partagée par les mêmes personnes que le coût d'origine. - Présentez le plan comme une proposition ; le groupe peut échanger qui paie qui tant que le net de chaque personne est inchangé. FILE:references/split-methods.md # Méthodes de répartition et format CSV ## Colonnes `date,description,paid_by,amount,currency,split,participants,details` - ligne d'en-tête requise ; tout tableur peut l'exporter. Lignes de commentaire facultatives avant l'en-tête : ``` # people: Ana, Ben, Cem, Dara, Eli (liste stricte : un nom mal orthographié est une erreur) # base: EUR # rates: USD=0.92, GBP=1.16 (1 USD = 0.92 EUR) ``` Sans `# people:` , toute personne nommée dans le fichier est membre et `all` signifie tous ; les noms sont comparés sans tenir compte de la casse. ## Choisir une répartition | Coût | Répartition | Exemple `participants` / `details` | |---|---|---| | Épicerie, taxi, carburant, billets partagés | `equal` | `all`, `Ana;Ben`, `all-Eli` | | Hébergement quand les gens ont séjourné des nuits différentes, chambres de tailles différentes | `shares` | `Ana:3;Ben:3;Dara:2` (nuits ou poids des chambres) | | Billets à prix différents, montant connu par personne | `exact` | `Ana:30;Ben:45` (les autres dans participants répartissent le reste également) | | Pourcentages convenus (loyer selon le revenu, vin pour ceux qui ont bu) | `percent` | `Ana:25;Ben:25;Dara:50` (doit totaliser 100) | | Facture de restaurant où les gens ont commandé séparément | `itemized` | `Ana:31;Ben:28.50;shared:42` | | Quelqu'un a déjà remboursé quelqu'un d'autre | `payment` | participants = le seul destinataire | ## Factures détaillées - `details` liste les articles de chaque personne avant taxes, service et pourboire ; `shared` est la somme des entrées, bouteilles ou tout ce qui a été partagé (réparti également entre `participants`). - `amount` est la facture finale. La différence (montant - articles) est la taxe, le service et le pourboire, répartis proportionnellement à ce que chaque personne a eu, donc une personne qui a commandé plus paie plus de pourboire. La sortie l'affiche comme `+17.3% tax/tip`. - Si les articles totalisent plus que la facture (une remise ou une faute de frappe), le planificateur s'arrête ; saisissez la remise en réduisant les prix des articles auxquels elle s'applique. ## Exclusions et participation partielle - `all-Eli` ou `all except Eli` = tout le monde sauf Eli ; enchaînez plusieurs : `all-Bo-Finn`. - Pour les personnes présentes une partie du temps, utilisez `shares` avec des nuits ou des jours au lieu de les exclure. - Un enfant ou un invité payé par un membre : listez-le sous le nom du membre (poids `shares` 1.5) ou ajoutez-le à `# people:` et réglez avec son parent. ## Payeurs - Un payeur : `Ana`. Plusieurs payeurs sur une facture : `Ben:40;Cem:20` (montants dans la devise de la ligne, doivent totaliser `amount`). - Une carte partagée par un couple : traitez le couple comme une personne ou divisez le montant du payeur. ## Remboursements, dépôts, paiements - Remboursement reçu après une annulation : `amount` négatif, `paid_by` = qui a reçu l'argent, mêmes participants que le coût d'origine. - Dépôt rendu par un propriétaire : `amount` négatif réparti de la même manière que le dépôt. - Argent remis entre membres : `payment` (de `paid_by` à la seule personne dans `participants`). Enregistrez les virements de règlement de la même manière ; une nouvelle exécution montre alors qui est encore ouvert. ## Erreurs courantes que le planificateur détecte - Montants exacts ou pourcentages qui ne totalisent pas, montants des payeurs différents de la facture, articles dépassant la facture. - Une devise sans taux, un mot de répartition inconnu, un nom mal orthographié quand `# people:` est défini. - Deux lignes identiques (même date, description, montant et payeur) - signalées comme avertissement, non supprimées. - Montants nuls - ignorés avec un avertissement. FILE:references/settle-up-and-currency.md # Règlement et devise ## Solde net Pour chaque personne : `net = payé + paiements envoyés - part - paiements reçus`. Positif = récupère de l'argent, négatif = doit. Les nets totalisent toujours exactement 0.00 car chaque ligne est répartie en centimes entiers qui totalisent le montant de la ligne. ## Pourquoi "moins de virements" peut être inférieur à personnes - 1 - Tout groupe de k personnes avec un solde peut se régler en au plus k - 1 virements (chaque virement solde au moins une personne). - Si le groupe se divise en sous-groupes dont les soldes totalisent chacun zéro, chaque sous-groupe se règle seul : avec g tels sous-groupes, le minimum est k - g. Exemple : Bo +300 et Finn -300 s'annulent, donc 6 colocataires ont besoin de 4 virements, pas 5. - Payer la plus grande dette à la plus grande créance (l'approche habituelle des applications) ne cherche pas ces sous-groupes et peut prendre plus de virements. - Trouver la meilleure division en sous-groupes à somme nulle est un problème difficile en général ; le planificateur le résout exactement pour jusqu'à 18 personnes avec un solde non nul (programmation dynamique sur les sous-ensembles, en moins d'une seconde) et retombe sur le plan glouton au-delà, en le disant. - Au sein de chaque sous-groupe, le plan paie la plus grande dette à la plus grande créance, ce qui utilise exactement (taille du sous-groupe - 1) virements. - Plusieurs plans peuvent avoir le même nombre minimal ; n'importe lequel est juste, car le total entrant ou sortant de chaque personne est fixé par son net. ## Arrondis - Chaque ligne est convertie en centimes de base une fois (`montant x taux`, arrondi au demi supérieur), puis répartie avec la méthode du plus grand reste : chacun reçoit le montant arrondi à l'inférieur, et les centimes restants vont aux plus grandes fractions, égalités à la personne listée en premier dans `# people:`. - Sur tout un voyage, cela déplace au plus quelques centimes par personne ; dites-le si quelqu'un demande pourquoi 10.00 / 3 est 3.34 pour une personne. ## Taux de change - Convenez d'un taux par devise avant de répartir : le taux du jour où la plupart des coûts ont été payés, la moyenne du voyage, ou le taux du relevé de carte du payeur (qui inclut la majoration de la banque). - Si la carte du payeur a facturé des frais de transaction étrangère, soit incluez-les dans le montant (le groupe les partage), soit laissez-les de côté (le payeur les absorbe) - décidez une fois pour tout le voyage. - Saisissez les montants dans la devise réellement payée et laissez le planificateur convertir ; ne pré-convertissez pas certaines lignes à la main. - Réglez dans la devise de base. Si quelqu'un paie dans une autre devise, convertissez le virement au même taux convenu et enregistrez-le comme ligne `payment` dans cette devise. ## Étiquette de paiement - Envoyez le message de chat de groupe avec les virements et un lien vers le registre ; donnez aux gens un jour ou deux pour contester une ligne avant de payer. - Les petits virements (moins d'environ 2 EUR/USD) peuvent être annulés ou intégrés au prochain coût partagé si les deux personnes sont d'accord - enregistrez cela comme ligne `payment` pour que le registre reste exact. - Quand tout le monde a payé, ajoutez chaque virement comme ligne `payment` et relancez : chaque net devrait être 0.00. FILE:templates/settle-up-summary.md # Règlement : <voyage / appartement / événement>, <dates> **Groupe :** <Ana, Ben, Cem, Dara, Eli> **Devise de base :** <EUR> **Total dépensé :** <1 583,07 EUR> sur <11> lignes **Taux convenus :** <1 USD = 0.92 EUR, 1 GBP = 1.16 EUR (moyenne du voyage)> ## Soldes | Personne | Payé | Part équitable | Déjà envoyé / reçu | Net | |---|---|---|---|---| | <Ana> | <990.00> | <377.31> | <reçu 100.00> | <+512.69 récupère> | | <Ben> | <64.00> | <307.17> | <-> | <-243.17 doit> | ## Virements (<n>, le minimum possible) 1. <Ben paie Ana 243.17 EUR> 2. <Dara paie Ana 140.94 EUR> 3. <...> ## Comment les lignes ont été réparties - <Appartement : par nuits, Dara et Eli 2 des 3 nuits> - <Taxi aéroport : tout le monde sauf Eli (venu en train)> - <Dîner : détaillé, 17.3% de taxes et pourboire répartis selon ce que chaque personne a commandé ; entrées 42.00 partagées> - <Remboursement des billets du palais : à Ben et Cem, qui les ont payés> - <Les 100.00 d'Eli à Ana sont déjà comptés> ## Hypothèses et points ouverts - <taux ci-dessus ; frais de carte non inclus> - <vérifiez la ligne 5 (cours de surf) : Ben était-il là?> ## Message de chat de groupe ``` Règlement (<EUR>, <11> lignes, taux : <USD 0.92, GBP 1.16>) : - <Ben -> Ana : 243.17 EUR> - <Dara -> Ana : 140.94 EUR> Tableau complet : <lien vers la feuille>. Criez sous 2 jours si une ligne est fausse. ``` **Quand payé :** ajoutez chaque virement comme ligne `payment` et relancez - chaque net devrait indiquer 0.00. FILE:examples/lisbon-trip.csv # people: Ana, Ben, Cem, Dara, Eli # base: EUR # rates: USD=0.92, GBP=1.16 date,description,paid_by,amount,currency,split,participants,details 2026-10-02,Apartment 3 nights (Dara and Eli 2 nights),Ana,840.00,EUR,shares,all,Ana:3;Ben:3;Cem:3;Dara:2;Eli:2 2026-10-02,Airport taxi (Eli came by train),Ben,48.00,EUR,equal,all-Eli, 2026-10-02,Groceries for the flat,Eli,63.75,EUR,equal,all, 2026-10-03,Dinner at Taberna do Mar,Cem,212.40,EUR,itemized,all,Ana:31;Ben:28.50;Cem:24;Dara:36;Eli:19.50;shared:42 2026-10-03,Surf lesson (booked on a US site),Dara,180.00,USD,equal,Ana;Dara;Eli, 2026-10-03,Fado tickets (premium seats for Dara and Eli),Ana,150.00,EUR,exact,all-Cem,Ana:30;Ben:30;Dara:45;Eli:45 2026-10-04,Train to Sintra,Ben:40;Cem:20,60.00,EUR,equal,all, 2026-10-04,Wine for the flat,Dara,36.00,EUR,percent,,Ana:25;Ben:25;Dara:50 2026-10-04,Palace tickets refunded (rain),Ben,-24.00,EUR,equal,Ben;Cem, 2026-10-05,Snacks at the London layover,Cem,27.00,GBP,equal,all, 2026-10-05,Eli paid Ana back for the apartment,Eli,100.00,EUR,payment,Ana, FILE:examples/lisbon-settle-output.md # Plan de règlement Devise de base : EUR Personnes : 5 Lignes : 11 Total dépensé : 1 583,07 EUR Taux utilisés : 1 GBP = 1.16 EUR, 1 USD = 0.92 EUR ## Dépenses | # | Date | Quoi | Payé par | Montant | En EUR | Répartition | Entre | |---|---|---|---|---|---|---|---| | 1 | 2026-10-02 | Appartement 3 nuits (Dara et Eli 2 nuits) | Ana | 840.00 EUR | 840.00 | shares | all | | 2 | 2026-10-02 | Taxi aéroport (Eli venu en train) | Ben | 48.00 EUR | 48.00 | equal | Ana, Ben, Cem, Dara | | 3 | 2026-10-02 | Épicerie pour l'appartement | Eli | 63.75 EUR | 63.75 | equal | all | | 4 | 2026-10-03 | Dîner à Taberna do Mar | Cem | 212.40 EUR | 212.40 | itemized (+17.3% tax/tip) | all | | 5 | 2026-10-03 | Cours de surf (réservé sur un site US) | Dara | 180.00 USD | 165.60 | equal | Ana, Dara, Eli | | 6 | 2026-10-03 | Billets de fado (sièges premium pour Dara et Eli) | Ana | 150.00 EUR | 150.00 | exact | Ana, Ben, Dara, Eli | | 7 | 2026-10-04 | Train pour Sintra | Ben 40.00, Cem 20.00 | 60.00 EUR | 60.00 | equal | all | | 8 | 2026-10-04 | Vin pour l'appartement | Dara | 36.00 EUR | 36.00 | percent | Ana, Ben, Dara | | 9 | 2026-10-04 | Billets du palais remboursés (pluie) | Ben | -24.00 EUR | -24.00 | equal | Ben, Cem | | 10 | 2026-10-05 | Snacks à l'escale de Londres | Cem | 27.00 GBP | 31.32 | equal | all | | 11 | 2026-10-05 | Eli a remboursé Ana pour l'appartement | Eli | 100.00 EUR | 100.00 | payment | à Ana | ## Soldes | Personne | Payé | Part | Envoyé | Reçu | Net | |---|---|---|---|---|---| | Ana | 990.00 | 377.31 | 0.00 | 100.00 | 512.69 (récupère) | | Ben | 64.00 | 307.17 | 0.00 | 0.00 | -243.17 (doit) | | Cem | 263.72 | 262.87 | 0.00 | 0.00 | 0.85 (récupère) | | Dara | 201.60 | 342.54 | 0.00 | 0.00 | -140.94 (doit) | | Eli | 63.75 | 293.18 | 100.00 | 0.00 | -129.43 (doit) | Vérification : les nets totalisent 0.00 (payé 1 583,07 = parts 1 583,07). ## Virements (4) 1. Ben paie Ana **243.17 EUR** 2. Dara paie Ana **140.94 EUR** 3. Eli paie Ana **128.58 EUR** 4. Eli paie Cem **0.85 EUR** Minimum possible : 4 virements pour 5 personnes avec un solde (aucun sous-groupe plus petit ne s'équilibre seul). ## Message pour le chat de groupe ``` Règlement (EUR, 11 lignes, taux : GBP 1.16, USD 0.92) : - Ben -> Ana : 243.17 EUR - Dara -> Ana : 140.94 EUR - Eli -> Ana : 128.58 EUR - Eli -> Cem : 0.85 EUR Tableau complet dans la feuille partagée. Criez sous 2 jours si une ligne est fausse. ``` FILE:examples/flat-october.csv # people: Asha, Bo, Chen, Dev, Ema, Finn # base: EUR date,description,paid_by,amount,currency,split,participants,details 2026-10-01,Festival tickets for Bo and Finn,Bo,600,EUR,exact,Bo;Finn,Bo:300;Finn:300 2026-10-31,October groceries (Bo and Finn on tour),Asha,800,EUR,equal,all-Bo-Finn, 2026-10-31,Electricity Sep-Oct (Bo and Finn on tour),Chen,600,EUR,equal,all-Bo-Finn, 2026-10-20,Second-hand sofa (Dev and Ema's part),Asha,250,EUR,exact,Dev;Ema,Dev:150;Ema:100 FILE:scripts/settle_up.py #!/usr/bin/env python3 """Répartiteur de dépenses partagées et planificateur de règlement (bibliothèque standard Python 3.8+ uniquement). Lit un registre CSV des coûts partagés (un voyage, un appartement, un événement), divise chaque ligne en centimes entiers, et trouve le nombre MINIMAL possible de virements qui règle le groupe (recherche exacte sur les sous-groupes à somme nulle, pas seulement "au plus personnes - 1"). Colonnes CSV (ligne d'en-tête requise, colonnes supplémentaires ignorées) : date,description,paid_by,amount,currency,split,participants,details paid_by Ana un payeur Ana:40;Ben:20 plusieurs payeurs (montants dans la devise de la ligne, doivent totaliser) amount total dans la devise de la ligne ; négatif = remboursement reçu par paid_by currency vide = devise de base split equal | shares | exact | percent | itemized | payment participants all | Ana;Ben;Cem | all-Eli (tout le monde sauf Eli) | vide = noms dans details details shares: Ana:3;Ben:2 (les personnes dans participants non listées obtiennent 1) exact: Ana:30;Ben:45 (le reste, s'il y en a, réparti également entre les autres participants) percent: Ana:50;Ben:30;Cem:20 (doit totaliser 100) itemized: Ana:18;Ben:22.50;shared:12 (prix des articles avant taxes/pourboire ; les articles "shared" sont répartis également ; montant - articles = taxes/service/pourboire, répartis en proportion de ce que chaque personne a commandé) payment: (aucun) paid_by a donné `amount` à la seule personne dans participants Les lignes de commentaire en haut définissent le groupe et les taux (ou utilisez les options CLI) : # people: Ana, Ben, Cem # base: EUR # rates: USD=0.92, GBP=1.16 (1 unité de cette devise = taux unités de la devise de base) Utilisation : python3 settle_up.py ledger.csv [--base EUR] [--rates USD=0.92,GBP=1.16] [--people Ana,Ben] [--detail Ana] [--json] python3 settle_up.py --self-test Codes de sortie : 0 = plan réglé imprimé, 2 = mauvaise entrée (répartition inconnue, sommes qui ne correspondent pas, taux de change manquant, personne inconnue dans une liste stricte). """ import argparse import csv import io import json import re import sys from decimal import Decimal, ROUND_HALF_UP, InvalidOperation MAX_EXACT = 18 # personnes avec un solde non nul pour la recherche exacte du minimum (2^18 états) SPLITS = ("equal", "shares", "exact", "percent", "itemized", "payment") class LedgerError(ValueError): pass def dec(s, what): try: return Decimal(str(s).replace(",", "").strip()) except (InvalidOperation, AttributeError): raise LedgerError("%s : %r n'est pas un nombre" % (what, s)) def to_cents(amount): return int((amount * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP)) def fmt(cents, cur=""): sign = "-" if cents < 0 else "" c = abs(cents) s = "%s%d.%02d" % (sign, c // 100, c % 100) s = re.sub(r"(\d)(?=(\d{3})+\.)", r"\1,", s) return (s + " " + cur).strip() def allocate(total, weights, order): """Divise le total entier `total` en centimes proportionnellement à `weights` (dict nom -> Decimal >= 0). Méthode du plus grand reste ; les égalités vont au nom le plus tôt dans `order`. La somme est exacte.""" names = [n for n in order if n in weights] wsum = sum(weights[n] for n in names) if wsum <= 0: raise LedgerError("les poids totalisent zéro") sign = -1 if total < 0 else 1 t = abs(total) raw = {n: Decimal(t) * weights[n] / wsum for n in names} base = {n: int(raw[n]) for n in names} left = t - sum(base.values()) rank = sorted(names, key=lambda n: (-(raw[n] - base[n]), order.index(n))) for n in rank[:left]: base[n] += 1 return {n: sign * v for n, v in base.items() if v} def parse_pairs(text, what): out = {} for part in [p for p in re.split(r"[;|]", text or "") if p.strip()]: if ":" not in part: raise LedgerError("%s : %r devrait ressembler à Nom:valeur" % (what, part.strip())) k, v = part.split(":", 1) k = k.strip() out[k] = out.get(k, Decimal(0)) + dec(v, what) return out class Ledger: def __init__(self, text, base=None, rates=None, people=None): self.warnings = [] self.base = base self.rates = {} self.people = [] self._canon = {} header_people = None body = [] for line in text.splitlines(): s = line.strip() if s.startswith("#"): m = re.match(r"#\s*(people|base|rates?)\s*:\s*(.*)$", s, re.I) if m: key, val = m.group(1).lower(), m.group(2) if key == "people": header_people = [p.strip() for p in re.split(r"[,;]", val) if p.strip()] elif key == "base" and not self.base: self.base = val.strip().upper() elif key.startswith("rate"): self._add_rates(val) continue body.append(line) self.base = (self.base or "EUR").upper() if rates: self._add_rates(rates) self.rates[self.base] = Decimal(1) for p in (people or header_people or []): self._person(p, declare=True) self.strict = bool(people or header_people) rows = list(csv.DictReader(io.StringIO("\n".join(body)))) if not rows: raise LedgerError("aucune ligne de dépense trouvée (une ligne d'en-tête et au moins une ligne sont requises)") need = {"description", "paid_by", "amount", "split"} cols = {c.strip().lower() for c in rows[0].keys() if c} if not need <= cols: raise LedgerError("colonnes CSV manquantes : %s" % ", ".join(sorted(need - cols))) self.rows = [] for r in rows: r = {(k or "").strip().lower(): (v or "").strip() for k, v in r.items()} if not any(r.values()): continue self.rows.append(r) # première passe : apprendre chaque nom quand aucune liste de personnes n'est donnée if not self.strict: for r in self.rows: for p in self._names_in(r): self._person(p, declare=True) self.lines = [self._line(n, r) for n, r in enumerate(self.rows, 1)] self._check_duplicates() # -- personnes et taux def _add_rates(self, text): for part in re.split(r"[,;]", text): if not part.strip(): continue if "=" not in part: raise LedgerError("taux %r devrait ressembler à USD=0.92" % part.strip()) k, v = part.split("=", 1) r = dec(v, "taux " + k.strip()) if r <= 0: raise LedgerError("le taux pour %s doit être positif" % k.strip()) self.rates[k.strip().upper()] = r def _person(self, name, declare=False): key = name.strip().lower() if not key: raise LedgerError("nom de personne vide") if key in self._canon: canon = self._canon[key] if canon != name.strip() and declare: self.warnings.append("'%s' et '%s' traités comme la même personne" % (canon, name.strip())) return canon if not declare: raise LedgerError("personne inconnue %r (pas dans la liste des personnes)" % name.strip()) self._canon[key] = name.strip() self.people.append(name.strip()) return name.strip() def _names_in(self, r): names = [] pb = r.get("paid_by", "") names += list(parse_pairs(pb, "paid_by").keys()) if ":" in pb else [pb] part = r.get("participants", "") if part and not part.lower().startswith("all"): names += [p for p in re.split(r"[;|]", part) if p.strip()] if r.get("split", "").lower() != "payment": names += [k for k in parse_pairs(r.get("details", ""), "details") if k.lower() != "shared"] return [n for n in names if n.strip()] def _participants(self, r, ln): part = r.get("participants", "").strip() if not part: return None if part.lower().startswith("all"): rest = part[3:].strip() excl = [] if rest: rest = re.sub(r"^(except|but|without)\s+", "", rest.lstrip("-").strip(), flags=re.I) excl = [self._person(x) for x in re.split(r"[-;|,]", rest) if x.strip()] return [p for p in self.people if p not in excl] return [self._person(p) for p in re.split(r"[;|]", part) if p.strip()] # -- une ligne CSV -> payé / part en centimes de base def _line(self, ln, r): desc = r.get("description") or "ligne %d" % ln where = "ligne %d (%s)" % (ln, desc) split = (r.get("split") or "equal").lower() if split not in SPLITS: raise LedgerError("%s : répartition inconnue %r (utilisez %s)" % (where, split, ", ".join(SPLITS))) cur = (r.get("currency") or self.base).upper() if cur not in self.rates: raise LedgerError("%s : pas de taux de change pour %s (ajoutez '# rates: %s=...' ou --rates)" % (where, cur, cur)) amount = dec(r.get("amount"), where + " montant") if amount == 0: self.warnings.append("%s a un montant de 0 et a été ignoré" % where) total = to_cents(amount * self.rates[cur]) # payeurs pb = r.get("paid_by", "") if ":" in pb: pays = {self._person(k): v for k, v in parse_pairs(pb, where + " paid_by").items()} if sum(pays.values()) != amount: raise LedgerError("%s : les payeurs totalisent %s mais le montant est %s" % (where, sum(pays.values()), amount)) paid = allocate(total, {k: abs(v) for k, v in pays.items()}, self.people) if total else {} if total < 0: paid = {k: -abs(v) for k, v in paid.items()} else: paid = {self._person(pb): total} if total else {} people = self._participants(r, ln) details = parse_pairs(r.get("details", ""), where + " details") if split != "payment" else {} share = {} if split == "payment": if not people or len(people) != 1: raise LedgerError("%s : un paiement nécessite exactement un destinataire dans participants" % where) share = {people[0]: total} elif split == "equal": people = people or self.people share = allocate(total, {p: Decimal(1) for p in people}, self.people) elif split == "shares": d = {self._person(k): v for k, v in details.items()} people = people or list(d) w = {p: d.get(p, Decimal(1)) for p in people} extra = [p for p in d if p not in people] if extra: raise LedgerError("%s : %s dans details mais pas dans participants" % (where, ", ".join(extra))) if any(v < 0 for v in w.values()): raise LedgerError("%s : les parts ne peuvent pas être négatives" % where) share = allocate(total, w, self.people) elif split == "percent": d = {self._person(k): v for k, v in details.items()} if sum(d.values()) != 100: raise LedgerError("%s : les pourcentages totalisent %s, pas 100" % (where, sum(d.values()))) people = list(d) share = allocate(total, d, self.people) elif split == "exact": d = {self._person(k): v for k, v in details.items()} people = people or list(d) fixed = sum(d.values()) rest_people = [p for p in people if p not in d] if fixed > abs(amount) or (fixed != abs(amount) and not rest_people): raise LedgerError("%s : les montants exacts totalisent %s mais la facture est %s" % (where, fixed, amount)) w = dict(d) if rest_people: each = (abs(amount) - fixed) / len(rest_people) for p in rest_people: w[p] = each share = allocate(total, w, self.people) elif split == "itemized": people = people or self.people shared = details.pop("shared", details.pop("Shared", Decimal(0))) d = {self._person(k): v for k, v in details.items()} items = sum(d.values()) + shared if items > abs(amount): raise LedgerError("%s : les articles totalisent %s, plus que la facture %s" % (where, items, amount)) per = {p: d.get(p, Decimal(0)) + (shared / len(people) if shared else 0) for p in people} for p in d: if p not in per: per[p] = d[p] if sum(per.values()) <= 0: raise LedgerError("%s : aucun article à répartir" % where) # taxes/pourboire proportionnels aux articles de chaque personne == répartir le total par les poids des articles share = allocate(total, per, self.people) rec = {"n": ln, "date": r.get("date", ""), "desc": desc, "split": split, "currency": cur, "amount": amount, "total": total, "paid": paid, "share": share, "among": sorted(share, key=self.people.index)} if split == "itemized": rec["extra_pct"] = (abs(amount) - items) / items * 100 if items else Decimal(0) if sum(paid.values()) != sum(share.values()): raise LedgerError("%s : erreur d'arrondi interne" % where) # jamais attendu return rec def _check_duplicates(self): seen = {} for l in self.lines: key = (l["date"], l["desc"].lower(), l["total"], tuple(sorted(l["paid"]))) if key in seen: self.warnings.append("la ligne %d ressemble à un doublon de la ligne %d (%s, %s)" % (l["n"], seen[key], l["desc"], fmt(l["total"], self.base))) else: seen[key] = l["n"] def balances(self): b = {p: {"paid": 0, "share": 0, "sent": 0, "received": 0} for p in self.people} for l in self.lines: if l["split"] == "payment": for p, c in l["paid"].items(): b[p]["sent"] += c for p, c in l["share"].items(): b[p]["received"] += c else: for p, c in l["paid"].items(): b[p]["paid"] += c for p, c in l["share"].items(): b[p]["share"] += c for p in b: b[p]["net"] = b[p]["paid"] + b[p]["sent"] - b[p]["share"] - b[p]["received"] return b # ----------------------------------------------------------------------------- règlement def greedy(net, order): """Le plus grand débiteur paie le plus grand créancier jusqu'à ce que tout le monde soit à zéro (au plus k-1 virements).""" cred = {p: v for p, v in net.items() if v > 0} debt = {p: -v for p, v in net.items() if v < 0} out = [] while cred and debt: c = max(cred, key=lambda p: (cred[p], -order.index(p))) d = max(debt, key=lambda p: (debt[p], -order.index(p))) x = min(cred[c], debt[d]) out.append((d, c, x)) cred[c] -= x debt[d] -= x if not cred[c]: del cred[c] if not debt[d]: del debt[d] return out def minimal(net, order): """Moins de virements : n - (nombre maximal de groupes à somme nulle disjoints). Renvoie (virements, groupes, exact).""" people = [p for p in order if net.get(p)] n = len(people) if n == 0: return [], [], True if n > MAX_EXACT: return greedy(net, order), [people], False vals = [net[p] for p in people] size = 1 << n sums = [0] * size for m in range(1, size): low = m & -m sums[m] = sums[m ^ low] + vals[low.bit_length() - 1] dp = [0] * size for m in range(1, size): best = 0 mm = m while mm: low = mm & -mm v = dp[m ^ low] if v > best: best = v mm ^= low dp[m] = best + (1 if sums[m] == 0 else 0) # descendre, coupant un groupe à chaque point de contrôle à somme nulle groups, m, last = [], size - 1, size - 1 while m: mm, pick = m, None target = dp[m] - (1 if sums[m] == 0 else 0) while mm: low = mm & -mm if dp[m ^ low] == target: pick = low break mm ^= low m ^= pick if sums[m] == 0: groups.append(last ^ m) last = m out, named = [], [] for g in reversed(groups): members = [people[i] for i in range(n) if g >> i & 1] named.append(members) out += greedy({p: net[p] for p in members}, order) return out, named, True # ----------------------------------------------------------------------------- sortie def render(led, detail=None): b = led.balances() net = {p: b[p]["net"] for p in led.people} tr, groups, exact = minimal(net, led.people) gr = greedy(net, led.people) B = led.base o = ["# Plan de règlement", "", "Devise de base : %s Personnes : %d Lignes : %d Total dépensé : %s" % ( B, len(led.people), len(led.lines), fmt(sum(l["total"] for l in led.lines if l["split"] != "payment"), B))] others = sorted(c for c in led.rates if c != B) if others: o.append("Taux utilisés : " + ", ".join("1 %s = %s %s" % (c, led.rates[c].normalize(), B) for c in others)) o += ["", "## Dépenses", "", "| # | Date | Quoi | Payé par | Montant | En %s | Répartition | Entre |" % B, "|---|---|---|---|---|---|---|---|"] for l in led.lines: payer = ", ".join("%s %s" % (p, fmt(c)) if len(l["paid"]) > 1 else p for p, c in l["paid"].items()) among = "all" if len(l["among"]) == len(led.people) and l["split"] != "payment" else ", ".join(l["among"]) split = l["split"] + (" (+%.1f%% tax/tip)" % l["extra_pct"] if l["split"] == "itemized" else "") if l["split"] == "payment": among = "à " + among o.append("| %d | %s | %s | %s | %s %s | %s | %s | %s |" % (l["n"], l["date"], l["desc"].replace("|", "/"), payer, fmt(to_cents(l["amount"])), l["currency"], fmt(l["total"]), split, among)) o += ["", "## Soldes", "", "| Personne | Payé | Part | Envoyé | Reçu | Net |", "|---|---|---|---|---|---|"] for p in led.people: x = b[p] state = "récupère" if x["net"] > 0 else ("doit" if x["net"] < 0 else "à zéro") o.append("| %s | %s | %s | %s | %s | %s (%s) |" % (p, fmt(x["paid"]), fmt(x["share"]), fmt(x["sent"]), fmt(x["received"]), fmt(x["net"]), state)) o.append("") o.append("Vérification : les nets totalisent %s (payé %s = parts %s)." % (fmt(sum(net.values())), fmt(sum(x["paid"] for x in b.values())), fmt(sum(x["share"] for x in b.values())))) nz = sum(1 for v in net.values() if v) o += ["", "## Virements (%d)" % len(tr), ""] if not tr: o.append("Tout le monde est à zéro. Rien à payer.") else: for i, (d, c, x) in enumerate(tr, 1): o.append("%d. %s paie %s **%s**" % (i, d, c, fmt(x, B))) o.append("") if exact: if len(groups) > 1: o.append("Minimum possible : %d virements. Le groupe se divise en %d ensembles qui se règlent entre eux (%s), donc %d personnes n'ont besoin que de %d virements ; payer la plus grande dette à la plus grande créance prendrait %d." % ( len(tr), len(groups), "; ".join(" + ".join(g) for g in groups), nz, len(tr), len(gr))) else: o.append("Minimum possible : %d virements pour %d personnes avec un solde (aucun sous-groupe plus petit ne s'équilibre seul)." % (len(tr), nz)) else: o.append("%d personnes ont un solde ; recherche exacte du minimum ignorée (limite %d), le plan glouton utilise au plus %d virements." % (nz, MAX_EXACT, nz - 1)) if detail: p = led._person(detail) o += ["", "## Détail : %s" % p, "", "| # | Quoi | Payé | Part | Net cumulé |", "|---|---|---|---|---|"] run = 0 for l in led.lines: pd, sh = l["paid"].get(p, 0), l["share"].get(p, 0) if not pd and not sh: continue run += pd - sh o.append("| %d | %s | %s | %s | %s |" % (l["n"], l["desc"], fmt(pd), fmt(sh), fmt(run))) if led.warnings: o += ["", "## Avertissements", ""] + ["- " + w for w in led.warnings] o += ["", "## Message pour le chat de groupe", "", "```"] msg = ["Règlement (%s, %d lignes, taux : %s) :" % (B, len(led.lines), ", ".join("%s %s" % (c, led.rates[c].normalize()) for c in others) or "aucun")] msg += ["- %s -> %s : %s" % (d, c, fmt(x, B)) for d, c, x in tr] or ["- tout le monde est à zéro"] msg.append("Tableau complet dans la feuille partagée. Criez sous 2 jours si une ligne est fausse.") o += msg + ["```"] return "\n".join(o) + "\n" def as_json(led): b = led.balances() net = {p: b[p]["net"] for p in led.people} tr, groups, exact = minimal(net, led.people) return {"base": led.base, "people": led.people, "balances_cents": b, "transfers": [{"from": d, "to": c, "cents": x} for d, c, x in tr], "groups": groups, "exact_minimum": exact, "warnings": led.warnings} # ----------------------------------------------------------------------------- auto-test def self_test(): import random cases = [] def case(name, fn): try: ok, err = bool(fn()), "" except Exception as e: # noqa ok, err = False, repr(e) cases.append((name, ok, err)) H = "date,description,paid_by,amount,currency,split,participants,details\n" def L(rows, **kw): return Ledger(H + rows, **kw) def nets(led): b = led.balances() return {p: b[p]["net"] for p in led.people} def raises(fn): try: fn() except LedgerError: return True return False case("répartition égale au centime", lambda: nets(L(",Taxi,Ana,10.00,,equal,Ana;Ben;Cem,\n")) == {"Ana": 666, "Ben": -333, "Cem": -333}) case("le centime restant va au premier dans l'ordre", lambda: L(",X,Ana,0.10,,equal,Ana;Ben;Cem,\n").lines[0]["share"] == {"Ana": 4, "Ben": 3, "Cem": 3}) case("tous moins exclusion", lambda: Ledger("# people: Ana, Ben, Cem\n" + H + ",X,Ana,30,,equal,all-Cem,\n").lines[0]["among"] == ["Ana", "Ben"]) case("tous sauf mots", lambda: Ledger("# people: Ana, Ben, Cem\n" + H + ",X,Ana,30,,equal,all except Ben,\n").lines[0]["among"] == ["Ana", "Cem"]) case("poids des parts", lambda: L(",Room,Ana,100,,shares,,Ana:3;Ben:1\n").lines[0]["share"] == {"Ana": 7500, "Ben": 2500}) case("parts défaut 1 pour participants non listés", lambda: L(",Room,Ana,90,,shares,Ana;Ben;Cem,Ana:2;Ben:0\n").lines[0]["share"] == {"Ana": 6000, "Cem": 3000}) case("montants exacts", lambda: L(",Tickets,Ana,75,,exact,,Ana:30;Ben:45\n").lines[0]["share"] == {"Ana": 3000, "Ben": 4500}) case("exact avec reste réparti également", lambda: L(",T,Ana,100,,exact,Ana;Ben;Cem,Ana:40\n").lines[0]["share"] == {"Ana": 4000, "Ben": 3000, "Cem": 3000}) case("exact non concordant rejeté", lambda: raises(lambda: L(",T,Ana,80,,exact,,Ana:30;Ben:45\n"))) case("pourcentages", lambda: L(",Wine,Ana,36,,percent,,Ana:25;Ben:25;Cem:50\n").lines[0]["share"] == {"Ana": 900, "Ben": 900, "Cem": 1800}) case("pourcentages pas 100 rejetés", lambda: raises(lambda: L(",W,Ana,36,,percent,,Ana:25;Ben:25\n"))) case("détaillé avec partagé et taxes/pourboire", lambda: L(",Dinner,Ana,66,,itemized,Ana;Ben,Ana:20;Ben:30;shared:10\n").lines[0]["share"] == {"Ana": 2750, "Ben": 3850}) case("détaillé dépassant la facture rejeté", lambda: raises(lambda: L(",D,Ana,40,,itemized,Ana;Ben,Ana:20;Ben:30\n"))) case("ligne de paiement", lambda: nets(L(",Food,Ana,20,,equal,Ana;Ben,\n,Paid back,Ben,10,,payment,Ana,\n")) == {"Ana": 0, "Ben": 0}) case("paiement nécessite un destinataire", lambda: raises(lambda: L(",P,Ben,10,,payment,Ana;Cem,\n"))) case("plusieurs payeurs", lambda: L(",Train,Ana:40;Ben:20,60,,equal,Ana;Ben;Cem,\n").lines[0]["paid"] == {"Ana": 4000, "Ben": 2000}) case("plusieurs payeurs doivent totaliser", lambda: raises(lambda: L(",Train,Ana:40;Ben:10,60,,equal,Ana;Ben,\n"))) case("remboursement (montant négatif)", lambda: nets(L(",Museum refund,Ben,-24,,equal,Ben;Cem,\n")) == {"Ben": -1200, "Cem": 1200}) case("conversion de devise", lambda: L(",Surf,Ana,100,USD,equal,Ana;Ben,\n", rates="USD=0.92").lines[0]["total"] == 9200) case("taux depuis la ligne de commentaire", lambda: Ledger("# base: EUR\n# rates: USD=0.92, GBP=1.16\n" + H + ",S,Ana,10,GBP,equal,Ana;Ben,\n").lines[0]["total"] == 1160) case("taux manquant rejeté", lambda: raises(lambda: L(",S,Ana,10,CHF,equal,Ana;Ben,\n"))) case("répartition inconnue rejetée", lambda: raises(lambda: L(",S,Ana,10,,halves,Ana;Ben,\n"))) case("liste stricte des personnes rejette la faute de frappe", lambda: raises(lambda: Ledger("# people: Ana, Ben\n" + H + ",S,Anna,10,,equal,all,\n"))) case("les noms insensibles à la casse fusionnent", lambda: L(",A,Ana,10,,equal,ana;Ben,\n").people == ["Ana", "Ben"]) case("avertissement de ligne en double", lambda: any("doublon" in w for w in L("1,Taxi,Ana,10,,equal,Ana;Ben,\n1,Taxi,Ana,10,,equal,Ana;Ben,\n").warnings)) case("colonnes manquantes rejetées", lambda: raises(lambda: Ledger("what,who\nx,y\n"))) case("mauvais nombre rejeté", lambda: raises(lambda: L(",S,Ana,ten,,equal,Ana;Ben,\n"))) # virements minimaux case("paires à somme nulle trouvées (4 personnes -> 2 virements)", lambda: len(minimal({"A": 500, "B": -500, "C": 300, "D": -300}, list("ABCD"))[0]) == 2) case("pas de sous-groupe à somme nulle -> personnes - 1 virements", lambda: len(greedy({"A": 600, "B": 400, "C": -500, "D": -500}, list("ABCD"))) == 3 and len(minimal({"A": 600, "B": 400, "C": -500, "D": -500}, list("ABCD"))[0]) == 3) case("le minimum bat le glouton sur 6 personnes", lambda: (lambda n: len(minimal(n, list("ABCDEF"))[0]) == 4 and len(greedy(n, list("ABCDEF"))) == 5)({"A": 700, "B": 300, "C": 250, "D": -500, "E": -450, "F": -300})) case("tout le monde à zéro -> aucun virement", lambda: minimal({"A": 0, "B": 0}, ["A", "B"])[0] == []) def brute_min(net): ps = [p for p in net if net[p]] best = len(ps) - 1 if ps else 0 for parts in _partitions(ps): if all(sum(net[p] for p in g) == 0 for g in parts): best = min(best, len(ps) - len(parts)) return best def check_random(): rnd = random.Random(7) for _ in range(60): k = rnd.randint(2, 7) vals = [rnd.choice([-1, 1]) * rnd.choice([100, 200, 300, 500, 250, 50]) for _ in range(k - 1)] vals.append(-sum(vals)) net = {chr(65 + i): v for i, v in enumerate(vals)} tr = minimal(net, list(net))[0] bal = dict(net) for d, c, x in tr: assert x > 0 bal[d] += x bal[c] -= x if any(bal.values()) or len(tr) != brute_min(net): return False return True case("minimal == force brute sur 60 groupes aléatoires et règle tout le monde", check_random) case("les virements règlent le registre de type exemple", lambda: _settles(L(",Flat,Ana,840,,shares,,Ana:3;Ben:3;Cem:2\n,Food,Cem,63.75,,equal,all,\n,Wine,Ben,36,,percent,,Ana:25;Ben:25;Cem:50\n"))) case("le rendu a des virements et un message de chat", lambda: "Message pour le chat de groupe" in render(L(",Taxi,Ana,10,,equal,Ana;Ben,\n"))) case("vue détaillée", lambda: "Détail : Ben" in render(L(",Taxi,Ana,10,,equal,Ana;Ben,\n"), "ben")) case("sortie json", lambda: as_json(L(",Taxi,Ana,10,,equal,Ana;Ben,\n"))["transfers"] == [{"from": "Ben", "to": "Ana", "cents": 500}]) case("format des milliers", lambda: fmt(123456789, "EUR") == "1,234,567.89 EUR" and fmt(-5) == "-0.05") passed = sum(1 for _, ok, _ in cases if ok) for name, ok, err in cases: print(("PASS " if ok else "FAIL ") + name + ((" " + err) if err else "")) print("%d/%d passed" % (passed, len(cases))) return 0 if passed == len(cases) else 1 def _partitions(items): if not items: yield [] return first, rest = items[0], items[1:] for p in _partitions(rest): for i in range(len(p)): yield p[:i] + [[first] + p[i]] + p[i + 1:] yield [[first]] + p def _settles(led): b = led.balances() net = {p: b[p]["net"] for p in led.people} for d, c, x in minimal(net, led.people)[0]: net[d] += x net[c] -= x return not any(net.values()) def main(argv=None): ap = argparse.ArgumentParser(description="Répartir les dépenses partagées et planifier le moins de virements de règlement.") ap.add_argument("file", nargs="?", help="registre CSV ('-' = entrée standard)") ap.add_argument("--base", default=None, help="devise de base (défaut : ligne '# base:' ou EUR)") ap.add_argument("--rates", default=None, help="USD=0.92,GBP=1.16 (1 unité = taux unités de la devise de base)") ap.add_argument("--people", default=None, help="membres du groupe séparés par des virgules (strict : les fautes de frappe sont des erreurs)") ap.add_argument("--detail", default=None, help="afficher la ventilation ligne par ligne pour une personne") ap.add_argument("--json", action="store_true") ap.add_argument("--self-test", action="store_true") a = ap.parse_args(argv) if a.self_test: return self_test() if not a.file: ap.error("donnez un registre CSV ou --self-test") try: text = sys.stdin.read() if a.file == "-" else open(a.file, encoding="utf-8-sig").read() people = [p.strip() for p in a.people.split(",")] if a.people else None led = Ledger(text, a.base, a.rates, people) out = json.dumps(as_json(led), indent=2) + "\n" if a.json else render(led, a.detail) except (OSError, LedgerError) as e: print("erreur : %s" % e, file=sys.stderr) return 2 sys.stdout.write(out) return 0 if __name__ == "__main__": sys.exit(main())

Connectez-vous pour voir le prompt complet

Continuer avec:

En vous connectant, vous acceptez nos Conditions et Confidentialité

Utilisation

Ce prompt est conçu pour être utilisé avec productivity. Copiez le contenu ci-dessus et collez-le dans votre outil d’IA préféré.

Pour de meilleurs résultats, personnalisez les espaces réservés (indiqués par des crochets ou des majuscules) selon vos besoins.

Références

Catégories :productivity| prompts.chat| expense-splitter| bill-splitter

Discussion