package FS::cust_main;
-
-require 5.006;
-use strict;
-use base qw( FS::cust_main::Packages FS::cust_main::Status
+use base qw( FS::cust_main::Packages
+ FS::cust_main::Status
FS::cust_main::NationalID
- FS::cust_main::Billing FS::cust_main::Billing_Realtime
+ FS::cust_main::Billing
+ FS::cust_main::Billing_Realtime
FS::cust_main::Billing_Discount
+ FS::cust_main::Billing_ThirdParty
FS::cust_main::Location
+ FS::cust_main::Credit_Limit
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
);
+
+require 5.006;
+use strict;
use vars qw( $DEBUG $me $conf
@encrypted_fields
$import
$ignore_expired_card $ignore_banned_card $ignore_illegal_zip
+ $ignore_invalid_card
$skip_fuzzyfiles
@paytypes
);
use Locale::Country;
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_main_exemption;
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 {
If I<prospectnum> is set, moves contacts and locations from that prospect.
+If I<contact> is set to an arrayref of FS::contact objects, inserts those
+new contacts with this new customer.
+
=cut
sub insert {
my $loc = delete $self->hashref->{$l};
# XXX if we're moving a prospect's locations, do that here
if ( !$loc ) {
- return "$l not set";
+ #return "$l not set";
+ #location-less customer records are now permitted
+ next;
}
if ( !$loc->locationnum ) {
foreach my $l (qw(bill_location ship_location)) {
warn " setting $l.custnum\n"
if $DEBUG > 1;
- my $loc = $self->$l;
+ my $loc = $self->$l or next;
unless ( $loc->custnum ) {
$loc->set(custnum => $self->custnum);
$error ||= $loc->replace;
}
+ my $contact = delete $options{'contact'};
+ if ( $contact ) {
+
+ foreach my $c ( @$contact ) {
+ $c->custnum($self->custnum);
+ my $error = $c->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
+ }
+
warn " setting cust_main_exemption\n"
if $DEBUG > 1;
}
-=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 ) {
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- my @locations = $self->bill_location;
- push @locations, $self->ship_location if $self->has_ship_address;
+ 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 = ();
+ push @locations, $self->bill_location if $self->bill_locationnum;
+ push @locations, $self->ship_location if @locations && $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_number('agentnum')
|| $self->ut_textn('agent_custid')
|| $self->ut_number('refnum')
- || $self->ut_foreign_key('bill_locationnum', 'cust_location','locationnum')
- || $self->ut_foreign_key('ship_locationnum', 'cust_location','locationnum')
+ || $self->ut_foreign_keyn('bill_locationnum', 'cust_location','locationnum')
+ || $self->ut_foreign_keyn('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('signupdate')
|| $self->ut_snumbern('birthdate')
+ || $self->ut_namen('spouse_last')
+ || $self->ut_namen('spouse_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"
# shouldn't payinfo_check do this?
- unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD|PPAL)$/;
+ 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 \,\.\-\'\&]+)$/
- or return gettext('illegal_name'). " payname: ". $self->payname;
- $self->payname($1);
+
+ if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
+ $self->payname =~ /^([\w \,\.\-\']*)$/
+ or return gettext('illegal_name'). " payname: ". $self->payname;
+ $self->payname($1);
+ } else {
+ $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
sub cust_location {
my $self = shift;
- qsearch('cust_location', { 'custnum' => $self->custnum,
+ qsearch('cust_location', { 'custnum' => $self->custnum,
'prospectnum' => '' } );
}
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>
Returns the agent (see L<FS::agent>) for this customer.
-=cut
-
-sub agent {
- my $self = shift;
- qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
-}
-
=item agent_name
Returns the agent name (see L<FS::agent>) for this customer.
Returns any tags associated with this customer, as FS::cust_tag objects,
or an empty list if there are no tags.
-=cut
-
-sub cust_tag {
- my $self = shift;
- qsearch('cust_tag', { 'custnum' => $self->custnum } );
-}
-
=item part_tag
Returns any tags associated with this customer, as FS::part_tag objects,
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
=item cust_main_exemption
-=cut
-
-sub cust_main_exemption {
- my $self = shift;
- qsearch( 'cust_main_exemption', { 'custnum' => $self->custnum } );
-}
-
=item invoicing_list [ ARRAYREF ]
If an arguement is given, sets these email addresses as invoice recipients
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,
)
};
}
=over 4
-=item batch_charge
-
-=cut
-
-sub batch_charge {
- my $param = shift;
- #warn join('-',keys %$param);
- my $fh = $param->{filehandle};
- my $agentnum = $param->{agentnum};
- my $format = $param->{format};
-
- my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
-
- my @fields;
- if ( $format eq 'simple' ) {
- @fields = qw( custnum agent_custid amount pkg );
- } else {
- die "unknown format $format";
- }
-
- eval "use Text::CSV_XS;";
- die $@ if $@;
-
- my $csv = new Text::CSV_XS;
- #warn $csv;
- #warn $fh;
-
- my $imported = 0;
- #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;
-
- #while ( $columns = $csv->getline($fh) ) {
- my $line;
- while ( defined($line=<$fh>) ) {
-
- $csv->parse($line) or do {
- $dbh->rollback if $oldAutoCommit;
- return "can't parse: ". $csv->error_input();
- };
-
- my @columns = $csv->fields();
- #warn join('-',@columns);
-
- my %row = ();
- foreach my $field ( @fields ) {
- $row{$field} = shift @columns;
- }
-
- if ( $row{custnum} && $row{agent_custid} ) {
- dbh->rollback if $oldAutoCommit;
- return "can't specify custnum with agent_custid $row{agent_custid}";
- }
-
- my %hash = ();
- if ( $row{agent_custid} && $agentnum ) {
- %hash = ( 'agent_custid' => $row{agent_custid},
- 'agentnum' => $agentnum,
- );
- }
-
- if ( $row{custnum} ) {
- %hash = ( 'custnum' => $row{custnum} );
- }
-
- unless ( scalar(keys %hash) ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't find customer without custnum or agent_custid and agentnum";
- }
-
- my $cust_main = qsearchs('cust_main', { %hash } );
- unless ( $cust_main ) {
- $dbh->rollback if $oldAutoCommit;
- my $custnum = $row{custnum} || $row{agent_custid};
- return "unknown custnum $custnum";
- }
-
- if ( $row{'amount'} > 0 ) {
- my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
- $imported++;
- } elsif ( $row{'amount'} < 0 ) {
- my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
- $row{'pkg'} );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
- $imported++;
- } else {
- #hmm?
- }
-
- }
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
-
- return "Empty file!" unless $imported;
-
- ''; #no error
-
-}
-
=item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
Deprecated. Use event notification and message templates
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;
}
my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
warn 'bill_and_collect custnum#'. $cust_main->custnum. "\n";#log custnum w/pid
+ #without this errors don't get rolled back
+ $args{'fatal'} = 1; # runs from job queue, will be caught
+
$cust_main->bill_and_collect( %args );
}
# 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);
}