FS::cust_main::NationalID
FS::cust_main::Billing FS::cust_main::Billing_Realtime
FS::cust_main::Billing_Discount
+ FS::cust_main::Billing_ThirdParty
FS::cust_main::Location
FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin
- FS::geocode_Mixin FS::Quotable_Mixin
+ FS::geocode_Mixin FS::Quotable_Mixin FS::Sales_Mixin
FS::o2m_Common
FS::Record
);
@encrypted_fields
$import
$ignore_expired_card $ignore_banned_card $ignore_illegal_zip
+ $ignore_invalid_card
$skip_fuzzyfiles
@paytypes
);
use File::Temp; #qw( tempfile );
use Business::CreditCard 0.28;
use Locale::Country;
-use FS::UID qw( getotaker dbh driver_name );
+use FS::UID qw( dbh driver_name );
use FS::Record qw( qsearchs qsearch dbdef regexp_sql );
+use FS::Cursor;
use FS::Misc qw( generate_email send_email generate_ps do_print );
use FS::Msgcat qw(gettext);
use FS::CurrentUser;
use FS::cust_tax_adjustment;
use FS::cust_tax_location;
use FS::agent;
+use FS::agent_currency;
use FS::cust_main_invoice;
use FS::cust_tag;
use FS::prepay_credit;
use FS::contact;
use FS::Locales;
use FS::upgrade_journal;
+use FS::sales;
+use FS::cust_payby;
# 1 is mostly method/subroutine entry and options
# 2 traces progress of some operations
$import = 0;
$ignore_expired_card = 0;
$ignore_banned_card = 0;
+$ignore_invalid_card = 0;
$skip_fuzzyfiles = 0;
install_callback FS::UID sub {
$conf = new FS::Conf;
#yes, need it for stuff below (prolly should be cached)
+ $ignore_invalid_card = $conf->exists('allow_invalid_cards');
};
sub _cache {
$payby = 'PREP' if $amount;
- } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
+ } elsif ( $self->payby =~ /^(CASH|WEST|MCRD|PPAL)$/ ) {
$payby = $1;
$self->payby('BILL');
}
-=item reexport
-
-This method is deprecated. See the I<depend_jobnum> option to the insert and
-order_pkgs methods for a better way to defer provisioning.
-
-Re-schedules all exports by calling the B<reexport> method of all associated
-packages (see L<FS::cust_pkg>). If there is an error, returns the error;
-otherwise returns false.
-
-=cut
-
-sub reexport {
- my $self = shift;
-
- carp "WARNING: FS::cust_main::reexport is deprectated; ".
- "use the depend_jobnum option to insert or order_pkgs to delay export";
-
- 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;
-
- foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
- my $error = $cust_pkg->reexport;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
- }
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
- '';
-
-}
-
=item delete [ OPTION => VALUE ... ]
This deletes the customer. If there is an error, returns the error, otherwise
}
tie my %financial_tables, 'Tie::IxHash',
- 'cust_bill' => 'invoices',
- 'cust_bill_void' => 'voided invoices',
- 'cust_statement' => 'statements',
- 'cust_credit' => 'credits',
- 'cust_pay' => 'payments',
- 'cust_pay_void' => 'voided payments',
- 'cust_refund' => 'refunds',
+ 'cust_bill' => 'invoices',
+ 'cust_bill_void' => 'voided invoices',
+ 'cust_statement' => 'statements',
+ 'cust_credit' => 'credits',
+ 'cust_credit_void' => 'voided credits',
+ 'cust_pay' => 'payments',
+ 'cust_pay_void' => 'voided payments',
+ 'cust_refund' => 'refunds',
;
foreach my $table ( keys %financial_tables ) {
my $old_loc = $old->$l;
my $new_loc = $self->$l;
- if ( !$new_loc->locationnum ) {
- # changing location
- # If the new location is all empty fields, or if it's identical to
- # the old location in all fields, don't replace.
- my @nonempty = grep { $new_loc->$_ } $self->location_fields;
- next if !@nonempty;
- my @unlike = grep { $new_loc->$_ ne $old_loc->$_ } $self->location_fields;
-
- if ( @unlike or $old_loc->disabled ) {
- warn " changed $l fields: ".join(',',@unlike)."\n"
- if $DEBUG;
- $new_loc->set(custnum => $self->custnum);
-
- # insert it--the old location will be disabled later
- my $error = $new_loc->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
-
- } else {
- # no fields have changed and $old_loc isn't disabled, so don't change it
- next;
- }
-
- }
- elsif ( $new_loc->custnum ne $self->custnum or $new_loc->prospectnum ) {
+ # find the existing location if there is one
+ $new_loc->set('custnum' => $self->custnum);
+ my $error = $new_loc->find_or_insert;
+ if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "$l belongs to customer ".$new_loc->custnum;
+ return $error;
}
- # else the new location belongs to this customer so we're good
-
- # set the foo_locationnum now that we have one.
$self->set($l.'num', $new_loc->locationnum);
-
} #for $l
+ # replace the customer record
my $error = $self->SUPER::replace($old);
if ( $error ) {
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ foreach my $field ( 'first', 'last', 'company', 'ship_company' ) {
+ my $queue = new FS::queue {
+ 'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
+ };
+ my @args = "cust_main.$field", $self->get($field);
+ my $error = $queue->insert( @args );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "queueing job (transaction rolled back): $error";
+ }
+ }
+
my @locations = $self->bill_location;
push @locations, $self->ship_location if $self->has_ship_address;
foreach my $location (@locations) {
my $queue = new FS::queue {
- 'job' => 'FS::cust_main::Search::append_fuzzyfiles'
+ 'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
};
- my @args = map $location->get($_), @FS::cust_main::Search::fuzzyfields;
+ my @args = 'cust_location.address1', $location->address1;
my $error = $queue->insert( @args );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
|| $self->ut_foreign_key('bill_locationnum', 'cust_location','locationnum')
|| $self->ut_foreign_key('ship_locationnum', 'cust_location','locationnum')
|| $self->ut_foreign_keyn('classnum', 'cust_class', 'classnum')
+ || $self->ut_foreign_keyn('salesnum', 'sales', 'salesnum')
|| $self->ut_textn('custbatch')
|| $self->ut_name('last')
|| $self->ut_name('first')
|| $self->ut_snumbern('spouse_birthdate')
|| $self->ut_snumbern('anniversary_date')
|| $self->ut_textn('company')
+ || $self->ut_textn('ship_company')
|| $self->ut_anything('comments')
|| $self->ut_numbern('referral_custnum')
|| $self->ut_textn('stateid')
|| $self->ut_flag('invoice_noemail')
|| $self->ut_flag('message_noemail')
|| $self->ut_enum('locale', [ '', FS::Locales->locales ])
+ || $self->ut_currencyn('currency')
;
- my $company = $self->company;
- $company =~ s/^\s+//;
- $company =~ s/\s+$//;
- $company =~ s/\s+/ /g;
- $self->company($company);
+ foreach (qw(company ship_company)) {
+ my $company = $self->get($_);
+ $company =~ s/^\s+//;
+ $company =~ s/\s+$//;
+ $company =~ s/\s+/ /g;
+ $self->set($_, $company);
+ }
#barf. need message catalogs. i18n. etc.
$error .= "Please select an advertising source."
if $error =~ /^Illegal or empty \(numeric\) refnum: /;
return $error if $error;
- return "Unknown agent"
- unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
+ my $agent = qsearchs( 'agent', { 'agentnum' => $self->agentnum } )
+ or return "Unknown agent";
+
+ if ( $self->currency ) {
+ my $agent_currency = qsearchs( 'agent_currency', {
+ 'agentnum' => $agent->agentnum,
+ 'currency' => $self->currency,
+ })
+ or return "Agent ". $agent->agent.
+ " not permitted to offer ". $self->currency. " invoicing";
+ }
return "Unknown refnum"
unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
}
- #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
- # or return "Illegal payby: ". $self->payby;
- #$self->payby($1);
- FS::payby->can_payby($self->table, $self->payby)
- or return "Illegal payby: ". $self->payby;
+ ### start of stuff moved to cust_payby
+ # then mostly kept here to support upgrades (can remove in 5.x)
+ # but modified to allow everything to be empty
+
+ if ( $self->payby ) {
+ FS::payby->can_payby($self->table, $self->payby)
+ or return "Illegal payby: ". $self->payby;
+ } else {
+ $self->payby('');
+ }
$error = $self->ut_numbern('paystart_month')
|| $self->ut_numbern('paystart_year')
# Need some kind of global flag to accept invalid cards, for testing
# on scrubbed data.
- if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
+ if ( !$import && !$ignore_invalid_card && $check_payinfo &&
+ $self->payby =~ /^(CARD|DCRD)$/ ) {
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
$self->payissue('');
}
- } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
+ } elsif ( !$ignore_invalid_card && $check_payinfo &&
+ $self->payby =~ /^(CHEK|DCHK)$/ ) {
my $payinfo = $self->payinfo;
$payinfo =~ s/[^\d\@\.]//g;
if ( $self->paydate eq '' || $self->paydate eq '-' ) {
return "Expiration date required"
- unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
+ # shouldn't payinfo_check do this?
+ unless ! $self->payby
+ || $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD|PPAL)$/;
$self->paydate('');
} else {
my( $m, $y );
) {
$self->payname( $self->first. " ". $self->getfield('last') );
} else {
- $self->payname =~ /^([\w \,\.\-\'\&]+)$/
+ $self->payname =~ /^([\w \,\.\-\'\&]*)$/
or return gettext('illegal_name'). " payname: ". $self->payname;
$self->payname($1);
}
+ ### end of stuff moved to cust_payby
+
return "Please select an invoicing locale"
if ! $self->locale
&& ! $self->custnum
qsearch('contact', { 'custnum' => $self->custnum } );
}
+=item cust_payby
+
+Returns all payment methods (see L<FS::cust_payby>) for this customer.
+
+=cut
+
+sub cust_payby {
+ my $self = shift;
+ qsearch({
+ 'table' => 'cust_payby',
+ 'hashref' => { 'custnum' => $self->custnum },
+ 'order_by' => 'ORDER BY weight ASC',
+ });
+}
+
=item unsuspend
Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
FS::reason_type for the new reason.
An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
+Likewise for I<eventnum>, I<commission_agentnum>, I<commission_salesnum> and
+I<commission_pkgnum>.
Any other options are passed to FS::cust_credit::insert.
$cust_credit->set('reason', $reason)
}
- for (qw( addlinfo eventnum )) {
- $cust_credit->$_( delete $options{$_} )
- if exists($options{$_});
- }
+ $cust_credit->$_( delete $options{$_} )
+ foreach grep exists($options{$_}),
+ qw( addlinfo eventnum ),
+ map "commission_$_", qw( agentnum salesnum pkgnum );
$cust_credit->insert(%options);
);
}
+=item cust_credit_void
+
+Returns all voided credits (see L<FS::cust_credit_void>) for this customer.
+
+=cut
+
+sub cust_credit_void {
+ my $self = shift;
+ map { $_ }
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_credit_void', { 'custnum' => $self->custnum } )
+}
+
=item cust_pay
Returns all the payments (see L<FS::cust_pay>) for this customer.
# code2country($self->country);
#}
+sub bill_country_full {
+ my $self = shift;
+ code2country($self->bill_location->country);
+}
+
+sub ship_country_full {
+ my $self = shift;
+ code2country($self->ship_location->country);
+}
+
=item county_state_county [ PREFIX ]
Returns a string consisting of just the county, state and country.
__PACKAGE__->statuscolors->{$self->cust_status};
}
-=item tickets
+=item tickets [ STATUS ]
Returns an array of hashes representing the customer's RT tickets.
+An optional status (or arrayref or hashref of statuses) may be specified.
+
=cut
sub tickets {
my $self = shift;
+ my $status = ( @_ && $_[0] ) ? shift : '';
my $num = $conf->config('cust_main-max_tickets') || 10;
my @tickets = ();
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,
+ undef,
+ $status,
+ )
+ };
} else {
@{ FS::TicketSystem->customer_tickets( $self->custnum,
$num - scalar(@tickets),
$priority,
+ $status,
)
};
}
my %opt = @_;
my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
- or die "invalid customer number: " . $opt{custvnum};
+ or die "invalid customer number: " . $opt{custnum};
- my $error = $self->print( $opt{template} );
+ my $error = $self->print( { 'template' => $opt{template} } );
die $error if $error;
}
$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($ignore_expired_card) = 1;
- local($ignore_illegal_zip) = 1;
- local($ignore_banned_card) = 1;
- local($skip_fuzzyfiles) = 1;
- 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;
-}
-
#starting to take quite a while for big dbs
# (JRNL: journaled so it only happens once per database)
# - seq scan of h_cust_main (yuck), but not going to index paycvv, so
# JRNL seq scan of cust_main on paydate... index on substrings? maybe set an
# JRNL seq scan of cust_main on payinfo.. certainly not going toi ndex that...
# JRNL leading/trailing spaces in first, last, company
+# JRNL migrate to cust_payby
# - otaker upgrade? journal and call it good? (double check to make sure
# we're not still setting otaker here)
#
}
+ unless ( FS::upgrade_journal->is_done('cust_main__cust_payby') ) {
+
+ #we don't want to decrypt them, just stuff them as-is into cust_payby
+ local(@encrypted_fields) = ();
+
+ local($FS::cust_payby::ignore_expired_card) = 1;
+ local($FS::cust_payby::ignore_banned_card) = 1;
+
+ my @payfields = qw( payby payinfo paycvv paymask
+ paydate paystart_month paystart_year payissue
+ payname paystate paytype payip
+ );
+
+ my $search = new FS::Cursor {
+ 'table' => 'cust_main',
+ 'extra_sql' => " WHERE ( payby IS NOT NULL AND payby != '' ) ",
+ };
+
+ while (my $cust_main = $search->fetch) {
+
+ my $cust_payby = new FS::cust_payby {
+ 'custnum' => $cust_main->custnum,
+ 'weight' => 1,
+ map { $_ => $cust_main->$_(); } @payfields
+ };
+
+ my $error = $cust_payby->insert;
+ die $error if $error;
+
+ $cust_main->setfield($_, '') foreach @payfields;
+ $error = $cust_main->replace;
+ die $error if $error;
+
+ };
+
+ FS::upgrade_journal->set_done('cust_main__cust_payby');
+ }
+
$class->_upgrade_otaker(%opts);
}