summaryrefslogtreecommitdiff
path: root/FS/bin
diff options
context:
space:
mode:
Diffstat (limited to 'FS/bin')
-rw-r--r--FS/bin/freeside-adduser57
-rwxr-xr-xFS/bin/freeside-apply-credits21
-rwxr-xr-xFS/bin/freeside-bill128
-rwxr-xr-xFS/bin/freeside-daily90
-rwxr-xr-xFS/bin/freeside-email61
-rwxr-xr-xFS/bin/freeside-overdue193
-rw-r--r--FS/bin/freeside-queued174
-rw-r--r--FS/bin/freeside-setinvoice42
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";
+}
+
+