[freeside-commits] freeside/FS/FS cust_main.pm, 1.397, 1.398 Schema.pm, 1.119, 1.120 Conf.pm, 1.258, 1.259 cust_pkg.pm, 1.107, 1.108
Ivan,,,
ivan at wavetail.420.am
Sat Jan 10 15:56:59 PST 2009
Update of /home/cvs/cvsroot/freeside/FS/FS
In directory wavetail.420.am:/tmp/cvs-serv3185/FS/FS
Modified Files:
cust_main.pm Schema.pm Conf.pm cust_pkg.pm
Log Message:
implement package changes w/location change, RT#4499
Index: Schema.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/Schema.pm,v
retrieving revision 1.119
retrieving revision 1.120
diff -u -d -r1.119 -r1.120
--- Schema.pm 8 Jan 2009 01:45:21 -0000 1.119
+++ Schema.pm 10 Jan 2009 23:56:56 -0000 1.120
@@ -661,13 +661,20 @@
'primary_key' => 'custnum',
'unique' => [ [ 'agentnum', 'agent_custid' ] ],
#'index' => [ ['last'], ['company'] ],
- 'index' => [ ['last'], [ 'company' ], [ 'referral_custnum' ],
- [ 'daytime' ], [ 'night' ], [ 'fax' ], [ 'refnum' ],
- [ 'county' ], [ 'state' ], [ 'country' ], [ 'zip' ],
+ 'index' => [
+ [ 'agentnum' ], [ 'refnum' ], [ 'custbatch' ],
+ [ 'referral_custnum' ],
+ [ 'payby' ], [ 'paydate' ],
+ #billing
+ [ 'last' ], [ 'company' ],
+ [ 'county' ], [ 'state' ], [ 'country' ],
+ [ 'zip' ],
+ [ 'daytime' ], [ 'night' ], [ 'fax' ],
+ #shipping
[ 'ship_last' ], [ 'ship_company' ],
+ [ 'ship_county' ], [ 'ship_state' ], [ 'ship_country' ],
+ [ 'ship_zip' ],
[ 'ship_daytime' ], [ 'ship_night' ], [ 'ship_fax' ],
- [ 'payby' ], [ 'paydate' ],
- [ 'agentnum' ], [ 'custbatch' ],
],
},
@@ -975,6 +982,7 @@
'change_date', @date_type, '', '',
'change_pkgnum', 'int', 'NULL', '', '', '',
'change_pkgpart', 'int', 'NULL', '', '', '',
+ 'change_locationnum', 'int', 'NULL', '', '', '',
'manual_flag', 'char', 'NULL', 1, '', '',
'quantity', 'int', 'NULL', '', '', '',
],
@@ -1994,7 +2002,7 @@
],
'primary_key' => 'acctid',
'unique' => [],
- 'index' => [ [ 'calldate' ], [ 'dst' ], [ 'accountcode' ], [ 'freesidestatus' ], [ 'cdrbatch' ], ],
+ 'index' => [ [ 'calldate' ], [ 'src' ], [ 'dst' ], [ 'charged_party' ], [ 'accountcode' ], [ 'freesidestatus' ], [ 'cdrbatch' ], ],
},
'cdr_calltype' => {
@@ -2039,6 +2047,21 @@
'index' => [],
},
+ #'cdr_file' => {
+ # 'columns' => [
+ # 'filenum', 'serial', '', '', '', '',
+ # 'filename', 'varchar', '', '', '', '',
+ # 'status', 'varchar', 'NULL', '', '', '',
+ # ],
+ # 'primary_key' => 'filenum',
+ # 'unique' => [ [ 'filename' ], ], #just change the index if we need to
+ # # agent-virtualize or have a customer
+ # # with dup-filename needs or something
+ # # (only used by cdr.http_and_import for
+ # # chrissakes)
+ # 'index' => [],
+ #},
+
'inventory_item' => {
'columns' => [
'itemnum', 'serial', '', '', '', '',
Index: Conf.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/Conf.pm,v
retrieving revision 1.258
retrieving revision 1.259
diff -u -d -r1.258 -r1.259
--- Conf.pm 10 Jan 2009 00:43:05 -0000 1.258
+++ Conf.pm 10 Jan 2009 23:56:56 -0000 1.259
@@ -767,6 +767,14 @@
},
{
+ 'key' => 'invoice_subject',
+ 'section' => 'billing',
+ 'description' => 'Subject: header on email invoices. Defaults to "Invoice". The following substitutions are available: $name, $name_short, $invoice_number, and $invoice_date.',
+ 'type' => 'text',
+ 'per_agent' => 1,
+ },
+
+ {
'key' => 'invoice_template',
'section' => 'billing',
'description' => 'Text template file for invoices. Used if no invoice_html template is defined, and also seen by users using non-HTML capable mail clients. See the <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:1.7:Documentation:Administration#Plaintext_invoice_templates">billing documentation</a> for details.',
Index: cust_main.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/cust_main.pm,v
retrieving revision 1.397
retrieving revision 1.398
diff -u -d -r1.397 -r1.398
--- cust_main.pm 10 Jan 2009 00:43:05 -0000 1.397
+++ cust_main.pm 10 Jan 2009 23:56:56 -0000 1.398
@@ -659,85 +659,9 @@
}
-=item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
-
-Like the insert method on an existing record, this method orders a package
-and included services atomicaly. Pass a Tie::RefHash data structure to this
-method containing FS::cust_pkg and FS::svc_I<tablename> objects. There should
-be a better explanation of this, but until then, here's an example:
-
- use Tie::RefHash;
- tie %hash, 'Tie::RefHash'; #this part is important
- %hash = (
- $cust_pkg => [ $svc_acct ],
- ...
- );
- $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
-
-Services can be new, in which case they are inserted, or existing unaudited
-services, in which case they are linked to the newly-created package.
-
-Currently available options are: I<depend_jobnum> and I<noexport>.
-
-If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
-on the supplied jobnum (they will not run until the specific job completes).
-This can be used to defer provisioning until some action completes (such
-as running the customer's credit card successfully).
-
-The I<noexport> option is deprecated. If I<noexport> is set true, no
-provisioning jobs (exports) are scheduled. (You can schedule them later with
-the B<reexport> method for each cust_pkg object. Using the B<reexport> method
-on the cust_main object is not recommended, as existing services will also be
-reexported.)
-
-=cut
-
-sub order_pkgs {
- my $self = shift;
- my $cust_pkgs = shift;
- my $seconds = shift;
- my %options = @_;
-
- warn "$me order_pkgs called with options ".
- join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
- if $DEBUG;
-
- local $SIG{HUP} = 'IGNORE';
- local $SIG{INT} = 'IGNORE';
- local $SIG{QUIT} = 'IGNORE';
- local $SIG{TERM} = 'IGNORE';
- local $SIG{TSTP} = 'IGNORE';
- local $SIG{PIPE} = 'IGNORE';
-
- my $oldAutoCommit = $FS::UID::AutoCommit;
- local $FS::UID::AutoCommit = 0;
- my $dbh = dbh;
-
- local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
-
- foreach my $cust_pkg ( keys %$cust_pkgs ) {
-
- my $error = $self->order_pkg( 'cust_pkg' => $cust_pkg,
- 'svcs' => $cust_pkgs->{$cust_pkg},
- 'seconds' => $seconds,
- 'depend_jobnum' => $options{'depend_jobnum'},
- );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
-
- }
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
- ''; #no error
-}
-
=item order_pkg HASHREF | OPTION => VALUE ...
-Orders a single package. This is the preferred and most flexible method for
-ordering a single package, including the ability to set a (new or existing)
-location as well as insert services.
+Orders a single package.
Options may be passed as a list of key/value pairs or as a hash reference.
Options are:
@@ -837,6 +761,81 @@
}
+=item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
+
+Like the insert method on an existing record, this method orders multiple
+packages and included services atomicaly. Pass a Tie::RefHash data structure
+to this method containing FS::cust_pkg and FS::svc_I<tablename> objects.
+There should be a better explanation of this, but until then, here's an
+example:
+
+ use Tie::RefHash;
+ tie %hash, 'Tie::RefHash'; #this part is important
+ %hash = (
+ $cust_pkg => [ $svc_acct ],
+ ...
+ );
+ $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
+
+Services can be new, in which case they are inserted, or existing unaudited
+services, in which case they are linked to the newly-created package.
+
+Currently available options are: I<depend_jobnum> and I<noexport>.
+
+If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
+on the supplied jobnum (they will not run until the specific job completes).
+This can be used to defer provisioning until some action completes (such
+as running the customer's credit card successfully).
+
+The I<noexport> option is deprecated. If I<noexport> is set true, no
+provisioning jobs (exports) are scheduled. (You can schedule them later with
+the B<reexport> method for each cust_pkg object. Using the B<reexport> method
+on the cust_main object is not recommended, as existing services will also be
+reexported.)
+
+=cut
+
+sub order_pkgs {
+ my $self = shift;
+ my $cust_pkgs = shift;
+ my $seconds = shift;
+ my %options = @_;
+
+ warn "$me order_pkgs called with options ".
+ join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
+ if $DEBUG;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
+
+ foreach my $cust_pkg ( keys %$cust_pkgs ) {
+
+ my $error = $self->order_pkg( 'cust_pkg' => $cust_pkg,
+ 'svcs' => $cust_pkgs->{$cust_pkg},
+ 'seconds' => $seconds,
+ 'depend_jobnum' => $options{'depend_jobnum'},
+ );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ ''; #no error
+}
+
=item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF, UPBYTEREF, DOWNBYTEREF ]
Recharges this (existing) customer with the specified prepaid card (see
@@ -3896,7 +3895,7 @@
my $templ_hash = { error => $transaction->error_message };
my $error = send_email(
- 'from' => $conf->config('invoice_from'),
+ 'from' => $conf->config('invoice_from', $self->agentnum ),
'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
'subject' => 'Your payment could not be processed',
'body' => [ $template->fill_in(HASH => $templ_hash) ],
@@ -5391,6 +5390,35 @@
}
}
+=item name_short
+
+Returns a name string for this customer, either "Company" or "First Last".
+
+=cut
+
+sub name_short {
+ my $self = shift;
+ $self->company !~ /^\s*$/ ? $self->company : $self->contact_firstlast;
+}
+
+=item ship_name_short
+
+Returns a name string for this (service/shipping) contact, either "Company"
+or "First Last".
+
+=cut
+
+sub ship_name_short {
+ my $self = shift;
+ if ( $self->get('ship_last') ) {
+ $self->ship_company !~ /^\s*$/
+ ? $self->ship_company
+ : $self->ship_contact_firstlast;
+ } else {
+ $self->name_company_or_firstlast;
+ }
+}
+
=item contact
Returns this customer's full (billing) contact name only, "Last, First"
@@ -5415,6 +5443,30 @@
: $self->contact;
}
+=item contact_firstlast
+
+Returns this customers full (billing) contact name only, "First Last".
+
+=cut
+
+sub contact_firstlast {
+ my $self = shift;
+ $self->first. ' '. $self->get('last');
+}
+
+=item ship_contact_firstlast
+
+Returns this customer's full (shipping) contact name only, "First Last".
+
+=cut
+
+sub ship_contact_firstlast {
+ my $self = shift;
+ $self->get('ship_last')
+ ? $self->first. ' '. $self->get('ship_last')
+ : $self->contact_firstlast;
+}
+
=item country_full
Returns this customer's full country name
@@ -6717,18 +6769,19 @@
=cut
sub notify {
- my ($customer, $template, %options) = @_;
+ my ($self, $template, %options) = @_;
return unless $conf->exists($template);
- my $from = $conf->config('invoice_from') if $conf->exists('invoice_from');
+ my $from = $conf->config('invoice_from', $self->agentnum)
+ if $conf->exists('invoice_from', $self->agentnum);
$from = $options{from} if exists($options{from});
- my $to = join(',', $customer->invoicing_list_emailonly);
+ my $to = join(',', $self->invoicing_list_emailonly);
$to = $options{to} if exists($options{to});
- my $subject = "Notice from " . $conf->config('company_name')
- if $conf->exists('company_name');
+ my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
+ if $conf->exists('company_name', $self->agentnum);
$subject = $options{subject} if exists($options{subject});
my $notify_template = new Text::Template (TYPE => 'ARRAY',
@@ -6739,16 +6792,17 @@
$notify_template->compile()
or die "can't compile template: Text::Template::ERROR";
- $FS::notify_template::_template::company_name = $conf->config('company_name');
+ $FS::notify_template::_template::company_name =
+ $conf->config('company_name', $self->agentnum);
$FS::notify_template::_template::company_address =
- join("\n", $conf->config('company_address') ). "\n";
+ join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
- my $paydate = $customer->paydate || '2037-12-31';
- $FS::notify_template::_template::first = $customer->first;
- $FS::notify_template::_template::last = $customer->last;
- $FS::notify_template::_template::company = $customer->company;
- $FS::notify_template::_template::payinfo = $customer->mask_payinfo;
- my $payby = $customer->payby;
+ my $paydate = $self->paydate || '2037-12-31';
+ $FS::notify_template::_template::first = $self->first;
+ $FS::notify_template::_template::last = $self->last;
+ $FS::notify_template::_template::company = $self->company;
+ $FS::notify_template::_template::payinfo = $self->mask_payinfo;
+ my $payby = $self->payby;
my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
@@ -6848,10 +6902,10 @@
);
if ( length($retadd) ) {
$letter_data{returnaddress} = $retadd;
- } elsif ( grep /\S/, $conf->config('company_address') ) {
+ } elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) {
$letter_data{returnaddress} =
join( '\\*'."\n", map s/( {2,})/'~' x length($1)/eg,
- $conf->config('company_address')
+ $conf->config('company_address', $self->agentnum)
);
} else {
$letter_data{returnaddress} = '~';
@@ -6860,7 +6914,7 @@
$letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
- $letter_data{company_name} = $conf->config('company_name');
+ $letter_data{company_name} = $conf->config('company_name', $self->agentnum);
my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
@@ -6910,6 +6964,8 @@
do_print [ $self->print_ps($template) ];
}
+#these three subs should just go away once agent stuff is all config overrides
+
sub agent_template {
my $self = shift;
$self->_agent_plandata('agent_templatename');
Index: cust_pkg.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/cust_pkg.pm,v
retrieving revision 1.107
retrieving revision 1.108
diff -u -d -r1.107 -r1.108
--- cust_pkg.pm 10 Jan 2009 00:43:05 -0000 1.107
+++ cust_pkg.pm 10 Jan 2009 23:56:56 -0000 1.108
@@ -174,6 +174,10 @@
Previous pkgpart
+=item change_locationnum
+
+Previous locationnum
+
=back
Note: setup, last_bill, bill, adjourn, susp, expire, cancel and change_date
@@ -620,7 +624,7 @@
if ( !$options{'quiet'} && $conf->exists('emailcancel') && @invoicing_list ) {
my $conf = new FS::Conf;
my $error = send_email(
- 'from' => $conf->config('invoice_from'),
+ 'from' => $conf->config('invoice_from', $self->cust_main->agentnum),
'to' => \@invoicing_list,
'subject' => ( $conf->config('cancelsubject') || 'Cancellation Notice' ),
'body' => [ map "$_\n", $conf->config('cancelmessage') ],
@@ -803,7 +807,8 @@
if ( $conf->config('suspend_email_admin') ) {
my $error = send_email(
- 'from' => $conf->config('invoice_from'), #??? well as good as any
+ 'from' => $conf->config('invoice_from', $self->cust_main->agentnum),
+ #invoice_from ??? well as good as any
'to' => $conf->config('suspend_email_admin'),
'subject' => 'FREESIDE NOTIFICATION: Customer package suspended',
'body' => [
@@ -1001,6 +1006,148 @@
}
+
+=item change HASHREF | OPTION => VALUE ...
+
+Changes this package: cancels it and creates a new one, with a different
+pkgpart or locationnum or both. All services are transferred to the new
+package (no change will be made if this is not possible).
+
+Options may be passed as a list of key/value pairs or as a hash reference.
+Options are:
+
+=over 4
+
+=item locaitonnum
+
+New locationnum, to change the location for this package.
+
+=item cust_location
+
+New FS::cust_location object, to create a new location and assign it
+to this package.
+
+=item pkgpart
+
+New pkgpart (see L<FS::part_pkg>).
+
+=item refnum
+
+New refnum (see L<FS::part_referral>).
+
+=back
+
+At least one option must be specified (otherwise, what's the point?)
+
+Returns either the new FS::cust_pkg object or a scalar error.
+
+For example:
+
+ my $err_or_new_cust_pkg = $old_cust_pkg->change
+
+=cut
+
+#some false laziness w/order
+sub change {
+ my $self = shift;
+ my $opt = ref($_[0]) ? shift : { @_ };
+
+# my ($custnum, $pkgparts, $remove_pkgnum, $return_cust_pkg, $refnum) = @_;
+#
+
+ my $conf = new FS::Conf;
+
+ # Transactionize this whole mess
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $error;
+
+ my %hash = ();
+
+ my $time = time;
+
+ #$hash{$_} = $self->$_() foreach qw( last_bill bill );
+
+ #$hash{$_} = $self->$_() foreach qw( setup );
+
+ $hash{'setup'} = $time if $self->setup;
+
+ $hash{'change_date'} = $time;
+ $hash{"change_$_"} = $self->$_()
+ foreach qw( pkgnum pkgpart locationnum );
+
+ if ( $opt->{'cust_location'} &&
+ ( ! $opt->{'locationnum'} || $opt->{'locationnum'} == -1 ) ) {
+ $error = $opt->{'cust_location'}->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "inserting cust_location (transaction rolled back): $error";
+ }
+ $opt->{'locationnum'} = $opt->{'cust_location'}->locationnum;
+ }
+
+ # Create the new package.
+ my $cust_pkg = new FS::cust_pkg {
+ custnum => $self->custnum,
+ pkgpart => ( $opt->{'pkgpart'} || $self->pkgpart ),
+ refnum => ( $opt->{'refnum'} || $self->refnum ),
+ locationnum => ( $opt->{'locationnum'} || $self->locationnum ),
+ %hash,
+ };
+
+ $error = $cust_pkg->insert( 'change' => 1 );
+ if ($error) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ # Transfer services and cancel old package.
+
+ $error = $self->transfer($cust_pkg);
+ if ($error and $error == 0) {
+ # $old_pkg->transfer failed.
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ if ( $error > 0 && $conf->exists('cust_pkg-change_svcpart') ) {
+ warn "trying transfer again with change_svcpart option\n" if $DEBUG;
+ $error = $self->transfer($cust_pkg, 'change_svcpart'=>1 );
+ if ($error and $error == 0) {
+ # $old_pkg->transfer failed.
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ if ($error > 0) {
+ # Transfers were successful, but we still had services left on the old
+ # package. We can't change the package under this circumstances, so abort.
+ $dbh->rollback if $oldAutoCommit;
+ return "Unable to transfer all services from package ". $self->pkgnum;
+ }
+
+ #Good to go, cancel old package.
+ $error = $self->cancel( quiet=>1 );
+ if ($error) {
+ $dbh->rollback;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ $cust_pkg;
+
+}
+
=item last_bill
Returns the last bill date, or if there is no last bill date, the setup date.
@@ -2246,8 +2393,8 @@
my $dbh = dbh;
my $error;
- my $cust_main = qsearchs('cust_main', { custnum => $custnum });
- return "Customer not found: $custnum" unless $cust_main;
+# my $cust_main = qsearchs('cust_main', { custnum => $custnum });
+# return "Customer not found: $custnum" unless $cust_main;
my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
@$remove_pkgnum;
@@ -2257,15 +2404,19 @@
my %hash = ();
if ( scalar(@old_cust_pkg) == 1 && scalar(@$pkgparts) == 1 ) {
- my $time = time;
+ my $err_or_cust_pkg =
+ $old_cust_pkg[0]->change( 'pkgpart' => $pkgparts->[0],
+ 'refnum' => $refnum,
+ );
- #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( last_bill bill );
-
- #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( setup );
- $hash{'setup'} = $time if $old_cust_pkg[0]->setup;
+ unless (ref($err_or_cust_pkg)) {
+ $dbh->rollback if $oldAutoCommit;
+ return $err_or_cust_pkg;
+ }
+
+ push @$return_cust_pkg, $err_or_cust_pkg;
+ return '';
- $hash{'change_date'} = $time;
- $hash{"change_$_"} = $old_cust_pkg[0]->$_() foreach qw( pkgnum pkgpart );
}
# Create the new packages.
@@ -2328,8 +2479,10 @@
=item bulk_change PKGPARTS_ARYREF, REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ]
+A bulk change method to change packages for multiple customers.
+
PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
-L<FS::part_pkg>) to order for this customer. Duplicates are of course
+L<FS::part_pkg>) to order for each customer. Duplicates are of course
permitted.
REMOVE_PKGNUMS is an list of pkgnums specifying the billing items to
More information about the freeside-commits
mailing list