#!/usr/bin/perl -w # accounting.pl -- A script to account a group of people living together # # Copyright (C) 2003 Jorgen Schäfer # URL: http://www.forcix.cx/computer/programs/accounting.html # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be of some # interest to somebody, but WITHOUT ANY WARRANTY; without even the # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; see the file COPYING or COPYING.txt. If # not, write to the Free Software Foundation, Inc., 59 Temple Place - # Suite 330, Boston, MA 02111-1307, USA. # Commentary: # This file will account the shoppings of different people, so they # can be equally distributed among everyone. use strict; use CGI; # Config options. my $basedir = "/var/db/accounting"; my @people = ("arisel", "felix", "forcer", "garou", "houdini"); my %peopleip = ("arisel" => "192.168.1.10", "houdini" => "192.168.1.20", "felix" => "192.168.1.31", "forcer" => "192.168.1.40", "garou" => "192.168.1.50" ); # The main routine. This is called at the end of the file. # Here we decide which mode we run in. sub main { if (!$ENV{"GATEWAY_INTERFACE"}) { # We are called from the shell to make a final accounting of the # LAST month. do_accounting(); } else { # We were called as a CGI script. if (defined(CGI::param("action")) && CGI::param("action") eq "add" && defined(CGI::param("date")) && defined(CGI::param("name")) && defined(CGI::param("value")) && defined(CGI::param("desc"))) { # Add this entry. add_entry(filename_of_month(), CGI::param("date"), CGI::param("value"), CGI::param("name"), CGI::param("desc")); } # Now provide an entry box for a new entry, and print a listing. do_list(); } } # Some helper functions for file access. # Return the filename for a month given as argument. If no argument, # use the current month. sub filename_of_month { my ($month, $year) = @_; if (not defined $month) { $month = (localtime())[4] + 1; } if (not defined $year) { $year = (localtime())[5] + 1900; } return sprintf("%s/Abrechnung-%04i-%02i.txt", $basedir, $year, $month); } # Read the file given as argument into a list of lists containing the # date, the value, the name of the submitter and the description of # the item. # [date, value, name, description] sub read_file ($) { my ($filename) = @_; my @list = (); if (open(FILE, "<", "$filename")) { while () { if (/(\d\d\d\d-\d\d-\d\d) (-?[0-9.,]+) (\S+) (.*)/) { push(@list, [$1, $2, $3, $4]); } } } return \@list; } # From a list as given by read_file, make a hash mapping usernames to # account totals. Returns a reference to this hash. sub account_totals ($) { my ($entries) = @_; my %totals; for my $entry (@{$entries}) { $totals{$entry->[2]} += $entry->[1]; } return \%totals; } # Add an entry to the file specified as the first argument. The other # arguments are the value, the name of the submitter and a description # of the item, in this order. sub add_entry ($$$$$) { my ($filename, $date, $value, $name, $description) = @_; $value =~ tr/,/./; # For those germans who use 100,00 instead of 100.00... my $entry = sprintf("%s %.02f %s %s", $date, $value, $name, $description); open(FILE, ">>", "$filename") || die("Couldn't open $filename for appending: $!\n"); print FILE "$entry\n"; close(FILE); } # The functions called to do the various tasks of this program. # Make a final accounting of the last month. Be happy about it. # That is, first print the account totals and the listing, and then # make a suggestion for the transactions to occur. sub do_accounting { my @today = localtime(); my $month = $today[4]+1; my $year = $today[5]+1900; my $lastmonth = $month == 1 ? 12 : $month - 1; my $lastyear = $month == 1 ? $year - 1 : $year; my $filename = filename_of_month($lastmonth, $lastyear); my $contents = read_file($filename); my %totals = %{account_totals($contents)}; my $number = @people; my $sum = 0; my %final; # Print the report for my $name (keys %totals) { $sum += $totals{$name}; } printf("%-10s %s\n", "Name", "Kontostand"); for my $name (@people) { my $thistotal = $totals{$name} || 0; my $amount = -1 * (($sum / $number) - $thistotal); $final{$name} = $amount; printf("%-10s %7.02f\n", $name, $amount); } # Summary printf("\n"); printf("Wir haben diesen Monat insgesamt %.02f EUR ausgegeben.\n", $sum); printf("Das sind pro Person %.02f EUR.\n", $sum/$number); # Transactions my $transactions = transactions(\%final); print("\n"); print("Überweisungen:\n"); for my $entry (@{$transactions}) { my $from = $entry->[0]; my $to = $entry->[1]; my $amount = $entry->[2]; printf("$from überweist %.02f EUR an $to\n", $amount); } # Detailed report print("\n"); printf("%-10s %7s %-10s %s\n", "Datum", "Wert", "Name", "Wofür"); for my $entry (@{$contents}) { my ($date, $value, $name, $desc) = @{$entry}; printf("%s %7.02f %-10s %s\n", $date, $value, $name, $desc); } } # List the entries of the current month and the account totals. sub do_list { my $filename = filename_of_month(); my $contents = read_file($filename); my %totals = %{account_totals($contents)}; print CGI::header(-expires => "now"), CGI::start_html('Kollektiv Accounting'), CGI::h1("Kollektiv Kontostand"); print_entry_box(); print_account_totals(account_totals($contents)); print_account_listing($contents); print CGI::end_html(); } # Print an entry box for a new entry here. sub print_entry_box { print CGI::start_form(); print ""; print ""; print ""; print ""; # Date my @today = localtime; my ($year, $month, $day) = ($today[5]+1900, $today[4]+1, $today[3]); my $date = sprintf("%04i-%02i-%02i", $year, $month, $day); print ""; # Name print ""; # Value print ""; # Description print ""; print "
Neuer Eintrag
KaufdatumNameWertWofür
"; print ""; print ""; print ""; print "
"; print ""; print CGI::submit(); print CGI::end_form(); } # Print the totals of all accounts. sub print_account_totals ($) { my %totals = %{$_[0]}; my $number = @people; my $sum = 0; for my $name (keys %totals) { $sum += $totals{$name}; } print ""; print ""; for my $name (@people) { my $thistotal = $totals{$name} || 0; my $amount = -1 * (($sum / $number) - $thistotal); printf("", $amount); } print "
NameKontostand
$name%.02f
"; printf("

Wir haben diesen Monat bisher %.02f EUR ausgegeben.
", $sum); printf("Das sind pro Person %.02f EUR.

\n", $sum/$number); } # Print a listing of all transactions so far. sub print_account_listing ($) { my @contents = @{$_[0]}; print ""; print ""; for my $entry (@contents) { my ($date, $value, $name, $desc) = @{$entry}; print ""; print ""; } print "
DatumWertNameWofür
$date$value$name$desc
"; } # The function to calculate who has to give how much to who. The # algorithm works by having the biggest debtor (lowest value) trying # to fill up the smallest creditors (highest value). sub transactions { my %accounts = %{$_[0]}; my @debtors; my @creditors; my @transactions; for my $name (keys %accounts) { if ($accounts{$name} < 0) { push(@debtors, [$name, -1*$accounts{$name}]); } elsif ($accounts{$name} > 0) { push(@creditors, [$name, $accounts{$name}]); } } @debtors = sort {$a->[1] <=> $b->[1]} @debtors; @creditors = sort {$b->[1] <=> $a->[1]} @creditors; # Now let's go through the debtors. Each fills one creditor as much # as possible. my $creditornum = 0; for my $debtor (@debtors) { my $creditor = $creditors[$creditornum]; while ($creditornum < @creditors && $debtor->[1] > $creditor->[1]) { push(@transactions, [$debtor->[0], $creditor->[0], $creditor->[1]]); $debtor->[1] -= $creditor->[1]; $creditornum++; $creditor = $creditors[$creditornum]; } if ($creditornum < @creditors) { push(@transactions, [$debtor->[0], $creditor->[0], $debtor->[1]]); $creditor->[1] -= $debtor->[1]; } } return \@transactions; } # Ok, make the whole thing go round. main();