use Carp;
use Exporter;
use Scalar::Util qw( blessed );
-use Time::Local qw(timelocal_nocheck);
+use Time::Local qw(timelocal);
use Data::Dumper;
use Tie::IxHash;
use Digest::MD5 qw(md5_base64);
use Date::Format;
-use Date::Parse;
#use Date::Manip;
-use File::Slurp qw( slurp );
use File::Temp qw( tempfile );
use String::Approx qw(amatch);
use Business::CreditCard 0.28;
use FS::cust_bill;
use FS::cust_bill_pkg;
use FS::cust_bill_pkg_display;
+use FS::cust_bill_pkg_tax_location;
use FS::cust_pay;
use FS::cust_pay_pending;
use FS::cust_pay_void;
use FS::cust_refund;
use FS::part_referral;
use FS::cust_main_county;
+use FS::cust_location;
+use FS::tax_rate;
use FS::cust_tax_location;
+use FS::part_pkg_taxrate;
use FS::agent;
use FS::cust_main_invoice;
use FS::cust_credit_bill;
=over 4
-=item custnum - primary key (assigned automatically for new customers)
+=item custnum
-=item agentnum - agent (see L<FS::agent>)
+Primary key (assigned automatically for new customers)
-=item refnum - Advertising source (see L<FS::part_referral>)
+=item agentnum
+
+Agent (see L<FS::agent>)
+
+=item refnum
+
+Advertising source (see L<FS::part_referral>)
+
+=item first
+
+First name
+
+=item last
+
+Last name
-=item first - name
+=item ss
-=item last - name
+Cocial security number (optional)
-=item ss - social security number (optional)
+=item company
-=item company - (optional)
+(optional)
=item address1
-=item address2 - (optional)
+=item address2
+
+(optional)
=item city
-=item county - (optional, see L<FS::cust_main_county>)
+=item county
+
+(optional, see L<FS::cust_main_county>)
-=item state - (see L<FS::cust_main_county>)
+=item state
+
+(see L<FS::cust_main_county>)
=item zip
-=item country - (see L<FS::cust_main_county>)
+=item country
+
+(see L<FS::cust_main_county>)
+
+=item daytime
+
+phone (optional)
+
+=item night
+
+phone (optional)
-=item daytime - phone (optional)
+=item fax
-=item night - phone (optional)
+phone (optional)
-=item fax - phone (optional)
+=item ship_first
-=item ship_first - name
+Shipping first name
-=item ship_last - name
+=item ship_last
-=item ship_company - (optional)
+Shipping last name
+
+=item ship_company
+
+(optional)
=item ship_address1
-=item ship_address2 - (optional)
+=item ship_address2
+
+(optional)
=item ship_city
-=item ship_county - (optional, see L<FS::cust_main_county>)
+=item ship_county
+
+(optional, see L<FS::cust_main_county>)
-=item ship_state - (see L<FS::cust_main_county>)
+=item ship_state
+
+(see L<FS::cust_main_county>)
=item ship_zip
-=item ship_country - (see L<FS::cust_main_county>)
+=item ship_country
+
+(see L<FS::cust_main_county>)
+
+=item ship_daytime
+
+phone (optional)
-=item ship_daytime - phone (optional)
+=item ship_night
-=item ship_night - phone (optional)
+phone (optional)
-=item ship_fax - phone (optional)
+=item ship_fax
-=item payby - Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
+phone (optional)
-=item payinfo - Payment Information (See L<FS::payinfo_Mixin> for data format)
+=item payby
+
+Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
+
+=item payinfo
+
+Payment Information (See L<FS::payinfo_Mixin> for data format)
-=item paymask - Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
+=item paymask
+
+Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
=item paycvv
Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
-=item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
+=item paydate
+
+Expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
+
+=item paystart_month
+
+Start date month (maestro/solo cards only)
-=item paystart_month - start date month (maestro/solo cards only)
+=item paystart_year
-=item paystart_year - start date year (maestro/solo cards only)
+Start date year (maestro/solo cards only)
-=item payissue - issue number (maestro/solo cards only)
+=item payissue
-=item payname - name on card or billing name
+Issue number (maestro/solo cards only)
-=item payip - IP address from which payment information was received
+=item payname
-=item tax - tax exempt, empty or `Y'
+Name on card or billing name
-=item otaker - order taker (assigned automatically, see L<FS::UID>)
+=item payip
-=item comments - comments (optional)
+IP address from which payment information was received
-=item referral_custnum - referring customer number
+=item tax
-=item spool_cdr - Enable individual CDR spooling, empty or `Y'
+Tax exempt, empty or `Y'
-=item dundate - a suggestion to events (see L<FS::part_bill_event">) to delay until this unix timestamp
+=item otaker
-=item squelch_cdr - Discourage individual CDR printing, empty or `Y'
+Order taker (assigned automatically, see L<FS::UID>)
+
+=item comments
+
+Comments (optional)
+
+=item referral_custnum
+
+Referring customer number
+
+=item spool_cdr
+
+Enable individual CDR spooling, empty or `Y'
+
+=item dundate
+
+A suggestion to events (see L<FS::part_bill_event">) to delay until this unix timestamp
+
+=item squelch_cdr
+
+Discourage individual CDR printing, empty or `Y'
=back
}
+=item order_pkg HASHREF | OPTION => VALUE ...
+
+Orders a single package.
+
+Options may be passed as a list of key/value pairs or as a hash reference.
+Options are:
+
+=over 4
+
+=item cust_pkg
+
+FS::cust_pkg object
+
+=item cust_location
+
+Optional FS::cust_location object
+
+=item svcs
+
+Optional arryaref of FS::svc_* service objects.
+
+=item depend_jobnum
+
+If this option is set to a job queue jobnum (see L<FS::queue>), all provisioning
+jobs will have a dependancy on the supplied job (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).
+
+=back
+
+=cut
+
+sub order_pkg {
+ my $self = shift;
+ my $opt = ref($_[0]) ? shift : { @_ };
+
+ warn "$me order_pkg called with options ".
+ join(', ', map { "$_: $opt->{$_}" } keys %$opt ). "\n"
+ if $DEBUG;
+
+ my $cust_pkg = $opt->{'cust_pkg'};
+ my $seconds = $opt->{'seconds'};
+ my $svcs = $opt->{'svcs'} || [];
+
+ my %svc_options = ();
+ $svc_options{'depend_jobnum'} = $opt->{'depend_jobnum'}
+ if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'};
+
+ 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;
+
+ if ( $opt->{'cust_location'} &&
+ ( ! $cust_pkg->locationnum || $cust_pkg->locationnum == -1 ) ) {
+ my $error = $opt->{'cust_location'}->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "inserting cust_location (transaction rolled back): $error";
+ }
+ $cust_pkg->locationnum($opt->{'cust_location'}->locationnum);
+ }
+
+ $cust_pkg->custnum( $self->custnum );
+
+ my $error = $cust_pkg->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "inserting cust_pkg (transaction rolled back): $error";
+ }
+
+ foreach my $svc_something ( @{ $opt->{'svcs'} } ) {
+ if ( $svc_something->svcnum ) {
+ my $old_cust_svc = $svc_something->cust_svc;
+ my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash };
+ $new_cust_svc->pkgnum( $cust_pkg->pkgnum);
+ $error = $new_cust_svc->replace($old_cust_svc);
+ } else {
+ $svc_something->pkgnum( $cust_pkg->pkgnum );
+ if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
+ $svc_something->seconds( $svc_something->seconds + $$seconds );
+ $$seconds = 0;
+ }
+ $error = $svc_something->insert(%svc_options);
+ }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "inserting svc_ (transaction rolled back): $error";
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ ''; #no error
+
+}
+
=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:
+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
my $cust_pkgs = shift;
my $seconds = shift;
my %options = @_;
- my %svc_options = ();
- $svc_options{'depend_jobnum'} = $options{'depend_jobnum'}
- if exists $options{'depend_jobnum'};
+
warn "$me order_pkgs called with options ".
join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
if $DEBUG;
local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
foreach my $cust_pkg ( keys %$cust_pkgs ) {
- $cust_pkg->custnum( $self->custnum );
- my $error = $cust_pkg->insert;
+
+ 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 "inserting cust_pkg (transaction rolled back): $error";
- }
- foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
- if ( $svc_something->svcnum ) {
- my $old_cust_svc = $svc_something->cust_svc;
- my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash };
- $new_cust_svc->pkgnum( $cust_pkg->pkgnum);
- $error = $new_cust_svc->replace($old_cust_svc);
- } else {
- $svc_something->pkgnum( $cust_pkg->pkgnum );
- if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
- $svc_something->seconds( $svc_something->seconds + $$seconds );
- $$seconds = 0;
- }
- $error = $svc_something->insert(%svc_options);
- }
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- #return "inserting svc_ (transaction rolled back): $error";
- return $error;
- }
+ return $error;
}
+
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
|| $self->ut_textn('stateid')
|| $self->ut_textn('stateid_state')
|| $self->ut_textn('invoice_terms')
+ || $self->ut_alphan('geocode')
;
#barf. need message catalogs. i18n. etc.
shift->all_pkgs(@_);
}
+=item cust_location
+
+Returns all locations (see L<FS::cust_location>) for this customer.
+
+=cut
+
+sub cust_location {
+ my $self = shift;
+ qsearch('cust_location', { 'custnum' => $self->custnum } );
+}
+
=item ncancelled_pkgs
Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
if $DEBUG;
my $time = $options{'time'} || time;
+ my $invoice_time = $options{'invoice_time'} || $time;
#put below somehow?
local $SIG{HUP} = 'IGNORE';
###
my( $total_setup, $total_recur, $postal_charge ) = ( 0, 0, 0 );
- my %tax;
my %taxlisthash;
- my %taxname;
my @precommit_hooks = ();
my @cust_pkgs = qsearch('cust_pkg', { 'custnum' => $self->custnum } );
}
warn "having a look at the taxes we found...\n" if $DEBUG > 2;
+
+ # keys are tax names (as printed on invoices / itemdesc )
+ # values are listrefs of taxlisthash keys (internal identifiers)
+ my %taxname = ();
+
+ # keys are taxlisthash keys (internal identifiers)
+ # values are (cumulative) amounts
+ my %tax = ();
+
+ # keys are taxlisthash keys (internal identifiers)
+ # values are listrefs of cust_bill_pkg_tax_location hashrefs
+ my %tax_location = ();
+
foreach my $tax ( keys %taxlisthash ) {
my $tax_object = shift @{ $taxlisthash{$tax} };
warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
- my $listref_or_error = $tax_object->taxline( @{ $taxlisthash{$tax} } );
- unless (ref($listref_or_error)) {
+ my $hashref_or_error =
+ $tax_object->taxline( $taxlisthash{$tax},
+ 'custnum' => $self->custnum,
+ 'invoice_time' => $invoice_time
+ );
+ unless ( ref($hashref_or_error) ) {
$dbh->rollback if $oldAutoCommit;
- return $listref_or_error;
+ return $hashref_or_error;
}
unshift @{ $taxlisthash{$tax} }, $tax_object;
- warn "adding ". $listref_or_error->[1].
- " as ". $listref_or_error->[0]. "\n"
- if $DEBUG > 2;
- $tax{ $tax_object->taxname } += $listref_or_error->[1];
- if ( $taxname{ $listref_or_error->[0] } ) {
- push @{ $taxname{ $listref_or_error->[0] } }, $tax_object->taxname;
- }else{
- $taxname{ $listref_or_error->[0] } = [ $tax_object->taxname ];
+ my $name = $hashref_or_error->{'name'};
+ my $amount = $hashref_or_error->{'amount'};
+
+ #warn "adding $amount as $name\n";
+ $taxname{ $name } ||= [];
+ push @{ $taxname{ $name } }, $tax;
+
+ $tax{ $tax } += $amount;
+
+ $tax_location{ $tax } ||= [];
+ if ( $tax_object->get('pkgnum') || $tax_object->get('locationnum') ) {
+ push @{ $tax_location{ $tax } },
+ {
+ 'taxnum' => $tax_object->taxnum,
+ 'taxtype' => ref($tax_object),
+ 'pkgnum' => $tax_object->get('pkgnum'),
+ 'locationnum' => $tax_object->get('locationnum'),
+ 'amount' => sprintf('%.2f', $amount ),
+ };
+ }
+
+ }
+
+ #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
+ my %packagemap = map { $_->pkgnum => $_ } @cust_bill_pkg;
+ foreach my $tax ( keys %taxlisthash ) {
+ foreach ( @{ $taxlisthash{$tax} }[1 ... scalar(@{ $taxlisthash{$tax} })] ) {
+ next unless ref($_) eq 'FS::cust_bill_pkg'; # shouldn't happen
+
+ push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
+ splice( @{ $_->_cust_tax_exempt_pkg } );
}
-
}
#some taxes are taxed
# existing taxes
warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
if ( exists( $totlisthash{ $totname } ) ) {
- push @{ $totlisthash{ $totname } }, $tax{ $tax_object->taxname };
+ push @{ $totlisthash{ $totname } }, $tax{ $tax };
}else{
- $totlisthash{ $totname } = [ $tot, $tax{ $tax_object->taxname } ];
+ $totlisthash{ $totname } = [ $tot, $tax{ $tax } ];
}
}
}
my $tax_object = shift @{ $totlisthash{$tax} };
warn "found previously found taxed tax ". $tax_object->taxname. "\n"
if $DEBUG > 2;
- my $listref_or_error = $tax_object->taxline( @{ $totlisthash{$tax} } );
+ my $listref_or_error =
+ $tax_object->taxline( $totlisthash{$tax},
+ 'custnum' => $self->custnum,
+ 'invoice_time' => $invoice_time
+ );
unless (ref($listref_or_error)) {
$dbh->rollback if $oldAutoCommit;
return $listref_or_error;
warn "adding taxed tax amount ". $listref_or_error->[1].
" as ". $tax_object->taxname. "\n"
if $DEBUG;
- $tax{ $tax_object->taxname } += $listref_or_error->[1];
+ $tax{ $tax } += $listref_or_error->[1];
}
#consolidate and create tax line items
foreach my $taxname ( keys %taxname ) {
my $tax = 0;
my %seen = ();
+ my @cust_bill_pkg_tax_location = ();
warn "adding $taxname\n" if $DEBUG > 1;
foreach my $taxitem ( @{ $taxname{$taxname} } ) {
- $tax += $tax{$taxitem} unless $seen{$taxitem};
+ next if $seen{$taxitem}++;
warn "adding $tax{$taxitem}\n" if $DEBUG > 1;
+ $tax += $tax{$taxitem};
+ push @cust_bill_pkg_tax_location,
+ map { new FS::cust_bill_pkg_tax_location $_ }
+ @{ $tax_location{ $taxitem } };
}
next unless $tax;
'sdate' => '',
'edate' => '',
'itemdesc' => $taxname,
+ 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
};
}
#create the new invoice
my $cust_bill = new FS::cust_bill ( {
'custnum' => $self->custnum,
- '_date' => ( $options{'invoice_time'} || $time ),
+ '_date' => ( $invoice_time ),
'charged' => $charged,
} );
my $error = $cust_bill->insert;
if ( $@ );
if ( $increment_next_bill ) {
-
- #change this bit to use Date::Manip? CAREFUL with timezones (see
- # mailing list archive)
- my ($sec,$min,$hour,$mday,$mon,$year) =
- (localtime($sdate) )[0,1,2,3,4,5];
- #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
+ my $next_bill = $part_pkg->add_freq($sdate);
+ return "unparsable frequency: ". $part_pkg->freq
+ if $next_bill == -1;
+
+ #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
# only for figuring next bill date, nothing else, so, reset $sdate again
# here
$sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
#no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
$cust_pkg->last_bill($sdate);
- if ( $part_pkg->freq =~ /^\d+$/ ) {
- $mon += $part_pkg->freq;
- until ( $mon < 12 ) { $mon -= 12; $year++; }
- } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
- my $weeks = $1;
- $mday += $weeks * 7;
- } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
- my $days = $1;
- $mday += $days;
- } elsif ( $part_pkg->freq =~ /^(\d+)h$/ ) {
- my $hours = $1;
- $hour += $hours;
- } else {
- return "unparsable frequency: ". $part_pkg->freq;
- }
- $cust_pkg->setfield('bill',
- timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
+ $cust_pkg->setfield('bill', $next_bill );
}
my %cust_bill_pkg = ();
my %taxes = ();
- my $prefix =
- ( $conf->exists('tax-ship_address') && length($self->ship_last) )
- ? 'ship_'
- : '';
-
my @classes;
#push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
push @classes, 'setup' if $cust_bill_pkg->setup;
push @classes, 'recur' if $cust_bill_pkg->recur;
- if ( $conf->exists('enable_taxproducts')
- && (scalar($part_pkg->part_pkg_taxoverride) || $part_pkg->has_taxproduct)
- && ( $self->tax !~ /Y/i && $self->payby ne 'COMP' )
- )
- {
+ if ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) {
- foreach my $class (@classes) {
- my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $prefix );
- return $err_or_ref unless ref($err_or_ref);
- $taxes{$class} = $err_or_ref;
- }
+ if ( $conf->exists('enable_taxproducts')
+ && ( scalar($part_pkg->part_pkg_taxoverride)
+ || $part_pkg->has_taxproduct
+ )
+ )
+ {
- unless (exists $taxes{''}) {
- my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $prefix );
- return $err_or_ref unless ref($err_or_ref);
- $taxes{''} = $err_or_ref;
- }
+ if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
+ return "fatal: Can't (yet) use tax-pkg_address with taxproducts";
+ }
- } elsif ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) {
+ foreach my $class (@classes) {
+ my $err_or_ref = $self->_gather_taxes( $part_pkg, $class );
+ return $err_or_ref unless ref($err_or_ref);
+ $taxes{$class} = $err_or_ref;
+ }
- my %taxhash = map { $_ => $self->get("$prefix$_") }
- qw( state county country );
+ unless (exists $taxes{''}) {
+ my $err_or_ref = $self->_gather_taxes( $part_pkg, '' );
+ return $err_or_ref unless ref($err_or_ref);
+ $taxes{''} = $err_or_ref;
+ }
- $taxhash{'taxclass'} = $part_pkg->taxclass;
+ } else {
- my @taxes = qsearch( 'cust_main_county', \%taxhash );
+ my @loc_keys = qw( state county country );
+ my %taxhash;
+ if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
+ my $cust_location = $cust_pkg->cust_location;
+ %taxhash = map { $_ => $cust_location->$_() } @loc_keys;
+ } else {
+ my $prefix =
+ ( $conf->exists('tax-ship_address') && length($self->ship_last) )
+ ? 'ship_'
+ : '';
+ %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys;
+ }
- unless ( @taxes ) {
- $taxhash{'taxclass'} = '';
- @taxes = qsearch( 'cust_main_county', \%taxhash );
- }
+ $taxhash{'taxclass'} = $part_pkg->taxclass;
- #one more try at a whole-country tax rate
- unless ( @taxes ) {
- $taxhash{$_} = '' foreach qw( state county );
- @taxes = qsearch( 'cust_main_county', \%taxhash );
- }
+ my @taxes = qsearch( 'cust_main_county', \%taxhash );
- $taxes{''} = [ @taxes ];
- $taxes{'setup'} = [ @taxes ];
- $taxes{'recur'} = [ @taxes ];
- $taxes{$_} = [ @taxes ] foreach (@classes);
-
- # maybe eliminate this entirely, along with all the 0% records
- unless ( @taxes ) {
- return
- "fatal: can't find tax rate for state/county/country/taxclass ".
- join('/', ( map $self->get("$prefix$_"),
- qw(state county country)
- ),
- $part_pkg->taxclass ). "\n";
- }
+ my %taxhash_elim = %taxhash;
- } #if $conf->exists('enable_taxproducts') ...
+ my @elim = qw( taxclass county state );
+ while ( !scalar(@taxes) && scalar(@elim) ) {
+ $taxhash_elim{ shift(@elim) } = '';
+ @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
+ }
+
+ if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
+ foreach (@taxes) {
+ $_->set('pkgnum', $cust_pkg->pkgnum );
+ $_->set('locationnum', $cust_pkg->locationnum );
+ }
+ }
+
+ $taxes{''} = [ @taxes ];
+ $taxes{'setup'} = [ @taxes ];
+ $taxes{'recur'} = [ @taxes ];
+ $taxes{$_} = [ @taxes ] foreach (@classes);
+
+ # maybe eliminate this entirely, along with all the 0% records
+ unless ( @taxes ) {
+ return
+ "fatal: can't find tax rate for state/county/country/taxclass ".
+ join('/', map $taxhash{$_}, qw(state county country taxclass) );
+ }
+
+ } #if $conf->exists('enable_taxproducts') ...
+
+ }
my @display = ();
if ( $conf->exists('separate_usage') ) {
my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
foreach my $tax ( @taxes ) {
- my $taxname = ref( $tax ). ' '. $tax->taxnum;
+
+ my $taxname = ref( $tax ). ' taxnum'. $tax->taxnum;
+# $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
+# ' locationnum'. $cust_pkg->locationnum
+# if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
+
if ( exists( $taxlisthash->{ $taxname } ) ) {
push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
}else{
my $self = shift;
my $part_pkg = shift;
my $class = shift;
- my $prefix = shift;
my @taxes = ();
my $geocode = $self->geocode('cch');
# maybe eliminate this entirely, along with all the 0% records
unless ( @taxes ) {
return
- "fatal: can't find tax rate for zip/taxproduct/pkgpart ".
- join('/', ( map $self->get("$prefix$_"),
- qw(zip)
- ),
+ "fatal: can't find tax rate for geocode/taxproduct/pkgpart ".
+ join('/', $geocode,
$part_pkg->taxproduct_description,
- $part_pkg->pkgpart ). "\n";
+ $part_pkg->pkgpart
+ );
}
warn "Found taxes ".
Explicitly pass the objects to be tested (typically used with eventtable).
+=item testonly
+
+Set to true to return the objects, but not actually insert them into the
+database.
+
=back
=cut
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- $self->select_for_update; #mutex
+ $self->select_for_update #mutex
+ unless $opt{testonly};
###
# 1: find possible events (initial search)
}
-=item realtime_collect [ OPTION => VALUE ... ]
-
-Runs a realtime credit card, ACH (electronic check) or phone bill transaction
-via a Business::OnlinePayment or Business::OnlineThirdPartyPayment realtime
-gateway. See L<http://420.am/business-onlinepayment> and
-L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
-
-On failure returns an error message.
-
-Returns false or a hashref upon success. The hashref contains keys popup_url reference, and collectitems. The first is a URL to which a browser should be redirected for completion of collection. The second is a reference id for the transaction suitable for the end user. The collectitems is a reference to a list of name value pairs suitable for assigning to a html form and posted to popup_url.
-
-Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
-
-I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
-then it is deduced from the customer record.
-
-If no I<amount> is specified, then the customer balance is used.
-
-The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
-I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
-if set, will override the value from the customer record.
-
-I<description> is a free-text field passed to the gateway. It defaults to
-"Internet services".
-
-If an I<invnum> is specified, this payment (if successful) is applied to the
-specified invoice. If you don't specify an I<invnum> you might want to
-call the B<apply_payments> method.
-
-I<quiet> can be set true to surpress email decline notices.
-
-I<paynum_ref> can be set to a scalar reference. It will be filled in with the
-resulting paynum, if any.
-
-I<payunique> is a unique identifier for this payment.
-
-=cut
-
-sub realtime_collect {
- my( $self, %options ) = @_;
- if ( $DEBUG ) {
- warn "$me realtime_collect:\n";
- warn " $_ => $options{$_}\n" foreach keys %options;
- }
-
- $options{'description'} ||= 'Internet services';
-
- my $payinfo = exists($options{'payinfo'})
- ? $options{'payinfo'}
- : $self->payinfo;
-
- my $invnum = exists($options{'invnum'})
- ? $options{'invnum'}
- : '';
-
- my $amount = exists($options{'amount'})
- ? $options{'amount'}
- : $self->balance;
-
- my $method = exists($options{'method'})
- ? $options{'method'}
- : FS::payby->payby2bop($self->payby);
-
-
- return $self->fake_bop($method, $amount, %options) if $options{'fake'};
-
- my %method2payby = (
- 'CC' => 'CARD',
- 'ECHECK' => 'CHEK',
- 'LEC' => 'LECB',
- );
-
- ###
- # select a gateway
- ###
-
- my $payment_gateway = $self->agent->payment_gateway( 'method' => $method,
- 'invnum' => $invnum,
- 'payinfo' => $payinfo,
- );
- my $namespace = $payment_gateway->gateway_namespace;
-
- ###
- # massage data
- ###
-
- if ($namespace eq 'Business::OnlineThirdPartyPayment') {
-
- #return $self->realtime_botpp($method, $amount, %options);
-
- my $transref = time; # pay unique too long; use pay_pending primary key?
- my $amt = $amount; $amt =~ s/\.//; # is amount always %.2f?
- return { popup_url => "https://webpay.interswitchng.com/webpay_pilot/purchase.aspx?CADPID=ISW&MERTID=ZIB030010000005&TXNREF=$transref&AMT=$amount&TRANTYPE=00",
- reference => $invnum,
- collectitems => [],
- };
-
- }elsif ($namespace eq '' || $namespace eq 'Business::OnlinePayment') {
- return $self->realtime_bop($method, $amount, %options);
- }
-
- #else
-
- return 'Fatal - unknown type of payment processor';
-}
-
=item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
Runs a realtime credit card, ACH (electronic check) or phone bill transaction
? $options{'payinfo'}
: $self->payinfo;
- my $invnum = exists($options{'invnum'})
- ? $options{'invnum'}
- : '';
-
my %method2payby = (
'CC' => 'CARD',
'ECHECK' => 'CHEK',
# select a gateway
###
- my $payment_gateway = $self->agent->payment_gateway( 'method' => $method,
- 'invnum' => $invnum,
- 'payinfo' => $payinfo,
- );
- my( $namespace, $processor, $login, $password, $action ) =
- map { my $method = "gateway_$_"; $payment_gateway->$method }
- qw( namespace processor login password action );
+ my $taxclass = '';
+ if ( $options{'invnum'} ) {
+ my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
+ die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
+ my @taxclasses =
+ map { $_->part_pkg->taxclass }
+ grep { $_ }
+ map { $_->cust_pkg }
+ $cust_bill->cust_bill_pkg;
+ unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are
+ #different taxclasses
+ $taxclass = $taxclasses[0];
+ }
+ }
- my @bop_options = $payment_gateway->gatewaynum
- ? $payment_gateway->options
- : @{ $payment_gateway->get('options') };
+ #look for an agent gateway override first
+ my $cardtype;
+ if ( $method eq 'CC' ) {
+ $cardtype = cardtype($payinfo);
+ } elsif ( $method eq 'ECHECK' ) {
+ $cardtype = 'ACH';
+ } else {
+ $cardtype = $method;
+ }
- ###
- # massage data
+ my $override =
+ qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => $cardtype,
+ taxclass => $taxclass, } )
+ || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => '',
+ taxclass => $taxclass, } )
+ || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => $cardtype,
+ taxclass => '', } )
+ || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => '',
+ taxclass => '', } );
+
+ my $payment_gateway = '';
+ my( $processor, $login, $password, $action, @bop_options );
+ if ( $override ) { #use a payment gateway override
+
+ $payment_gateway = $override->payment_gateway;
+
+ $processor = $payment_gateway->gateway_module;
+ $login = $payment_gateway->gateway_username;
+ $password = $payment_gateway->gateway_password;
+ $action = $payment_gateway->gateway_action;
+ @bop_options = $payment_gateway->options;
+
+ } else { #use the standard settings from the config
+
+ ( $processor, $login, $password, $action, @bop_options ) =
+ $self->default_payment_gateway($method);
+
+ }
+
+ ###
+ # massage data
###
my $address = exists($options{'address1'})
'payinfo' => $payinfo,
'paydate' => $paydate,
'status' => 'new',
- 'gatewaynum' => $payment_gateway->gatewaynum || '',
+ 'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ),
};
$cust_pay_pending->payunique( $options{payunique} )
if defined($options{payunique}) && length($options{payunique});
if ( $transaction->is_success() ) {
my $paybatch = '';
- if ( $payment_gateway->gatewaynum ) { # agent override
+ if ( $payment_gateway ) { # agent override
$paybatch = $payment_gateway->gatewaynum. '-';
}
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) ],
);
#my $paybatch = '';
- #if ( $payment_gateway->gatewaynum ) { # agent override
+ #if ( $payment_gateway ) { # agent override
# $paybatch = $payment_gateway->gatewaynum. '-';
#}
#
}
-=item realtime_botpp AMOUNT [ OPTION => VALUE ... ]
-
-Runs a realtime credit card, ACH (electronic check) or phone bill transaction
-via a Business::OnlineThirdPartyPayment realtime gateway. See
-L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
-
-Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
-
-The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
-I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
-if set, will override the value from the customer record.
-
-I<description> is a free-text field passed to the gateway. It defaults to
-"Internet services".
-
-If an I<invnum> is specified, this payment (if successful) is applied to the
-specified invoice. If you don't specify an I<invnum> you might want to
-call the B<apply_payments> method.
-
-I<quiet> can be set true to surpress email decline notices.
-
-I<paynum_ref> can be set to a scalar reference. It will be filled in with the
-resulting paynum, if any.
-
-I<payunique> is a unique identifier for this payment.
-
-=cut
-
-sub realtime_botpp {
- my( $self, $method, $amount, %options ) = @_;
- if ( $DEBUG ) {
- warn "$me realtime_botpp: $amount\n";
- warn " $_ => $options{$_}\n" foreach keys %options;
- }
-
- $options{'description'} ||= 'Internet services';
-
- return $self->fake_bop("CC", $amount, %options) if $options{'fake'};
-
- eval "use Business::OnlineThirdPartyPayment";
- die $@ if $@;
-
- #my $payinfo = exists($options{'payinfo'})
- # ? $options{'payinfo'}
- # : $self->payinfo;
-
- my $invnum = exists($options{'invnum'})
- ? $options{'invnum'}
- : '';
-
- my %method2payby = (
- 'CC' => 'CARD',
- 'ECHECK' => 'CHEK',
- 'LEC' => 'LECB',
- );
-
- ###
- # select a gateway
- ###
-
- my $payment_gateway = $self->agent->payment_gateway( 'method' => $method,
- 'invnum' => $invnum,
- #'payinfo' => $payinfo,
- );
- my( $namespace, $processor, $login, $password, $action ) =
- map { my $method = "gateway_$_"; $payment_gateway->$method }
- qw( namespace processor login password action );
-
- my @botpp_options = $payment_gateway->gatewaynum
- ? $payment_gateway->options
- : @{ $payment_gateway->get('options') };
-
- ###
- # massage data
- ###
-
- my $address = exists($options{'address1'})
- ? $options{'address1'}
- : $self->address1;
- my $address2 = exists($options{'address2'})
- ? $options{'address2'}
- : $self->address2;
- $address .= ", ". $address2 if length($address2);
-
- my $o_payname = exists($options{'payname'})
- ? $options{'payname'}
- : $self->payname;
- my($payname, $payfirst, $paylast);
- if ( $o_payname && $method ne 'ECHECK' ) {
- ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
- or return "Illegal payname $payname";
- ($payfirst, $paylast) = ($1, $2);
- } else {
- $payfirst = $self->getfield('first');
- $paylast = $self->getfield('last');
- $payname = "$payfirst $paylast";
- }
-
- my @invoicing_list = $self->invoicing_list_emailonly;
- if ( $conf->exists('emailinvoiceautoalways')
- || $conf->exists('emailinvoiceauto') && ! @invoicing_list
- || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
- push @invoicing_list, $self->all_emails;
- }
-
- my $email = ($conf->exists('business-onlinepayment-email-override'))
- ? $conf->config('business-onlinepayment-email-override')
- : $invoicing_list[0];
-
- my %content = ();
-
- $content{invoice_number} = $options{'invnum'}
- if exists($options{'invnum'}) && length($options{'invnum'});
-
- $content{email_customer} =
- ( $conf->exists('business-onlinepayment-email_customer')
- || $conf->exists('business-onlinepayment-email-override') );
-
- my $paydate = '';
-
- #$paydate = exists($options{'paydate'})
- # ? $options{'paydate'}
- # : $self->paydate;
- #$paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
- #
- #$content{recurring_billing} = 'YES'
- # if qsearch('cust_pay', { 'custnum' => $self->custnum,
- # 'payby' => 'CARD',
- # 'payinfo' => $payinfo,
- # } )
- # || qsearch('cust_pay', { 'custnum' => $self->custnum,
- # 'payby' => 'CARD',
- # 'paymask' => $self->mask_payinfo('CARD', $payinfo),
- # } );
-
- ###
- # run transaction(s)
- ###
-
- my $balance = exists( $options{'balance'} )
- ? $options{'balance'}
- : $self->balance;
-
- $self->select_for_update; #mutex ... just until we get our pending record in
-
- #the checks here are intended to catch concurrent payments
- #double-form-submission prevention is taken care of in cust_pay_pending::check
-
- #check the balance
- return "The customer's balance has changed; $method transaction aborted."
- if $self->balance < $balance;
- #&& $self->balance < $amount; #might as well anyway?
-
- #also check and make sure there aren't *other* pending payments for this cust
-
- my @pending = qsearch('cust_pay_pending', {
- 'custnum' => $self->custnum,
- 'status' => { op=>'!=', value=>'done' }
- });
- return "A payment is already being processed for this customer (".
- join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
- "); $method transaction aborted."
- if scalar(@pending);
-
- #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
-
- my $cust_pay_pending = new FS::cust_pay_pending {
- 'custnum' => $self->custnum,
- #'invnum' => $options{'invnum'},
- 'paid' => $amount,
- '_date' => '',
- 'payby' => $method2payby{$method},
- #'payinfo' => $payinfo,
- 'paydate' => $paydate,
- 'status' => 'new',
- 'gatewaynum' => $payment_gateway->gatewaynum || '',
- };
- $cust_pay_pending->payunique( $options{payunique} )
- if defined($options{payunique}) && length($options{payunique});
- my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
- return $cpp_new_err if $cpp_new_err;
-
- my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
-
- my $transaction = new Business::OnlineThirdPartyPayment( $processor, @botpp_options );
- $transaction->content(
- 'type' => $method,
- 'login' => $login,
- 'password' => $password,
- 'action' => $action1,
- 'description' => $options{'description'},
- 'amount' => $amount,
- #'invoice_number' => $options{'invnum'},
- 'customer_id' => $self->custnum,
- 'last_name' => $paylast,
- 'first_name' => $payfirst,
- 'name' => $payname,
- 'address' => $address,
- 'city' => ( exists($options{'city'})
- ? $options{'city'}
- : $self->city ),
- 'state' => ( exists($options{'state'})
- ? $options{'state'}
- : $self->state ),
- 'zip' => ( exists($options{'zip'})
- ? $options{'zip'}
- : $self->zip ),
- 'country' => ( exists($options{'country'})
- ? $options{'country'}
- : $self->country ),
- 'referer' => 'http://cleanwhisker.420.am/',
- 'email' => $email,
- 'phone' => $self->daytime || $self->night,
- %content, #after
- );
-
- $cust_pay_pending->status('pending');
- my $cpp_pending_err = $cust_pay_pending->replace;
- return $cpp_pending_err if $cpp_pending_err;
-
- #config?
- my $BOP_TESTING = 0;
- my $BOP_TESTING_SUCCESS = 1;
-
- unless ( $BOP_TESTING ) {
- $transaction->submit();
- } else {
- if ( $BOP_TESTING_SUCCESS ) {
- $transaction->is_success(1);
- $transaction->authorization('fake auth');
- } else {
- $transaction->is_success(0);
- $transaction->error_message('fake failure');
- }
- }
-
- $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
- my $cpp_captured_err = $cust_pay_pending->replace;
- return $cpp_captured_err if $cpp_captured_err;
-
- ###
- # result handling
- ###
-
- if ( $transaction->is_success() ) {
-
- my $paybatch = '';
- if ( $payment_gateway->gatewaynum ) { # agent override
- $paybatch = $payment_gateway->gatewaynum. '-';
- }
-
- $paybatch .= "$processor:". $transaction->authorization;
-
- $paybatch .= ':'. $transaction->order_number
- if $transaction->can('order_number')
- && length($transaction->order_number);
-
- my $cust_pay = new FS::cust_pay ( {
- 'custnum' => $self->custnum,
- 'invnum' => $options{'invnum'},
- 'paid' => $amount,
- '_date' => '',
- 'payby' => $method2payby{$method},
- #'payinfo' => $payinfo,
- 'paybatch' => $paybatch,
- 'paydate' => $paydate,
- } );
- #doesn't hurt to know, even though the dup check is in cust_pay_pending now
- $cust_pay->payunique( $options{payunique} )
- if defined($options{payunique}) && length($options{payunique});
-
- my $oldAutoCommit = $FS::UID::AutoCommit;
- local $FS::UID::AutoCommit = 0;
- my $dbh = dbh;
-
- #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
-
- my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
-
- if ( $error ) {
- $cust_pay->invnum(''); #try again with no specific invnum
- my $error2 = $cust_pay->insert( $options{'manual'} ?
- ( 'manual' => 1 ) : ()
- );
- if ( $error2 ) {
- # gah. but at least we have a record of the state we had to abort in
- # from cust_pay_pending now.
- my $e = "WARNING: $method captured but payment not recorded - ".
- "error inserting payment ($processor): $error2".
- " (previously tried insert with invnum #$options{'invnum'}" .
- ": $error ) - pending payment saved as paypendingnum ".
- $cust_pay_pending->paypendingnum. "\n";
- warn $e;
- return $e;
- }
- }
-
- if ( $options{'paynum_ref'} ) {
- ${ $options{'paynum_ref'} } = $cust_pay->paynum;
- }
-
- $cust_pay_pending->status('done');
- $cust_pay_pending->statustext('captured');
- my $cpp_done_err = $cust_pay_pending->replace;
-
- if ( $cpp_done_err ) {
-
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
- my $e = "WARNING: $method captured but payment not recorded - ".
- "error updating status for paypendingnum ".
- $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
- warn $e;
- return $e;
-
- } else {
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
- return ''; #no error
-
- }
-
- } else {
-
- my $perror = "$processor error: ". $transaction->error_message;
-
- unless ( $transaction->error_message ) {
-
- my $t_response;
- if ( $transaction->can('response_page') ) {
- $t_response = {
- 'page' => ( $transaction->can('response_page')
- ? $transaction->response_page
- : ''
- ),
- 'code' => ( $transaction->can('response_code')
- ? $transaction->response_code
- : ''
- ),
- 'headers' => ( $transaction->can('response_headers')
- ? $transaction->response_headers
- : ''
- ),
- };
- } else {
- $t_response .=
- "No additional debugging information available for $processor";
- }
-
- $perror .= "No error_message returned from $processor -- ".
- ( ref($t_response) ? Dumper($t_response) : $t_response );
-
- }
-
- if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
- && $conf->exists('emaildecline')
- && grep { $_ ne 'POST' } $self->invoicing_list
- && ! grep { $transaction->error_message =~ /$_/ }
- $conf->config('emaildecline-exclude')
- ) {
- my @templ = $conf->config('declinetemplate');
- my $template = new Text::Template (
- TYPE => 'ARRAY',
- SOURCE => [ map "$_\n", @templ ],
- ) or return "($perror) can't create template: $Text::Template::ERROR";
- $template->compile()
- or return "($perror) can't compile template: $Text::Template::ERROR";
-
- my $templ_hash = { error => $transaction->error_message };
-
- my $error = send_email(
- 'from' => $conf->config('invoice_from'),
- 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
- 'subject' => 'Your payment could not be processed',
- 'body' => [ $template->fill_in(HASH => $templ_hash) ],
- );
-
- $perror .= " (also received error sending decline notification: $error)"
- if $error;
-
- }
-
- $cust_pay_pending->status('done');
- $cust_pay_pending->statustext("declined: $perror");
- my $cpp_done_err = $cust_pay_pending->replace;
- if ( $cpp_done_err ) {
- my $e = "WARNING: $method declined but pending payment not resolved - ".
- "error updating status for paypendingnum ".
- $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
- warn $e;
- $perror = "$e ($perror)";
- }
-
- return $perror;
- }
-
-}
-
-=item default_payment_gateway DEPRECATED -- use agent->payment_gateway
+=item default_payment_gateway
=cut
die "Real-time processing not enabled\n"
unless $conf->exists('business-onlinepayment');
- warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
-
#load up config
my $bop_config = 'business-onlinepayment';
$bop_config .= '-ach'
} else { #try the default gateway
- my $conf_processor;
- my $payment_gateway = $self->agent->payment_gateway('method' => $method);
-
- ( $conf_processor, $login, $password ) =
- map { $payment_gateway->$_ } qw( namespace processor login password );
-
- @bop_options = $payment_gateway->gatewaynum
- ? $payment_gateway->options
- : @{ $payment_gateway->get('options') };
+ my( $conf_processor, $unused_action );
+ ( $conf_processor, $login, $password, $unused_action, @bop_options ) =
+ $self->default_payment_gateway($method);
return "processor of payment $options{'paynum'} $processor does not".
" match default processor $conf_processor"
} else { # didn't specify a paynum, so look for agent gateway overrides
# like a normal transaction
+
+ my $cardtype;
+ if ( $method eq 'CC' ) {
+ $cardtype = cardtype($self->payinfo);
+ } elsif ( $method eq 'ECHECK' ) {
+ $cardtype = 'ACH';
+ } else {
+ $cardtype = $method;
+ }
+ my $override =
+ qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => $cardtype,
+ taxclass => '', } )
+ || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => '',
+ taxclass => '', } );
+
+ if ( $override ) { #use a payment gateway override
- my $payment_gateway = $self->agent->payment_gateway( 'method' => $method,
- #'payinfo' => $payinfo,
- );
- my( $namespace, $processor, $login, $password ) =
- map { $payment_gateway->$_ } qw( namespace processor login password );
+ my $payment_gateway = $override->payment_gateway;
+
+ $processor = $payment_gateway->gateway_module;
+ $login = $payment_gateway->gateway_username;
+ $password = $payment_gateway->gateway_password;
+ #$action = $payment_gateway->gateway_action;
+ @bop_options = $payment_gateway->options;
+
+ } else { #use the standard settings from the config
- my @bop_options = $payment_gateway->gatewaynum
- ? $payment_gateway->options
- : @{ $payment_gateway->get('options') };
+ my $unused_action;
+ ( $processor, $login, $password, $unused_action, @bop_options ) =
+ $self->default_payment_gateway($method);
+
+ }
}
return "neither amount nor paynum specified" unless $amount;
die $error;
}
- my $unapplied = $self->total_credited + $self->total_unapplied_payments + $self->in_transit_payments;
+ my $unapplied = $self->total_unapplied_credits
+ + $self->total_unapplied_payments
+ + $self->in_transit_payments;
foreach my $cust_bill ($self->open_cust_bill) {
#$dbh->commit or die $dbh->errstr if $oldAutoCommit;
my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
$error = $cust_bill_pay_batch->insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- die $error;
- }
- }
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
- '';
-}
-
-=item total_owed
-
-Returns the total owed for this customer on all invoices
-(see L<FS::cust_bill/owed>).
-
-=cut
-
-sub total_owed {
- my $self = shift;
- $self->total_owed_date(2145859200); #12/31/2037
-}
-
-=item total_owed_date TIME
-
-Returns the total owed for this customer on all invoices with date earlier than
-TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
-see L<Time::Local> and L<Date::Parse> for conversion functions.
-
-=cut
-
-sub total_owed_date {
- my $self = shift;
- my $time = shift;
- my $total_bill = 0;
- foreach my $cust_bill (
- grep { $_->_date <= $time }
- qsearch('cust_bill', { 'custnum' => $self->custnum, } )
- ) {
- $total_bill += $cust_bill->owed;
+ die $error;
+ }
}
- sprintf( "%.2f", $total_bill );
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
}
=item apply_payments_and_credits
$self->select_for_update; #mutex
- unless ( $self->total_credited ) {
+ unless ( $self->total_unapplied_credits ) {
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
return 0;
}
}
- my $total_credited = $self->total_credited;
+ my $total_unapplied_credits = $self->total_unapplied_credits;
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
- return $total_credited;
+ return $total_unapplied_credits;
}
=item apply_payments
#return 0 unless
- my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
- qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
+ my @payments = sort { $b->_date <=> $a->_date }
+ grep { $_->unapplied > 0 }
+ $self->cust_pay;
- my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
- qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
+ my @invoices = sort { $a->_date <=> $b->_date}
+ grep { $_->owed > 0 }
+ $self->cust_bill;
my $payment;
return $total_unapplied_payments;
}
-=item total_credited
+=item total_owed
+
+Returns the total owed for this customer on all invoices
+(see L<FS::cust_bill/owed>).
+
+=cut
+
+sub total_owed {
+ my $self = shift;
+ $self->total_owed_date(2145859200); #12/31/2037
+}
+
+=item total_owed_date TIME
+
+Returns the total owed for this customer on all invoices with date earlier than
+TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
+see L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+sub total_owed_date {
+ my $self = shift;
+ my $time = shift;
+ my $total_bill = 0;
+ foreach my $cust_bill (
+ grep { $_->_date <= $time }
+ qsearch('cust_bill', { 'custnum' => $self->custnum, } )
+ ) {
+ $total_bill += $cust_bill->owed;
+ }
+ sprintf( "%.2f", $total_bill );
+}
+
+=item total_paid
+
+Returns the total amount of all payments.
+
+=cut
+
+sub total_paid {
+ my $self = shift;
+ my $total = 0;
+ $total += $_->paid foreach $self->cust_pay;
+ sprintf( "%.2f", $total );
+}
+
+=item total_unapplied_credits
Returns the total outstanding credit (see L<FS::cust_credit>) for this
customer. See L<FS::cust_credit/credited>.
+=item total_credited
+
+Old name for total_unapplied_credits. Don't use.
+
=cut
sub total_credited {
+ #carp "total_credited deprecated, use total_unapplied_credits";
+ shift->total_unapplied_credits(@_);
+}
+
+sub total_unapplied_credits {
my $self = shift;
my $total_credit = 0;
- foreach my $cust_credit ( qsearch('cust_credit', {
- 'custnum' => $self->custnum,
- } ) ) {
- $total_credit += $cust_credit->credited;
- }
+ $total_credit += $_->credited foreach $self->cust_credit;
sprintf( "%.2f", $total_credit );
}
sub total_unapplied_payments {
my $self = shift;
my $total_unapplied = 0;
- foreach my $cust_pay ( qsearch('cust_pay', {
- 'custnum' => $self->custnum,
- } ) ) {
- $total_unapplied += $cust_pay->unapplied;
- }
+ $total_unapplied += $_->unapplied foreach $self->cust_pay;
sprintf( "%.2f", $total_unapplied );
}
sub total_unapplied_refunds {
my $self = shift;
my $total_unapplied = 0;
- foreach my $cust_refund ( qsearch('cust_refund', {
- 'custnum' => $self->custnum,
- } ) ) {
- $total_unapplied += $cust_refund->unapplied;
- }
+ $total_unapplied += $_->unapplied foreach $self->cust_refund;
sprintf( "%.2f", $total_unapplied );
}
=item balance
Returns the balance for this customer (total_owed plus total_unrefunded, minus
-total_credited minus total_unapplied_payments).
+total_unapplied_credits minus total_unapplied_payments).
=cut
sprintf( "%.2f",
$self->total_owed
+ $self->total_unapplied_refunds
- - $self->total_credited
+ - $self->total_unapplied_credits
- $self->total_unapplied_payments
);
}
sprintf( "%.2f",
$self->total_owed_date($time)
+ $self->total_unapplied_refunds
- - $self->total_credited
+ - $self->total_unapplied_credits
- $self->total_unapplied_payments
);
}
qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
}
-=item credit AMOUNT, REASON
+=item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
Applies a credit to this customer. If there is an error, returns the error,
otherwise returns false.
+REASON can be a text string, an FS::reason object, or a scalar reference to
+a reasonnum. If a text string, it will be automatically inserted as a new
+reason, and a 'reason_type' option must be passed to indicate the
+FS::reason_type for the new reason.
+
+An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
+
+Any other options are passed to FS::cust_credit::insert.
+
=cut
sub credit {
my( $self, $amount, $reason, %options ) = @_;
+
my $cust_credit = new FS::cust_credit {
'custnum' => $self->custnum,
'amount' => $amount,
- 'reason' => $reason,
};
+
+ if ( ref($reason) ) {
+
+ if ( ref($reason) eq 'SCALAR' ) {
+ $cust_credit->reasonnum( $$reason );
+ } else {
+ $cust_credit->reasonnum( $reason->reasonnum );
+ }
+
+ } else {
+ $cust_credit->set('reason', $reason)
+ }
+
+ $cust_credit->addlinfo( delete $options{'addlinfo'} )
+ if exists($options{'addlinfo'});
+
$cust_credit->insert(%options);
+
}
=item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
}
}
+=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"
: $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
sub geocode {
my ($self, $data_vendor) = (shift, shift); #always cch for now
+ my $geocode = $self->get('geocode'); #XXX only one data_vendor for geocode
+ return $geocode if $geocode;
+
my $prefix = ( $conf->exists('tax-ship_address') && length($self->ship_last) )
? 'ship_'
: '';
#CCH specific location stuff
my $extra_sql = "AND plus4lo <= '$plus4' AND plus4hi >= '$plus4'";
- my $geocode = '';
my @cust_tax_location =
qsearch( {
'table' => 'cust_tax_location',
my $num = $conf->config('cust_main-max_tickets') || 10;
my @tickets = ();
- unless ( $conf->config('ticket_system-custom_priority_field') ) {
+ if ( $conf->config('ticket_system') ) {
+ unless ( $conf->config('ticket_system-custom_priority_field') ) {
- @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) };
+ @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) };
- } else {
+ } else {
- foreach my $priority (
- $conf->config('ticket_system-custom_priority_field-values'), ''
- ) {
- last if scalar(@tickets) >= $num;
- push @tickets,
- @{ FS::TicketSystem->customer_tickets( $self->custnum,
- $num - scalar(@tickets),
- $priority,
- )
- };
+ foreach my $priority (
+ $conf->config('ticket_system-custom_priority_field-values'), ''
+ ) {
+ last if scalar(@tickets) >= $num;
+ push @tickets,
+ @{ FS::TicketSystem->customer_tickets( $self->custnum,
+ $num - scalar(@tickets),
+ $priority,
+ )
+ };
+ }
}
}
(@tickets);
Returns an SQL fragment to retreive the balance for this customer, only
considering invoices with date earlier than START_TIME, and optionally not
-later than END_TIME (total_owed_date minus total_credited minus
+later than END_TIME (total_owed_date minus total_unapplied_credits minus
total_unapplied_payments).
Times are specified as SQL fragments or numeric
1;
}
-=item process_batch_import
-
-Load a batch import as a queued JSRPC job
-
-=cut
-
-use Storable qw(thaw);
-use Data::Dumper;
-use MIME::Base64;
-sub process_batch_import {
- my $job = shift;
-
- my $param = thaw(decode_base64(shift));
- warn Dumper($param) if $DEBUG;
-
- my $files = $param->{'uploaded_files'}
- or die "No files provided.\n";
-
- my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
-
- my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/';
- my $file = $dir. $files{'file'};
-
- my $type;
- if ( $file =~ /\.(\w+)$/i ) {
- $type = lc($1);
- } else {
- #or error out???
- warn "can't parse file type from filename $file; defaulting to CSV";
- $type = 'csv';
- }
-
- my $error =
- FS::cust_main::batch_import( {
- job => $job,
- file => $file,
- type => $type,
- custbatch => $param->{custbatch},
- agentnum => $param->{'agentnum'},
- refnum => $param->{'refnum'},
- pkgpart => $param->{'pkgpart'},
- #'fields' => [qw( cust_pkg.setup dayphone first last address1 address2
- # city state zip comments )],
- 'format' => $param->{'format'},
- } );
-
- unlink $file;
-
- die "$error\n" if $error;
-
-}
-
-=item batch_import
-
-=cut
-
-use FS::svc_acct;
-use FS::svc_external;
-
-#some false laziness w/cdr.pm now
-sub batch_import {
- my $param = shift;
-
- my $job = $param->{job};
-
- my $filename = $param->{file};
- my $type = $param->{type} || 'csv';
-
- my $custbatch = $param->{custbatch};
-
- my $agentnum = $param->{agentnum};
- my $refnum = $param->{refnum};
- my $pkgpart = $param->{pkgpart};
-
- my $format = $param->{'format'};
-
- my @fields;
- my $payby;
- if ( $format eq 'simple' ) {
- @fields = qw( cust_pkg.setup dayphone first last
- address1 address2 city state zip comments );
- $payby = 'BILL';
- } elsif ( $format eq 'extended' ) {
- @fields = qw( agent_custid refnum
- last first address1 address2 city state zip country
- daytime night
- ship_last ship_first ship_address1 ship_address2
- ship_city ship_state ship_zip ship_country
- payinfo paycvv paydate
- invoicing_list
- cust_pkg.pkgpart
- svc_acct.username svc_acct._password
- );
- $payby = 'BILL';
- } elsif ( $format eq 'extended-plus_company' ) {
- @fields = qw( agent_custid refnum
- last first company address1 address2 city state zip country
- daytime night
- ship_last ship_first ship_company ship_address1 ship_address2
- ship_city ship_state ship_zip ship_country
- payinfo paycvv paydate
- invoicing_list
- cust_pkg.pkgpart
- svc_acct.username svc_acct._password
- );
- $payby = 'BILL';
- } elsif ( $format eq 'svc_external' ) {
- @fields = qw( agent_custid refnum
- last first company address1 address2 city state zip country
- daytime night
- ship_last ship_first ship_company ship_address1 ship_address2
- ship_city ship_state ship_zip ship_country
- payinfo paycvv paydate
- invoicing_list
- cust_pkg.pkgpart cust_pkg.bill
- svc_external.id svc_external.title
- );
- $payby = 'BILL';
- } else {
- die "unknown format $format";
- }
-
- my $count;
- my $parser;
- my @buffer = ();
- if ( $type eq 'csv' ) {
-
- eval "use Text::CSV_XS;";
- die $@ if $@;
-
- $parser = new Text::CSV_XS;
-
- @buffer = split(/\r?\n/, slurp($filename) );
- $count = scalar(@buffer);
-
- } elsif ( $type eq 'xls' ) {
-
- eval "use Spreadsheet::ParseExcel;";
- die $@ if $@;
-
- my $excel = new Spreadsheet::ParseExcel::Workbook->Parse($filename);
- $parser = $excel->{Worksheet}[0]; #first sheet
-
- $count = $parser->{MaxRow} || $parser->{MinRow};
- $count++;
-
- } else {
- die "Unknown file type $type\n";
- }
-
- #my $columns;
-
- 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 $line;
- my $row = 0;
- my( $last, $min_sec ) = ( time, 5 ); #progressbar foo
- while (1) {
-
- my @columns = ();
- if ( $type eq 'csv' ) {
-
- last unless scalar(@buffer);
- $line = shift(@buffer);
-
- $parser->parse($line) or do {
- $dbh->rollback if $oldAutoCommit;
- return "can't parse: ". $parser->error_input();
- };
- @columns = $parser->fields();
-
- } elsif ( $type eq 'xls' ) {
-
- last if $row > ($parser->{MaxRow} || $parser->{MinRow});
-
- my @row = @{ $parser->{Cells}[$row] };
- @columns = map $_->{Val}, @row;
-
- #my $z = 'A';
- #warn $z++. ": $_\n" for @columns;
-
- } else {
- die "Unknown file type $type\n";
- }
-
- #warn join('-',@columns);
-
- my %cust_main = (
- custbatch => $custbatch,
- agentnum => $agentnum,
- refnum => $refnum,
- country => $conf->config('countrydefault') || 'US',
- payby => $payby, #default
- paydate => '12/2037', #default
- );
- my $billtime = time;
- my %cust_pkg = ( pkgpart => $pkgpart );
- my %svc_x = ();
- foreach my $field ( @fields ) {
-
- if ( $field =~ /^cust_pkg\.(pkgpart|setup|bill|susp|adjourn|expire|cancel)$/ ) {
-
- #$cust_pkg{$1} = str2time( shift @$columns );
- if ( $1 eq 'pkgpart' ) {
- $cust_pkg{$1} = shift @columns;
- } elsif ( $1 eq 'setup' ) {
- $billtime = str2time(shift @columns);
- } else {
- $cust_pkg{$1} = str2time( shift @columns );
- }
-
- } elsif ( $field =~ /^svc_acct\.(username|_password)$/ ) {
-
- $svc_x{$1} = shift @columns;
-
- } elsif ( $field =~ /^svc_external\.(id|title)$/ ) {
-
- $svc_x{$1} = shift @columns;
-
- } else {
-
- #refnum interception
- if ( $field eq 'refnum' && $columns[0] !~ /^\s*(\d+)\s*$/ ) {
-
- my $referral = $columns[0];
- my %hash = ( 'referral' => $referral,
- 'agentnum' => $agentnum,
- 'disabled' => '',
- );
-
- my $part_referral = qsearchs('part_referral', \%hash )
- || new FS::part_referral \%hash;
-
- unless ( $part_referral->refnum ) {
- my $error = $part_referral->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't auto-insert advertising source: $referral: $error";
- }
- }
-
- $columns[0] = $part_referral->refnum;
- }
-
- my $value = shift @columns;
- $cust_main{$field} = $value if length($value);
- }
- }
-
- $cust_main{'payby'} = 'CARD'
- if defined $cust_main{'payinfo'}
- && length $cust_main{'payinfo'};
-
- my $invoicing_list = $cust_main{'invoicing_list'}
- ? [ delete $cust_main{'invoicing_list'} ]
- : [];
-
- my $cust_main = new FS::cust_main ( \%cust_main );
-
- use Tie::RefHash;
- tie my %hash, 'Tie::RefHash'; #this part is important
-
- if ( $cust_pkg{'pkgpart'} ) {
- my $cust_pkg = new FS::cust_pkg ( \%cust_pkg );
-
- my @svc_x = ();
- my $svcdb = '';
- if ( $svc_x{'username'} ) {
- $svcdb = 'svc_acct';
- } elsif ( $svc_x{'id'} || $svc_x{'title'} ) {
- $svcdb = 'svc_external';
- }
- if ( $svcdb ) {
- my $part_pkg = $cust_pkg->part_pkg;
- unless ( $part_pkg ) {
- $dbh->rollback if $oldAutoCommit;
- return "unknown pkgpart: ". $cust_pkg{'pkgpart'};
- }
- $svc_x{svcpart} = $part_pkg->svcpart( $svcdb );
- my $class = "FS::$svcdb";
- push @svc_x, $class->new( \%svc_x );
- }
-
- $hash{$cust_pkg} = \@svc_x;
- }
-
- my $error = $cust_main->insert( \%hash, $invoicing_list );
-
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't insert customer". ( $line ? " for $line" : '' ). ": $error";
- }
-
- if ( $format eq 'simple' ) {
-
- #false laziness w/bill.cgi
- $error = $cust_main->bill( 'time' => $billtime );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't bill customer for $line: $error";
- }
-
- $error = $cust_main->apply_payments_and_credits;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't bill customer for $line: $error";
- }
-
- $error = $cust_main->collect();
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't collect customer for $line: $error";
- }
-
- }
-
- $row++;
-
- if ( $job && time - $min_sec > $last ) { #progress bar
- $job->update_statustext( int(100 * $row / $count) );
- $last = time;
- }
-
- }
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;;
-
- return "Empty file!" unless $row;
-
- ''; #no error
-
-}
-
=item batch_charge
=cut
=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',
$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";
-
- 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;
+ join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
+
+ 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);
);
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} = '~';
$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 $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
DIR => $dir,
SUFFIX => '.tex',
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');
AND peo_agentnum.optionname = 'agentnum'
AND peo_agentnum.optionvalue }. $regexp. q{ '(^|,)}. $agentnum. q{(,|$)'
)
- LEFT JOIN part_event_option AS peo_cust_bill_age
- ON ( part_event.eventpart = peo_cust_bill_age.eventpart
- AND peo_cust_bill_age.optionname = 'cust_bill_age'
+ LEFT JOIN part_event_condition
+ ON ( part_event.eventpart = part_event_condition.eventpart
+ AND part_event_condition.conditionname = 'cust_bill_age'
+ )
+ LEFT JOIN part_event_condition_option
+ ON ( part_event_condition.eventconditionnum = part_event_condition_option.eventconditionnum
+ AND part_event_condition_option.optionname = 'age'
)
},
#'hashref' => { 'optionname' => $option },
" AND peo_agentnum.optionname = 'agentnum' ".
" AND ( agentnum IS NULL OR agentnum = $agentnum ) ".
" ORDER BY
- CASE WHEN peo_cust_bill_age.optionname != 'cust_bill_age'
+ CASE WHEN part_event_condition_option.optionname IS NULL
THEN -1
- ELSE ". FS::part_event::Condition->age2seconds_sql('peo_cust_bill_age.optionvalue').
+ ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
" END
, part_event.weight".
" LIMIT 1"