#!/usr/local/bin/perl -w-- # -*- perl -*-
#
# $Id: expense_report,v 1.6 1994/11/13 16:46:18 qjb Exp $
# $Source: /home/qjb/scripts/RCS/expense_report,v $
# $Author: qjb $
#

# This code, from the perl manual page, forces this to be run by perl from 
# perl, sh, or csh.  It must be first.
eval '(exit $?0)' && eval 'exec /usr/local/bin/perl -S $0 ${1+"$@"}'
& eval 'exec /usr/local/bin/perl -S $0 $argv:q'
    if 0;

require 5.000;

$whoami = ($0 =~ m,([^/]*)$,) ? $1 : $0;
&usage unless @ARGV == 1;
$arg = shift(@ARGV);

&help if $arg eq "-help";
&print_template if $arg eq "-template";
$file = $arg;

@monthdays = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);

# Initialize everything so that everything has a sensible default value

$name_string = "";
$date_string = "";
@purpose_string = ("", "");
$per_diem_type = 0;

@from_string = ("", "", "", "");
@from_date = ("", "", "", "");
@from_time = ("", "", "", "");
@to_string = ("", "", "", "");
@to_date = ("", "", "", "");
@to_time = ("", "", "", "");
@airfare = ("", "", "", "");
@car = ("", "", "", "");
@chargeno_string = ("", "", "", "");

@epe_date = ("", "", "", "", "", "", "", "");
@epe_chargeno = ("", "", "", "", "", "", "", "");
@epe_per_diem = ("", "", "", "", "", "", "", "");
@epe_lodging = ("", "", "", "", "", "", "", "");
@epe_personal_auto = ("", "", "", "", "", "", "", "");
@epe_tolls = ("", "", "", "", "", "", "", "");
@epe_business_meals = ("", "", "", "", "", "", "", "");
@epe_other1 = ("", "", "", "", "", "", "", "");
@epe_other2 = ("", "", "", "", "", "", "", "");
@epe_other3 = ("", "", "", "", "", "", "", "");
@epe_total = ("", "", "", "", "", "", "", "");

@epe_other_explan = ("", "", "");

@notes_string = ("", "", "", "");

@dist_chargeno = ("", "", "", "");
@dist_companypaid = (0, 0, 0, 0);
@dist_employeepaid = (0, 0, 0, 0);
@dist_total = (0, 0, 0, 0);

$charge_yn = 0;

$prior_balance = "";
$advance = "";
$available = "";
$employee_paid = "";
$amount_due = "";
$due_whom = "";

$chargeno = "";
@chargenos = ();
$epe_sat_date = "";
$epe_main_chargeno = "";

$single_chargeno = 1;
$multiple_chargeno = 2;
$charge_no_mode = 0;

&process_file;
&gen_ps;

sub process_file {
    open(FILE, "<$file") or die "$whoami: can't read $file: $!\n";
    while (<FILE>)
    {
	chop;
	s/\#.*//;
	next if m/^\s*$/;
	s/\|\s*$//;
	@fields = split('\|');
	&fix_fields(@fields);
	$keyword = shift(@fields);

	if ($keyword eq "charge_number")
	{
	    if ($charge_no_mode)
	    {
		die "$whoami: Only one of charge_number or charge_numbers " .
		    "may be specified.\n";
	    }
	    &assert_fields($keyword, scalar(@fields), 1);
	    $chargeno = $fields[0];
	    $charge_no_mode = $single_chargeno;
	}
	elsif ($keyword eq "charge_numbers")
	{
	    if ($charge_no_mode)
	    {
		die "$whoami: Only one of charge_number or charge_numbers " .
		    "may be specified.\n";
	    }
	    &assert_fields($keyword, scalar(@fields), 3);
	    @chargenos = @fields;
	    $charge_no_mode = $multiple_chargeno;
	}
	elsif ($keyword eq "name")
	{
	    &assert_fields($keyword, scalar(@fields), 1);
	    $name_string = $fields[0];
	}
	elsif ($keyword eq "date")
	{
	    &assert_fields($keyword, scalar(@fields), 1);
	    $date_string = $fields[0];
	}
	elsif ($keyword =~ m/^purpose([12])$/)
	{
	    $idx = $1 - 1;
	    &assert_fields($keyword, scalar(@fields), 1);
	    $purpose_string[$idx] = $fields[0];
	}
	elsif ($keyword eq "per_diem_type")
	{
	    &assert_fields($keyword, scalar(@fields), 1);
	    $per_diem_type = $fields[0];
	}
	elsif ($keyword =~ m/^from([1234])$/)
	{
	    $idx = $1 - 1;
	    &assert_fields($keyword, scalar(@fields), 3);
	    $from_string[$idx] = $fields[0];
	    $from_date[$idx] = $fields[1];
	    $from_time[$idx] = $fields[2];
	}
	elsif ($keyword =~ m/^to([1234])$/)
	{
	    $idx = $1 - 1;
	    &assert_fields($keyword, scalar(@fields), 3);
	    $to_string[$idx] = $fields[0];
	    $to_date[$idx] = $fields[1];
	    $to_time[$idx] = $fields[2];
	}
	elsif ($keyword =~ m/^travel_costs([1234])$/)
	{
	    $idx = $1 - 1;
	    &assert_fields($keyword, scalar(@fields), 2);
	    $airfare[$idx] = $fields[0];
	    $car[$idx] = $fields[1];
	}
	elsif ($keyword =~ m/^travel_chargeno([1234])$/)
	{
	    if ($charge_no_mode != $multiple_chargeno)
	    {
		die "$whoami: travel_chargeno is valid only in " .
		    "multiple charge number mode\n"
	    }
	    $idx = $1 - 1;
	    &assert_fields($keyword, scalar(@fields), 1);
	    $chargeno_string[$idx] = $fields[0];
	}
	elsif ($keyword eq "epe_sat_date")
	{
	    &assert_fields($keyword, scalar(@fields), 1);
	    $epe_sat_date = $fields[0];
	}
	elsif ($keyword eq "epe_chargeno")
	{
	    if ($charge_no_mode != $multiple_chargeno)
	    {
		die "$whoami: epe_chargeno is valid only in " .
		    "multiple charge number mode\n"
	    }
	    &assert_fields($keyword, scalar(@fields), 7);
	    @epe_chargeno = (@fields, "");
	}
	elsif ($keyword eq "epe_per_diem")
	{
	    &assert_fields($keyword, scalar(@fields), 7);
	    @epe_per_diem = (@fields, "");
	}
	elsif ($keyword eq "epe_lodging")
	{
	    &assert_fields($keyword, scalar(@fields), 7);
	    @epe_lodging = (@fields, "");
	}
	elsif ($keyword eq "epe_personal_auto")
	{
	    &assert_fields($keyword, scalar(@fields), 7);
	    @epe_personal_auto = (@fields, "");
	}
	elsif ($keyword eq "epe_tolls")
	{
	    &assert_fields($keyword, scalar(@fields), 7);
	    @epe_tolls = (@fields, "");
	}
	elsif ($keyword eq "epe_business_meals")
	{
	    &assert_fields($keyword, scalar(@fields), 7);
	    @epe_business_meals = (@fields, "");
	}
	elsif ($keyword eq "epe_other1")
	{
	    &assert_fields($keyword, scalar(@fields), 7);
	    @epe_other1 = (@fields, "");
	}
	elsif ($keyword eq "epe_other2")
	{
	    &assert_fields($keyword, scalar(@fields), 7);
	    @epe_other2 = (@fields, "");
	}
	elsif ($keyword eq "epe_other3")
	{
	    &assert_fields($keyword, scalar(@fields), 7);
	    @epe_other3 = (@fields, "");
	}
	elsif ($keyword =~ m/epe_other_explan([1234])/)
	{
	    $idx = $1 - 1;
	    &assert_fields($keyword, scalar(@fields), 1);
	    $epe_other_explan[$idx] = $fields[0];
	}
	elsif ($keyword =~ m/notes([1234])/)
	{
	    $idx = $1 - 1;
	    &assert_fields($keyword, scalar(@fields), 1);
	    $notes_string[$idx] = $fields[0];
	}
	elsif ($keyword eq "chargeno_match")
	{
	    &assert_fields($keyword, scalar(@fields), 1);
	    $charge_yn = $fields[0];
	}
	elsif ($keyword eq "prior_balance")
	{
	    &assert_fields($keyword, scalar(@fields), 1);
	    $prior_balance = $fields[0];
	}
	elsif ($keyword eq "advance")
	{
	    &assert_fields($keyword, scalar(@fields), 1);
	    $advance = $fields[0];
	}
	else
	{
	    die "$whoami: unrecognized keyword $keyword in $file line $.\n";
	}
    }
    close(FILE);

    &set_chargeno_fields;
    &fix_dates;
    &compute_totals;
    die "$whoami: per_diem_type is a required field\n"
	if ($per_diem_type == 0);
    die "$whoami: chargeno_match is a required field\n"
	if ($charge_yn == 0);
    &fix_dollar_amounts;
}

sub set_chargeno_fields {
    if ($charge_no_mode == $single_chargeno)
    {
	die "$whoami: charge_number is a required field\n" if ($chargeno eq "");
	for ($i = 0; $i < scalar(@from_string); $i++)
	{
	    $chargeno_string[$i] = $chargeno if (($airfare[$i] ne "") ||
						 ($car[$i] ne ""));
	}
	$epe_main_chargeno = $chargeno;
	$dist_chargeno[0] = $chargeno;
    }
    elsif ($charge_no_mode == $multiple_chargeno)
    {
	for ($i = 0; $i < scalar(@from_string); $i++)
	{
	    $j = 0;
	    if (($airfare[$i] ne "") || ($car[$i] ne ""))
	    {
		die sprintf("$whoami: travel_chargeno%d must be specified\n",
			    $i+1)
		    if ($chargeno_string[$i] eq "");
		$j = $chargeno_string[$i] - 1;
		$chargeno_string[$i] = $chargenos[$j];
	    }
	    $dist_companypaid[$j] += $airfare[$i] if ($airfare[$i] ne "");
	    $dist_companypaid[$j] += $car[$i] if ($car[$i] ne "");
	}
	# epe_chargeno should have been set above.  It will be checked
	# in compute_totals.
	@dist_chargeno = (@chargenos, "");
    }
    else
    {
	die "$whoami: one of charge_number or charge_numbers is required\n";
    }
}

sub fix_dates {
    die "$whoami: epe_sat_date is a required field\n" if ($epe_sat_date eq "");
    local(@dayfields) = split('/', $epe_sat_date);
    die "$whoami: epe_sat_date must be month/day/year\n" if (@dayfields != 3);
    local($month, $day, $year) = @dayfields;
    die "$whoami: month for epe_sat_date ($month) out of range\n"
	if (($month < 1) || ($month > 12));
    # This won't work in 2100, but I don't care.
    $monthdays[2] = 29 if ($year % 4 == 0);
    die "$whoami: month $month doesn't have a day ${day}\n"
	if (($day < 1) || ($day > $monthdays[$month]));
    for ($i = 0; $i < 7; $i++)
    {
	$week_dates[$i] = "$month/$day/$year";
	$day++;
	if ($day > $monthdays[$month])
	{
	    $day = 1;
	    $month++;
	}
	if ($month > 12)
	{
	    $month = 1;
	    $year++;
	}
    }
}

sub compute_totals {
    # While computing totals, fill in dates for days that have any expenses.
    # Compute totals in employee-paid area
    for ($i = 0; $i < 8; $i++)
    {
	$epe_per_diem[$i] = 0 if ($epe_per_diem[$i] eq "");
	$epe_lodging[$i] = 0 if ($epe_lodging[$i] eq "");
	$epe_personal_auto[$i] = 0 if ($epe_personal_auto[$i] eq "");
	$epe_tolls[$i] = 0 if ($epe_tolls[$i] eq "");
	$epe_business_meals[$i] = 0 if ($epe_business_meals[$i] eq "");
	$epe_other1[$i] = 0 if ($epe_other1[$i] eq "");
	$epe_other2[$i] = 0 if ($epe_other2[$i] eq "");
	$epe_other3[$i] = 0 if ($epe_other3[$i] eq "");
	$epe_total[$i] = 0 if ($epe_total[$i] eq "");
    }
    for ($i = 0; $i < 7; $i++)
    {
	$val = ($epe_per_diem[$i] +
		$epe_personal_auto[$i] +
		$epe_tolls[$i] +
		$epe_business_meals[$i] +
		$epe_other1[$i] +
		$epe_other2[$i] +
		$epe_other3[$i]);
	$val += $epe_lodging[$i] if ($per_diem_type != 2);
	if ($val != 0)
	{
	    $epe_total[$i] = $val;
	    $epe_date[$i] = $week_dates[$i];
	    $j = 0;
	    if ($charge_no_mode == $multiple_chargeno)
	    {
		if ($epe_chargeno[$i] eq "")
		{
		    die sprintf("$whoami: charge number must be specified " .
				"for day in column %d\n", $i+1);
		}
		else
		{
		    $j = $epe_chargeno[$i] - 1;
		    $epe_chargeno[$i] = $chargenos[$j];
		}
	    }
	    $dist_employeepaid[$j] += $val;
	}
	$epe_per_diem[7] += $epe_per_diem[$i];
	$epe_lodging[7] += $epe_lodging[$i];
	$epe_personal_auto[7] += $epe_personal_auto[$i];
	$epe_tolls[7] += $epe_tolls[$i];
	$epe_business_meals[7] += $epe_business_meals[$i];
	$epe_other1[7] += $epe_other1[$i];
	$epe_other2[7] += $epe_other2[$i];
	$epe_other3[7] += $epe_other3[$i];
	$epe_total[7] += $epe_total[$i];
    }

    # Compute totals under expense distribution
    # if in multiple charge number mode, we've already computed
    # company-paid expenses.
    if ($charge_no_mode == $single_chargeno)
    {
	for ($i = 0; $i < 4; $i++)
	{
	    $dist_companypaid[0] += $airfare[$i] if ($airfare[$i] ne "");
	    $dist_companypaid[0] += $car[$i] if ($car[$i] ne "");
	}
    }

    for ($i = 0; $i < 3; $i++)
    {
	$dist_total[$i] = $dist_companypaid[$i] + $dist_employeepaid[$i];
	$dist_companypaid[3] += $dist_companypaid[$i];
	$dist_employeepaid[3] += $dist_employeepaid[$i];
	$dist_total[3] += $dist_total[$i];
    }

    $prior_balance = 0 if $prior_balance eq "";
    $advance = 0 if $advance eq "";

    $available = $prior_balance + $advance;
    $employee_paid = $dist_employeepaid[3];
    $amount_due = $available - $employee_paid;
    if ($amount_due < 0)
    {
	$amount_due = -$amount_due;
	$due_whom = "Employee";
    }
    else
    {
	$due_whom = "ERA";
    }
}

sub fix_dollar_amounts {
    # While computing totals, fill in dates for days that have any expenses.
    # Compute totals in employee-paid area
    for ($i = 0; $i < 8; $i++)
    {
	&fix_dollar_amount($epe_per_diem[$i]);
	&fix_dollar_amount($epe_lodging[$i]);
	&fix_dollar_amount($epe_personal_auto[$i]);
	&fix_dollar_amount($epe_tolls[$i]);
	&fix_dollar_amount($epe_business_meals[$i]);
	&fix_dollar_amount($epe_other1[$i]);
	&fix_dollar_amount($epe_other2[$i]);
	&fix_dollar_amount($epe_other3[$i]);
	&fix_dollar_amount($epe_total[$i]);
    }
    for ($i = 0; $i < 4; $i++)
    {
	&fix_dollar_amount($airfare[$i]);
	&fix_dollar_amount($car[$i]);
    }
    for ($i = 0; $i < 4; $i++)
    {
	&fix_dollar_amount($dist_companypaid[$i]);
	&fix_dollar_amount($dist_employeepaid[$i]);
	&fix_dollar_amount($dist_total[$i]);
    }
    &fix_dollar_amount($prior_balance);
    &fix_dollar_amount($advance);
    &fix_dollar_amount($available);
    &fix_dollar_amount($employee_paid);
    &fix_dollar_amount($amount_due);
}

sub fix_dollar_amount {
    return if ($_[0] eq "");
    if ($_[0] == 0)
    {
	$_[0] = "";
    }
    else
    {
	$_[0] = sprintf("%.2f", $_[0]);
    }
}

sub gen_ps {
    $prolog = "/usr/local/lib/expense-report.ps";
    open(PROLOG, "<$prolog") or die "$whoami: can't open $prolog: $!\n";
    
    while (<PROLOG>)
    {
	print;
	last if m/%%% INSERT HERE %%%/;
    }
    
    print <<EOF;

/name-string ${\&ps_string($name_string)} def
/date-string ${\&ps_string($date_string)} def
/purpose1-string ${\&ps_string($purpose_string[0])} def
/purpose2-string ${\&ps_string($purpose_string[1])} def
/per-diem-type $per_diem_type def

/from-string ${\&ps_array(@from_string)} def
/from-date ${\&ps_array(@from_date)} def
/from-time ${\&ps_array(@from_time)} def
/to-string ${\&ps_array(@to_string)} def
/to-date ${\&ps_array(@to_date)} def
/to-time ${\&ps_array(@to_time)} def
/airfare ${\&ps_array(@airfare)} def
/car ${\&ps_array(@car)} def
/chargeno-string ${\&ps_array(@chargeno_string)} def

/epe-date ${\&ps_array(@epe_date)} def
/epe-chargeno ${\&ps_array(@epe_chargeno)} def
/epe-per-diem ${\&ps_array(@epe_per_diem)} def
/epe-lodging ${\&ps_array(@epe_lodging)} def
/epe-personal-auto ${\&ps_array(@epe_personal_auto)} def
/epe-tolls ${\&ps_array(@epe_tolls)} def
/epe-business-meals ${\&ps_array(@epe_business_meals)} def
/epe-other1 ${\&ps_array(@epe_other1)} def
/epe-other2 ${\&ps_array(@epe_other2)} def
/epe-other3 ${\&ps_array(@epe_other3)} def
/epe-total ${\&ps_array(@epe_total)} def
/epe-other-explan1 ${\&ps_string($epe_other_explan[0])} def
/epe-other-explan2 ${\&ps_string($epe_other_explan[1])} def
/epe-other-explan3 ${\&ps_string($epe_other_explan[2])} def
/epe-main-chargeno ${\&ps_string($epe_main_chargeno)} def

/notes1-string ${\&ps_string($notes_string[0])} def
/notes2-string ${\&ps_string($notes_string[1])} def
/notes3-string ${\&ps_string($notes_string[2])} def
/notes4-string ${\&ps_string($notes_string[3])} def

/dist-chargeno ${\&ps_array(@dist_chargeno)} def
/dist-companypaid ${\&ps_array(@dist_companypaid)} def
/dist-employeepaid ${\&ps_array(@dist_employeepaid)} def
/dist-total ${\&ps_array(@dist_total)} def

/charge-yn $charge_yn def

/prior-balance ${\&ps_string($prior_balance)} def
/advance ${\&ps_string($advance)} def
/available ${\&ps_string($available)} def
/employee-paid ${\&ps_string($employee_paid)} def
/amount-due ${\&ps_string($amount_due)} def
/due-whom ${\&ps_string($due_whom)} def

EOF
    ;

    while (<PROLOG>)
    {
	(print, last) if m/%%% END OF INSERT %%%/;
    }
    
    while (<PROLOG>)
    {
	print;
    }
}

sub ps_string {
    local($str) = @_;
    $str = "" unless defined($str);
    $str =~ s/\(/\\\(/g;
    $str =~ s/\)/\\\)/g;
    "(" . $str . ")";
}

sub ps_array {
    local(@ary) = @_;
    local($result) = "[ ";
    for (@ary)
    {
	s/\(/\\\(/g;
	s/\)/\\\)/g;
	$result .= "(" . $_ . ")";
    }
    $result . " ]";
}

sub print_template {
    print <<EOF;
# Please fill in values for fields between the | signs.
# It should be clear what fields are based on the printed form.
# Some clarifying comments appear.  If you have suggestions for
# improving these comments, please say so.  All fields can be
# left blank except for per_diem_type, chargeno_match, epe_sat_date,
# and charge_number.

# Only one of "charge_number" and "charge_numbers" is legal.  If everything 
# is being charged to one number, use charge_number.  Otherwise, up to three
# charge numbers may be filled in in the charge_numbers entry.  If more than
# one charge number is used, various other fields become required throughout
# the template.  Uncomment whichever of these you wish to use.

#charge_number  |           |
#charge_numbers |           |           |           |

name |  |
date |  |

purpose1 |  |
purpose2 |  |

# Per diem types: 1 = lodgings plus, 2 = fixed, 3 = actual subsistence
per_diem_type |  |

#     |   from    |   date   |   time   |
#-----+-----------+----------+-----------
from1 |           |          |          |
from2 |           |          |          |
from3 |           |          |          |
from4 |           |          |          |

#   |   to      |   date   |   time   |
#---+-----------+----------+-----------
to1 |           |          |          |
to2 |           |          |          |
to3 |           |          |          |
to4 |           |          |          |

#             |  airfare  | car rental |
#-------------+-----------+------------+
travel_costs1 |           |            |
travel_costs2 |           |            |
travel_costs3 |           |            |
travel_costs4 |           |            |

# If multiple charge numbers are being used, the following are
# required.  Please use the number 1, 2, or 3 to refer to charge
# numbers entered above in the charge_numbers entry.  These
# entries are invalid in single-charge-number mode.

#travel_chargeno1 |   |
#travel_chargeno2 |   |
#travel_chargeno3 |   |
#travel_chargeno4 |   |

# Enter the date for Saturday here.  The program will automatically fill in
# remaining dates.  The date must be in the form month/day/year

epe_sat_date |  |

# The epe_charge_no entry is invalid in single-charge-number
# mode.  Otherwise, please uncomment the line and fill in a number
# from 1 to 3 to correspond to the numbers filled in in the charge_numbers
# entry. 

#                  |  SAT  |  SUN  |  MON  |  TUE  |  WED  |  THU  |  FRI  |
#------------------+-------+-------+-------+-------+-------+-------+-------+
#epe_chargeno	   |       |       |       |       |       |       |       |
epe_per_diem       |       |       |       |       |       |       |       |
epe_lodging        |       |       |       |       |       |       |       |
epe_personal_auto  |       |       |       |       |       |       |       |
epe_tolls          |       |       |       |       |       |       |       |
epe_business_meals |       |       |       |       |       |       |       |
epe_other1	   |       |       |       |       |       |       |       |
epe_other2	   |       |       |       |       |       |       |       |
epe_other3	   |       |       |       |       |       |       |       |

# Explanations for "Other" fields
epe_other_explan1 |  |
epe_other_explan2 |  |
epe_other_explan3 |  |

notes1 |  |
notes2 |  |
notes3 |  |
notes4 |  |

# Do charge numbers match what is on time sheet?  1 = yes, 2 = no
# Remember to fill in something above under notes if you say no here.
chargeno_match |  |

prior_balance |  |
advance       |  |
EOF
    ;
    exit 0;
}


sub usage {
    print STDERR "Usage: $whoami [ -help | -template | file ]
  -help gives help information for using this program.
  -template prints a template that you can fill in to stdout
  Otherwise, the given file name is taken to be a filled in expense
  report form to be converted.";
    exit 1;
}

sub help {
}

sub assert_fields {
    local($keyword, $actual_fields, $desired_fields) = @_;
    if ($actual_fields != $desired_fields)
    {
	print STDERR "$whoami: $keyword needs $desired_fields fields; found " .
	    "$actual_fields\n  ($file, line $.)\n";
	exit 1;
    }
}

sub fix_fields {
    for (@_)
    {
	s/^\s*(.*?)\s*$/$1/;
    }
}
