require 5.006;
use strict;
-use vars qw( @ISA @EXPORT_OK $DEBUG $me $conf @encrypted_fields
- $import $skip_fuzzyfiles $ignore_expired_card @paytypes);
+use vars qw( @ISA @EXPORT_OK $DEBUG $me $conf
+ @encrypted_fields
+ $import $ignore_expired_card
+ $skip_fuzzyfiles @fuzzyfields
+ @paytypes
+ );
use vars qw( $realtime_bop_decline_quiet ); #ugh
use Safe;
use Carp;
use FS::part_referral;
use FS::cust_main_county;
use FS::cust_location;
+use FS::cust_class;
use FS::cust_main_exemption;
use FS::cust_tax_adjustment;
use FS::tax_rate;
$me = '[FS::cust_main]';
$import = 0;
-$skip_fuzzyfiles = 0;
$ignore_expired_card = 0;
+$skip_fuzzyfiles = 0;
+@fuzzyfields = ( 'first', 'last', 'company', 'address1' );
+
@encrypted_fields = ('payinfo', 'paycvv');
sub nohistory_fields { ('paycvv'); }
my $dbh = dbh;
my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
- my $error = $queue->insert( map $self->getfield($_),
- qw(first last company)
- );
+ my $error = $queue->insert( map $self->getfield($_), @fuzzyfields );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "queueing job (transaction rolled back): $error";
if ( $self->ship_last ) {
$queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
- $error = $queue->insert( map $self->getfield("ship_$_"),
- qw(first last company)
- );
+ $error = $queue->insert( map $self->getfield("ship_$_"), @fuzzyfields );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "queueing job (transaction rolled back): $error";
|| $self->ut_number('agentnum')
|| $self->ut_textn('agent_custid')
|| $self->ut_number('refnum')
+ || $self->ut_foreign_keyn('classnum', 'cust_class', 'classnum')
|| $self->ut_textn('custbatch')
|| $self->ut_name('last')
|| $self->ut_name('first')
qsearch('cust_location', { 'custnum' => $self->custnum } );
}
+=item location_label_short
+
+Returns the short label of the service location (see analog in L<FS::cust_location>) for this customer.
+
+=cut
+
+# false laziness with FS::cust_location::line_short
+
+sub location_label_short {
+ my $self = shift;
+ my $cydefault = FS::conf->new->config('countrydefault') || 'US';
+
+ my $line = $self->address1;
+ #$line .= ', '. $self->address2 if $self->address2;
+ $line .= ', '. $self->city;
+ $line .= ', '. $self->state if $self->state;
+ $line .= ' '. $self->zip if $self->zip;
+ $line .= ' '. code2country($self->country) if $self->country ne $cydefault;
+
+ $line;
+}
+
=item ncancelled_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ]
Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
# This should be generalized to use config options to determine order.
sub sort_packages {
+ my $locationsort = $a->locationnum <=> $b->locationnum;
+ return $locationsort if $locationsort;
+
if ( $a->get('cancel') xor $b->get('cancel') ) {
return -1 if $b->get('cancel');
return 1 if $a->get('cancel');
qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
}
+=item cust_class
+
+Returns the customer class, as an FS::cust_class object, or the empty string
+if there is no customer class.
+
+=cut
+
+sub cust_class {
+ my $self = shift;
+ if ( $self->classnum ) {
+ qsearchs('cust_class', { 'classnum' => $self->classnum } );
+ } else {
+ return '';
+ }
+}
+
+=item categoryname
+
+Returns the customer category name, or the empty string if there is no customer
+category.
+
+=cut
+
+sub categoryname {
+ my $self = shift;
+ my $cust_class = $self->cust_class;
+ $cust_class
+ ? $cust_class->categoryname
+ : '';
+}
+
+=item classname
+
+Returns the customer class name, or the empty string if there is no customer
+class.
+
+=cut
+
+sub classname {
+ my $self = shift;
+ my $cust_class = $self->cust_class;
+ $cust_class
+ ? $cust_class->classname
+ : '';
+}
+
+
=item bill_and_collect
Cancels and suspends any packages due, generates bills, applies payments and
fees since the last billing. Setup charges may be charged. Not all package
plans support this feature (they tend to charge 0).
+=item invoice_terms
+
+Optional terms to be printed on this invoice. Otherwise, customer-specific
+terms or the default terms are used.
+
=back
=cut
}
+ my $listref_or_error =
+ $self->calculate_taxes( \@cust_bill_pkg, \%taxlisthash, $invoice_time);
+
+ unless ( ref( $listref_or_error ) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $listref_or_error;
+ }
+
+ foreach my $taxline ( @$listref_or_error ) {
+ $total_setup = sprintf('%.2f', $total_setup+$taxline->setup );
+ push @cust_bill_pkg, $taxline;
+ }
+
+ #add tax adjustments
+ warn "adding tax adjustments...\n" if $DEBUG > 2;
+ foreach my $cust_tax_adjustment (
+ qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum,
+ 'billpkgnum' => '',
+ }
+ )
+ ) {
+
+ my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
+
+ my $itemdesc = $cust_tax_adjustment->taxname;
+ $itemdesc = '' if $itemdesc eq 'Tax';
+
+ push @cust_bill_pkg, new FS::cust_bill_pkg {
+ 'pkgnum' => 0,
+ 'setup' => $tax,
+ 'recur' => 0,
+ 'sdate' => '',
+ 'edate' => '',
+ 'itemdesc' => $itemdesc,
+ 'itemcomment' => $cust_tax_adjustment->comment,
+ 'cust_tax_adjustment' => $cust_tax_adjustment,
+ #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
+ };
+
+ }
+
+ my $charged = sprintf('%.2f', $total_setup + $total_recur );
+
+ my @cust_bill = $self->cust_bill;
+ my $balance = $self->balance;
+ my $previous_balance = scalar(@cust_bill)
+ ? ( $cust_bill[$#cust_bill]->billing_balance || 0 )
+ : 0;
+
+ $previous_balance += $cust_bill[$#cust_bill]->charged
+ if scalar(@cust_bill);
+ #my $balance_adjustments =
+ # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged);
+
+ #create the new invoice
+ my $cust_bill = new FS::cust_bill ( {
+ 'custnum' => $self->custnum,
+ '_date' => ( $invoice_time ),
+ 'charged' => $charged,
+ 'billing_balance' => $balance,
+ 'previous_balance' => $previous_balance,
+ 'invoice_terms' => $options{'invoice_terms'},
+ } );
+ $error = $cust_bill->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't create invoice for customer #". $self->custnum. ": $error";
+ }
+
+ foreach my $cust_bill_pkg ( @cust_bill_pkg ) {
+ $cust_bill_pkg->invnum($cust_bill->invnum);
+ my $error = $cust_bill_pkg->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't create invoice line item: $error";
+ }
+ }
+
+
+ foreach my $hook ( @precommit_hooks ) {
+ eval {
+ &{$hook}; #($self) ?
+ };
+ if ( $@ ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "$@ running precommit hook $hook\n";
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ ''; #no error
+}
+
+=item calculate_taxes LINEITEMREF TAXHASHREF INVOICE_TIME
+
+This is a weird one. Perhaps it should not even be exposed.
+
+Generates tax line items (see L<FS::cust_bill_pkg>) for this customer.
+Usually used internally by bill method B<bill>.
+
+If there is an error, returns the error, otherwise returns reference to a
+list of line items suitable for insertion.
+
+=over 4
+
+=item LINEITEMREF
+
+An array ref of the line items being billed.
+
+=item TAXHASHREF
+
+A strange beast. The keys to this hash are internal identifiers consisting
+of the name of the tax object type, a space, and its unique identifier ( e.g.
+ 'cust_main_county 23' ). The values of the hash are listrefs. The first
+item in the list is the tax object. The remaining items are either line
+items or floating point values (currency amounts).
+
+The taxes are calculated on this entity. Calculated exemption records are
+transferred to the LINEITEMREF items on the assumption that they are related.
+
+Read the source.
+
+=item INVOICE_TIME
+
+This specifies the date appearing on the associated invoice. Some
+jurisdictions (i.e. Texas) have tax exemptions which are date sensitive.
+
+=back
+
+=cut
+sub calculate_taxes {
+ my ($self, $cust_bill_pkg, $taxlisthash, $invoice_time) = @_;
+
+ my @tax_line_items = ();
+
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 cust_bill_pkg_tax_rate_location hashrefs
my %tax_rate_location = ();
- foreach my $tax ( keys %taxlisthash ) {
- my $tax_object = shift @{ $taxlisthash{$tax} };
+ foreach my $tax ( keys %$taxlisthash ) {
+ my $tax_object = shift @{ $taxlisthash->{$tax} };
warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
- warn " ". join('/', @{ $taxlisthash{$tax} } ). "\n" if $DEBUG > 2;
+ warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2;
my $hashref_or_error =
- $tax_object->taxline( $taxlisthash{$tax},
+ $tax_object->taxline( $taxlisthash->{$tax},
'custnum' => $self->custnum,
'invoice_time' => $invoice_time
);
- unless ( ref($hashref_or_error) ) {
- $dbh->rollback if $oldAutoCommit;
- return $hashref_or_error;
- }
- unshift @{ $taxlisthash{$tax} }, $tax_object;
+ return $hashref_or_error unless ref($hashref_or_error);
+
+ unshift @{ $taxlisthash->{$tax} }, $tax_object;
my $name = $hashref_or_error->{'name'};
my $amount = $hashref_or_error->{'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} })] ) {
+ 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';
push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
next unless $tax;
$tax = sprintf('%.2f', $tax );
- $total_setup = sprintf('%.2f', $total_setup+$tax );
my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
'disabled' => '',
}
- push @cust_bill_pkg, new FS::cust_bill_pkg {
+ push @tax_line_items, new FS::cust_bill_pkg {
'pkgnum' => 0,
'setup' => $tax,
'recur' => 0,
}
- #add tax adjustments
- warn "adding tax adjustments...\n" if $DEBUG > 2;
- foreach my $cust_tax_adjustment (
- qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum,
- 'billpkgnum' => '',
- }
- )
- ) {
-
- my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
- $total_setup = sprintf('%.2f', $total_setup+$tax );
-
- my $itemdesc = $cust_tax_adjustment->taxname;
- $itemdesc = '' if $itemdesc eq 'Tax';
-
- push @cust_bill_pkg, new FS::cust_bill_pkg {
- 'pkgnum' => 0,
- 'setup' => $tax,
- 'recur' => 0,
- 'sdate' => '',
- 'edate' => '',
- 'itemdesc' => $itemdesc,
- 'itemcomment' => $cust_tax_adjustment->comment,
- 'cust_tax_adjustment' => $cust_tax_adjustment,
- #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
- };
-
- }
-
- my $charged = sprintf('%.2f', $total_setup + $total_recur );
-
- my @cust_bill = $self->cust_bill;
- my $balance = $self->balance;
- my $previous_balance = scalar(@cust_bill)
- ? ( $cust_bill[$#cust_bill]->billing_balance || 0 )
- : 0;
-
- $previous_balance += $cust_bill[$#cust_bill]->charged
- if scalar(@cust_bill);
- #my $balance_adjustments =
- # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged);
-
- #create the new invoice
- my $cust_bill = new FS::cust_bill ( {
- 'custnum' => $self->custnum,
- '_date' => ( $invoice_time ),
- 'charged' => $charged,
- 'billing_balance' => $balance,
- 'previous_balance' => $previous_balance,
- } );
- $error = $cust_bill->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't create invoice for customer #". $self->custnum. ": $error";
- }
-
- foreach my $cust_bill_pkg ( @cust_bill_pkg ) {
- $cust_bill_pkg->invnum($cust_bill->invnum);
- my $error = $cust_bill_pkg->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't create invoice line item: $error";
- }
- }
-
-
- foreach my $hook ( @precommit_hooks ) {
- eval {
- &{$hook}; #($self) ?
- };
- if ( $@ ) {
- $dbh->rollback if $oldAutoCommit;
- return "$@ running precommit hook $hook\n";
- }
- }
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
- ''; #no error
+ \@tax_line_items;
}
-
sub _make_lines {
my ($self, %params) = @_;
} else {
- my @loc_keys = qw( state county country );
+ my @loc_keys = qw( city county state country );
my %taxhash;
if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
my $cust_location = $cust_pkg->cust_location;
$taxhash{'taxclass'} = $part_pkg->taxclass;
- my @taxes = qsearch( 'cust_main_county', \%taxhash );
-
+ my @taxes = ();
my %taxhash_elim = %taxhash;
+ my @elim = qw( city county state );
+ do {
- my @elim = qw( taxclass county state );
- while ( !scalar(@taxes) && scalar(@elim) ) {
- $taxhash_elim{ shift(@elim) } = '';
+ #first try a match with taxclass
@taxes = qsearch( 'cust_main_county', \%taxhash_elim );
- }
+
+ if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
+ #then try a match without taxclass
+ my %no_taxclass = %taxhash_elim;
+ $no_taxclass{ 'taxclass' } = '';
+ @taxes = qsearch( 'cust_main_county', \%no_taxclass );
+ }
+
+ $taxhash_elim{ shift(@elim) } = '';
+
+ } while ( !scalar(@taxes) && scalar(@elim) );
@taxes = grep { ! $_->taxname or ! $self->tax_exemption($_->taxname) }
@taxes
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".
+the value defined by the business-onlinepayment-description configuration
+option, or "Internet services" if that is unset.
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.
+call the B<apply_payments> method or set the I<apply> option.
+
+I<apply> can be set to true to apply a resulting payment.
I<quiet> can be set true to surpress email decline notices.
return $self->_new_realtime_bop(@_)
if $self->_new_bop_required();
- my( $method, $amount, %options ) = @_;
+ my($method, $amount);
+ my %options = ();
+ if (ref($_[0]) eq 'HASH') {
+ %options = %{$_[0]};
+ $method = $options{method};
+ $amount = $options{amount};
+ } else {
+ ( $method, $amount ) = ( shift, shift );
+ %options = @_;
+ }
if ( $DEBUG ) {
warn "$me realtime_bop: $method $amount\n";
warn " $_ => $options{$_}\n" foreach keys %options;
}
- $options{'description'} ||= 'Internet services';
+ unless ( $options{'description'} ) {
+ if ( $conf->exists('business-onlinepayment-description') ) {
+ my $dtempl = $conf->config('business-onlinepayment-description');
+
+ my $agent = $self->agent->agent;
+ #$pkgs... not here
+ $options{'description'} = eval qq("$dtempl");
+ } else {
+ $options{'description'} = 'Internet services';
+ }
+ }
return $self->fake_bop($method, $amount, %options) if $options{'fake'};
} else {
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ if ( $options{'apply'} ) {
+ my $apply_error = $self->apply_payments_and_credits;
+ if ( $apply_error ) {
+ warn "WARNING: error applying payment: $apply_error\n";
+ #but we still should return no error cause the payment otherwise went
+ #through...
+ }
+ }
+
return ''; #no error
}
sub _bop_recurring_billing {
my( $self, %opt ) = @_;
- my $method = $conf->config('credit_card-recurring_billing_flag');
+ my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
- if ( $method eq 'transaction_is_recur' ) {
+ if ( defined($method) && $method eq 'transaction_is_recur' ) {
return 1 if $opt{'trans_is_recur'};
) {
warn " attempting void\n" if $DEBUG > 1;
my $void = new Business::OnlinePayment( $processor, @bop_options );
+ $content{'card_number'} = $cust_pay->payinfo
+ if $cust_pay->payby eq 'CARD'
+ && $void->can('info') && $void->info('CC_void_requires_card');
$void->content( 'action' => 'void', %content );
$void->submit();
if ( $void->is_success ) {
'';
}
-
=item realtime_collect [ OPTION => VALUE ... ]
Runs a realtime credit card, ACH (electronic check) or phone bill transaction
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".
+the value defined by the business-onlinepayment-description configuration
+option, or "Internet services" if that is unset.
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.
+call the B<apply_payments> method or set the I<apply> option.
+
+I<apply> can be set to true to apply a resulting payment.
I<quiet> can be set true to surpress email decline notices.
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".
+the value defined by the business-onlinepayment-description configuration
+option, or "Internet services" if that is unset.
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
sub _bop_defaults {
my ($self, $options) = @_;
- $options->{description} ||= 'Internet services';
+ unless ( $options->{'description'} ) {
+ if ( $conf->exists('business-onlinepayment-description') ) {
+ my $dtempl = $conf->config('business-onlinepayment-description');
+
+ my $agent = $self->agent->agent;
+ #$pkgs... not here
+ $options->{'description'} = eval qq("$dtempl");
+ } else {
+ $options->{'description'} = 'Internet services';
+ }
+ }
+
$options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
$options->{invnum} ||= '';
$options->{payname} = $self->payname unless exists( $options->{payname} );
} else {
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ if ( $options{'apply'} ) {
+ my $apply_error = $self->apply_payments_and_credits;
+ if ( $apply_error ) {
+ warn "WARNING: error applying payment: $apply_error\n";
+ #but we still should return no error cause the payment otherwise went
+ #through...
+ }
+ }
+
return ''; #no error
}
) {
warn " attempting void\n" if $DEBUG > 1;
my $void = new Business::OnlinePayment( $processor, @bop_options );
+ $content{'card_number'} = $cust_pay->payinfo
+ if $cust_pay->payby eq 'CARD'
+ && $void->can('info') && $void->info('CC_void_requires_card');
$void->content( 'action' => 'void', %content );
$void->submit();
if ( $void->is_success ) {
If true, a payment receipt is sent instead of a statement when
'payment_receipt_email' configuration option is set.
-
Dies if there is an error.
=cut
);
}
+=item balance_date_range START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
+
+Returns 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_unapplied_credits minus total_unapplied_payments).
+
+Times are specified as SQL fragments or numeric
+UNIX timestamps; see L<perlfunc/"time">). Also see L<Time::Local> and
+L<Date::Parse> for conversion functions. The empty string can be passed
+to disable that time constraint completely.
+
+Available options are:
+
+=over 4
+
+=item unapplied_date
+
+set to true to disregard unapplied credits, payments and refunds outside the specified time period - by default the time period restriction only applies to invoices (useful for reporting, probably a bad idea for event triggering)
+
+=back
+
+=cut
+
+sub balance_date_range {
+ my $self = shift;
+ my $sql = 'SELECT SUM('. $self->balance_date_sql(@_).
+ ') FROM cust_main WHERE custnum='. $self->custnum;
+ sprintf( "%.2f", $self->scalar_sql($sql) );
+}
+
=item balance_pkgnum PKGNUM
Returns the balance for this customer's specific package when using
#vendor taxation
'taxproduct' => 2, #part_pkg_taxproduct
'override' => {}, #XXX describe
+
+ #will be filled in with the new object
+ 'cust_pkg_ref' => \$cust_pkg,
+
+ #generate an invoice immediately
+ 'bill_now' => 0,
+ 'invoice_terms' => '', #with these terms
}
);
my ( $pkg, $comment, $additional );
my ( $setuptax, $taxclass ); #internal taxes
my ( $taxproduct, $override ); #vendor (CCH) taxes
+ my $cust_pkg_ref = '';
+ my ( $bill_now, $invoice_terms ) = ( 0, '' );
if ( ref( $_[0] ) ) {
$amount = $_[0]->{amount};
$quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
$additional = $_[0]->{additional} || [];
$taxproduct = $_[0]->{taxproductnum};
$override = { '' => $_[0]->{tax_override} };
+ $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
+ $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
+ $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
} else {
$amount = shift;
$quantity = 1;
'plan' => 'flat',
'freq' => 0,
'disabled' => 'Y',
- 'classnum' => $classnum ? $classnum : '',
+ 'classnum' => ( $classnum ? $classnum : '' ),
'setuptax' => $setuptax,
'taxclass' => $taxclass,
'taxproductnum' => $taxproduct,
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
+ } elsif ( $cust_pkg_ref ) {
+ ${$cust_pkg_ref} = $cust_pkg;
+ }
+
+ if ( $bill_now ) {
+ my $error = $self->bill( 'invoice_terms' => $invoice_terms,
+ 'pkg_list' => [ $cust_pkg ],
+ );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
- '';
+ return '';
}
}
-=item search_sql HASHREF
+=item search HASHREF
(Class method)
listref
+=item paydate_year
+
+=item paydate_month
+
=item current_balance
listref (list returned by FS::UI::Web::parse_lt_gt($cgi, 'current_balance'))
=cut
-sub search_sql {
+sub search {
my ($class, $params) = @_;
my $dbh = dbh;
}
###
+ # classnum
+ ###
+
+ if ( $params->{'classnum'} ) {
+
+ my @classnum = ref( $params->{'classnum'} )
+ ? @{ $params->{'classnum'} }
+ : ( $params->{'classnum'} );
+
+ @classnum = grep /^(\d*)$/, @classnum;
+
+ if ( @classnum ) {
+ push @where, '( '. join(' OR ', map {
+ $_ ? "cust_main.classnum = $_"
+ : "cust_main.classnum IS NULL"
+ }
+ @classnum
+ ).
+ ' )';
+ }
+
+ }
+
+ ###
# payby
###
- my @payby = grep /^([A-Z]{4})$/, @{ $params->{'payby'} };
- if ( @payby ) {
- push @where, '( '. join(' OR ', map "cust_main.payby = '$_'", @payby). ' )';
+ if ( $params->{'payby'} ) {
+
+ my @payby = ref( $params->{'payby'} )
+ ? @{ $params->{'payby'} }
+ : ( $params->{'payby'} );
+
+ @payby = grep /^([A-Z]{4})$/, @{ $params->{'payby'} };
+
+ push @where, '( '. join(' OR ', map "cust_main.payby = '$_'", @payby). ' )'
+ if @payby;
+
+ }
+
+ ###
+ # paydate_year / paydate_month
+ ###
+
+ if ( $params->{'paydate_year'} =~ /^(\d{4})$/ ) {
+ my $year = $1;
+ $params->{'paydate_month'} =~ /^(\d\d?)$/
+ or die "paydate_year without paydate_month?";
+ my $month = $1;
+
+ push @where,
+ 'paydate IS NOT NULL',
+ "paydate != ''",
+ "CAST(paydate AS timestamp) < CAST('$year-$month-01' AS timestamp )"
+;
+ }
+
+ ###
+ # invoice terms
+ ###
+
+ if ( $params->{'invoice_terms'} =~ /^([\w ]+)$/ ) {
+ my $terms = $1;
+ if ( $1 eq 'NULL' ) {
+ push @where,
+ "( cust_main.invoice_terms IS NULL OR cust_main.invoice_terms = '' )";
+ } else {
+ push @where,
+ "cust_main.invoice_terms IS NOT NULL",
+ "cust_main.invoice_terms = '$1'";
+ }
}
##
# amounts
##
- #my $balance_sql = $class->balance_sql();
- my $balance_sql = FS::cust_main->balance_sql();
+ if ( $params->{'current_balance'} ) {
+
+ #my $balance_sql = $class->balance_sql();
+ my $balance_sql = FS::cust_main->balance_sql();
- push @where, map { s/current_balance/$balance_sql/; $_ }
- @{ $params->{'current_balance'} };
+ my @current_balance =
+ ref( $params->{'current_balance'} )
+ ? @{ $params->{'current_balance'} }
+ : ( $params->{'current_balance'} );
+
+ push @where, map { s/current_balance/$balance_sql/; $_ }
+ @current_balance;
+
+ }
##
# custbatch
}
-=item email_search_sql HASHREF
+=item email_search_result HASHREF
(Class method)
Emails a notice to the specified customers.
-Valid parameters are those of the L<search_sql> method, plus the following:
+Valid parameters are those of the L<search> method, plus the following:
=over 4
=cut
-sub email_search_sql {
+sub email_search_result {
my($class, $params) = @_;
my $from = delete $params->{from};
$params->{'payby'} = [ split(/\0/, $params->{'payby'}) ]
unless ref($params->{'payby'});
- my $sql_query = $class->search_sql($params);
+ my $sql_query = $class->search($params);
my $count_query = delete($sql_query->{'count_query'});
my $count_sth = dbh->prepare($count_query)
use Storable qw(thaw);
use Data::Dumper;
use MIME::Base64;
-sub process_email_search_sql {
+sub process_email_search_result {
my $job = shift;
#warn "$me process_re_X $method for job $job\n" if $DEBUG;
$param->{'payby'} = [ split(/\0/, $param->{'payby'}) ]
unless ref($param->{'payby'});
- my $error = FS::cust_main->email_search_sql( $param );
+ my $error = FS::cust_main->email_search_result( $param );
die $error if $error;
}
=item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
Performs a fuzzy (approximate) search and returns the matching FS::cust_main
-records. Currently, I<first>, I<last> and/or I<company> may be specified (the
-appropriate ship_ field is also searched).
+records. Currently, I<first>, I<last>, I<company> and/or I<address1> may be
+specified (the appropriate ship_ field is also searched).
Additional options are the same as FS::Record::qsearch
}
if ( $search =~ /^\s*(\d+)\s*$/
- || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+'
- && $search =~ /^\s*(\w\w?\d+)\s*$/
- )
- )
+ || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+'
+ && $search =~ /^\s*(\w\w?\d+)\s*$/
+ )
+ || ( $conf->exists('address1-search' )
+ && $search =~ /^\s*(\d+\-?\w*)\s*$/ #i.e. 1234A or 9432-D
+ )
+ )
{
my $num = $1;
- if ( $num <= 2147483647 ) { #need a bigint custnum? wow.
+ if ( $num =~ /^(\d+)$/ && $num <= 2147483647 ) { #need a bigint custnum? wow
push @cust_main, qsearch( {
'table' => 'cust_main',
'hashref' => { 'custnum' => $num, %options },
'extra_sql' => " AND $agentnums_sql", #agent virtualization
} );
+ if ( $conf->exists('address1-search') ) {
+ my $len = length($num);
+ $num = lc($num);
+ foreach my $prefix ( '', 'ship_' ) {
+ push @cust_main, qsearch( {
+ 'table' => 'cust_main',
+ 'hashref' => { %options, },
+ 'extra_sql' =>
+ ( keys(%options) ? ' AND ' : ' WHERE ' ).
+ " LOWER(SUBSTRING(${prefix}address1 FROM 1 FOR $len)) = '$num' ".
+ " AND $agentnums_sql",
+ } );
+ }
+ }
+
} elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) {
my($company, $last, $first) = ( $1, $2, $3 );
# "Company (Last, First)"
#this is probably something a browser remembered,
- #so just do an exact search
+ #so just do an exact search (but case-insensitive, so USPS standardization
+ #doesn't throw a wrench in the works)
foreach my $prefix ( '', 'ship_' ) {
push @cust_main, qsearch( {
'table' => 'cust_main',
- 'hashref' => { $prefix.'first' => $first,
- $prefix.'last' => $last,
- $prefix.'company' => $company,
- %options,
- },
- 'extra_sql' => " AND $agentnums_sql",
+ 'hashref' => { %options },
+ 'extra_sql' =>
+ ( keys(%options) ? ' AND ' : ' WHERE ' ).
+ join(' AND ',
+ " LOWER(${prefix}first) = ". dbh->quote(lc($first)),
+ " LOWER(${prefix}last) = ". dbh->quote(lc($last)),
+ " LOWER(${prefix}company) = ". dbh->quote(lc($company)),
+ $agentnums_sql,
+ ),
} );
}
#exact
my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
- $sql .= " ( LOWER(last) = $q_value
- OR LOWER(company) = $q_value
- OR LOWER(ship_last) = $q_value
- OR LOWER(ship_company) = $q_value
- )";
+ $sql .= " ( LOWER(last) = $q_value
+ OR LOWER(company) = $q_value
+ OR LOWER(ship_last) = $q_value
+ OR LOWER(ship_company) = $q_value
+ ";
+ $sql .= " OR LOWER(address1) = $q_value
+ OR LOWER(ship_address1) = $q_value
+ "
+ if $conf->exists('address1-search');
+ $sql .= " )";
push @cust_main, qsearch( {
'table' => 'cust_main',
#getting complaints searches are not returning enough
unless ( @cust_main && $skip_fuzzy || $conf->exists('disable-fuzzy') ) {
- #still some false laziness w/search_sql (was search/cust_main.cgi)
+ #still some false laziness w/search (was search/cust_main.cgi)
#substring
;
}
+ if ( $conf->exists('address1-search') ) {
+ push @hashrefs,
+ { 'address1' => { op=>'ILIKE', value=>"%$value%" }, },
+ { 'ship_address1' => { op=>'ILIKE', value=>"%$value%" }, },
+ ;
+ }
+
foreach my $hashref ( @hashrefs ) {
push @cust_main, qsearch( {
push @cust_main,
FS::cust_main->fuzzy_search( { $field => $value }, @fuzopts );
}
+ if ( $conf->exists('address1-search') ) {
+ push @cust_main,
+ FS::cust_main->fuzzy_search( { 'address1' => $value }, @fuzopts );
+ }
}
=cut
-use vars qw(@fuzzyfields);
-@fuzzyfields = ( 'last', 'first', 'company' );
-
sub check_and_rebuild_fuzzyfiles {
my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
rebuild_fuzzyfiles() if grep { ! -e "$dir/cust_main.$_" } @fuzzyfields
\@array;
}
-=item append_fuzzyfiles LASTNAME COMPANY
+=item append_fuzzyfiles FIRSTNAME LASTNAME COMPANY ADDRESS1
=cut
my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
- foreach my $field (qw( first last company )) {
+ foreach my $field (@fuzzyfields) {
my $value = shift;
if ( $value ) {