summaryrefslogblamecommitdiff
path: root/comptes
blob: 7a78f21e80664cec9c6fb738fde00382529c10cd (plain) (tree)
1
                   





















                                                                               


                                                                       









                                       










































































































                                                                     
     











                                                                     
     
















































































































































































































































































                                                                                                      


                   


















                                                              
     









                                                                                             
     





































                                                                                                                                               
 




























                                                                                   
     

                   
     
   
 
#!/usr/bin/env perl
# The MIT License (MIT)
# 
# Copyright (c) 2011-2015 Ismaël Bouya http://www.normalesup.org/~bouya/
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


# Feel like tipping/donating? https://www.immae.eu/licenses_and_tipping

use Switch;
use strict;

my $file = "comptes.data";

my @actions=('Ajouter un participant');
my %participants = ();
my @transactions = ();
my @descriptions = ();
my $evenement_def;
my @remboursements = ();
my %initiales = ();

if($#ARGV == 0) {
  open FILE, $ARGV[0] || die "Impossible de lire le fichier";
  $evenement_def = <FILE>;
  chomp($evenement_def);
  while(my $noms = <FILE>) {
    if($noms eq "\n") { last; }
    my ($numero,$nom) = split(/ /,$noms,2);
    chomp($nom);
    $participants{$nom} = $numero;
  }
  while(my $transaction = <FILE>) {
    if($transaction eq "\n") { last; }
    chomp($transaction);
    push(@transactions,$transaction);
    my $description = <FILE>;
    chomp($description);
    push(@descriptions,$description);
  }
  while(my $remboursement = <FILE>) {
    if($remboursement eq "\n") { last; }
    chomp($remboursement);
    my @remb = split(/ /, $remboursement);
    push(@remboursements,\@remb);
  }
  $file = $ARGV[0];
}

sub choose_action {
  my @actions = ('Ajouter un participant');
  my $action;

  if (keys(%participants) > 0) {
    $actions[1] = "Ajouter une dépense";
    $actions[2] = "Enregistrer";
  }
  if(scalar(@transactions) > 0) {
    $actions[2] = "Enregistrer";
    $actions[3] = "Voir les frais de quelqu'un (dépenses - dettes)";
    $actions[4] = "Voir les frais de tout le monde";
    $actions[5] = "Equilibrage";
    $actions[6] = "Voir une transaction";
  }
  if(scalar(@remboursements) > 0) {
    $actions[7] = "Afficher les remboursements";
  }

  while(1) {
    my $i = 1;
    my $var;
    my @vart = ( @actions, 'Sortir' );
    my $message = '';
    foreach $var ( @vart ) {
      $message .= $i++.') '.$var."\n";
    }
    print $message;
    $var = <STDIN>;
    chop($var);
    if(/\D/ || $var < 1 || $var > $i) {
      print "Mauvaise entrée\n";
      next;
    }
    $action = $vart[$var-1];
    last;
  }

  return $action;
}

sub pp {
  my ($price, $noteur) = @_;

  if (!$noteur) {
    return sprintf("%.2f", $price)." EUR";
  } else {
    return sprintf("%.2f", $price);
  }
}

sub compute_initiales {
  %initiales = ();
}

sub add_participant {
  print "Nom du participant ?\n";
  my $participant = <STDIN>;
  chomp($participant);
  $participant =~ s/\s/_/g;
  $participants{$participant} = keys(%participants)+1;
  compute_initiales();
}

sub select_menu {
  my (@vart) = @_;
  while(1) {
    my $i = 1;
    my $var;
    foreach $var ( @vart ) {
      print $i++.') '.$var."\n";
    }
    $var = <STDIN>;
    chop($var);
    if(/\D/ || $var < 1 || $var > scalar @vart) {
      print "Mauvaise entrée\n";
      next;
    }
    return $vart[$var-1];
  }
}

sub select_participant {
  my ($annuler, @supplementaires) = @_;
  while(1) {
    my $i = 1;
    my $var;
    my @vart = ( keys(%participants), @supplementaires, ($annuler) );
    foreach $var ( @vart ) {
      print $i++.') '.$var."\n";
    }
    $var = <STDIN>;
    chop($var);
    if(/\D/ || $var < 1 || $var > scalar @vart) {
      print "Mauvaise entrée\n";
      next;
    }
    if ($var != scalar @vart) {
      return $vart[$var-1];
    } else {
      return;
    }
  }
}

sub select_transaction {
  my $transaction;
  while(1) {
    my $i = 1;
    my $var;
    my @vart = ( @descriptions, "Annuler" );
    foreach $var ( @vart ) {
      print $i++.') '.$var."\n";
    }
    $var = <STDIN>;
    chop($var);
    if(/\D/ || $var < 1 || $var > $i) {
      print "Mauvaise entrée\n";
      next;
    }
    $transaction = $var-1;
    last;
  }

  my ($payeur, $prix, $total_parts, $split, $parts) = &parse_transaction($transactions[$transaction]);
  return ($descriptions[$transaction], $payeur, $prix, $total_parts, $split, $parts);
}

sub add_transaction {
  print "Qui a payé ?\n";
  my $paye = select_participant("Annuler");
  if(!$paye) {
    print "Annulé";
    return;
  }
  print "Quoi ?\n";
  my $description = <STDIN>;
  chomp($description);
  print "Combien ?\n";
  my $prix = <STDIN>;
  chomp($prix);
  print "Pour qui ?\n";
  my $repartition = "";
  my $total_parts = 0;
  DESTS: while(1) {
    my $dest = select_participant("Fini", "Ajouter à tous");
    switch($dest) {
      case undef {
        if($repartition eq "") {
          print "Annulé\n";
          return;
        }
        elsif($total_parts == 0) {
          print "Le nombre total de parts est nul, il faut en ajouter !\n";
          next DESTS;
        }
        else {
          push(@transactions, $participants{$paye}.' '.$prix.$repartition);
          push(@descriptions, $description);
          return;
        }
      }
      case "Ajouter à tous" {
        print "Nombre de parts ?\n";
        my $parts = <STDIN>;
        chomp($parts);
        if(!($parts == 0)) {
          foreach my $participant (keys(%participants)) {
            $repartition .= ' '.$participants{$participant}.':'.scalar($parts);
            $total_parts += scalar($parts);
          }
        }
      }
      else {
        print "Nombre de parts ?\n";
        my $parts = <STDIN>;
        chomp($parts);
        if(!($parts == 0)) {
          $repartition .= ' '.$participants{$dest}.':'.scalar($parts);
          $total_parts += scalar($parts);
        }
      }
    }
  }
}

sub find_participant {
  my ($participant_number) = @_;

  foreach my $name (keys(%participants)) {
    if ($participant_number == $participants{$name}) {
      return $name;
    }
  }
}

sub parse_transaction {
  my ($transaction) = @_;
  my @table_trans = split(/ /,$transaction);

  my $payeur = shift(@table_trans);
  $payeur = find_participant($payeur);
  my $prix = shift(@table_trans);
  my %parts;
  my %split;
  my $total_parts = 0;
  foreach my $repartition (@table_trans) {
    my ($participant, $part) = split(/:/, $repartition);
    $participant = find_participant($participant);
    $parts{$participant} ||= 0;
    $parts{$participant} += $part;
    $total_parts += $part;
  }
  if (!$parts{$payeur}) {
    $parts{$payeur} = 0;
  }
  foreach my $participant (keys(%parts)) {
    $split{$participant} = - ($parts{$participant} * $prix) / $total_parts;
    if ($participant eq $payeur) {
      $split{$participant} += $prix;
    }
  }

  return ($payeur, $prix, $total_parts, \%split, \%parts);
}

sub max_length {
  my (@strings) = @_;
  my @string_lengths = sort { length($a) <=> length($b) } @strings;
  return length($string_lengths[-1]);
}

sub list_transactions_for_participants {
  my (@participants) = @_;
  my $one_participant = 0;
  if (scalar @participants == 0) {
    @participants = keys(%participants);
  }
  if (scalar @participants == 1) {
    $one_participant = 1;
  }

  my %depenses;
  my %solde;

  my @zipped = map {($transactions[$_], $descriptions[$_])} (0 .. $#descriptions);
  while (scalar(@zipped) > 0) {
    my $transaction = shift @zipped;
    my $description = shift @zipped;
    my ($payeur, $prix, $total_parts, $parsed_transaction, $parts) = &parse_transaction($transaction);
    $depenses{$payeur} ||= 0;
    $depenses{$payeur} += $prix;

    my @intersection = grep( $parsed_transaction->{$_}, @participants);
    if (scalar @intersection) {
      my $phantom = " " x length($description.":");
      my $max_participant_length = max_length(@intersection);

      print $description.":";
      foreach my $participant (@intersection) {
        $solde{$participant} ||= 0;
        $solde{$participant} += $parsed_transaction->{$participant};

        if ($one_participant) {
          print " ".pp($parsed_transaction->{$participant});
          if ($payeur eq $participant) {
            print " (a payé, ".pp($prix).")";
          }
          print "\n";
        } else {
          my $participant_padding = " " x ($max_participant_length - length($participant));
          print "  ".$participant.$participant_padding." ".pp($parsed_transaction->{$participant});
          if ($payeur eq $participant) {
            print " (a payé, ".pp($prix).")";
          }
          print "\n".$phantom;
        }
      }
      if (!$one_participant) {
        print "\n";
      }
    }
  }

  foreach my $personne (@participants) {
    my $d = $depenses{$personne} || 0;
    my $s = $solde{$personne} || 0;
    if ($d) {
      print $personne." a payé ".pp($d);
    } else {
      print $personne." n'a rien payé";
    }
    if ($s) {
      print " et a pour ".pp($d - $s)." de frais au total.\n";
    } else {
      print " et n'a pas eu de frais.\n";
    }
    print $personne." a donc un solde de ".pp($s)."\n";
  }
}

sub compute_equity {
  my %depenses;
  my %frais;
  my %soldes;
  foreach my $transaction (@transactions) {
    my ($payeur, $prix, $total_parts, $split, $parts) = &parse_transaction($transaction);
    $depenses{$payeur} += $prix;

    foreach my $personne (keys(%{$split})) {
      $soldes{$personne} += $split->{$personne};
    }
  }
  foreach my $personne (keys(%soldes)) {
    $frais{$personne} = $depenses{$personne} - $soldes{$personne};
  }

  return (\%depenses, \%frais, \%soldes);
}

sub delete_zeros {
  my ($soldes, @participants) = @_;

  foreach my $participant (@participants) {
    delete $soldes->{$participant} if pp($soldes->{$participant}, 1) == 0;
  }
}

sub compute_reimbursment {
  my ($a, $b, $soldes) = @_;
  my $s_a = $soldes->{$a};
  my $s_b = $soldes->{$b};

  return if (pp($s_a, 1) * pp($s_b, 1) >= 0);
  my $debtor   = pp($s_a, 1) < 0 ? $a : $b;
  my $creditor = pp($s_a, 1) > 0 ? $a : $b;
  my $exchange = (abs($s_a) < abs($s_b)) ? abs($s_a) : abs($s_b);

  my @remboursement = ($debtor, $creditor, $exchange);
  push @remboursements, \@remboursement;
  $soldes->{$creditor} -= $exchange;
  $soldes->{$debtor}   += $exchange;
  delete_zeros($soldes, $a, $b);
}

sub select_exchange_couple {
  my ($soldes) = @_;
  my $bd; # biggest debtor
  my $sc; # smallest creditor

  foreach my $p (keys(%{$soldes})) {
    $bd = $p if $soldes->{$p} < 0 && (!$bd || $soldes->{$p} < $soldes->{$bd});
    $sc = $p if $soldes->{$p} > 0 && (!$sc || $soldes->{$p} < $soldes->{$sc});
  }

  return ($bd, $sc);
}

sub print_remboursements {
  foreach my $remboursement (@remboursements) {
    my ($debtor, $creditor, $exchange) = (@{$remboursement});
    print $debtor." doit ".pp($exchange)." à ".$creditor."\n";
  }
  my $pause = <STDIN>;
}

GLOBALE: while(1) {
  my $action = choose_action();
  switch ($action) {
    case "Ajouter un participant" {
      add_participant();
    }
    case "Ajouter une dépense" {
      add_transaction();
    }
    case "Voir les frais de quelqu'un (dépenses - dettes)" {
      my $personne = select_participant("Annuler");
      if(!$personne) {
        next GLOBALE;
      }
      list_transactions_for_participants(($personne));
      my $pause = <STDIN>;
    }
    case "Voir les frais de tout le monde" {
      list_transactions_for_participants(keys(%participants));
      my $pause = <STDIN>;
    }
    case "Voir une transaction" {
      my ($description, $payeur, $prix, $total_parts, $split, $parts) = select_transaction();
      print $description." (payé par ".$payeur."), ".pp($prix)."\n";
      foreach my $pers (keys(%{$split})) {
        print $pers.", ".$parts->{$pers}." part(s), soit ". pp($split->{$pers});
        if ($pers eq $payeur) {
          print " (".pp($prix)." - ".pp($prix - $split->{$pers}).")";
        }
        print "\n";
      }
    }
    case "Enregistrer" {
      print "Nom de l'événement ?";
      if(!($evenement_def eq "")) {
        print " [".$evenement_def."]";
      }
      print "\n";
      my $evenement = <STDIN>;
      chomp($evenement);
      if($evenement eq "") {
        $evenement = $evenement_def;
      }
      open FILE, ">".$file;
      print FILE $evenement."\n";
      my $key;
      foreach $key (keys(%participants)) {
        print FILE $participants{$key}." ".$key."\n";
      }
      print FILE "\n";
      for(my $i=0;$i<scalar(@transactions);$i++) {
        print FILE $transactions[$i]."\n";
        print FILE $descriptions[$i]."\n";
      }
      print FILE "\n";
      foreach my $remboursement (@remboursements) {
        print FILE join(" ", @{$remboursement})."\n";
      }
      close FILE;
    }
    case "Equilibrage" {
      my ($depenses, $frais, $soldes) = compute_equity();
      @remboursements = ();

      foreach my $personne (keys(%participants)) {
        print $personne." a un solde de ".pp($soldes->{$personne})." (+" . pp($depenses->{$personne}). " / -". pp($frais->{$personne}) . ")\n";
      }
      my $pause = <STDIN>;
      print "Y-a-t-il des remboursements plus aisés ?\n";
      my $ouinon = select_menu("Oui", "Non");

      AISE: while($ouinon eq "Oui") {
        print "Qui ?\n";
        my $personnea = select_participant("Fini");
        if(!$personnea) { last AISE; }
        print "Avec qui ?\n";
        my $personneb = select_participant("Annuler");
        if(!$personneb) { next AISE; }
        compute_reimbursment($personnea, $personneb, $soldes);
      }
      # Une boucle pour détecter si on a des couples qui s'annulent mutuellement
      delete_zeros($soldes, keys(%{$soldes}));
      COUPLE: foreach my $personnea (keys(%participants)) {
        next if(pp($soldes->{$personnea}, 1) == 0);
        foreach my $personneb (keys(%participants)) {
          next if (pp($soldes->{$personneb} + $soldes->{$personnea}, 1) != 0);
          compute_reimbursment($personnea, $personneb, $soldes);
          next COUPLE;
        }
      }
      REMB: while(1) {
        last REMB if(keys(%{$soldes}) == 0);

        my ($biggest_debtor, $smallest_creditor) = select_exchange_couple($soldes);
        compute_reimbursment($biggest_debtor, $smallest_creditor, $soldes);
      }
      print_remboursements();
    }
    case "Afficher les remboursements" {
      print_remboursements();
    }
    case "Sortir" {
      last GLOBALE;
    }
  }
}