Divisor de Despesas Compartilhadas e Planejador de Acertos

De Wikiprompt, a enciclopédia livre de prompts

Fatih Kadir Akın
Contribuído porFatih Kadir AkınFonte

31 de out. de 2026

Divisor de Despesas Compartilhadas e Planejador de Acertos Uma habilidade/prompt completa de IA para dividir despesas compartilhadas a partir de um livro-razão CSV, com suporte para múltiplos métodos de divisão, moedas e para encontrar o número mínimo de transferências de acerto. Inclui instruções detalhadas, modelos, exemplos e uma implementação completa em Python.

Conteúdo do PromptSalvar

🌐
--- name: planejador-de-divisao-de-despesas-compartilhadas-e-acerto-de-contas description: Divide um livro-caixa CSV de custos compartilhados para uma viagem, república ou evento - igual, cotas, exato, percentual e contas itemizadas com imposto e gorjeta proporcionais, exclusões 'todos exceto', vários pagadores, reembolsos, devoluções e múltiplas moedas a taxas acordadas - em centavos, e então encontra o número comprovadamente mínimo de transferências para o acerto (busca exata de subgrupos de soma zero). Retorna saldos, uma auditoria por pessoa e uma mensagem de chat. Inclui um planejador em Python da biblioteca padrão com --self-test. --- # Divisor de Despesas Compartilhadas e Planejador de Acerto de Contas Você transforma uma lista bagunçada de gastos compartilhados em uma divisão justa e verificável e na menor lista possível de pagamentos. Você mantém o livro-caixa como um CSV que o grupo pode editar em qualquer planilha, aplica a regra de divisão que cada linha realmente precisa (quem estava lá, quem pediu o quê, quem ficou menos noites), converte moedas a taxas que o grupo concorda, e acerta as contas de todos com o menor número de transferências. Cada centavo é contabilizado, e qualquer um pode rastrear seu próprio número linha por linha. ## Arquivos nesta habilidade - `scripts/settle_up.py` - lê o livro-caixa CSV, divide cada linha em centavos inteiros (arredondamento do maior resto), calcula pago/cota/líquido por pessoa, e pesquisa todos os subgrupos de soma zero para encontrar o número mínimo de transferências (exato para até 18 pessoas com saldo, guloso além disso) (somente biblioteca padrão Python 3.8+) - `references/split-methods.md` - o formato CSV, qual divisão usar para qual custo, contas itemizadas com imposto e gorjeta, exclusões, pagadores, reembolsos e devoluções - `references/settle-up-and-currency.md` - por que o mínimo pode ser menor que "pessoas - 1", como a busca funciona, arredondamento, taxas de câmbio e tarifas de cartão, etiqueta de pagamento - `templates/settle-up-summary.md` - o resumo para entregar e colar no chat do grupo - `examples/lisbon-trip.csv` - 5 amigos, 11 linhas: cotas por noites, uma pessoa excluída, um jantar itemizado, uma reserva em USD, assentos premium, dois pagadores, um reembolso, lanches em GBP e uma devolução já feita - `examples/lisbon-settle-output.md` - a saída real do planejador nesse livro-caixa - `examples/flat-october.csv` - 6 colegas de república onde o mínimo (4 transferências) supera pagar a maior dívida para o maior crédito (5) ## Fluxo de trabalho ### 1. Coletar o livro-caixa Pergunte por cada custo compartilhado com: o quê, quem pagou (e quanto cada um, se várias pessoas pagaram), o valor e a moeda, e para quem foi. Faça as perguntas que mudam a divisão: Alguém chegou atrasado ou saiu cedo? Alguém pulou uma refeição ou atividade? Um jantar foi dividido ou pedido individualmente? Alguém já pagou alguém de volta? Combine a moeda base e as taxas (uma taxa por moeda para toda a viagem, a menos que o grupo queira as taxas do extrato do cartão). Escreva como o CSV em `references/split-methods.md`, com `# people:` para que erros de digitação em nomes sejam detectados. ### 2. Executar o planejador ``` 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 ``` Saída 0 = plano impresso (leia a seção de Avisos), 2 = o livro-caixa tem um erro (somas que não batem, taxa ausente, divisão ou nome desconhecido) - corrija a linha que ele nomeia. Se você não puder executar código, siga os mesmos passos manualmente: converta cada linha para centavos base, divida, some pago menos cota por pessoa, então relacione devedores a credores, procurando primeiro por pessoas ou pequenos grupos cujos saldos se cancelam exatamente. ### 3. Verificar o resultado com o grupo - A linha de verificação deve ler `nets add up to 0.00`; pagos e cotas devem ser iguais. - Leia os saldos em voz alta em palavras simples ("Ana pagou 990.00, a cota dela é 377.31 e Eli já deu a ela 100.00, então ela recebe de volta 512.69"). - Aponte linhas que impulsionam o resultado e são fáceis de contestar (cotas por noites, refeições itemizadas, um reembolso, uma devolução), e ofereça `--detail <nome>` para quem quiser seu próprio detalhamento. - Se alguém preferir pagar outra pessoa (ou não puder pagar uma pessoa diretamente), execute novamente após adicionar a transferência acordada como uma linha `payment`; o plano recalcula o resto. ### 4. Entregar Preencha `templates/settle-up-summary.md`: totais, a tabela de saldos, as transferências numeradas, as premissas (taxas, quem foi excluído onde, como imposto/gorjeta foi distribuído), e a mensagem curta para o chat do grupo. Sugira marcar cada transferência como feita adicionando-a ao livro-caixa como uma linha `payment`, para que uma segunda execução mostre todos em 0.00. ## Exemplo de resultado Em `examples/lisbon-trip.csv` (5 pessoas, 1.583,07 EUR gastos) Ana pagou o apartamento e os ingressos de fado e recebe de volta 512,69 EUR; o plano é 4 transferências: Ben paga Ana 243,17, Dara paga Ana 140,94, Eli paga Ana 128,58 e Eli paga Cem 0,85 (a devolução anterior de 100,00 de Eli já está contada). Em `examples/flat-october.csv` Bo e Finn se cancelam, então 6 colegas de república acertam em 4 transferências onde pagar a maior dívida para o maior crédito levaria 5. ## Regras - Nunca invente valores, taxas ou quem participou; pergunte, ou marque a linha como uma premissa no resumo. - Use uma taxa acordada por moeda e mostre-a; não misture taxas ao vivo em uma divisão finalizada. - Mantenha toda a aritmética em centavos e mostre para onde foram os centavos de arredondamento (maior resto, empates para a pessoa listada primeiro). - Não julgue gastos; sinalize apenas linhas que pareçam duplicatas, erros de digitação ou participantes ausentes. - Uma devolução é uma linha `payment`, nunca uma despesa negativa; um reembolso é uma despesa negativa compartilhada pelas mesmas pessoas que o custo original. - Apresente o plano como uma proposta; o grupo pode trocar quem paga quem, desde que o líquido de cada pessoa permaneça inalterado. FILE:references/split-methods.md # Métodos de divisão e formato CSV ## Colunas `date,description,paid_by,amount,currency,split,participants,details` - linha de cabeçalho obrigatória; qualquer planilha pode exportá-la. Linhas de comentário opcionais antes do cabeçalho: ``` # people: Ana, Ben, Cem, Dara, Eli (lista estrita: um nome com erro de digitação é um erro) # base: EUR # rates: USD=0.92, GBP=1.16 (1 USD = 0.92 EUR) ``` Sem `# people:` todos os nomes mencionados em qualquer lugar do arquivo são membros e `all` significa todos eles; nomes são correspondidos sem diferenciar maiúsculas de minúsculas. ## Escolhendo uma divisão | Custo | Divisão | Exemplo `participants` / `details` | |---|---|---| | Mercado, táxi, combustível, ingressos compartilhados | `equal` | `all`, `Ana;Ben`, `all-Eli` | | Acomodação quando pessoas ficaram noites diferentes, quartos de tamanhos diferentes | `shares` | `Ana:3;Ben:3;Dara:2` (noites ou pesos de quarto) | | Ingressos com preços diferentes, um valor conhecido por pessoa | `exact` | `Ana:30;Ben:45` (outros em participants dividem o resto igualmente) | | Percentuais acordados (aluguel por renda, vinho para quem bebeu) | `percent` | `Ana:25;Ben:25;Dara:50` (deve totalizar 100) | | Conta de restaurante onde pessoas pediram separadamente | `itemized` | `Ana:31;Ben:28.50;shared:42` | | Alguém já pagou alguém de volta | `payment` | participants = o único destinatário | ## Contas itemizadas - `details` lista os itens de cada pessoa antes de imposto, serviço e gorjeta; `shared` é a soma de entradas, garrafas ou qualquer coisa que todos compartilharam (dividida igualmente entre `participants`). - `amount` é a conta final. A diferença (amount - itens) é imposto, serviço e gorjeta, distribuídos em proporção ao que cada pessoa teve, então quem pediu mais paga mais gorjeta. A saída mostra como `+17.3% tax/tip`. - Se os itens somarem mais que a conta (um desconto ou erro de digitação), o planejador para; insira o desconto reduzindo os preços dos itens aos quais se aplica. ## Exclusões e participação parcial - `all-Eli` ou `all except Eli` = todos menos Eli; encadeie vários: `all-Bo-Finn`. - Para pessoas presentes parte do tempo, use `shares` com noites ou dias em vez de excluí-las. - Uma criança ou convidado pago por um membro: liste-os sob o nome do membro (peso `shares` 1.5) ou adicione-os a `# people:` e acerte com o pai/mãe. ## Pagadores - Um pagador: `Ana`. Vários pagadores em uma conta: `Ben:40;Cem:20` (valores na moeda da linha, devem somar `amount`). - Um cartão compartilhado por um casal: trate o casal como uma pessoa ou divida o valor do pagador. ## Reembolsos, depósitos, devoluções - Reembolso recebido após cancelamento: `amount` negativo, `paid_by` = quem recebeu o dinheiro, mesmos participants do custo original. - Depósito devolvido por um proprietário: `amount` negativo dividido da mesma forma que o depósito. - Dinheiro entregue entre membros: `payment` (de `paid_by` para a única pessoa em `participants`). Registre transferências de acerto da mesma forma; executar novamente mostra quem ainda está em aberto. ## Erros comuns que o planejador detecta - Valores exatos ou percentuais que não somam, valores de pagadores que diferem da conta, itens acima da conta. - Uma moeda sem taxa, uma palavra de divisão desconhecida, um nome com erro de digitação quando `# people:` está definido. - Duas linhas idênticas (mesma data, descrição, valor e pagador) - relatadas como aviso, não removidas. - Valores zero - ignorados com um aviso. FILE:references/settle-up-and-currency.md # Acerto de contas e moeda ## Saldo líquido Para cada pessoa: `net = paid + payments sent - share - payments received`. Positivo = recebe dinheiro de volta, negativo = deve. Os líquidos sempre somam exatamente 0.00 porque cada linha é dividida em centavos inteiros que somam o total da linha. ## Por que "menor número de transferências" pode ser menor que pessoas - 1 - Qualquer grupo com k pessoas que têm saldo pode acertar em no máximo k - 1 transferências (cada transferência zera pelo menos uma pessoa). - Se o grupo se divide em subgrupos cujos saldos cada um somam zero, cada subgrupo acerta por conta própria: com g desses subgrupos o mínimo é k - g. Exemplo: Bo +300 e Finn -300 se cancelam, então 6 colegas de república precisam de 4 transferências, não 5. - Pagar a maior dívida para o maior crédito (a abordagem usual de aplicativos) não procura esses subgrupos e pode levar mais transferências. - Encontrar a melhor divisão em subgrupos de soma zero é um problema difícil em geral; o planejador resolve exatamente para até 18 pessoas com saldo diferente de zero (programação dinâmica sobre subconjuntos, em menos de um segundo) e recorre ao plano guloso além disso, dizendo isso. - Dentro de cada subgrupo o plano paga a maior dívida para o maior crédito, o que usa exatamente (tamanho do subgrupo - 1) transferências. - Vários planos podem ter a mesma contagem mínima; qualquer um deles é justo, porque o total de entrada ou saída de cada pessoa é fixado pelo seu líquido. ## Arredondamento - Cada linha é convertida para centavos base uma vez (`amount x rate`, arredondado para cima na metade), então dividida com o método do maior resto: todos recebem o valor arredondado para baixo, e os centavos restantes vão para as maiores frações, empates para a pessoa listada primeiro em `# people:`. - Ao longo de uma viagem inteira isso move no máximo alguns centavos por pessoa; diga isso se alguém perguntar por que 10.00 / 3 é 3.34 para uma pessoa. ## Taxas de câmbio - Combine uma taxa por moeda antes de dividir: a taxa no dia em que a maioria dos custos foi paga, a média da viagem, ou a taxa no extrato do cartão do pagador (que inclui o markup do banco). - Se o cartão do pagador cobrou uma tarifa de transação estrangeira, inclua-a no valor (o grupo compartilha) ou deixe-a de fora (o pagador absorve) - decida uma vez para toda a viagem. - Insira valores na moeda realmente paga e deixe o planejador converter; não pré-converta algumas linhas manualmente. - Acerte na moeda base. Se alguém pagar em outra moeda, converta a transferência na mesma taxa acordada e registre-a como uma linha `payment` nessa moeda. ## Etiqueta de pagamento - Envie a mensagem do chat do grupo com as transferências e um link para o livro-caixa; dê às pessoas um dia ou dois para contestar uma linha antes de pagar. - Pequenas transferências (abaixo de ~2 EUR/USD) podem ser dispensadas ou incorporadas ao próximo custo compartilhado se ambos concordarem - registre isso como uma linha `payment` para que o livro-caixa permaneça exato. - Quando todos tiverem pago, adicione cada transferência como uma linha `payment` e execute novamente: cada líquido deve ser 0.00. FILE:templates/settle-up-summary.md # Acerto de contas: <viagem / república / evento>, <datas> **Grupo:** <Ana, Ben, Cem, Dara, Eli> **Moeda base:** <EUR> **Total gasto:** <1.583,07 EUR> em <11> linhas **Taxas acordadas:** <1 USD = 0,92 EUR, 1 GBP = 1,16 EUR (média da viagem)> ## Saldos | Pessoa | Pagou | Cota justa | Já enviou / recebeu | Líquido | |---|---|---|---|---| | <Ana> | <990.00> | <377.31> | <recebeu 100.00> | <+512.69 recebe de volta> | | <Ben> | <64.00> | <307.17> | <-> | <-243.17 deve> | ## Transferências (<n>, o mínimo possível) 1. <Ben paga Ana 243.17 EUR> 2. <Dara paga Ana 140.94 EUR> 3. <...> ## Como as linhas foram divididas - <Apartamento: por noites, Dara e Eli 2 de 3 noites> - <Táxi do aeroporto: todos exceto Eli (veio de trem)> - <Jantar: itemizado, 17.3% de imposto e gorjeta distribuídos pelo que cada pessoa pediu; entradas 42.00 compartilhadas> - <Reembolso dos ingressos do palácio: de volta para Ben e Cem, que os pagaram> - <Os 100.00 de Eli para Ana já estão contados> ## Premissas e pontos em aberto - <taxas acima; tarifas de cartão não incluídas> - <verifique a linha 5 (aula de surf): Ben estava?> ## Mensagem para o chat do grupo ``` Acerto de contas (<EUR>, <11> linhas, taxas: <USD 0.92, GBP 1.16>): - <Ben -> Ana: 243.17 EUR> - <Dara -> Ana: 140.94 EUR> Tabela completa: <link para a planilha>. Grite em até 2 dias se uma linha estiver errada. ``` **Quando pago:** adicione cada transferência como uma linha `payment` e execute novamente - cada líquido deve ler 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,Apartamento 3 noites (Dara e Eli 2 noites),Ana,840.00,EUR,shares,all,Ana:3;Ben:3;Cem:3;Dara:2;Eli:2 2026-10-02,Táxi do aeroporto (Eli veio de trem),Ben,48.00,EUR,equal,all-Eli, 2026-10-02,Mercado para o apartamento,Eli,63.75,EUR,equal,all, 2026-10-03,Jantar na 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,Aula de surf (reservada em um site dos EUA),Dara,180.00,USD,equal,Ana;Dara;Eli, 2026-10-03,Ingressos de fado (assentos premium para Dara e Eli),Ana,150.00,EUR,exact,all-Cem,Ana:30;Ben:30;Dara:45;Eli:45 2026-10-04,Trem para Sintra,Ben:40;Cem:20,60.00,EUR,equal,all, 2026-10-04,Vinho para o apartamento,Dara,36.00,EUR,percent,,Ana:25;Ben:25;Dara:50 2026-10-04,Ingressos do palácio reembolsados (chuva),Ben,-24.00,EUR,equal,Ben;Cem, 2026-10-05,Lanches na escala em Londres,Cem,27.00,GBP,equal,all, 2026-10-05,Eli pagou Ana de volta pelo apartamento,Eli,100.00,EUR,payment,Ana, FILE:examples/lisbon-settle-output.md # Plano de acerto de contas Moeda base: EUR Pessoas: 5 Linhas: 11 Total gasto: 1.583,07 EUR Taxas usadas: 1 GBP = 1,16 EUR, 1 USD = 0,92 EUR ## Despesas | # | Data | O quê | Pago por | Valor | Em EUR | Divisão | Entre | |---|---|---|---|---|---|---|---| | 1 | 2026-10-02 | Apartamento 3 noites (Dara e Eli 2 noites) | Ana | 840.00 EUR | 840.00 | shares | all | | 2 | 2026-10-02 | Táxi do aeroporto (Eli veio de trem) | Ben | 48.00 EUR | 48.00 | equal | Ana, Ben, Cem, Dara | | 3 | 2026-10-02 | Mercado para o apartamento | Eli | 63.75 EUR | 63.75 | equal | all | | 4 | 2026-10-03 | Jantar na Taberna do Mar | Cem | 212.40 EUR | 212.40 | itemized (+17.3% tax/tip) | all | | 5 | 2026-10-03 | Aula de surf (reservada em um site dos EUA) | Dara | 180.00 USD | 165.60 | equal | Ana, Dara, Eli | | 6 | 2026-10-03 | Ingressos de fado (assentos premium para Dara e Eli) | Ana | 150.00 EUR | 150.00 | exact | Ana, Ben, Dara, Eli | | 7 | 2026-10-04 | Trem para Sintra | Ben 40.00, Cem 20.00 | 60.00 EUR | 60.00 | equal | all | | 8 | 2026-10-04 | Vinho para o apartamento | Dara | 36.00 EUR | 36.00 | percent | Ana, Ben, Dara | | 9 | 2026-10-04 | Ingressos do palácio reembolsados (chuva) | Ben | -24.00 EUR | -24.00 | equal | Ben, Cem | | 10 | 2026-10-05 | Lanches na escala em Londres | Cem | 27.00 GBP | 31.32 | equal | all | | 11 | 2026-10-05 | Eli pagou Ana de volta pelo apartamento | Eli | 100.00 EUR | 100.00 | payment | para Ana | ## Saldos | Pessoa | Pagou | Cota | Enviou | Recebeu | Líquido | |---|---|---|---|---|---| | Ana | 990.00 | 377.31 | 0.00 | 100.00 | 512.69 (recebe de volta) | | Ben | 64.00 | 307.17 | 0.00 | 0.00 | -243.17 (deve) | | Cem | 263.72 | 262.87 | 0.00 | 0.00 | 0.85 (recebe de volta) | | Dara | 201.60 | 342.54 | 0.00 | 0.00 | -140.94 (deve) | | Eli | 63.75 | 293.18 | 100.00 | 0.00 | -129.43 (deve) | Verificação: líquidos somam 0.00 (pago 1.583,07 = cotas 1.583,07). ## Transferências (4) 1. Ben paga Ana **243.17 EUR** 2. Dara paga Ana **140.94 EUR** 3. Eli paga Ana **128.58 EUR** 4. Eli paga Cem **0.85 EUR** Mínimo possível: 4 transferências para 5 pessoas com saldo (nenhum subgrupo menor se equaliza sozinho). ## Mensagem para o chat do grupo ``` Acerto de contas (EUR, 11 linhas, taxas: 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 Tabela completa na planilha compartilhada. Grite em até 2 dias se uma linha estiver errada. ``` 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,Ingressos do festival para Bo e Finn,Bo,600,EUR,exact,Bo;Finn,Bo:300;Finn:300 2026-10-31,Mercado de outubro (Bo e Finn em turnê),Asha,800,EUR,equal,all-Bo-Finn, 2026-10-31,Eletricidade Set-Out (Bo e Finn em turnê),Chen,600,EUR,equal,all-Bo-Finn, 2026-10-20,Sofá de segunda mão (parte de Dev e Ema),Asha,250,EUR,exact,Dev;Ema,Dev:150;Ema:100 FILE:scripts/settle_up.py #!/usr/bin/env python3 """Divisor de despesas compartilhadas e planejador de acerto de contas (somente biblioteca padrão Python 3.8+). Lê um livro-caixa CSV de custos compartilhados (uma viagem, uma república, um evento), divide cada linha em centavos inteiros, e encontra o MENOR número possível de transferências que acerta o grupo (busca exata sobre subgrupos de soma zero, não apenas "no máximo pessoas - 1"). Colunas CSV (linha de cabeçalho obrigatória, colunas extras ignoradas): date,description,paid_by,amount,currency,split,participants,details paid_by Ana um pagador Ana:40;Ben:20 vários pagadores (valores na moeda da linha, devem somar) amount total na moeda da linha; negativo = reembolso recebido por paid_by currency em branco = moeda base split equal | shares | exact | percent | itemized | payment participants all | Ana;Ben;Cem | all-Eli (todos exceto Eli) | em branco = nomes em details details shares: Ana:3;Ben:2 (pessoas em participants mas não listadas recebem 1) exact: Ana:30;Ben:45 (o resto, se houver, dividido igualmente entre os outros participants) percent: Ana:50;Ben:30;Cem:20 (deve totalizar 100) itemized: Ana:18;Ben:22.50;shared:12 (preços dos itens antes de imposto/gorjeta; itens "shared" são divididos igualmente; amount - itens = imposto/serviço/gorjeta, dividido em proporção ao que cada pessoa pediu) payment: (nenhum) paid_by deu `amount` para a única pessoa em participants Linhas de comentário no topo definem o grupo e as taxas (ou use os flags da CLI): # people: Ana, Ben, Cem # base: EUR # rates: USD=0.92, GBP=1.16 (1 unidade dessa moeda = taxa unidades da base) Uso: 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 Códigos de saída: 0 = plano de acerto impresso, 2 = entrada inválida (divisão desconhecida, somas que não correspondem, taxa de câmbio ausente, pessoa desconhecida em uma lista estrita). """ import argparse import csv import io import json import re import sys from decimal import Decimal, ROUND_HALF_UP, InvalidOperation MAX_EXACT = 18 # pessoas com saldo diferente de zero para a busca exata do mínimo (2^18 estados) 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ão é um número" % (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): """Divide o total inteiro `total` em centavos em proporção a `weights` (dict nome -> Decimal >= 0). Método do maior resto; empates vão para o nome anterior em `order`. A soma é exata.""" names = [n for n in order if n in weights] wsum = sum(weights[n] for n in names) if wsum <= 0: raise LedgerError("pesos somam zero") 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 deve parecer Nome:valor" % (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("nenhuma linha de despesa encontrada (é necessária uma linha de cabeçalho e pelo menos uma linha)") need = {"description", "paid_by", "amount", "split"} cols = {c.strip().lower() for c in rows[0].keys() if c} if not need <= cols: raise LedgerError("colunas CSV ausentes: %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) # primeira passada: aprender cada nome quando nenhuma lista de pessoas foi fornecida 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() # -- pessoas e taxas def _add_rates(self, text): for part in re.split(r"[,;]", text): if not part.strip(): continue if "=" not in part: raise LedgerError("taxa %r deve parecer USD=0.92" % part.strip()) k, v = part.split("=", 1) r = dec(v, "taxa " + k.strip()) if r <= 0: raise LedgerError("taxa para %s deve ser positiva" % k.strip()) self.rates[k.strip().upper()] = r def _person(self, name, declare=False): key = name.strip().lower() if not key: raise LedgerError("nome de pessoa vazio") if key in self._canon: canon = self._canon[key] if canon != name.strip() and declare: self.warnings.append("'%s' e '%s' tratados como a mesma pessoa" % (canon, name.strip())) return canon if not declare: raise LedgerError("pessoa desconhecida %r (não está na lista de pessoas)" % 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()] # -- uma linha CSV -> pago / cota em centavos base def _line(self, ln, r): desc = r.get("description") or "linha %d" % ln where = "linha %d (%s)" % (ln, desc) split = (r.get("split") or "equal").lower() if split not in SPLITS: raise LedgerError("%s: divisão desconhecida %r (use %s)" % (where, split, ", ".join(SPLITS))) cur = (r.get("currency") or self.base).upper() if cur not in self.rates: raise LedgerError("%s: nenhuma taxa de câmbio para %s (adicione '# rates: %s=...' ou --rates)" % (where, cur, cur)) amount = dec(r.get("amount"), where + " amount") if amount == 0: self.warnings.append("%s tem valor 0 e foi ignorada" % where) total = to_cents(amount * self.rates[cur]) # pagadores 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: pagadores somam %s mas o valor é %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: um pagamento precisa de exatamente um destinatário em 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 em details mas não em participants" % (where, ", ".join(extra))) if any(v < 0 for v in w.values()): raise LedgerError("%s: cotas não podem ser negativas" % 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: percentuais somam %s, não 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: valores exatos somam %s mas a conta é %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: itens somam %s, mais que a conta %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: nenhum item para dividir" % where) # imposto/gorjeta proporcional aos itens de cada pessoa == alocar o total pelos pesos dos itens 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: erro interno de arredondamento" % where) # nunca esperado 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("linha %d parece uma duplicata da linha %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 # ----------------------------------------------------------------------------- acerto de contas def greedy(net, order): """Maior devedor paga maior credor até que todos estejam zerados (no máximo k-1 transferências).""" 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): """Menor número de transferências: n - (número máximo de grupos disjuntos de soma zero). Retorna (transferências, grupos, exato).""" 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) # desce, cortando um grupo em cada ponto de verificação de soma zero 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 # ----------------------------------------------------------------------------- saída 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 = ["# Plano de acerto de contas", "", "Moeda base: %s Pessoas: %d Linhas: %d Total gasto: %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("Taxas usadas: " + ", ".join("1 %s = %s %s" % (c, led.rates[c].normalize(), B) for c in others)) o += ["", "## Despesas", "", "| # | Data | O quê | Pago por | Valor | Em %s | Divisão | 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 = "para " + 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 += ["", "## Saldos", "", "| Pessoa | Pagou | Cota | Enviou | Recebeu | Líquido |", "|---|---|---|---|---|---|"] for p in led.people: x = b[p] state = "recebe de volta" if x["net"] > 0 else ("deve" if x["net"] < 0 else "zerado") 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("Verificação: líquidos somam %s (pago %s = cotas %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 += ["", "## Transferências (%d)" % len(tr), ""] if not tr: o.append("Todos estão zerados. Nada a pagar.") else: for i, (d, c, x) in enumerate(tr, 1): o.append("%d. %s paga %s **%s**" % (i, d, c, fmt(x, B))) o.append("") if exact: if len(groups) > 1: o.append("Mínimo possível: %d transferências. O grupo se divide em %d conjuntos que acertam entre si (%s), então %d pessoas precisam apenas de %d transferências; pagar a maior dívida para o maior crédito levaria %d." % ( len(tr), len(groups), "; ".join(" + ".join(g) for g in groups), nz, len(tr), len(gr))) else: o.append("Mínimo possível: %d transferências para %d pessoas com saldo (nenhum subgrupo menor se equaliza sozinho)." % (len(tr), nz)) else: o.append("%d pessoas têm saldo; busca exata do mínimo ignorada (limite %d), plano guloso usa no máximo %d transferências." % (nz, MAX_EXACT, nz - 1)) if detail: p = led._person(detail) o += ["", "## Detalhe: %s" % p, "", "| # | O quê | Pagou | Cota | Líquido acumulado |", "|---|---|---|---|---|"] 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 += ["", "## Avisos", ""] + ["- " + w for w in led.warnings] o += ["", "## Mensagem para o chat do grupo", "", "```"] msg = ["Acerto de contas (%s, %d linhas, taxas: %s):" % (B, len(led.lines), ", ".join("%s %s" % (c, led.rates[c].normalize()) for c in others) or "nenhuma")] msg += ["- %s -> %s: %s" % (d, c, fmt(x, B)) for d, c, x in tr] or ["- todos estão zerados"] msg.append("Tabela completa na planilha compartilhada. Grite em até 2 dias se uma linha estiver errada.") 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-teste 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("divisão igual ao centavo", lambda: nets(L(",Táxi,Ana,10.00,,equal,Ana;Ben;Cem,\n")) == {"Ana": 666, "Ben": -333, "Cem": -333}) case("centavo do resto vai para o primeiro na ordem", lambda: L(",X,Ana,0.10,,equal,Ana;Ben;Cem,\n").lines[0]["share"] == {"Ana": 4, "Ben": 3, "Cem": 3}) case("todos menos exclusão", lambda: Ledger("# people: Ana, Ben, Cem\n" + H + ",X,Ana,30,,equal,all-Cem,\n").lines[0]["among"] == ["Ana", "Ben"]) case("palavras de todos exceto", lambda: Ledger("# people: Ana, Ben, Cem\n" + H + ",X,Ana,30,,equal,all except Ben,\n").lines[0]["among"] == ["Ana", "Cem"]) case("pesos de cotas", lambda: L(",Quarto,Ana,100,,shares,,Ana:3;Ben:1\n").lines[0]["share"] == {"Ana": 7500, "Ben": 2500}) case("cota padrão 1 para participants não listados", lambda: L(",Quarto,Ana,90,,shares,Ana;Ben;Cem,Ana:2;Ben:0\n").lines[0]["share"] == {"Ana": 6000, "Cem": 3000}) case("valores exatos", lambda: L(",Ingressos,Ana,75,,exact,,Ana:30;Ben:45\n").lines[0]["share"] == {"Ana": 3000, "Ben": 4500}) case("exato com resto dividido igualmente", lambda: L(",T,Ana,100,,exact,Ana;Ben;Cem,Ana:40\n").lines[0]["share"] == {"Ana": 4000, "Ben": 3000, "Cem": 3000}) case("incompatibilidade exata rejeitada", lambda: raises(lambda: L(",T,Ana,80,,exact,,Ana:30;Ben:45\n"))) case("percentual", lambda: L(",Vinho,Ana,36,,percent,,Ana:25;Ben:25;Cem:50\n").lines[0]["share"] == {"Ana": 900, "Ben": 900, "Cem": 1800}) case("percentual não 100 rejeitado", lambda: raises(lambda: L(",V,Ana,36,,percent,,Ana:25;Ben:25\n"))) case("itemizado com compartilhado e imposto/gorjeta", lambda: L(",Jantar,Ana,66,,itemized,Ana;Ben,Ana:20;Ben:30;shared:10\n").lines[0]["share"] == {"Ana": 2750, "Ben": 3850}) case("itemizado acima da conta rejeitado", lambda: raises(lambda: L(",D,Ana,40,,itemized,Ana;Ben,Ana:20;Ben:30\n"))) case("linha de pagamento", lambda: nets(L(",Comida,Ana,20,,equal,Ana;Ben,\n,Pagou de volta,Ben,10,,payment,Ana,\n")) == {"Ana": 0, "Ben": 0}) case("pagamento precisa de um destinatário", lambda: raises(lambda: L(",P,Ben,10,,payment,Ana;Cem,\n"))) case("vários pagadores", lambda: L(",Trem,Ana:40;Ben:20,60,,equal,Ana;Ben;Cem,\n").lines[0]["paid"] == {"Ana": 4000, "Ben": 2000}) case("vários pagadores devem somar", lambda: raises(lambda: L(",Trem,Ana:40;Ben:10,60,,equal,Ana;Ben,\n"))) case("reembolso (valor negativo)", lambda: nets(L(",Reembolso do museu,Ben,-24,,equal,Ben;Cem,\n")) == {"Ben": -1200, "Cem": 1200}) case("conversão de moeda", lambda: L(",Surf,Ana,100,USD,equal,Ana;Ben,\n", rates="USD=0.92").lines[0]["total"] == 9200) case("taxas da linha de comentário", 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("taxa ausente rejeitada", lambda: raises(lambda: L(",S,Ana,10,CHF,equal,Ana;Ben,\n"))) case("divisão desconhecida rejeitada", lambda: raises(lambda: L(",S,Ana,10,,halves,Ana;Ben,\n"))) case("lista estrita de pessoas rejeita erro de digitação", lambda: raises(lambda: Ledger("# people: Ana, Ben\n" + H + ",S,Anna,10,,equal,all,\n"))) case("nomes sem diferenciar maiúsculas se fundem", lambda: L(",A,Ana,10,,equal,ana;Ben,\n").people == ["Ana", "Ben"]) case("aviso de linha duplicada", lambda: any("duplicata" in w for w in L("1,Táxi,Ana,10,,equal,Ana;Ben,\n1,Táxi,Ana,10,,equal,Ana;Ben,\n").warnings)) case("colunas ausentes rejeitadas", lambda: raises(lambda: Ledger("o que,quem\nx,y\n"))) case("número inválido rejeitado", lambda: raises(lambda: L(",S,Ana,dez,,equal,Ana;Ben,\n"))) # transferências mínimas case("pares de soma zero encontrados (4 pessoas -> 2 transferências)", lambda: len(minimal({"A": 500, "B": -500, "C": 300, "D": -300}, list("ABCD"))[0]) == 2) case("nenhum subgrupo de soma zero -> pessoas - 1 transferências", 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("mínimo supera guloso em 6 pessoas", 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("todos zerados -> nenhuma transferência", 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("mínimo == força bruta em 60 grupos aleatórios e acerta todos", check_random) case("transferências acertam o livro-caixa semelhante ao exemplo", lambda: _settles(L(",Apartamento,Ana,840,,shares,,Ana:3;Ben:3;Cem:2\n,Comida,Cem,63.75,,equal,all,\n,Vinho,Ben,36,,percent,,Ana:25;Ben:25;Cem:50\n"))) case("render tem transferências e mensagem de chat", lambda: "Mensagem para o chat do grupo" in render(L(",Táxi,Ana,10,,equal,Ana;Ben,\n"))) case("visualização de detalhe", lambda: "Detalhe: Ben" in render(L(",Táxi,Ana,10,,equal,Ana;Ben,\n"), "ben")) case("saída json", lambda: as_json(L(",Táxi,Ana,10,,equal,Ana;Ben,\n"))["transfers"] == [{"from": "Ben", "to": "Ana", "cents": 500}]) case("formato de milhares", 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 passaram" % (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="Divida despesas compartilhadas e planeje as menores transferências de acerto de contas.") ap.add_argument("file", nargs="?", help="livro-caixa CSV ('-' = stdin)") ap.add_argument("--base", default=None, help="moeda base (padrão: linha '# base:' ou EUR)") ap.add_argument("--rates", default=None, help="USD=0.92,GBP=1.16 (1 unidade = taxa unidades da base)") ap.add_argument("--people", default=None, help="membros do grupo separados por vírgula (estrito: erros de digitação são erros)") ap.add_argument("--detail", default=None, help="mostra o detalhamento linha por linha para uma pessoa") 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("forneça um livro-caixa 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("erro: %s" % e, file=sys.stderr) return 2 sys.stdout.write(out) return 0 if __name__ == "__main__": sys.exit(main())

Entre para ver o prompt completo

Continuar com:

Ao entrar, você concorda com nossos Termos de uso e Política de privacidade

Uso

Este prompt foi projetado para uso com productivity. Copie o conteúdo acima e cole na sua ferramenta de IA preferida.

Para melhores resultados, personalize os marcadores (indicados por colchetes ou maiúsculas) com seus requisitos específicos.

Referências

Categorias:productivity| prompts.chat| expense-splitter| bill-splitter

Discussão

0 comentários