@encrypted_fields
$import
$ignore_expired_card $ignore_illegal_zip $ignore_banned_card
- $skip_fuzzyfiles @fuzzyfields
+ $skip_fuzzyfiles
@paytypes
);
use Carp;
use FS::cust_pkg;
use FS::cust_svc;
use FS::cust_bill;
+use FS::legacy_cust_bill;
use FS::cust_pay;
use FS::cust_pay_pending;
use FS::cust_pay_void;
use FS::cust_main_note;
use FS::cust_attachment;
use FS::contact;
+use FS::Locales;
# 1 is mostly method/subroutine entry and options
# 2 traces progress of some operations
$ignore_banned_card = 0;
$skip_fuzzyfiles = 0;
-@fuzzyfields = ( 'first', 'last', 'company', 'address1' );
@encrypted_fields = ('payinfo', 'paycvv');
sub nohistory_fields { ('payinfo', 'paycvv'); }
phone (optional)
+=item mobile
+
+phone (optional)
+
=item ship_first
Shipping first name
phone (optional)
+=item ship_mobile
+
+phone (optional)
+
=item payby
Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
Allow self-service editing of ticket subjects, empty or 'Y'
+=item calling_list_exempt
+
+Do not call, empty or 'Y'
+
=back
=head1 METHODS
$self->signupdate(time) unless $self->signupdate;
+ $self->censusyear($conf->config('census_year')) if $self->censustract;
+
$self->auto_agent_custid()
if $conf->config('cust_main-auto_agent_custid') && ! $self->agent_custid;
}
}
+ # FS::geocode_Mixin::after_insert or something?
+ if ( $conf->config('tax_district_method') and !$import ) {
+ # if anything non-empty, try to look it up
+ my $queue = new FS::queue {
+ 'job' => 'FS::geocode_Mixin::process_district_update',
+ 'custnum' => $self->custnum,
+ };
+ my $error = $queue->insert( ref($self), $self->custnum );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "queueing tax district update: $error";
+ }
+ }
+
# cust_main exports!
warn " exporting\n" if $DEBUG > 1;
&& length($self->get($pre.'zip')) >= 10;
}
+ for my $pre ( grep $old->get($_.'coord_auto'), ( '', 'ship_' ) ) {
+
+ $self->set($pre.'coord_auto', '') && next
+ if $self->get($pre.'latitude') && $self->get($pre.'longitude')
+ && ( $self->get($pre.'latitude') != $old->get($pre.'latitude')
+ || $self->get($pre.'longitude') != $old->get($pre.'longitude')
+ );
+
+ $self->set_coord($pre)
+ if $old->get($pre.'address1') ne $self->get($pre.'address1')
+ || $old->get($pre.'city') ne $self->get($pre.'city')
+ || $old->get($pre.'state') ne $self->get($pre.'state')
+ || $old->get($pre.'country') ne $self->get($pre.'country');
+
+ }
+
+ unless ( $import ) {
+ $self->set_coord
+ if ! $self->coord_auto && ! $self->latitude && ! $self->longitude;
+
+ $self->set_coord('ship_')
+ if $self->has_ship_address && ! $self->ship_coord_auto
+ && ! $self->ship_latitude && ! $self->ship_longitude;
+ }
+
local($ignore_expired_card) = 1
if $old->payby =~ /^(CARD|DCRD)$/
&& $self->payby =~ /^(CARD|DCRD)$/
|| $old->payby =~ /^(CHEK|DCHK)$/ && $self->payby =~ /^(CHEK|DCHK)$/ )
&& ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
+ if ( $self->censustract ne '' and $self->censustract ne $old->censustract ) {
+ # update censusyear whenever tract code changes
+ $self->censusyear($conf->config('census_year'));
+ }
+
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
}
}
+ # FS::geocode_Mixin::after_replace ?
+ # though this will go away anyway once we move customer bill/service
+ # locations into cust_location
+ # We can trigger this on any address change--just have to make sure
+ # not to trigger it on itself.
+ if ( $conf->config('tax_district_method') and !$import
+ and ( $self->get('ship_address1') ne $old->get('ship_address1')
+ or $self->get('address1') ne $old->get('address1') ) ) {
+ my $queue = new FS::queue {
+ 'job' => 'FS::geocode_Mixin::process_district_update',
+ 'custnum' => $self->custnum,
+ };
+ my $error = $queue->insert( ref($self), $self->custnum );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "queueing tax district update: $error";
+ }
+ }
+
# cust_main exports!
my $export_args = $options{'export_args'} || [];
=cut
+use FS::cust_main::Search;
sub queue_fuzzyfiles_update {
my $self = shift;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
- my $error = $queue->insert( map $self->getfield($_), @fuzzyfields );
+ my $queue = new FS::queue { 'job' => 'FS::cust_main::Search::append_fuzzyfiles' };
+ my $error = $queue->insert( map $self->getfield($_), @FS::cust_main::Search::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_$_"), @fuzzyfields );
+ $queue = new FS::queue { 'job' => 'FS::cust_main::Search::append_fuzzyfiles' };
+ $error = $queue->insert( map $self->getfield("ship_$_"), @FS::cust_main::Search::fuzzyfields );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "queueing job (transaction rolled back): $error";
|| $self->ut_textn('county')
|| $self->ut_textn('state')
|| $self->ut_country('country')
+ || $self->ut_coordn('latitude')
+ || $self->ut_coordn('longitude')
+ || $self->ut_enum('coord_auto', [ '', 'Y' ])
+ || $self->ut_numbern('censusyear')
|| $self->ut_anything('comments')
|| $self->ut_numbern('referral_custnum')
|| $self->ut_textn('stateid')
|| $self->ut_textn('stateid_state')
|| $self->ut_textn('invoice_terms')
|| $self->ut_alphan('geocode')
+ || $self->ut_alphan('district')
|| $self->ut_floatn('cdr_termination_percentage')
|| $self->ut_floatn('credit_limit')
|| $self->ut_numbern('billday')
|| $self->ut_enum('edit_subject', [ '', 'Y' ] )
+ || $self->ut_enum('calling_list_exempt', [ '', 'Y' ] )
+ || $self->ut_enum('locale', [ '', FS::Locales->locales ])
;
+ $self->set_coord
+ unless $import || ($self->latitude && $self->longitude);
+
#barf. need message catalogs. i18n. etc.
$error .= "Please select an advertising source."
if $error =~ /^Illegal or empty \(numeric\) refnum: /;
}
$error =
- $self->ut_phonen('daytime', $self->country)
- || $self->ut_phonen('night', $self->country)
- || $self->ut_phonen('fax', $self->country)
+ $self->ut_phonen('daytime', $self->country)
+ || $self->ut_phonen('night', $self->country)
+ || $self->ut_phonen('fax', $self->country)
+ || $self->ut_phonen('mobile', $self->country)
;
return $error if $error;
}
if ( $conf->exists('cust_main-require_phone')
- && ! length($self->daytime) && ! length($self->night)
+ && ! length($self->daytime) && ! length($self->night) && ! length($self->mobile)
) {
my $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/
my $night_label = FS::Msgcat::_gettext('night') =~ /^(night)?$/
? 'Night Phone'
: FS::Msgcat::_gettext('night');
-
- return "$daytime_label or $night_label is required"
+
+ my $mobile_label = FS::Msgcat::_gettext('mobile') =~ /^(mobile)?$/
+ ? 'Mobile Phone'
+ : FS::Msgcat::_gettext('mobile');
+
+ return "$daytime_label, $night_label or $mobile_label is required"
}
|| $self->ut_textn('ship_county')
|| $self->ut_textn('ship_state')
|| $self->ut_country('ship_country')
+ || $self->ut_coordn('ship_latitude')
+ || $self->ut_coordn('ship_longitude')
+ || $self->ut_enum('ship_coord_auto', [ '', 'Y' ] )
;
return $error if $error;
+ $self->set_coord('ship_')
+ unless $import || ($self->ship_latitude && $self->ship_longitude);
+
#false laziness with above
unless ( qsearchs('cust_main_county', {
'country' => $self->ship_country,
#eofalse
$error =
- $self->ut_phonen('ship_daytime', $self->ship_country)
- || $self->ut_phonen('ship_night', $self->ship_country)
- || $self->ut_phonen('ship_fax', $self->ship_country)
+ $self->ut_phonen('ship_daytime', $self->ship_country)
+ || $self->ut_phonen('ship_night', $self->ship_country)
+ || $self->ut_phonen('ship_fax', $self->ship_country)
+ || $self->ut_phonen('ship_mobile', $self->ship_country)
;
return $error if $error;
my $payinfo = $self->payinfo;
$payinfo =~ s/[^\d\@\.]//g;
- if ( $conf->exists('cust_main-require-bank-branch') ) {
- $payinfo =~ /^(\d+)\@(\d+)\.(\d+)$/ or return 'invalid echeck account@branch.bank';
+ if ( $conf->config('echeck-country') eq 'CA' ) {
+ $payinfo =~ /^(\d+)\@(\d{5})\.(\d{3})$/
+ or return 'invalid echeck account@branch.bank';
$payinfo = "$1\@$2.$3";
- }
- elsif ( $conf->exists('echeck-nonus') ) {
- $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba';
+ } elsif ( $conf->config('echeck-country') eq 'US' ) {
+ $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
$payinfo = "$1\@$2";
} else {
- $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
+ $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@routing';
$payinfo = "$1\@$2";
}
$self->payinfo($payinfo);
sub addr_fields {
qw( last first company
address1 address2 city county state zip country
- daytime night fax
+ latitude longitude
+ daytime night fax mobile
);
}
=item location_hash
-Returns a list of key/value pairs, with the following keys: address1, adddress2,
-city, county, state, zip, country, and geocode. The shipping address is used if present.
+Returns a list of key/value pairs, with the following keys: address1,
+adddress2, city, county, state, zip, country, district, and geocode. The
+shipping address is used if present.
=cut
'status' => 'O',
'payby' => FS::payby->payby2payment($payby),
);
+ $pay_batch{agentnum} = $self->agentnum if $conf->exists('batch-spoolagent');
my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
'custnum' => $self->custnum,
} );
- foreach (qw( address1 address2 city state zip country payby payinfo paydate
- payname )) {
+ foreach (qw( address1 address2 city state zip country latitude longitude
+ payby payinfo paydate payname ))
+ {
$options{$_} = '' unless exists($options{$_});
}
}
+=item legacy_cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
+
+Returns all the legacy invoices (see L<FS::legacy_cust_bill>) for this customer.
+
+=cut
+
+sub legacy_cust_bill {
+ my $self = shift;
+
+ #return $self->num_legacy_cust_bill unless wantarray;
+
+ map { $_ } #behavior of sort undefined in scalar context
+ sort { $a->_date <=> $b->_date }
+ qsearch({ 'table' => 'legacy_cust_bill',
+ 'hashref' => { 'custnum' => $self->custnum, },
+ 'order_by' => 'ORDER BY _date ASC',
+ });
+}
+
=item cust_statement [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
Returns all the statements (see L<FS::cust_statement>) for this customer.
qsearch($opt);
}
+=item svc_x SVCDB [ OPTION => VALUE | EXTRA_QSEARCH_PARAMS_HASHREF ]
+
+Returns all services of type SVCDB (such as 'svc_acct') for this customer.
+
+Optionally, a list or hashref of additional arguments to the qsearch call can
+be passed following the SVCDB.
+
+=cut
+
+sub svc_x {
+ my $self = shift;
+ my $svcdb = shift;
+ if ( ! $svcdb =~ /^svc_\w+$/ ) {
+ warn "$me svc_x requires a svcdb";
+ return;
+ }
+ my $opt = ref($_[0]) ? shift : { @_ };
+
+ $opt->{'table'} = $svcdb;
+ $opt->{'addl_from'} =
+ 'LEFT JOIN cust_svc USING (svcnum) LEFT JOIN cust_pkg USING (pkgnum) '.
+ ($opt->{'addl_from'} || '');
+
+ my $custnum = $self->custnum;
+ $custnum =~ /^\d+$/ or die "bad custnum '$custnum'";
+ my $where = "cust_pkg.custnum = $custnum";
+
+ my $extra_sql = $opt->{'extra_sql'} || '';
+ if ( keys %{ $opt->{'hashref'} } ) {
+ $extra_sql = " AND $where $extra_sql";
+ }
+ else {
+ if ( $opt->{'extra_sql'} =~ /^\s*where\s(.*)/si ) {
+ $extra_sql = "WHERE $where AND $1";
+ }
+ else {
+ $extra_sql = "WHERE $where $extra_sql";
+ }
+ }
+ $opt->{'extra_sql'} = $extra_sql;
+
+ qsearch($opt);
+}
+
+# required for use as an eventtable;
+sub svc_acct {
+ my $self = shift;
+ $self->svc_x('svc_acct', @_);
+}
+
=item cust_credit
Returns all the credits (see L<FS::cust_credit>) for this customer.
my $self = shift;
if ( $conf->exists('cust_main-default_agent_custid') && $self->agent_custid ){
return $self->agent_custid;
+ } elsif ( $conf->config('cust_main-custnum-display_prefix') ) {
+ return $conf->config('cust_main-custnum-display_prefix').
+ sprintf('%08d', $self->custnum)
} else {
return $self->custnum;
}
=over 4
-=item append_fuzzyfiles FIRSTNAME LASTNAME COMPANY ADDRESS1
-
-=cut
-
-use FS::cust_main::Search;
-sub append_fuzzyfiles {
- #my( $first, $last, $company ) = @_;
-
- FS::cust_main::Search::check_and_rebuild_fuzzyfiles();
-
- use Fcntl qw(:flock);
-
- my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
-
- foreach my $field (@fuzzyfields) {
- my $value = shift;
-
- if ( $value ) {
-
- open(CACHE,">>$dir/cust_main.$field")
- or die "can't open $dir/cust_main.$field: $!";
- flock(CACHE,LOCK_EX)
- or die "can't lock $dir/cust_main.$field: $!";
-
- print CACHE "$value\n";
-
- flock(CACHE,LOCK_UN)
- or die "can't unlock $dir/cust_main.$field: $!";
- close CACHE;
- }
-
- }
-
- 1;
-}
-
=item batch_charge
=cut
$cust_main->bill_and_collect( %$param );
}
+=item process_censustract_update CUSTNUM
+
+Queueable function to update the census tract to the current year (as set in
+the 'census_year' configuration variable) and retrieve the new tract code.
+
+=cut
+
+sub process_censustract_update {
+ eval "use FS::Misc::Geo qw(get_censustract)";
+ die $@ if $@;
+ my $custnum = shift;
+ my $cust_main = qsearchs( 'cust_main', { custnum => $custnum })
+ or die "custnum '$custnum' not found!\n";
+
+ my $new_year = $conf->config('census_year') or return;
+ my $new_tract = get_censustract({ $cust_main->location_hash }, $new_year);
+ if ( $new_tract =~ /^\d/ ) {
+ # then it's a tract code
+ $cust_main->set('censustract', $new_tract);
+ $cust_main->set('censusyear', $new_year);
+
+ local($import) = 1; #prevent automatic geocoding (need its own variable?)
+ my $error = $cust_main->replace;
+ die $error if $error;
+ }
+ else {
+ # it's an error message
+ die $new_tract;
+ }
+ return;
+}
+
sub _upgrade_data { #class method
my ($class, %opts) = @_;
"UPDATE cust_main SET paydate = SUBSTRING(paydate FROM 1 FOR 5) || '0' || SUBSTRING(paydate FROM 6) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
}
+ push @statements, #fix the weird BILL with a cc# in payinfo problem
+ #DCRD to be safe, or CARD?
+ "UPDATE cust_main SET payby = 'DCRD' WHERE payby = 'BILL' and length(payinfo) = 16";
+
foreach my $sql ( @statements ) {
my $sth = dbh->prepare($sql) or die dbh->errstr;
$sth->execute or die $sth->errstr;
local($ignore_illegal_zip) = 1;
local($ignore_banned_card) = 1;
local($skip_fuzzyfiles) = 1;
+ local($import) = 1; #prevent automatic geocoding (need its own variable?)
$class->_upgrade_otaker(%opts);
}