diff options
Diffstat (limited to 'FS/bin')
| -rw-r--r-- | FS/bin/freeside-adduser | 57 | ||||
| -rwxr-xr-x | FS/bin/freeside-apply-credits | 21 | ||||
| -rwxr-xr-x | FS/bin/freeside-bill | 128 | ||||
| -rwxr-xr-x | FS/bin/freeside-daily | 90 | ||||
| -rwxr-xr-x | FS/bin/freeside-email | 61 | ||||
| -rwxr-xr-x | FS/bin/freeside-overdue | 193 | ||||
| -rw-r--r-- | FS/bin/freeside-queued | 174 | ||||
| -rw-r--r-- | FS/bin/freeside-setinvoice | 42 | 
8 files changed, 766 insertions, 0 deletions
diff --git a/FS/bin/freeside-adduser b/FS/bin/freeside-adduser new file mode 100644 index 000000000..9d424634b --- /dev/null +++ b/FS/bin/freeside-adduser @@ -0,0 +1,57 @@ +#!/usr/bin/perl -w +# +# $Id: freeside-adduser,v 1.4 2002-02-06 14:58:05 ivan Exp $ + +use strict; +use vars qw($opt_h $opt_c $opt_s); +use Getopt::Std; + +my $FREESIDE_CONF = "/usr/local/etc/freeside"; + +getopts("ch:s:"); +die &usage if $opt_c && ! $opt_h; +my $user = shift or die &usage; + +if ( $opt_h ) { +  my @args = ( 'htpasswd' ); +  push @args, '-c' if $opt_c; +  push @args, $opt_h, $user; +  system(@args) == 0 or die "htpasswd failed: $?"; +} + +my $secretfile = $opt_s || 'secrets'; + +open(MAPSECRETS,">>$FREESIDE_CONF/mapsecrets") +  or die "can't open $FREESIDE_CONF/mapsecrets: $!"; +print MAPSECRETS "$user $secretfile\n"; +close MAPSECRETS or die "can't close $FREESIDE_CONF/mapsecrets: $!"; + +sub usage { +  die "Usage:\n\n  freeside-adduser [ -h htpasswd_file [ -c ] ] [ -s secretfile ] username" +} + +=head1 NAME + +freeside-adduser - Command line interface to add (freeside) users. + +=head1 SYNOPSIS + +  freeside-adduser [ -h htpasswd_file [ -c ] ] [ -s secretfile ] username + +=head1 DESCRIPTION + +Adds a user to the Freeside billing system.  This is for adding users (internal +sales/tech folks) to the web interface, not for adding customer accounts. + +  -h: Also call htpasswd for this user with the given filename + +  -c: Passed to htpasswd + +  -s: Specify an alternate secret file + +=head1 SEE ALSO + +L<htpasswd>, base Freeside documentation + +=cut + diff --git a/FS/bin/freeside-apply-credits b/FS/bin/freeside-apply-credits new file mode 100755 index 000000000..ea6a7bdd0 --- /dev/null +++ b/FS/bin/freeside-apply-credits @@ -0,0 +1,21 @@ +#!/usr/bin/perl -Tw + +use strict; +use vars qw( $user $cust_main @customers ); +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearch); +use FS::cust_main; + +$user = shift or die &usage; +&adminsuidsetup( $user ); + +my @customers = qsearch('cust_main', {} ); +die "No customers" unless (scalar(@customers) > 0); + +foreach $cust_main (@customers) { +  print "Applying credits for customer #". $cust_main->custnum; +  $cust_main->apply_credits; +} + + + diff --git a/FS/bin/freeside-bill b/FS/bin/freeside-bill new file mode 100755 index 000000000..49ad4a768 --- /dev/null +++ b/FS/bin/freeside-bill @@ -0,0 +1,128 @@ +#!/usr/bin/perl -w +# don't take any world-facing input +#!/usr/bin/perl -Tw + +use strict; +use Fcntl qw(:flock); +use Date::Parse; +use Getopt::Std; +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearch qsearchs); +use FS::cust_main; + +&untaint_argv;	#what it sounds like  (eww) +use vars qw($opt_a $opt_c $opt_d $opt_p); +getopts("acd:p"); +my $user = shift or die &usage; + +adminsuidsetup $user; + +my %bill_only = map { $_ => 1 } ( +  @ARGV ? @ARGV : ( map $_->custnum, qsearch('cust_main', {} ) ) +); + +#we're at now now (and later). +my($time)= $opt_d ? str2time($opt_d) : $^T; + +# find packages w/ bill < time && cancel != '', and create corresponding +# customer objects + +my($cust_main,%saw); +foreach $cust_main ( +  map { +    unless ( exists $saw{ $_->custnum } && defined $saw{ $_->custnum} ) { +      $saw{ $_->custnum } = 0; # to avoid 'use of uninitialized value' errors +    } +    if ( +      ( $opt_a || ( ( $_->getfield('bill') || 0 ) <= $time ) ) +      && $bill_only{ $_->custnum } +      && !$saw{ $_->custnum }++ +    ) { +      qsearchs('cust_main',{'custnum'=> $_->custnum } ); +    } else { +      (); +    } +  } ( qsearch('cust_pkg', { 'cancel' => '' }), +      qsearch('cust_pkg', { 'cancel' => 0  }), +    ) +) { + +  # and bill them + +  print "Billing customer #" . $cust_main->getfield('custnum') . "\n"; + +  my($error); + +  $error=$cust_main->bill('time'=>$time); +  warn "Error billing,  customer #" . $cust_main->getfield('custnum') .  +    ":" . $error if $error; + +  if ($opt_p) { +    $cust_main->apply_payments; +    $cust_main->apply_credits; +  } + +  if ($opt_c) { +    $error=$cust_main->collect( 'invoice_time' => $time); +    warn "Error collecting from customer #" . $cust_main->custnum.  ":$error" +      if $error; + +    #sleep 1; +  } + +} + +# subroutines + +sub untaint_argv { +  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV +    #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\""; +    # Date::Parse +    $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\""; +    $ARGV[$_]=$1; +  } +} + +sub usage { +  die "Usage:\n\n  freeside-bill [ -c [ -p ] ] [ -d 'date' ] user [ custnum custnum ... ]\n"; +} + +=head1 NAME + +freeside-bill - Command line (crontab, script) interface to customer billing. + +=head1 SYNOPSIS + +  freeside-bill [ -c [ -p ] [ -a ] ] [ -d 'date' ] user [ custnum custnum ... ] + +=head1 DESCRIPTION + +This script is deprecated in 1.4.0.  You should use freeside-daily instead. + +Bills customers.  Searches for customers who are due for billing and calls +the bill and collect methods of a cust_main object.  See L<FS::cust_main>. + +  -c: Turn on collecting (you probably want this). + +  -p: Apply unapplied payments and credits before collecting (you probably want +      this too) + +  -a: Call collect even if there isn't a new invoice (probably a bad idea for +      daily use) + +  -d: Pretend it's 'date'.  Date is in any format Date::Parse is happy with, +      but be careful. + +user: From the mapsecrets file - see config.html from the base documentation + +custnum: if one or more customer numbers are specified, only bills those +customers.  Otherwise, bills all customers. + +=head1 BUGS + +=head1 SEE ALSO + +L<freeside-daily>, L<FS::cust_main>, config.html from the base documentation + +=cut + diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily new file mode 100755 index 000000000..8d839cb21 --- /dev/null +++ b/FS/bin/freeside-daily @@ -0,0 +1,90 @@ +#!/usr/bin/perl -w + +use strict; +use Fcntl qw(:flock); +use Date::Parse; +use Getopt::Std; +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearch qsearchs); +use FS::cust_main; + +&untaint_argv;	#what it sounds like  (eww) +use vars qw($opt_d); +getopts("d:"); +my $user = shift or die &usage; + +adminsuidsetup $user; + +my @cust_main = @ARGV +  ? map { qsearchs('cust_main', { custnum => $_ } ) } @ARGV +  : qsearch('cust_main', {} ) +; + +#we're at now now (and later). +my($time)= $opt_d ? str2time($opt_d) : $^T; + +my($cust_main,%saw); +foreach $cust_main ( @cust_main ) { + +  my $error; + +  $error = $cust_main->bill( 'time' => $time ); +  warn "Error billing, custnum ". $cust_main->custnum. ": $error" if $error; + +  $cust_main->apply_payments; +  $cust_main->apply_credits; + +  $error=$cust_main->collect( 'invoice_time' => $time ); +  warn "Error collecting, custnum". $cust_main->custnum. ": $error" if $error; + +} + +# subroutines + +sub untaint_argv { +  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV +    #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\""; +    # Date::Parse +    $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\""; +    $ARGV[$_]=$1; +  } +} + +sub usage { +  die "Usage:\n\n  freeside-daily [ -d 'date' ] user [ custnum custnum ... ]\n"; +} + +=head1 NAME + +freeside-daily - Run daily billing and invoice collection events. + +=head1 SYNOPSIS + +  freeside-daily [ -d 'date' ] user [ custnum custnum ... ] + +=head1 DESCRIPTION + +Bills customers and runs invoice collection events.  Should be run from +crontab daily. + +This script replaces freeside-bill from 1.3.1. + +Bills customers.  Searches for customers who are due for billing and calls +the bill and collect methods of a cust_main object.  See L<FS::cust_main>. + +  -d: Pretend it's 'date'.  Date is in any format Date::Parse is happy with, +      but be careful. + +user: From the mapsecrets file - see config.html from the base documentation + +custnum: if one or more customer numbers are specified, only bills those +customers.  Otherwise, bills all customers. + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::cust_main>, config.html from the base documentation + +=cut + diff --git a/FS/bin/freeside-email b/FS/bin/freeside-email new file mode 100755 index 000000000..c7ff41114 --- /dev/null +++ b/FS/bin/freeside-email @@ -0,0 +1,61 @@ +#!/usr/bin/perl -Tw + +use strict; +use FS::UID qw(adminsuidsetup); +use FS::Conf; +use FS::Record qw(qsearch); +use FS::svc_acct; + +&untaint_argv;	#what it sounds like  (eww) +my $user = shift or die &usage; + +adminsuidsetup $user; + +my $conf = new FS::Conf; +my $domain = $conf->config('domain'); + +my @svc_acct = qsearch('svc_acct', {}); +my @usernames = map $_->username, @svc_acct; +my @emails = map "$_\@$domain", @usernames; + +print join("\n", @emails), "\n"; + +# subroutines + +sub untaint_argv { +  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV +    #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\""; +    # Date::Parse +    $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\""; +    $ARGV[$_]=$1; +  } +} + +sub usage { +  die "Usage:\n\n  freeside-email user\n"; +} + +=head1 NAME + +freeside-email - Prints email addresses of all users on STDOUT + +=head1 SYNOPSIS + +  freeside-email user + +=head1 DESCRIPTION + +Prints the email addresses of all customers on STDOUT, separated by newlines. + +user: From the mapsecrets file - see config.html from the base documentation + +=head1 VERSION + +$Id: freeside-email,v 1.1 2001-05-15 07:52:34 ivan Exp $ + +=head1 BUGS + +=head1 SEE ALSO + +=cut + diff --git a/FS/bin/freeside-overdue b/FS/bin/freeside-overdue new file mode 100755 index 000000000..db99e62b4 --- /dev/null +++ b/FS/bin/freeside-overdue @@ -0,0 +1,193 @@ +#!/usr/bin/perl -w + +use strict; +use vars qw( $days_to_pay $cust_main $cust_pkg  +             $cust_svc $svc_acct ); +use Getopt::Std; +use FS::cust_main; +use FS::cust_pkg; +use FS::cust_svc; +use FS::svc_acct; +use FS::Record qw(qsearch qsearchs); +use FS::UID qw(adminsuidsetup); + +&untaint_argv; +my %opt; +getopts('ed:qpl:scbyoi', \%opt); +my $user = shift or die &usage; + +adminsuidsetup $user; + +my $now = time; #eventually take a time option like freeside-bill +my ($sec,$min,$hour,$mday,$mon,$year) = +  (localtime($now) )[0,1,2,3,4,5]; +$mon++; +$year += 1900; + +foreach $cust_main ( qsearch('cust_main',{} ) ) { + +  my ( $eyear, $emon, $eday ) = ( 2037, 12, 31 ); +  if ( $cust_main->paydate =~ /^(\d{4})\-(\d{1,2})\-(\d{1,2})$/ +       && $cust_main->payby eq 'BILL') { +    ( $eyear, $emon, $eday ) = ( $1, $2, $3 ); +  } + +  if ( ( $opt{d} +           && $cust_main->balance_date(time - $opt{d} * 86400) > 0 +           && qsearchs( 'cust_pkg', { 'custnum' => $cust_main->custnum, +                                      'susp' => "" } ) ) +       || ( $opt{e} +            && $cust_main->payby eq 'BILL' +            && ( $eyear < $year +                 || ( $eyear == $year && $emon < $mon ) ) ) +  ) {  + +    unless ( $opt{q} ) { +      print $cust_main->custnum, "\t", +            $cust_main->last, "\t", $cust_main->first, "\t", +            $cust_main->balance_date(time-$opt{d} * 86400); +    } + +    if ( $opt{p} && ! grep { $_ eq 'POST' } $cust_main->invoicing_list ) { +      print "\n\tAdding postal invoicing" unless $opt{q}; +      my @invoicing_list = $cust_main->invoicing_list; +      push @invoicing_list, 'POST'; +      $cust_main->invoicing_list(\@invoicing_list); +    } + +    if ( $opt{l} ) { +      print "\n\tCharging late fee of \$$opt{l}" unless $opt{q}; +      my $error = $cust_main->charge($opt{l}, 'Late fee'); +      # comment or plandata with info so we don't redo the same late fee every +      # day +    } + +    foreach $cust_pkg ( qsearch( 'cust_pkg',  +                                 { 'custnum' => $cust_main->custnum } ) ) { + +      if ($opt{s}) { +        print "\n\tSuspending pkgnum " . $cust_pkg->pkgnum unless $opt{q}; +        $cust_pkg->suspend; +      } + +      if ($opt{c}) { +        print "\n\tCancelling pkgnum " . $cust_pkg->pkgnum unless $opt{q}; +        $cust_pkg->cancel; +      } +       +    } + +    if ( $opt{b} ) { +      print "\n\tBilling" unless $opt{q}; +      my $error = $cust_main->bill('time'=>$now); +      warn "Error billing,  customer #" . $cust_main->custnum .  +        ":" . $error if $error; +    } + +    if ( $opt{y} ) { +      print "\n\tApplying outstanding payments and credits" unless $opt{q}; +      $cust_main->apply_payments; +      $cust_main->apply_credits; +    } + +    if ( $opt{o} ) { +      print "\n\tCollecting" unless $opt{q}; +      my $error = $cust_main->collect( +        'invoice_time' => $now, +        'batch_card'   => $opt{i} ? 'no' : 'yes', +        'force_print'  => 'yes', +      ); +      warn "Error collecting from customer #" . $cust_main->custnum.  ":$error" +        if $error; +    } + +    print "\n" unless $opt{q}; + +  } + +} + +sub untaint_argv { +  foreach $_ ( $[ .. $#ARGV ) {  +    $ARGV[$_] =~ /^([\w\-\/\.]*)$/ || die "Illegal arguement \"$ARGV[$_]\""; +    $ARGV[$_]=$1; +  } +} + +sub usage { +  die "Usage:\n\n    freeside-overdue [ -e ] [ -d days ] [ -q ] [ -p ] [ -l amount ] [ -s ] [ -c ] [ -b ] [ -y ] [ -o [ -i ] ] user\n"; +} + + +=head1 NAME + +freeside-overdue - Perform actions on overdue and/or expired accounts. + +=head1 SYNOPSIS + +  freeside-overdue [ -e ] [ -d days ] [ -q ] [ -p ] [ -l amount ] [ -s ] [ -c ] [ -b ] [ -y ] [ -o [ -i ] ] user + +=head1 DESCRIPTION + +Performs actions on overdue and/or expired accounts. + +Selection options (at least one selection option is required): + +  -d:  Customers with a balance due on invoices older than the supplied number +       of days.  Requires an integer argument. + +  -e:  Customers with a billing expiration date in the past. + +Action options:  + +  -q:  Be quiet (by default, selected accounts are printed). + +  -p:  Add postal invoicing to the relevant customers. + +  -l:  Add a charge of the given amount to the relevant customers. + +  -s:  Suspend accounts. + +  -c:  Cancel accounts. + +  -b:  Bill customers (create invoices) + +  -y:  Apply unapplied payments and credits + +  -o:  Collect from customers (charge cards, print invoices) + +    -i:  real-time billing (as opposed to batch billing).  only relevant +         for credit cards. + +  user: From the mapsecrets file - see config.html from the base documentation + +=head1 CRONTAB + +Example crontab entries: + +# suspend expired accounts +20 4 * * * freeside-overdue -e -s user + +# quietly add postal invoicing to customers over 30 days past due +20 4 * * * freeside-overdue -d 30 -p -q user + +# suspend accounts and charge a $10.23 fee for customers over 60 days past due +20 4 * * * freeside-overdue -d 60 -s -l 10.23 user + +# cancel accounts over 90 days past due +20 4 * * * freeside-overdue -d 90 -c user + +=head1 ORIGINAL AUTHORS + +Original disable-overdue version by mw/kwh: Mark W.? and Kristian Hoffmann ? + +Ivan seems to be turning it into the "do-everything" CLI. + +=head1 BUGS + +Hell now that this is the do-everything CLI it should have --longoptions + +=cut + +1; + diff --git a/FS/bin/freeside-queued b/FS/bin/freeside-queued new file mode 100644 index 000000000..87e3cb422 --- /dev/null +++ b/FS/bin/freeside-queued @@ -0,0 +1,174 @@ +#!/usr/bin/perl -w + +use strict; +use vars qw( $log_file $sigterm $sigint ); +use subs qw( _die _logmsg ); +use Fcntl qw(:flock); +use POSIX qw(setsid); +use Date::Format; +use IO::File; +use FS::UID qw(adminsuidsetup forksuidsetup driver_name); +use FS::Record qw(qsearchs); +use FS::queue; + +# no autoloading just yet +use FS::cust_main; +use FS::svc_acct; +use Net::SSH; + +my $pid_file = '/var/run/freeside-queued.pid'; + +my $user = shift or die &usage; + +&daemonize; + +sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; } +$SIG{CHLD} =  \&REAPER; + + $sigterm = 0; + $sigint = 0; +$SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $sigint++; }; +$SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $sigterm++; }; + +$> = $FS::UID::freeside_uid unless $>; +$< = $>; +$ENV{HOME} = (getpwuid($>))[7]; #for ssh +adminsuidsetup $user; + +$log_file = "/usr/local/etc/freeside/queuelog.". $FS::UID::datasrc; + +$SIG{__DIE__} = \&_die; +$SIG{__WARN__} = \&_logmsg; + +warn "freeside-queued starting\n"; + +while (1) { + +  my $job = qsearchs( +    'queue', +    { 'status' => 'new' }, +    '', +    driver_name =~ /^mysql$/i +      ? 'ORDER BY jobnum LIMIT 1 FOR UPDATE' +      : 'ORDER BY jobnum FOR UPDATE LIMIT 1' +  ) or do { +    sleep 5; +    next; +  }; + +  my %hash = $job->hash; +  $hash{'status'} = 'locked'; +  my $ljob = new FS::queue ( \%hash ); +  my $error = $ljob->replace($job); +  die $error if $error; + +  my @args = $ljob->args; + +  # number of children limit? +  defined( my $pid = fork ) or do { +    warn "WARNING: can't fork: $!\n"; +    my %hash = $job->hash; +    $hash{'status'} = 'failed'; +    my $ljob = new FS::queue ( \%hash ); +    my $error = $ljob->replace($job); +    die $error if $error; +  }; + +  unless ( $pid ) { #kid time + +    #get new db handles +    $FS::UID::dbh->{InactiveDestroy} = 1; +    $FS::svc_acct::icradius_dbh->{InactiveDestroy} = 1 +      if $FS::svc_acct::icradius_dbh; +    forksuidsetup($user); + +    my $eval = "&". $ljob->job. '(@args);'; +    warn "running $eval"; +    eval $eval; +    if ( $@ ) { +      warn "job $eval failed"; +      my %hash = $ljob->hash; +      $hash{'status'} = 'failed'; +      my $fjob = new FS::queue( \%hash ); +      my $error = $fjob->replace($ljob); +      die $error if $error; +    } else { +      $ljob->delete; +    } + +    exit; +    #end-of-kid +  } + +} continue { +  if ( $sigterm ) { +    warn "received TERM signal; exiting\n"; +    exit; +  } +  if ( $sigint ) { +    warn "received INT signal; exiting\n"; +    exit; +  } +} + +sub usage { +  die "Usage:\n\n  freeside-queued user\n"; +} + +sub _die { +  my $msg = shift; +  unlink $pid_file if -e $pid_file; +  _logmsg($msg); +} + +sub _logmsg { +  chomp( my $msg = shift ); +  my $log = new IO::File ">>$log_file"; +  flock($log, LOCK_EX); +  seek($log, 0, 2); +  print $log "[". time2str("%a %b %e %T %Y",time). "] [$$] $msg\n"; +  flock($log, LOCK_UN); +  close $log; +} + +sub daemonize { + +  chdir "/" or die "Can't chdir to /: $!"; +  open STDIN, '/dev/null'   or die "Can't read /dev/null: $!"; +  defined(my $pid = fork) or die "Can't fork: $!"; +  if ( $pid ) { +    print "freeside-queued started with pid $pid\n"; #logging to $log_file\n"; +    exit unless $pid_file; +    my $pidfh = new IO::File ">$pid_file" or exit; +    print $pidfh "$pid\n"; +    exit; +  } +  open STDOUT, '>/dev/null' +                            or die "Can't write to /dev/null: $!"; +  setsid                  or die "Can't start a new session: $!"; +  open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; + +} + +=head1 NAME + +freeside-queued - Job queue daemon + +=head1 SYNOPSIS + +  freeside-queued user + +=head1 DESCRIPTION + +Job queue daemon.  Should be running at all times. + +user: from the mapsecrets file - see config.html from the base documentation + +=head1 VERSION + +=head1 BUGS + +=head1 SEE ALSO + +=cut + diff --git a/FS/bin/freeside-setinvoice b/FS/bin/freeside-setinvoice new file mode 100644 index 000000000..708e2fa30 --- /dev/null +++ b/FS/bin/freeside-setinvoice @@ -0,0 +1,42 @@ +#!/usr/bin/perl + +use strict; +use FS::UID qw(adminsuidsetup); +use FS::Conf; +use FS::Record qw(qsearch qsearchs); +use FS::cust_main; +use FS::svc_acct; + +&untaint_argv;  #what it sounds like  (eww) +my $user = shift or die &usage; + +adminsuidsetup $user; + +foreach my $cust_main ( +   grep { ! scalar($_->invoicing_list) } +     qsearch( 'cust_main', {} ) +) { +  my @dest; +  my @cust_pkg = $cust_main->ncancelled_pkgs; +  foreach my $cust_pkg ( @cust_pkg ) { +    foreach my $cust_svc ( $cust_pkg->cust_svc ) { +      my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $cust_svc->svcnum } ); +      push @dest, $svc_acct->svcnum if $svc_acct; +    } +  } +  push @dest, 'POST' unless @dest; +  $cust_main->invoicing_list(\@dest); +} + +sub untaint_argv { +  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV +    $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\""; +    $ARGV[$_]=$1; +  } +} + +sub usage { +  die "Usage:\n\n  freeside-setinvoice user\n"; +} + +  | 
