use Time::Local qw(timelocal);
use Data::Dumper;
use Tie::IxHash;
-use Digest::MD5 qw(md5_base64);
use Date::Format;
#use Date::Manip;
use File::Temp; #qw( tempfile );
use FS::banned_pay;
use FS::cust_main_note;
use FS::cust_attachment;
-use FS::contact;
+use FS::cust_contact;
use FS::Locales;
use FS::upgrade_journal;
use FS::sales;
use FS::cust_payby;
+use FS::contact;
# 1 is mostly method/subroutine entry and options
# 2 traces progress of some operations
our $ucfirst_nowarn = 0;
+#this info is in cust_payby as of 4.x
+#this and the fields themselves can be removed in 5.x
our @encrypted_fields = ('payinfo', 'paycvv');
sub nohistory_fields { ('payinfo', 'paycvv'); }
-our @paytypes = ('', 'Personal checking', 'Personal savings', 'Business checking', 'Business savings');
-
our $conf;
#ask FS::UID to run this stuff for us later
#$FS::UID::callback{'FS::cust_main'} = sub {
Do not call, empty or 'Y'
+=item invoice_ship_address
+
+Display ship_address ("Service address") on invoices for this customer, empty or 'Y'
+
=back
=head1 METHODS
$cust_main->insert( {}, [ $email, 'POST' ] );
Currently available options are: I<depend_jobnum>, I<noexport>,
-I<tax_exemption> and I<prospectnum>.
+I<tax_exemption>, I<prospectnum>, I<contact> and I<contact_params>.
If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
on the supplied jobnum (they will not run until the specific job completes).
If I<contact> is set to an arrayref of FS::contact objects, inserts those
new contacts with this new customer.
+If I<contact_params> is set to a hashref of CGI parameters (and I<contact> is
+unset), inserts those new contacts with this new customer. Handles CGI
+paramaters for an "m2" multiple entry field as passed by edit/cust_main.cgi
+
+If I<cust_payby_params> is set to a hashref o fCGI parameters, inserts those
+new stored payment records with this new customer. Handles CGI parameters
+for an "m2" multiple entry field as passed by edit/cust_main.cgi
+
=cut
sub insert {
my $payby = '';
if ( $self->payby eq 'PREPAY' ) {
- $self->payby('BILL');
+ $self->payby(''); #'BILL');
$prepay_identifier = $self->payinfo;
$self->payinfo('');
} elsif ( $self->payby =~ /^(CASH|WEST|MCRD|MCHK|PPAL)$/ ) {
$payby = $1;
- $self->payby('BILL');
+ $self->payby(''); #'BILL');
$amount = $self->paid;
}
return $error;
}
- my @contact = $prospect_main->contact;
+ foreach my $prospect_contact ( $prospect_main->prospect_contact ) {
+ my $cust_contact = new FS::cust_contact {
+ 'custnum' => $self->custnum,
+ map { $_ => $prospect_contact->$_() } qw( contactnum classnum comment )
+ };
+ my $error = $cust_contact->insert
+ || $prospect_contact->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
my @cust_location = $prospect_main->cust_location;
my @qual = $prospect_main->qual;
- foreach my $r ( @contact, @cust_location, @qual ) {
+ foreach my $r ( @cust_location, @qual ) {
$r->prospectnum('');
$r->custnum($self->custnum);
my $error = $r->replace;
}
- my $contact = delete $options{'contact'};
- if ( $contact ) {
+ warn " setting contacts\n"
+ if $DEBUG > 1;
+
+ if ( my $contact = delete $options{'contact'} ) {
foreach my $c ( @$contact ) {
$c->custnum($self->custnum);
}
+ } elsif ( my $contact_params = delete $options{'contact_params'} ) {
+
+ my $error = $self->process_o2m( 'table' => 'contact',
+ 'fields' => FS::contact->cgi_contact_fields,
+ 'params' => $contact_params,
+ );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ warn " setting cust_payby\n"
+ if $DEBUG > 1;
+
+ if ( my $cust_payby_params = delete $options{'cust_payby_params'} ) {
+
+ my $error = $self->process_o2m(
+ 'table' => 'cust_payby',
+ 'fields' => FS::cust_payby->cgi_cust_payby_fields,
+ 'params' => $cust_payby_params,
+ 'hash_callback' => \&FS::cust_payby::cgi_hash_callback,
+ );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
}
warn " setting cust_main_exemption\n"
#cust_tax_adjustment in financials?
#cust_pay_pending? ouch
- #cust_recon?
foreach my $table (qw(
cust_main_invoice cust_main_exemption cust_tag cust_attachment contact
- cust_location cust_main_note cust_tax_adjustment
+ cust_payby cust_location cust_main_note cust_tax_adjustment
cust_pay_void cust_pay_batch queue cust_tax_exempt
)) {
foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
if $DEBUG;
my $curuser = $FS::CurrentUser::CurrentUser;
- if ( $self->payby eq 'COMP'
- && $self->payby ne $old->payby
- && ! $curuser->access_right('Complimentary customer')
- )
- {
- return "You are not permitted to create complimentary accounts.";
- }
+ return "You are not permitted to create complimentary accounts."
+ if $self->complimentary eq 'Y'
+ && $self->complimentary ne $old->complimentary
+ && ! $curuser->access_right('Complimentary customer');
local($ignore_expired_card) = 1
if $old->payby =~ /^(CARD|DCRD)$/
my $dbh = dbh;
for my $l (qw(bill_location ship_location)) {
- my $old_loc = $old->$l;
- my $new_loc = $self->$l;
+ #my $old_loc = $old->$l;
+ my $new_loc = $self->$l or next;
# find the existing location if there is one
$new_loc->set('custnum' => $self->custnum);
}
- if ( $self->payby =~ /^(CARD|CHEK|LECB)$/
- && ( ( $self->get('payinfo') ne $old->get('payinfo')
- && $self->get('payinfo') !~ /^99\d{14}$/
- )
- || grep { $self->get($_) ne $old->get($_) } qw(paydate payname)
- )
- )
- {
+ if ( my $cust_payby_params = delete $options{'cust_payby_params'} ) {
- # card/check/lec info has changed, want to retry realtime_ invoice events
- my $error = $self->retry_realtime;
+ my $error = $self->process_o2m(
+ 'table' => 'cust_payby',
+ 'fields' => FS::cust_payby->cgi_cust_payby_fields,
+ 'params' => $cust_payby_params,
+ 'hash_callback' => \&FS::cust_payby::cgi_hash_callback,
+ );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
}
+
}
unless ( $import || $skip_fuzzyfiles ) {
|| $self->ut_flag('message_noemail')
|| $self->ut_enum('locale', [ '', FS::Locales->locales ])
|| $self->ut_currencyn('currency')
+ || $self->ut_alphan('po_number')
+ || $self->ut_enum('complimentary', [ '', 'Y' ])
+ || $self->ut_flag('invoice_ship_address')
;
foreach (qw(company ship_company)) {
$self->ss("$1-$2-$3");
}
+ #turn off invoice_ship_address if ship & bill are the same
+ if ($self->bill_locationnum eq $self->ship_locationnum) {
+ $self->invoice_ship_address('');
+ }
+
# cust_main_county verification now handled by cust_location check
$error =
}
+ return "You are not permitted to create complimentary accounts."
+ if ! $self->custnum
+ && $self->complimentary eq 'Y'
+ && ! $FS::CurrentUser->CurrentUser->access_right('Complimentary customer');
+
if ( $self->paydate eq '' || $self->paydate eq '-' ) {
return "Expiration date required"
# shouldn't payinfo_check do this?
$self->SUPER::check;
}
+=item replace_check
+
+Additional checks for replace only.
+
+=cut
+
+sub replace_check {
+ my ($new,$old) = @_;
+ #preserve old value if global config is set
+ if ($old && $conf->exists('invoice-ship_address')) {
+ $new->invoice_ship_address($old->invoice_ship_address);
+ }
+ return '';
+}
+
=item addr_fields
Returns a list of fields which have ship_ duplicates.
=item cust_contact
-Returns all contacts (see L<FS::contact>) for this customer.
+Returns all contact associations (see L<FS::cust_contact>) for this customer.
=cut
-#already used :/ sub contact {
sub cust_contact {
my $self = shift;
- qsearch('contact', { 'custnum' => $self->custnum } );
+ qsearch('cust_contact', { 'custnum' => $self->custnum } );
}
=item cust_payby
qsearch({
'table' => 'cust_payby',
'hashref' => { 'custnum' => $self->custnum },
- 'order_by' => 'ORDER BY weight ASC',
+ 'order_by' => "ORDER BY payby IN ('CARD','CHEK') DESC, weight ASC",
});
}
return ( 'access denied' )
unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
- if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
+ if ( $opt{'ban'} ) {
+
+ foreach my $cust_payby ( $self->cust_payby ) {
- #should try decryption (we might have the private key)
- # and if not maybe queue a job for the server that does?
- return ( "Can't (yet) ban encrypted credit cards" )
- if $self->is_encrypted($self->payinfo);
+ #well, if they didn't get decrypted on search, then we don't have to
+ # try again... queue a job for the server that does have decryption
+ # capability if we're in a paranoid multi-server implementation?
+ return ( "Can't (yet) ban encrypted credit cards" )
+ if $cust_payby->is_encrypted($cust_payby->payinfo);
- my $ban = new FS::banned_pay $self->_new_banned_pay_hashref;
- my $error = $ban->insert;
- return ( $error ) if $error;
+ my $ban = new FS::banned_pay $cust_payby->_new_banned_pay_hashref;
+ my $error = $ban->insert;
+ return ( $error ) if $error;
+
+ }
}
};
}
-sub _new_banned_pay_hashref {
- my $self = shift;
- my $hr = $self->_banned_pay_hashref;
- $hr->{payinfo} = md5_base64($hr->{payinfo});
- $hr;
-}
-
=item notes
Returns all notes (see L<FS::cust_main_note>) for this customer.
sub notes {
my($self,$orderby_classnum) = (shift,shift);
- my $orderby = "_DATE DESC";
- $orderby = "CLASSNUM ASC, $orderby" if $orderby_classnum;
+ my $orderby = "sticky DESC, _date DESC";
+ $orderby = "classnum ASC, $orderby" if $orderby_classnum;
qsearch( 'cust_main_note',
{ 'custnum' => $self->custnum },
'',
my ( $setuptax, $taxclass ); #internal taxes
my ( $taxproduct, $override ); #vendor (CCH) taxes
my $no_auto = '';
+ my $separate_bill = '';
my $cust_pkg_ref = '';
my ( $bill_now, $invoice_terms ) = ( 0, '' );
my $locationnum;
$bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
$invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
$locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
- } else {
+ $separate_bill = $_[0]->{separate_bill} || '';
+ } else { # yuck
$amount = shift;
$setup_cost = '';
$quantity = 1;
'quantity' => $quantity,
'start_date' => $start_date,
'no_auto' => $no_auto,
+ 'separate_bill' => $separate_bill,
'locationnum'=> $locationnum,
} );
my $classnum = $self->scalar_sql(
'SELECT classnum FROM contact_class WHERE classname = \'Service\''
) || 0; #if it's zero, qsearchs will return nothing
- $self->{service_contact} = qsearchs('contact', {
- 'classnum' => $classnum, 'custnum' => $self->custnum
- }) || undef;
+ my $cust_contact = qsearchs('cust_contact', {
+ 'classnum' => $classnum,
+ 'custnum' => $self->custnum,
+ });
+ $self->{service_contact} = $cust_contact->contact if $cust_contact;
}
$self->{service_contact};
}
}
}
+=item is_status_delay_cancel
+
+Returns true if customer status is 'suspended'
+and all suspended cust_pkg return true for
+cust_pkg->is_status_delay_cancel.
+
+This is not a real status, this only meant for hacking display
+values, because otherwise treating the customer as suspended is
+really the whole point of the delay_cancel option.
+
+=cut
+
+sub is_status_delay_cancel {
+ my ($self) = @_;
+ return 0 unless $self->status eq 'suspended';
+ foreach my $cust_pkg ($self->ncancelled_pkgs) {
+ return 0 unless $cust_pkg->is_status_delay_cancel;
+ }
+ return 1;
+}
+
=item ucfirst_cust_status
=item ucfirst_status
return unless $conf->exists($template);
- my $from = $conf->config('invoice_from_name', $self->agentnum) ?
- $conf->config('invoice_from_name', $self->agentnum) . ' <' .
- $conf->config('invoice_from', $self->agentnum) . '>' :
- $conf->config('invoice_from', $self->agentnum)
+ my $from = $conf->invoice_from_full($self->agentnum)
if $conf->exists('invoice_from', $self->agentnum);
$from = $options{from} if exists($options{from});
}
+sub process_o2m_qsearch {
+ my $self = shift;
+ my $table = shift;
+ return qsearch($table, @_) unless $table eq 'contact';
+
+ my $hashref = shift;
+ my %hash = %$hashref;
+ ( my $custnum = delete $hash{'custnum'} ) =~ /^(\d+)$/
+ or die 'guru meditation #4343';
+
+ qsearch({ 'table' => 'contact',
+ 'addl_from' => 'LEFT JOIN cust_contact USING ( contactnum )',
+ 'hashref' => \%hash,
+ 'extra_sql' => ( keys %hash ? ' AND ' : ' WHERE ' ).
+ " cust_contact.custnum = $custnum "
+ });
+}
+
+sub process_o2m_qsearchs {
+ my $self = shift;
+ my $table = shift;
+ return qsearchs($table, @_) unless $table eq 'contact';
+
+ my $hashref = shift;
+ my %hash = %$hashref;
+ ( my $custnum = delete $hash{'custnum'} ) =~ /^(\d+)$/
+ or die 'guru meditation #2121';
+
+ qsearchs({ 'table' => 'contact',
+ 'addl_from' => 'LEFT JOIN cust_contact USING ( contactnum )',
+ 'hashref' => \%hash,
+ 'extra_sql' => ( keys %hash ? ' AND ' : ' WHERE ' ).
+ " cust_contact.custnum = $custnum "
+ });
+}
+
=item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
Subroutine (not a method), designed to be called from the queue.
while (my $cust_main = $search->fetch) {
- my $cust_payby = new FS::cust_payby {
- 'custnum' => $cust_main->custnum,
- 'weight' => 1,
- map { $_ => $cust_main->$_(); } @payfields
- };
+ unless ( $cust_main->payby =~ /^(BILL|COMP)$/ ) {
- my $error = $cust_payby->insert;
- die $error if $error;
+ 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->complimentary('Y') if $cust_main->payby eq 'COMP';
+
+ $cust_main->invoice_attn( $cust_main->payname )
+ if $cust_main->payby eq 'BILL' && $cust_main->payname;
+ $cust_main->po_number( $cust_main->payinfo )
+ if $cust_main->payby eq 'BILL' && $cust_main->payinfo;
$cust_main->setfield($_, '') foreach @payfields;
- $error = $cust_main->replace;
- die $error if $error;
+ my $error = $cust_main->replace;
+ die "Error upgradging payment information for custnum ".
+ $cust_main->custnum. ": $error"
+ if $error;
};