2 use base qw( FS::cust_main::Packages
4 FS::cust_main::NationalID
6 FS::cust_main::Billing_Realtime
7 FS::cust_main::Billing_Batch
8 FS::cust_main::Billing_Discount
9 FS::cust_main::Billing_ThirdParty
10 FS::cust_main::Location
11 FS::cust_main::Credit_Limit
14 FS::otaker_Mixin FS::cust_main_Mixin
15 FS::geocode_Mixin FS::Quotable_Mixin FS::Sales_Mixin
24 use Scalar::Util qw( blessed );
25 use List::Util qw(min);
27 use File::Temp; #qw( tempfile );
29 use Time::Local qw(timelocal);
33 use Business::CreditCard 0.28;
34 use FS::UID qw( dbh driver_name );
35 use FS::Record qw( qsearchs qsearch dbdef regexp_sql );
37 use FS::Misc qw( generate_ps do_print money_pretty card_types );
38 use FS::Msgcat qw(gettext);
45 use FS::cust_bill_void;
46 use FS::legacy_cust_bill;
48 use FS::cust_pay_pending;
49 use FS::cust_pay_void;
50 use FS::cust_pay_batch;
53 use FS::part_referral;
54 use FS::cust_main_county;
55 use FS::cust_location;
58 use FS::cust_main_exemption;
59 use FS::cust_tax_adjustment;
60 use FS::cust_tax_location;
61 use FS::agent_currency;
62 use FS::cust_main_invoice;
64 use FS::prepay_credit;
70 use FS::payment_gateway;
71 use FS::agent_payment_gateway;
73 use FS::cust_main_note;
74 use FS::cust_attachment;
77 use FS::upgrade_journal;
82 use FS::Misc::Savepoint;
84 # 1 is mostly method/subroutine entry and options
85 # 2 traces progress of some operations
86 # 3 is even more information including possibly sensitive data
88 our $me = '[FS::cust_main]';
91 our $ignore_expired_card = 0;
92 our $ignore_banned_card = 0;
93 our $ignore_invalid_card = 0;
95 our $skip_fuzzyfiles = 0;
97 our $ucfirst_nowarn = 0;
99 #this info is in cust_payby as of 4.x
100 #this and the fields themselves can be removed in 5.x
101 our @encrypted_fields = ('payinfo', 'paycvv');
102 sub nohistory_fields { ('payinfo', 'paycvv'); }
105 our $default_agent_custid;
106 our $custnum_display_length;
107 #ask FS::UID to run this stuff for us later
108 #$FS::UID::callback{'FS::cust_main'} = sub {
109 install_callback FS::UID sub {
110 $conf = new FS::Conf;
111 $ignore_invalid_card = $conf->exists('allow_invalid_cards');
112 $default_agent_custid = $conf->exists('cust_main-default_agent_custid');
113 $custnum_display_length = $conf->config('cust_main-custnum-display_length');
118 my ( $hashref, $cache ) = @_;
119 if ( exists $hashref->{'pkgnum'} ) {
120 #@{ $self->{'_pkgnum'} } = ();
121 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
122 $self->{'_pkgnum'} = $subcache;
123 #push @{ $self->{'_pkgnum'} },
124 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
130 FS::cust_main - Object methods for cust_main records
136 $record = new FS::cust_main \%hash;
137 $record = new FS::cust_main { 'column' => 'value' };
139 $error = $record->insert;
141 $error = $new_record->replace($old_record);
143 $error = $record->delete;
145 $error = $record->check;
147 @cust_pkg = $record->all_pkgs;
149 @cust_pkg = $record->ncancelled_pkgs;
151 @cust_pkg = $record->suspended_pkgs;
153 $error = $record->bill;
154 $error = $record->bill %options;
155 $error = $record->bill 'time' => $time;
157 $error = $record->collect;
158 $error = $record->collect %options;
159 $error = $record->collect 'invoice_time' => $time,
164 An FS::cust_main object represents a customer. FS::cust_main inherits from
165 FS::Record. The following fields are currently supported:
171 Primary key (assigned automatically for new customers)
175 Agent (see L<FS::agent>)
179 Advertising source (see L<FS::part_referral>)
191 Cocial security number (optional)
215 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
219 Payment Information (See L<FS::payinfo_Mixin> for data format)
223 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
227 Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
231 Expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
235 Start date month (maestro/solo cards only)
239 Start date year (maestro/solo cards only)
243 Issue number (maestro/solo cards only)
247 Name on card or billing name
251 IP address from which payment information was received
255 Tax exempt, empty or `Y'
259 Order taker (see L<FS::access_user>)
265 =item referral_custnum
267 Referring customer number
271 Enable individual CDR spooling, empty or `Y'
275 A suggestion to events (see L<FS::part_bill_event">) to delay until this unix timestamp
279 Discourage individual CDR printing, empty or `Y'
283 Allow self-service editing of ticket subjects, empty or 'Y'
285 =item calling_list_exempt
287 Do not call, empty or 'Y'
289 =item invoice_ship_address
291 Display ship_address ("Service address") on invoices for this customer, empty or 'Y'
301 Creates a new customer. To add the customer to the database, see L<"insert">.
303 Note that this stores the hash reference, not a distinct copy of the hash it
304 points to. You can ask the object for a copy with the I<hash> method.
308 sub table { 'cust_main'; }
310 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
312 Adds this customer to the database. If there is an error, returns the error,
313 otherwise returns false.
315 Usually the customer's location will not yet exist in the database, and
316 the C<bill_location> and C<ship_location> pseudo-fields must be set to
317 uninserted L<FS::cust_location> objects. These will be inserted and linked
318 (in both directions) to the new customer record. If they're references
319 to the same object, they will become the same location.
321 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
322 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
323 are inserted atomicly, or the transaction is rolled back. Passing an empty
324 hash reference is equivalent to not supplying this parameter. There should be
325 a better explanation of this, but until then, here's an example:
328 tie %hash, 'Tie::RefHash'; #this part is important
330 $cust_pkg => [ $svc_acct ],
333 $cust_main->insert( \%hash );
335 INVOICING_LIST_ARYREF: No longer supported.
337 Currently available options are: I<depend_jobnum>, I<noexport>,
338 I<tax_exemption>, I<prospectnum>, I<contact> and I<contact_params>.
340 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
341 on the supplied jobnum (they will not run until the specific job completes).
342 This can be used to defer provisioning until some action completes (such
343 as running the customer's credit card successfully).
345 The I<noexport> option is deprecated. If I<noexport> is set true, no
346 provisioning jobs (exports) are scheduled. (You can schedule them later with
347 the B<reexport> method.)
349 The I<tax_exemption> option can be set to an arrayref of tax names or a hashref
350 of tax names and exemption numbers. FS::cust_main_exemption records will be
351 created and inserted.
353 If I<prospectnum> is set, moves contacts and locations from that prospect.
355 If I<contact> is set to an arrayref of FS::contact objects, those will be
358 If I<contact_params> is set to a hashref of CGI parameters (and I<contact> is
359 unset), inserts those new contacts with this new customer. Handles CGI
360 paramaters for an "m2" multiple entry field as passed by edit/cust_main.cgi
362 If I<cust_payby_params> is set to a hashref o fCGI parameters, inserts those
363 new stored payment records with this new customer. Handles CGI parameters
364 for an "m2" multiple entry field as passed by edit/cust_main.cgi
370 my $cust_pkgs = @_ ? shift : {};
372 if ( $_[0] and ref($_[0]) eq 'ARRAY' ) {
373 warn "cust_main::insert using deprecated invoicing list argument";
374 $invoicing_list = shift;
377 warn "$me insert called with options ".
378 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
381 return "You are not permitted to change customer invoicing terms."
382 if $self->invoice_terms #i.e. not the default
383 && ! $FS::CurrentUser::CurrentUser->access_right('Edit customer invoice terms');
385 local $SIG{HUP} = 'IGNORE';
386 local $SIG{INT} = 'IGNORE';
387 local $SIG{QUIT} = 'IGNORE';
388 local $SIG{TERM} = 'IGNORE';
389 local $SIG{TSTP} = 'IGNORE';
390 local $SIG{PIPE} = 'IGNORE';
392 my $oldAutoCommit = $FS::UID::AutoCommit;
393 local $FS::UID::AutoCommit = 0;
396 my $prepay_identifier = '';
397 my( $amount, $seconds, $upbytes, $downbytes, $totalbytes ) = (0, 0, 0, 0, 0);
399 if ( $self->payby eq 'PREPAY' ) {
401 $self->payby(''); #'BILL');
402 $prepay_identifier = $self->payinfo;
405 warn " looking up prepaid card $prepay_identifier\n"
408 my $error = $self->get_prepay( $prepay_identifier,
409 'amount_ref' => \$amount,
410 'seconds_ref' => \$seconds,
411 'upbytes_ref' => \$upbytes,
412 'downbytes_ref' => \$downbytes,
413 'totalbytes_ref' => \$totalbytes,
416 $dbh->rollback if $oldAutoCommit;
417 #return "error applying prepaid card (transaction rolled back): $error";
421 $payby = 'PREP' if $amount;
423 } elsif ( $self->payby =~ /^(CASH|WEST|MCRD|MCHK|PPAL)$/ ) {
426 $self->payby(''); #'BILL');
427 $amount = $self->paid;
432 foreach my $l (qw(bill_location ship_location)) {
434 my $loc = delete $self->hashref->{$l} or next;
436 if ( !$loc->locationnum ) {
437 # warn the location that we're going to insert it with no custnum
438 $loc->set(custnum_pending => 1);
439 warn " inserting $l\n"
441 my $error = $loc->insert;
443 $dbh->rollback if $oldAutoCommit;
444 my $label = $l eq 'ship_location' ? 'service' : 'billing';
445 return "$error (in $label location)";
448 } elsif ( $loc->prospectnum ) {
450 $loc->prospectnum('');
451 $loc->set(custnum_pending => 1);
452 my $error = $loc->replace;
454 $dbh->rollback if $oldAutoCommit;
455 my $label = $l eq 'ship_location' ? 'service' : 'billing';
456 return "$error (moving $label location)";
459 } elsif ( ($loc->custnum || 0) > 0 ) {
460 # then it somehow belongs to another customer--shouldn't happen
461 $dbh->rollback if $oldAutoCommit;
462 return "$l belongs to customer ".$loc->custnum;
464 # else it already belongs to this customer
465 # (happens when ship_location is identical to bill_location)
467 $self->set($l.'num', $loc->locationnum);
469 if ( $self->get($l.'num') eq '' ) {
470 $dbh->rollback if $oldAutoCommit;
475 warn " inserting $self\n"
478 $self->signupdate(time) unless $self->signupdate;
480 $self->auto_agent_custid()
481 if $conf->config('cust_main-auto_agent_custid') && ! $self->agent_custid;
483 my $error = $self->check_payinfo_cardtype
484 || $self->SUPER::insert;
486 $dbh->rollback if $oldAutoCommit;
487 #return "inserting cust_main record (transaction rolled back): $error";
491 # now set cust_location.custnum
492 foreach my $l (qw(bill_location ship_location)) {
493 warn " setting $l.custnum\n"
495 my $loc = $self->$l or next;
496 unless ( $loc->custnum ) {
497 $loc->set(custnum => $self->custnum);
498 $error ||= $loc->replace;
502 $dbh->rollback if $oldAutoCommit;
503 return "error setting $l custnum: $error";
507 warn " setting customer tags\n"
510 foreach my $tagnum ( @{ $self->tagnum || [] } ) {
511 my $cust_tag = new FS::cust_tag { 'tagnum' => $tagnum,
512 'custnum' => $self->custnum };
513 my $error = $cust_tag->insert;
515 $dbh->rollback if $oldAutoCommit;
520 my $prospectnum = delete $options{'prospectnum'};
521 if ( $prospectnum ) {
523 warn " moving contacts and locations from prospect $prospectnum\n"
527 qsearchs('prospect_main', { 'prospectnum' => $prospectnum } );
528 unless ( $prospect_main ) {
529 $dbh->rollback if $oldAutoCommit;
530 return "Unknown prospectnum $prospectnum";
532 $prospect_main->custnum($self->custnum);
533 $prospect_main->disabled('Y');
534 my $error = $prospect_main->replace;
536 $dbh->rollback if $oldAutoCommit;
540 foreach my $prospect_contact ( $prospect_main->prospect_contact ) {
541 my $cust_contact = new FS::cust_contact {
542 'custnum' => $self->custnum,
543 'invoice_dest' => 'Y', # invoice_dest currently not set for prospect contacts
544 map { $_ => $prospect_contact->$_() } qw( contactnum classnum comment )
546 my $error = $cust_contact->insert
547 || $prospect_contact->delete;
549 $dbh->rollback if $oldAutoCommit;
554 my @cust_location = $prospect_main->cust_location;
555 my @qual = $prospect_main->qual;
557 foreach my $r ( @cust_location, @qual ) {
559 $r->custnum($self->custnum);
560 my $error = $r->replace;
562 $dbh->rollback if $oldAutoCommit;
566 # since we set invoice_dest on all migrated prospect contacts (for now),
567 # don't process invoicing_list.
568 delete $options{'invoicing_list'};
569 $invoicing_list = undef;
572 warn " setting contacts\n"
575 $invoicing_list ||= $options{'invoicing_list'};
576 if ( $invoicing_list ) {
578 $invoicing_list = [ $invoicing_list ] if !ref($invoicing_list);
581 foreach my $dest (@$invoicing_list ) {
582 if ($dest eq 'POST') {
583 $self->set('postal_invoice', 'Y');
586 my $contact_email = qsearchs('contact_email', { emailaddress => $dest });
587 if ( $contact_email ) {
588 my $cust_contact = FS::cust_contact->new({
589 contactnum => $contact_email->contactnum,
590 custnum => $self->custnum,
592 $cust_contact->set('invoice_dest', 'Y');
593 my $error = $cust_contact->insert;
595 $dbh->rollback if $oldAutoCommit;
596 return "$error (linking to email address $dest)";
600 # this email address is not yet linked to any contact
601 $email .= ',' if length($email);
609 my $contact = FS::contact->new({
610 'custnum' => $self->get('custnum'),
611 'last' => $self->get('last'),
612 'first' => $self->get('first'),
613 'emailaddress' => $email,
614 'invoice_dest' => 'Y', # yes, you can set this via the contact
616 my $error = $contact->insert;
618 $dbh->rollback if $oldAutoCommit;
626 if ( my $contact = delete $options{'contact'} ) {
628 foreach my $c ( @$contact ) {
629 $c->custnum($self->custnum);
630 my $error = $c->insert;
632 $dbh->rollback if $oldAutoCommit;
638 } elsif ( my $contact_params = delete $options{'contact_params'} ) {
640 my $error = $self->process_o2m( 'table' => 'contact',
641 'fields' => FS::contact->cgi_contact_fields,
642 'params' => $contact_params,
645 $dbh->rollback if $oldAutoCommit;
650 warn " setting cust_payby\n"
653 if ( $options{cust_payby} ) {
655 foreach my $cust_payby ( @{ $options{cust_payby} } ) {
656 $cust_payby->custnum($self->custnum);
657 my $error = $cust_payby->insert;
659 $dbh->rollback if $oldAutoCommit;
664 } elsif ( my $cust_payby_params = delete $options{'cust_payby_params'} ) {
666 my $error = $self->process_o2m(
667 'table' => 'cust_payby',
668 'fields' => FS::cust_payby->cgi_cust_payby_fields,
669 'params' => $cust_payby_params,
670 'hash_callback' => \&FS::cust_payby::cgi_hash_callback,
673 $dbh->rollback if $oldAutoCommit;
679 warn " setting cust_main_exemption\n"
682 my $tax_exemption = delete $options{'tax_exemption'};
683 if ( $tax_exemption ) {
685 $tax_exemption = { map { $_ => '' } @$tax_exemption }
686 if ref($tax_exemption) eq 'ARRAY';
688 foreach my $taxname ( keys %$tax_exemption ) {
689 my $cust_main_exemption = new FS::cust_main_exemption {
690 'custnum' => $self->custnum,
691 'taxname' => $taxname,
692 'exempt_number' => $tax_exemption->{$taxname},
694 my $error = $cust_main_exemption->insert;
696 $dbh->rollback if $oldAutoCommit;
697 return "inserting cust_main_exemption (transaction rolled back): $error";
702 warn " ordering packages\n"
705 $error = $self->order_pkgs( $cust_pkgs,
707 'seconds_ref' => \$seconds,
708 'upbytes_ref' => \$upbytes,
709 'downbytes_ref' => \$downbytes,
710 'totalbytes_ref' => \$totalbytes,
713 $dbh->rollback if $oldAutoCommit;
718 $dbh->rollback if $oldAutoCommit;
719 return "No svc_acct record to apply pre-paid time";
721 if ( $upbytes || $downbytes || $totalbytes ) {
722 $dbh->rollback if $oldAutoCommit;
723 return "No svc_acct record to apply pre-paid data";
727 warn " inserting initial $payby payment of $amount\n"
729 $error = $self->insert_cust_pay($payby, $amount, $prepay_identifier);
731 $dbh->rollback if $oldAutoCommit;
732 return "inserting payment (transaction rolled back): $error";
736 unless ( $import || $skip_fuzzyfiles ) {
737 warn " queueing fuzzyfiles update\n"
739 $error = $self->queue_fuzzyfiles_update;
741 $dbh->rollback if $oldAutoCommit;
742 return "updating fuzzy search cache: $error";
747 warn " exporting\n" if $DEBUG > 1;
749 my $export_args = $options{'export_args'} || [];
752 map qsearch( 'part_export', {exportnum=>$_} ),
753 $conf->config('cust_main-exports'); #, $agentnum
755 foreach my $part_export ( @part_export ) {
756 my $error = $part_export->export_insert($self, @$export_args);
758 $dbh->rollback if $oldAutoCommit;
759 return "exporting to ". $part_export->exporttype.
760 " (transaction rolled back): $error";
764 #foreach my $depend_jobnum ( @$depend_jobnums ) {
765 # warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
767 # foreach my $jobnum ( @jobnums ) {
768 # my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
769 # warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
771 # my $error = $queue->depend_insert($depend_jobnum);
773 # $dbh->rollback if $oldAutoCommit;
774 # return "error queuing job dependancy: $error";
781 #if ( exists $options{'jobnums'} ) {
782 # push @{ $options{'jobnums'} }, @jobnums;
785 warn " insert complete; committing transaction\n"
788 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
793 use File::CounterFile;
794 sub auto_agent_custid {
797 my $format = $conf->config('cust_main-auto_agent_custid');
799 if ( $format eq '1YMMXXXXXXXX' ) {
801 my $counter = new File::CounterFile 'cust_main.agent_custid';
804 my $ym = 100000000000 + time2str('%y%m00000000', time);
805 if ( $ym > $counter->value ) {
806 $counter->{'value'} = $agent_custid = $ym;
807 $counter->{'updated'} = 1;
809 $agent_custid = $counter->inc;
815 die "Unknown cust_main-auto_agent_custid format: $format";
818 $self->agent_custid($agent_custid);
822 =item PACKAGE METHODS
824 Documentation on customer package methods has been moved to
825 L<FS::cust_main::Packages>.
827 =item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF, UPBYTEREF, DOWNBYTEREF ]
829 Recharges this (existing) customer with the specified prepaid card (see
830 L<FS::prepay_credit>), specified either by I<identifier> or as an
831 FS::prepay_credit object. If there is an error, returns the error, otherwise
834 Optionally, five scalar references can be passed as well. They will have their
835 values filled in with the amount, number of seconds, and number of upload,
836 download, and total bytes applied by this prepaid card.
840 #the ref bullshit here should be refactored like get_prepay. MyAccount.pm is
841 #the only place that uses these args
842 sub recharge_prepay {
843 my( $self, $prepay_credit, $amountref, $secondsref,
844 $upbytesref, $downbytesref, $totalbytesref ) = @_;
846 local $SIG{HUP} = 'IGNORE';
847 local $SIG{INT} = 'IGNORE';
848 local $SIG{QUIT} = 'IGNORE';
849 local $SIG{TERM} = 'IGNORE';
850 local $SIG{TSTP} = 'IGNORE';
851 local $SIG{PIPE} = 'IGNORE';
853 my $oldAutoCommit = $FS::UID::AutoCommit;
854 local $FS::UID::AutoCommit = 0;
857 my( $amount, $seconds, $upbytes, $downbytes, $totalbytes) = ( 0, 0, 0, 0, 0 );
859 my $error = $self->get_prepay( $prepay_credit,
860 'amount_ref' => \$amount,
861 'seconds_ref' => \$seconds,
862 'upbytes_ref' => \$upbytes,
863 'downbytes_ref' => \$downbytes,
864 'totalbytes_ref' => \$totalbytes,
866 || $self->increment_seconds($seconds)
867 || $self->increment_upbytes($upbytes)
868 || $self->increment_downbytes($downbytes)
869 || $self->increment_totalbytes($totalbytes)
870 || $self->insert_cust_pay_prepay( $amount,
872 ? $prepay_credit->identifier
877 $dbh->rollback if $oldAutoCommit;
881 if ( defined($amountref) ) { $$amountref = $amount; }
882 if ( defined($secondsref) ) { $$secondsref = $seconds; }
883 if ( defined($upbytesref) ) { $$upbytesref = $upbytes; }
884 if ( defined($downbytesref) ) { $$downbytesref = $downbytes; }
885 if ( defined($totalbytesref) ) { $$totalbytesref = $totalbytes; }
887 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
892 =item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , OPTION => VALUE ... ]
894 Looks up and deletes a prepaid card (see L<FS::prepay_credit>),
895 specified either by I<identifier> or as an FS::prepay_credit object.
897 Available options are: I<amount_ref>, I<seconds_ref>, I<upbytes_ref>, I<downbytes_ref>, and I<totalbytes_ref>. The scalars (provided by references) will be
898 incremented by the values of the prepaid card.
900 If the prepaid card specifies an I<agentnum> (see L<FS::agent>), it is used to
901 check or set this customer's I<agentnum>.
903 If there is an error, returns the error, otherwise returns false.
909 my( $self, $prepay_credit, %opt ) = @_;
911 local $SIG{HUP} = 'IGNORE';
912 local $SIG{INT} = 'IGNORE';
913 local $SIG{QUIT} = 'IGNORE';
914 local $SIG{TERM} = 'IGNORE';
915 local $SIG{TSTP} = 'IGNORE';
916 local $SIG{PIPE} = 'IGNORE';
918 my $oldAutoCommit = $FS::UID::AutoCommit;
919 local $FS::UID::AutoCommit = 0;
922 unless ( ref($prepay_credit) ) {
924 my $identifier = $prepay_credit;
926 $prepay_credit = qsearchs(
928 { 'identifier' => $identifier },
933 unless ( $prepay_credit ) {
934 $dbh->rollback if $oldAutoCommit;
935 return "Invalid prepaid card: ". $identifier;
940 if ( $prepay_credit->agentnum ) {
941 if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
942 $dbh->rollback if $oldAutoCommit;
943 return "prepaid card not valid for agent ". $self->agentnum;
945 $self->agentnum($prepay_credit->agentnum);
948 my $error = $prepay_credit->delete;
950 $dbh->rollback if $oldAutoCommit;
951 return "removing prepay_credit (transaction rolled back): $error";
954 ${ $opt{$_.'_ref'} } += $prepay_credit->$_()
955 for grep $opt{$_.'_ref'}, qw( amount seconds upbytes downbytes totalbytes );
957 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
962 =item increment_upbytes SECONDS
964 Updates this customer's single or primary account (see L<FS::svc_acct>) by
965 the specified number of upbytes. If there is an error, returns the error,
966 otherwise returns false.
970 sub increment_upbytes {
971 _increment_column( shift, 'upbytes', @_);
974 =item increment_downbytes SECONDS
976 Updates this customer's single or primary account (see L<FS::svc_acct>) by
977 the specified number of downbytes. If there is an error, returns the error,
978 otherwise returns false.
982 sub increment_downbytes {
983 _increment_column( shift, 'downbytes', @_);
986 =item increment_totalbytes SECONDS
988 Updates this customer's single or primary account (see L<FS::svc_acct>) by
989 the specified number of totalbytes. If there is an error, returns the error,
990 otherwise returns false.
994 sub increment_totalbytes {
995 _increment_column( shift, 'totalbytes', @_);
998 =item increment_seconds SECONDS
1000 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1001 the specified number of seconds. If there is an error, returns the error,
1002 otherwise returns false.
1006 sub increment_seconds {
1007 _increment_column( shift, 'seconds', @_);
1010 =item _increment_column AMOUNT
1012 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1013 the specified number of seconds or bytes. If there is an error, returns
1014 the error, otherwise returns false.
1018 sub _increment_column {
1019 my( $self, $column, $amount ) = @_;
1020 warn "$me increment_column called: $column, $amount\n"
1023 return '' unless $amount;
1025 my @cust_pkg = grep { $_->part_pkg->svcpart('svc_acct') }
1026 $self->ncancelled_pkgs;
1028 if ( ! @cust_pkg ) {
1029 return 'No packages with primary or single services found'.
1030 ' to apply pre-paid time';
1031 } elsif ( scalar(@cust_pkg) > 1 ) {
1032 #maybe have a way to specify the package/account?
1033 return 'Multiple packages found to apply pre-paid time';
1036 my $cust_pkg = $cust_pkg[0];
1037 warn " found package pkgnum ". $cust_pkg->pkgnum. "\n"
1041 $cust_pkg->cust_svc( $cust_pkg->part_pkg->svcpart('svc_acct') );
1043 if ( ! @cust_svc ) {
1044 return 'No account found to apply pre-paid time';
1045 } elsif ( scalar(@cust_svc) > 1 ) {
1046 return 'Multiple accounts found to apply pre-paid time';
1049 my $svc_acct = $cust_svc[0]->svc_x;
1050 warn " found service svcnum ". $svc_acct->pkgnum.
1051 ' ('. $svc_acct->email. ")\n"
1054 $column = "increment_$column";
1055 $svc_acct->$column($amount);
1059 =item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
1061 Inserts a prepayment in the specified amount for this customer. An optional
1062 second argument can specify the prepayment identifier for tracking purposes.
1063 If there is an error, returns the error, otherwise returns false.
1067 sub insert_cust_pay_prepay {
1068 shift->insert_cust_pay('PREP', @_);
1071 =item insert_cust_pay_cash AMOUNT [ PAYINFO ]
1073 Inserts a cash payment in the specified amount for this customer. An optional
1074 second argument can specify the payment identifier for tracking purposes.
1075 If there is an error, returns the error, otherwise returns false.
1079 sub insert_cust_pay_cash {
1080 shift->insert_cust_pay('CASH', @_);
1083 =item insert_cust_pay_west AMOUNT [ PAYINFO ]
1085 Inserts a Western Union payment in the specified amount for this customer. An
1086 optional second argument can specify the prepayment identifier for tracking
1087 purposes. If there is an error, returns the error, otherwise returns false.
1091 sub insert_cust_pay_west {
1092 shift->insert_cust_pay('WEST', @_);
1095 sub insert_cust_pay {
1096 my( $self, $payby, $amount ) = splice(@_, 0, 3);
1097 my $payinfo = scalar(@_) ? shift : '';
1099 my $cust_pay = new FS::cust_pay {
1100 'custnum' => $self->custnum,
1101 'paid' => sprintf('%.2f', $amount),
1102 #'_date' => #date the prepaid card was purchased???
1104 'payinfo' => $payinfo,
1110 =item delete [ OPTION => VALUE ... ]
1112 This deletes the customer. If there is an error, returns the error, otherwise
1115 This will completely remove all traces of the customer record. This is not
1116 what you want when a customer cancels service; for that, cancel all of the
1117 customer's packages (see L</cancel>).
1119 If the customer has any uncancelled packages, you need to pass a new (valid)
1120 customer number for those packages to be transferred to, as the "new_customer"
1121 option. Cancelled packages will be deleted. Did I mention that this is NOT
1122 what you want when a customer cancels service and that you really should be
1123 looking at L<FS::cust_pkg/cancel>?
1125 You can't delete a customer with invoices (see L<FS::cust_bill>),
1126 statements (see L<FS::cust_statement>), credits (see L<FS::cust_credit>),
1127 payments (see L<FS::cust_pay>) or refunds (see L<FS::cust_refund>), unless you
1128 set the "delete_financials" option to a true value.
1133 my( $self, %opt ) = @_;
1135 local $SIG{HUP} = 'IGNORE';
1136 local $SIG{INT} = 'IGNORE';
1137 local $SIG{QUIT} = 'IGNORE';
1138 local $SIG{TERM} = 'IGNORE';
1139 local $SIG{TSTP} = 'IGNORE';
1140 local $SIG{PIPE} = 'IGNORE';
1142 my $oldAutoCommit = $FS::UID::AutoCommit;
1143 local $FS::UID::AutoCommit = 0;
1146 if ( qsearch('agent', { 'agent_custnum' => $self->custnum } ) ) {
1147 $dbh->rollback if $oldAutoCommit;
1148 return "Can't delete a master agent customer";
1151 #use FS::access_user
1152 if ( qsearch('access_user', { 'user_custnum' => $self->custnum } ) ) {
1153 $dbh->rollback if $oldAutoCommit;
1154 return "Can't delete a master employee customer";
1157 tie my %financial_tables, 'Tie::IxHash',
1158 'cust_bill' => 'invoices',
1159 'cust_statement' => 'statements',
1160 'cust_credit' => 'credits',
1161 'cust_pay' => 'payments',
1162 'cust_refund' => 'refunds',
1165 foreach my $table ( keys %financial_tables ) {
1167 my @records = $self->$table();
1169 if ( @records && ! $opt{'delete_financials'} ) {
1170 $dbh->rollback if $oldAutoCommit;
1171 return "Can't delete a customer with ". $financial_tables{$table};
1174 foreach my $record ( @records ) {
1175 my $error = $record->delete;
1177 $dbh->rollback if $oldAutoCommit;
1178 return "Error deleting ". $financial_tables{$table}. ": $error\n";
1184 my @cust_pkg = $self->ncancelled_pkgs;
1186 my $new_custnum = $opt{'new_custnum'};
1187 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
1188 $dbh->rollback if $oldAutoCommit;
1189 return "Invalid new customer number: $new_custnum";
1191 foreach my $cust_pkg ( @cust_pkg ) {
1192 my %hash = $cust_pkg->hash;
1193 $hash{'custnum'} = $new_custnum;
1194 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
1195 my $error = $new_cust_pkg->replace($cust_pkg,
1196 options => { $cust_pkg->options },
1199 $dbh->rollback if $oldAutoCommit;
1204 my @cancelled_cust_pkg = $self->all_pkgs;
1205 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
1206 my $error = $cust_pkg->delete;
1208 $dbh->rollback if $oldAutoCommit;
1213 #cust_tax_adjustment in financials?
1214 #cust_pay_pending? ouch
1215 foreach my $table (qw(
1216 cust_main_invoice cust_main_exemption cust_tag cust_attachment contact
1217 cust_payby cust_location cust_main_note cust_tax_adjustment
1218 cust_pay_void cust_pay_batch queue cust_tax_exempt
1220 foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
1221 my $error = $record->delete;
1223 $dbh->rollback if $oldAutoCommit;
1229 my $sth = $dbh->prepare(
1230 'UPDATE cust_main SET referral_custnum = NULL WHERE referral_custnum = ?'
1232 my $errstr = $dbh->errstr;
1233 $dbh->rollback if $oldAutoCommit;
1236 $sth->execute($self->custnum) or do {
1237 my $errstr = $sth->errstr;
1238 $dbh->rollback if $oldAutoCommit;
1244 my $ticket_dbh = '';
1245 if ($conf->config('ticket_system') eq 'RT_Internal') {
1247 } elsif ($conf->config('ticket_system') eq 'RT_External') {
1248 my ($datasrc, $user, $pass) = $conf->config('ticket_system-rt_external_datasrc');
1249 $ticket_dbh = DBI->connect($datasrc, $user, $pass, { 'ChopBlanks' => 1 });
1250 #or die "RT_External DBI->connect error: $DBI::errstr\n";
1253 if ( $ticket_dbh ) {
1255 my $ticket_sth = $ticket_dbh->prepare(
1256 'DELETE FROM Links WHERE Target = ?'
1258 my $errstr = $ticket_dbh->errstr;
1259 $dbh->rollback if $oldAutoCommit;
1262 $ticket_sth->execute('freeside://freeside/cust_main/'.$self->custnum)
1264 my $errstr = $ticket_sth->errstr;
1265 $dbh->rollback if $oldAutoCommit;
1269 #check and see if the customer is the only link on the ticket, and
1270 #if so, set the ticket to deleted status in RT?
1271 #maybe someday, for now this will at least fix tickets not displaying
1275 #delete the customer record
1277 my $error = $self->SUPER::delete;
1279 $dbh->rollback if $oldAutoCommit;
1283 # cust_main exports!
1285 #my $export_args = $options{'export_args'} || [];
1288 map qsearch( 'part_export', {exportnum=>$_} ),
1289 $conf->config('cust_main-exports'); #, $agentnum
1291 foreach my $part_export ( @part_export ) {
1292 my $error = $part_export->export_delete( $self ); #, @$export_args);
1294 $dbh->rollback if $oldAutoCommit;
1295 return "exporting to ". $part_export->exporttype.
1296 " (transaction rolled back): $error";
1300 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1305 =item replace [ OLD_RECORD ] [ INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
1307 Replaces the OLD_RECORD with this one in the database. If there is an error,
1308 returns the error, otherwise returns false.
1310 To change the customer's address, set the pseudo-fields C<bill_location> and
1311 C<ship_location>. The address will still only change if at least one of the
1312 address fields differs from the existing values.
1314 INVOICING_LIST_ARYREF: If you pass an arrayref to this method, it will be
1315 set as the contact email address for a default contact with the same name as
1318 Currently available options are: I<tax_exemption>, I<cust_payby_params>,
1319 I<contact_params>, I<invoicing_list>, and I<move_pkgs>.
1321 The I<tax_exemption> option can be set to an arrayref of tax names or a hashref
1322 of tax names and exemption numbers. FS::cust_main_exemption records will be
1323 deleted and inserted as appropriate.
1325 I<cust_payby_params> and I<contact_params> can be hashrefs of named parameter
1326 groups (describing the customer's payment methods and contacts, respectively)
1327 in the style supported by L<FS::o2m_Common/process_o2m>. See L<FS::cust_payby>
1328 and L<FS::contact> for the fields these can contain.
1330 I<invoicing_list> is a synonym for the INVOICING_LIST_ARYREF parameter, and
1331 should be used instead if possible.
1333 If I<move_pkgs> is an arrayref, it will override the list of packages
1334 to be moved to the new address (see L<FS::cust_location/move_pkgs>.)
1341 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
1343 : $self->replace_old;
1347 warn "$me replace called\n"
1350 my $curuser = $FS::CurrentUser::CurrentUser;
1351 return "You are not permitted to create complimentary accounts."
1352 if $self->complimentary eq 'Y'
1353 && $self->complimentary ne $old->complimentary
1354 && ! $curuser->access_right('Complimentary customer');
1356 local($ignore_expired_card) = 1
1357 if $old->payby =~ /^(CARD|DCRD)$/
1358 && $self->payby =~ /^(CARD|DCRD)$/
1359 && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
1361 local($ignore_banned_card) = 1
1362 if ( $old->payby =~ /^(CARD|DCRD)$/ && $self->payby =~ /^(CARD|DCRD)$/
1363 || $old->payby =~ /^(CHEK|DCHK)$/ && $self->payby =~ /^(CHEK|DCHK)$/ )
1364 && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
1366 if ( $self->payby =~ /^(CARD|DCRD)$/
1367 && $old->payinfo ne $self->payinfo
1368 && $old->paymask ne $self->paymask )
1370 my $error = $self->check_payinfo_cardtype;
1371 return $error if $error;
1374 return "Invoicing locale is required"
1377 && $conf->exists('cust_main-require_locale');
1379 return "You are not permitted to change customer invoicing terms."
1380 if $old->invoice_terms ne $self->invoice_terms
1381 && ! $curuser->access_right('Edit customer invoice terms');
1383 local $SIG{HUP} = 'IGNORE';
1384 local $SIG{INT} = 'IGNORE';
1385 local $SIG{QUIT} = 'IGNORE';
1386 local $SIG{TERM} = 'IGNORE';
1387 local $SIG{TSTP} = 'IGNORE';
1388 local $SIG{PIPE} = 'IGNORE';
1390 my $oldAutoCommit = $FS::UID::AutoCommit;
1391 local $FS::UID::AutoCommit = 0;
1394 for my $l (qw(bill_location ship_location)) {
1395 #my $old_loc = $old->$l;
1396 my $new_loc = $self->$l or next;
1398 # find the existing location if there is one
1399 $new_loc->set('custnum' => $self->custnum);
1400 my $error = $new_loc->find_or_insert;
1402 $dbh->rollback if $oldAutoCommit;
1405 $self->set($l.'num', $new_loc->locationnum);
1409 if ( @param && ref($param[0]) eq 'ARRAY' ) { # INVOICING_LIST_ARYREF
1410 warn "cust_main::replace: using deprecated invoicing list argument";
1411 $invoicing_list = shift @param;
1414 my %options = @param;
1416 $invoicing_list ||= $options{invoicing_list};
1418 my @contacts = map { $_->contact } $self->cust_contact;
1419 # find a contact that matches the customer's name
1420 my ($implicit_contact) = grep { $_->first eq $old->get('first')
1421 and $_->last eq $old->get('last') }
1423 $implicit_contact ||= FS::contact->new({
1424 'custnum' => $self->custnum,
1425 'locationnum' => $self->get('bill_locationnum'),
1428 # for any of these that are already contact emails, link to the existing
1430 if ( $invoicing_list ) {
1433 # kind of like process_m2m on these, except:
1434 # - the other side is two tables in a join
1435 # - and we might have to create new contact_emails
1436 # - and possibly a new contact
1438 # Find existing invoice emails that aren't on the implicit contact.
1439 # Any of these that are not on the new invoicing list will be removed.
1440 my %old_email_cust_contact;
1441 foreach my $cust_contact ($self->cust_contact) {
1442 next if !$cust_contact->invoice_dest;
1443 next if $cust_contact->contactnum == ($implicit_contact->contactnum || 0);
1445 foreach my $contact_email ($cust_contact->contact->contact_email) {
1446 $old_email_cust_contact{ $contact_email->emailaddress } = $cust_contact;
1450 foreach my $dest (@$invoicing_list) {
1452 if ($dest eq 'POST') {
1454 $self->set('postal_invoice', 'Y');
1456 } elsif ( exists($old_email_cust_contact{$dest}) ) {
1458 delete $old_email_cust_contact{$dest}; # don't need to remove it, then
1462 # See if it belongs to some other contact; if so, link it.
1463 my $contact_email = qsearchs('contact_email', { emailaddress => $dest });
1465 and $contact_email->contactnum != ($implicit_contact->contactnum || 0) ) {
1466 my $cust_contact = qsearchs('cust_contact', {
1467 contactnum => $contact_email->contactnum,
1468 custnum => $self->custnum,
1469 }) || FS::cust_contact->new({
1470 contactnum => $contact_email->contactnum,
1471 custnum => $self->custnum,
1473 $cust_contact->set('invoice_dest', 'Y');
1474 my $error = $cust_contact->custcontactnum ?
1475 $cust_contact->replace : $cust_contact->insert;
1477 $dbh->rollback if $oldAutoCommit;
1478 return "$error (linking to email address $dest)";
1482 # This email address is not yet linked to any contact, so it will
1483 # be added to the implicit contact.
1484 $email .= ',' if length($email);
1490 foreach my $remove_dest (keys %old_email_cust_contact) {
1491 my $cust_contact = $old_email_cust_contact{$remove_dest};
1492 # These were not in the list of requested destinations, so take them off.
1493 $cust_contact->set('invoice_dest', '');
1494 my $error = $cust_contact->replace;
1496 $dbh->rollback if $oldAutoCommit;
1497 return "$error (unlinking email address $remove_dest)";
1501 # make sure it keeps up with the changed customer name, if any
1502 $implicit_contact->set('last', $self->get('last'));
1503 $implicit_contact->set('first', $self->get('first'));
1504 $implicit_contact->set('emailaddress', $email);
1505 $implicit_contact->set('invoice_dest', 'Y');
1506 $implicit_contact->set('custnum', $self->custnum);
1507 my $i_cust_contact =
1508 qsearchs('cust_contact', {
1509 contactnum => $implicit_contact->contactnum,
1510 custnum => $self->custnum,
1513 if ( $i_cust_contact ) {
1514 $implicit_contact->set($_, $i_cust_contact->$_)
1515 foreach qw( classnum selfservice_access comment );
1519 if ( $implicit_contact->contactnum ) {
1520 $error = $implicit_contact->replace;
1521 } elsif ( length($email) ) { # don't create a new contact if not needed
1522 $error = $implicit_contact->insert;
1526 $dbh->rollback if $oldAutoCommit;
1527 return "$error (adding email address $email)";
1532 # replace the customer record
1533 my $error = $self->SUPER::replace($old);
1536 $dbh->rollback if $oldAutoCommit;
1540 # now move packages to the new service location
1541 $self->set('ship_location', ''); #flush cache
1542 if ( $old->ship_locationnum and # should only be null during upgrade...
1543 $old->ship_locationnum != $self->ship_locationnum ) {
1544 $error = $old->ship_location->move_to($self->ship_location, move_pkgs => $options{'move_pkgs'});
1546 $dbh->rollback if $oldAutoCommit;
1550 # don't move packages based on the billing location, but
1551 # disable it if it's no longer in use
1552 if ( $old->bill_locationnum and
1553 $old->bill_locationnum != $self->bill_locationnum ) {
1554 $error = $old->bill_location->disable_if_unused;
1556 $dbh->rollback if $oldAutoCommit;
1561 if ( $self->exists('tagnum') ) { #so we don't delete these on edit by accident
1563 #this could be more efficient than deleting and re-inserting, if it matters
1564 foreach my $cust_tag (qsearch('cust_tag', {'custnum'=>$self->custnum} )) {
1565 my $error = $cust_tag->delete;
1567 $dbh->rollback if $oldAutoCommit;
1571 foreach my $tagnum ( @{ $self->tagnum || [] } ) {
1572 my $cust_tag = new FS::cust_tag { 'tagnum' => $tagnum,
1573 'custnum' => $self->custnum };
1574 my $error = $cust_tag->insert;
1576 $dbh->rollback if $oldAutoCommit;
1583 my $tax_exemption = delete $options{'tax_exemption'};
1584 if ( $tax_exemption ) {
1586 $tax_exemption = { map { $_ => '' } @$tax_exemption }
1587 if ref($tax_exemption) eq 'ARRAY';
1589 my %cust_main_exemption =
1590 map { $_->taxname => $_ }
1591 qsearch('cust_main_exemption', { 'custnum' => $old->custnum } );
1593 foreach my $taxname ( keys %$tax_exemption ) {
1595 if ( $cust_main_exemption{$taxname} &&
1596 $cust_main_exemption{$taxname}->exempt_number eq $tax_exemption->{$taxname}
1599 delete $cust_main_exemption{$taxname};
1603 my $cust_main_exemption = new FS::cust_main_exemption {
1604 'custnum' => $self->custnum,
1605 'taxname' => $taxname,
1606 'exempt_number' => $tax_exemption->{$taxname},
1608 my $error = $cust_main_exemption->insert;
1610 $dbh->rollback if $oldAutoCommit;
1611 return "inserting cust_main_exemption (transaction rolled back): $error";
1615 foreach my $cust_main_exemption ( values %cust_main_exemption ) {
1616 my $error = $cust_main_exemption->delete;
1618 $dbh->rollback if $oldAutoCommit;
1619 return "deleting cust_main_exemption (transaction rolled back): $error";
1625 if ( my $cust_payby_params = delete $options{'cust_payby_params'} ) {
1627 my $error = $self->process_o2m(
1628 'table' => 'cust_payby',
1629 'fields' => FS::cust_payby->cgi_cust_payby_fields,
1630 'params' => $cust_payby_params,
1631 'hash_callback' => \&FS::cust_payby::cgi_hash_callback,
1634 $dbh->rollback if $oldAutoCommit;
1640 if ( my $contact_params = delete $options{'contact_params'} ) {
1642 # this can potentially replace contacts that were created by the
1643 # invoicing list argument, but the UI shouldn't allow both of them
1646 my $error = $self->process_o2m(
1647 'table' => 'contact',
1648 'fields' => FS::contact->cgi_contact_fields,
1649 'params' => $contact_params,
1652 $dbh->rollback if $oldAutoCommit;
1658 unless ( $import || $skip_fuzzyfiles ) {
1659 $error = $self->queue_fuzzyfiles_update;
1661 $dbh->rollback if $oldAutoCommit;
1662 return "updating fuzzy search cache: $error";
1666 # tax district update in cust_location
1668 # cust_main exports!
1670 my $export_args = $options{'export_args'} || [];
1673 map qsearch( 'part_export', {exportnum=>$_} ),
1674 $conf->config('cust_main-exports'); #, $agentnum
1676 foreach my $part_export ( @part_export ) {
1677 my $error = $part_export->export_replace( $self, $old, @$export_args);
1679 $dbh->rollback if $oldAutoCommit;
1680 return "exporting to ". $part_export->exporttype.
1681 " (transaction rolled back): $error";
1685 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1690 =item queue_fuzzyfiles_update
1692 Used by insert & replace to update the fuzzy search cache
1696 use FS::cust_main::Search;
1697 sub queue_fuzzyfiles_update {
1700 local $SIG{HUP} = 'IGNORE';
1701 local $SIG{INT} = 'IGNORE';
1702 local $SIG{QUIT} = 'IGNORE';
1703 local $SIG{TERM} = 'IGNORE';
1704 local $SIG{TSTP} = 'IGNORE';
1705 local $SIG{PIPE} = 'IGNORE';
1707 my $oldAutoCommit = $FS::UID::AutoCommit;
1708 local $FS::UID::AutoCommit = 0;
1711 foreach my $field ( 'first', 'last', 'company', 'ship_company' ) {
1712 my $queue = new FS::queue {
1713 'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
1715 my @args = "cust_main.$field", $self->get($field);
1716 my $error = $queue->insert( @args );
1718 $dbh->rollback if $oldAutoCommit;
1719 return "queueing job (transaction rolled back): $error";
1724 push @locations, $self->bill_location if $self->bill_locationnum;
1725 push @locations, $self->ship_location if @locations && $self->has_ship_address;
1726 foreach my $location (@locations) {
1727 my $queue = new FS::queue {
1728 'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
1730 my @args = 'cust_location.address1', $location->address1;
1731 my $error = $queue->insert( @args );
1733 $dbh->rollback if $oldAutoCommit;
1734 return "queueing job (transaction rolled back): $error";
1738 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1745 Checks all fields to make sure this is a valid customer record. If there is
1746 an error, returns the error, otherwise returns false. Called by the insert
1747 and replace methods.
1754 warn "$me check BEFORE: \n". $self->_dump
1758 $self->ut_numbern('custnum')
1759 || $self->ut_number('agentnum')
1760 || $self->ut_textn('agent_custid')
1761 || $self->ut_number('refnum')
1762 || $self->ut_foreign_keyn('bill_locationnum', 'cust_location','locationnum')
1763 || $self->ut_foreign_keyn('ship_locationnum', 'cust_location','locationnum')
1764 || $self->ut_foreign_keyn('classnum', 'cust_class', 'classnum')
1765 || $self->ut_foreign_keyn('salesnum', 'sales', 'salesnum')
1766 || $self->ut_foreign_keyn('taxstatusnum', 'tax_status', 'taxstatusnum')
1767 || $self->ut_textn('custbatch')
1768 || $self->ut_name('last')
1769 || $self->ut_name('first')
1770 || $self->ut_snumbern('signupdate')
1771 || $self->ut_snumbern('birthdate')
1772 || $self->ut_namen('spouse_last')
1773 || $self->ut_namen('spouse_first')
1774 || $self->ut_snumbern('spouse_birthdate')
1775 || $self->ut_snumbern('anniversary_date')
1776 || $self->ut_textn('company')
1777 || $self->ut_textn('ship_company')
1778 || $self->ut_anything('comments')
1779 || $self->ut_numbern('referral_custnum')
1780 || $self->ut_textn('stateid')
1781 || $self->ut_textn('stateid_state')
1782 || $self->ut_textn('invoice_terms')
1783 || $self->ut_floatn('cdr_termination_percentage')
1784 || $self->ut_floatn('credit_limit')
1785 || $self->ut_numbern('billday')
1786 || $self->ut_numbern('prorate_day')
1787 || $self->ut_flag('force_prorate_day')
1788 || $self->ut_flag('edit_subject')
1789 || $self->ut_flag('calling_list_exempt')
1790 || $self->ut_flag('invoice_noemail')
1791 || $self->ut_flag('message_noemail')
1792 || $self->ut_enum('locale', [ '', FS::Locales->locales ])
1793 || $self->ut_currencyn('currency')
1794 || $self->ut_textn('po_number')
1795 || $self->ut_enum('complimentary', [ '', 'Y' ])
1796 || $self->ut_flag('invoice_ship_address')
1797 || $self->ut_flag('invoice_dest')
1800 foreach (qw(company ship_company)) {
1801 my $company = $self->get($_);
1802 $company =~ s/^\s+//;
1803 $company =~ s/\s+$//;
1804 $company =~ s/\s+/ /g;
1805 $self->set($_, $company);
1808 #barf. need message catalogs. i18n. etc.
1809 $error .= "Please select an advertising source."
1810 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
1811 return $error if $error;
1813 my $agent = qsearchs( 'agent', { 'agentnum' => $self->agentnum } )
1814 or return "Unknown agent";
1816 if ( $self->currency ) {
1817 my $agent_currency = qsearchs( 'agent_currency', {
1818 'agentnum' => $agent->agentnum,
1819 'currency' => $self->currency,
1821 or return "Agent ". $agent->agent.
1822 " not permitted to offer ". $self->currency. " invoicing";
1825 return "Unknown refnum"
1826 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
1828 return "Unknown referring custnum: ". $self->referral_custnum
1829 unless ! $self->referral_custnum
1830 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
1832 if ( $self->ss eq '' ) {
1837 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
1838 or return "Illegal social security number: ". $self->ss;
1839 $self->ss("$1-$2-$3");
1842 #turn off invoice_ship_address if ship & bill are the same
1843 if ($self->bill_locationnum eq $self->ship_locationnum) {
1844 $self->invoice_ship_address('');
1847 # cust_main_county verification now handled by cust_location check
1850 $self->ut_phonen('daytime', $self->country)
1851 || $self->ut_phonen('night', $self->country)
1852 || $self->ut_phonen('fax', $self->country)
1853 || $self->ut_phonen('mobile', $self->country)
1855 return $error if $error;
1857 if ( $conf->exists('cust_main-require_phone', $self->agentnum)
1859 && ! length($self->daytime) && ! length($self->night) && ! length($self->mobile)
1862 my $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/
1864 : FS::Msgcat::_gettext('daytime');
1865 my $night_label = FS::Msgcat::_gettext('night') =~ /^(night)?$/
1867 : FS::Msgcat::_gettext('night');
1869 my $mobile_label = FS::Msgcat::_gettext('mobile') =~ /^(mobile)?$/
1871 : FS::Msgcat::_gettext('mobile');
1873 return "$daytime_label, $night_label or $mobile_label is required"
1877 return "Please select an invoicing locale"
1880 && $conf->exists('cust_main-require_locale');
1882 return "Please select a customer class"
1883 if ! $self->classnum
1884 && $conf->exists('cust_main-require_classnum');
1886 foreach my $flag (qw( tax spool_cdr squelch_cdr archived email_csv_cdr )) {
1887 $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
1891 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
1893 warn "$me check AFTER: \n". $self->_dump
1896 $self->SUPER::check;
1899 sub check_payinfo_cardtype {
1902 return '' unless $self->payby =~ /^(CARD|DCRD)$/;
1904 my $payinfo = $self->payinfo;
1905 $payinfo =~ s/\D//g;
1907 return '' if $self->tokenized($payinfo); #token
1909 my %bop_card_types = map { $_=>1 } values %{ card_types() };
1910 my $cardtype = cardtype($payinfo);
1912 return "$cardtype not accepted" unless $bop_card_types{$cardtype};
1920 Additional checks for replace only.
1925 my ($new,$old) = @_;
1926 #preserve old value if global config is set
1927 if ($old && $conf->exists('invoice-ship_address')) {
1928 $new->invoice_ship_address($old->invoice_ship_address);
1935 Returns a list of fields which have ship_ duplicates.
1940 qw( last first company
1942 address1 address2 city county state zip country
1944 daytime night fax mobile
1948 =item has_ship_address
1950 Returns true if this customer record has a separate shipping address.
1954 sub has_ship_address {
1956 $self->bill_locationnum != $self->ship_locationnum;
1961 Returns a list of key/value pairs, with the following keys: address1,
1962 adddress2, city, county, state, zip, country, district, and geocode. The
1963 shipping address is used if present.
1969 $self->ship_location->location_hash;
1974 Returns all locations (see L<FS::cust_location>) for this customer.
1981 'table' => 'cust_location',
1982 'hashref' => { 'custnum' => $self->custnum,
1983 'prospectnum' => '',
1985 'order_by' => 'ORDER BY country, LOWER(state), LOWER(city), LOWER(county), LOWER(address1), LOWER(address2)',
1991 Returns all contact associations (see L<FS::cust_contact>) for this customer.
1997 qsearch('cust_contact', { 'custnum' => $self->custnum } );
2000 =item cust_payby PAYBY
2002 Returns all payment methods (see L<FS::cust_payby>) for this customer.
2004 If one or more PAYBY are specified, returns only payment methods for specified PAYBY.
2005 Does not validate PAYBY.
2013 'table' => 'cust_payby',
2014 'hashref' => { 'custnum' => $self->custnum },
2015 'order_by' => "ORDER BY payby IN ('CARD','CHEK') DESC, weight ASC",
2017 $search->{'extra_sql'} = ' AND payby IN ( '.
2018 join(',', map dbh->quote($_), @payby).
2025 =item has_cust_payby_auto
2027 Returns true if customer has an automatic payment method ('CARD' or 'CHEK')
2031 sub has_cust_payby_auto {
2034 'table' => 'cust_payby',
2035 'hashref' => { 'custnum' => $self->custnum, },
2036 'extra_sql' => " AND payby IN ( 'CARD', 'CHEK' ) ",
2037 'order_by' => 'LIMIT 1',
2044 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
2045 and L<FS::cust_pkg>) for this customer, except those on hold.
2047 Returns a list: an empty list on success or a list of errors.
2053 grep { ($_->get('setup')) && $_->unsuspend } $self->suspended_pkgs(@_);
2058 Unsuspends all suspended packages in the on-hold state (those without setup
2059 dates) for this customer.
2065 grep { (!$_->setup) && $_->unsuspend } $self->suspended_pkgs;
2070 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
2072 Returns a list: an empty list on success or a list of errors.
2078 grep { $_->suspend(@_) } $self->unsuspended_pkgs;
2081 =item suspend_if_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2083 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
2084 PKGPARTs (see L<FS::part_pkg>). Preferred usage is to pass a hashref instead
2085 of a list of pkgparts; the hashref has the following keys:
2089 =item pkgparts - listref of pkgparts
2091 =item (other options are passed to the suspend method)
2096 Returns a list: an empty list on success or a list of errors.
2100 sub suspend_if_pkgpart {
2102 my (@pkgparts, %opt);
2103 if (ref($_[0]) eq 'HASH'){
2104 @pkgparts = @{$_[0]{pkgparts}};
2109 grep { $_->suspend(%opt) }
2110 grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
2111 $self->unsuspended_pkgs;
2114 =item suspend_unless_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2116 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
2117 given PKGPARTs (see L<FS::part_pkg>). Preferred usage is to pass a hashref
2118 instead of a list of pkgparts; the hashref has the following keys:
2122 =item pkgparts - listref of pkgparts
2124 =item (other options are passed to the suspend method)
2128 Returns a list: an empty list on success or a list of errors.
2132 sub suspend_unless_pkgpart {
2134 my (@pkgparts, %opt);
2135 if (ref($_[0]) eq 'HASH'){
2136 @pkgparts = @{$_[0]{pkgparts}};
2141 grep { $_->suspend(%opt) }
2142 grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
2143 $self->unsuspended_pkgs;
2146 =item cancel [ OPTION => VALUE ... ]
2148 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
2149 The cancellation time will be now.
2153 Always returns a list: an empty list on success or a list of errors.
2160 warn "$me cancel called on customer ". $self->custnum. " with options ".
2161 join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
2163 my @pkgs = $self->ncancelled_pkgs;
2165 $self->cancel_pkgs( %opt, 'cust_pkg' => \@pkgs );
2168 =item cancel_pkgs OPTIONS
2170 Cancels a specified list of packages. OPTIONS can include:
2174 =item cust_pkg - an arrayref of the packages. Required.
2176 =item time - the cancellation time, used to calculate final bills and
2177 unused-time credits if any. Will be passed through to the bill() and
2178 FS::cust_pkg::cancel() methods.
2180 =item quiet - can be set true to supress email cancellation notices.
2182 =item reason - can be set to a cancellation reason (see L<FS:reason>), either a
2183 reasonnum of an existing reason, or passing a hashref will create a new reason.
2184 The hashref should have the following keys:
2185 typenum - Reason type (see L<FS::reason_type>)
2186 reason - Text of the new reason.
2188 =item cust_pkg_reason - can be an arrayref of L<FS::cust_pkg_reason> objects
2189 for the individual packages, parallel to the C<cust_pkg> argument. The
2190 reason and reason_otaker arguments will be taken from those objects.
2192 =item ban - can be set true to ban this customer's credit card or ACH information, if present.
2194 =item nobill - can be set true to skip billing if it might otherwise be done.
2199 my( $self, %opt ) = @_;
2201 # we're going to cancel services, which is not reversible
2202 # unless exports are suppressed
2203 die "cancel_pkgs cannot be run inside a transaction"
2204 if !$FS::UID::AutoCommit && !$FS::svc_Common::noexport_hack;
2206 my $oldAutoCommit = $FS::UID::AutoCommit;
2207 local $FS::UID::AutoCommit = 0;
2209 savepoint_create('cancel_pkgs');
2211 return ( 'access denied' )
2212 unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
2214 if ( $opt{'ban'} ) {
2216 foreach my $cust_payby ( $self->cust_payby ) {
2218 #well, if they didn't get decrypted on search, then we don't have to
2219 # try again... queue a job for the server that does have decryption
2220 # capability if we're in a paranoid multi-server implementation?
2221 return ( "Can't (yet) ban encrypted credit cards" )
2222 if $cust_payby->is_encrypted($cust_payby->payinfo);
2224 my $ban = new FS::banned_pay $cust_payby->_new_banned_pay_hashref;
2225 my $error = $ban->insert;
2227 savepoint_rollback_and_release('cancel_pkgs');
2228 dbh->rollback if $oldAutoCommit;
2236 my @pkgs = @{ delete $opt{'cust_pkg'} };
2237 my $cancel_time = $opt{'time'} || time;
2239 # bill all packages first, so we don't lose usage, service counts for
2240 # bulk billing, etc.
2241 if ( !$opt{nobill} && $conf->exists('bill_usage_on_cancel') ) {
2243 my $error = $self->bill( 'pkg_list' => [ @pkgs ],
2245 'time' => $cancel_time );
2247 warn "Error billing during cancel, custnum ". $self->custnum. ": $error";
2248 savepoint_rollback_and_release('cancel_pkgs');
2249 dbh->rollback if $oldAutoCommit;
2250 return ( "Error billing during cancellation: $error" );
2253 savepoint_release('cancel_pkgs');
2254 dbh->commit if $oldAutoCommit;
2257 # try to cancel each service, the same way we would for individual packages,
2258 # but in cancel weight order.
2259 my @cust_svc = map { $_->cust_svc } @pkgs;
2260 my @sorted_cust_svc =
2262 sort { $a->[1] <=> $b->[1] }
2263 map { [ $_, $_->svc_x ? $_->svc_x->table_info->{'cancel_weight'} : -1 ]; } @cust_svc
2265 warn "$me removing ".scalar(@sorted_cust_svc)." service(s) for customer ".
2269 foreach my $cust_svc (@sorted_cust_svc) {
2270 my $savepoint = 'cancel_pkgs_'.$i++;
2271 savepoint_create( $savepoint );
2272 my $part_svc = $cust_svc->part_svc;
2273 next if ( defined($part_svc) and $part_svc->preserve );
2274 # immediate cancel, no date option
2275 # transactionize individually
2276 my $error = try { $cust_svc->cancel } catch { $_ };
2278 savepoint_rollback_and_release( $savepoint );
2279 dbh->rollback if $oldAutoCommit;
2280 push @errors, $error;
2282 savepoint_release( $savepoint );
2283 dbh->commit if $oldAutoCommit;
2290 warn "$me cancelling ". scalar(@pkgs) ." package(s) for customer ".
2291 $self->custnum. "\n"
2295 if ($opt{'cust_pkg_reason'}) {
2296 @cprs = @{ delete $opt{'cust_pkg_reason'} };
2302 my $savepoint = 'cancel_pkgs_'.$i++;
2303 savepoint_create( $savepoint );
2305 my $cpr = shift @cprs;
2307 $lopt{'reason'} = $cpr->reasonnum;
2308 $lopt{'reason_otaker'} = $cpr->otaker;
2310 warn "no reason found when canceling package ".$_->pkgnum."\n";
2311 # we're not actually required to pass a reason to cust_pkg::cancel,
2312 # but if we're getting to this point, something has gone awry.
2313 $null_reason ||= FS::reason->new_or_existing(
2314 reason => 'unknown reason',
2315 type => 'Cancel Reason',
2318 $lopt{'reason'} = $null_reason->reasonnum;
2319 $lopt{'reason_otaker'} = $FS::CurrentUser::CurrentUser->username;
2322 my $error = $_->cancel(%lopt);
2324 savepoint_rollback_and_release( $savepoint );
2325 dbh->rollback if $oldAutoCommit;
2326 push @errors, 'pkgnum '.$_->pkgnum.': '.$error;
2328 savepoint_release( $savepoint );
2329 dbh->commit if $oldAutoCommit;
2336 sub _banned_pay_hashref {
2337 die 'cust_main->_banned_pay_hashref deprecated';
2349 'payby' => $payby2ban{$self->payby},
2350 'payinfo' => $self->payinfo,
2351 #don't ever *search* on reason! #'reason' =>
2357 Returns all notes (see L<FS::cust_main_note>) for this customer.
2362 my($self,$orderby_classnum) = (shift,shift);
2363 my $orderby = "sticky DESC, _date DESC";
2364 $orderby = "classnum ASC, $orderby" if $orderby_classnum;
2365 qsearch( 'cust_main_note',
2366 { 'custnum' => $self->custnum },
2368 "ORDER BY $orderby",
2374 Returns the agent (see L<FS::agent>) for this customer.
2378 Returns the agent name (see L<FS::agent>) for this customer.
2384 $self->agent->agent;
2389 Returns any tags associated with this customer, as FS::cust_tag objects,
2390 or an empty list if there are no tags.
2394 Returns any tags associated with this customer, as FS::part_tag objects,
2395 or an empty list if there are no tags.
2401 map $_->part_tag, $self->cust_tag;
2407 Returns the customer class, as an FS::cust_class object, or the empty string
2408 if there is no customer class.
2412 Returns the customer category name, or the empty string if there is no customer
2419 my $cust_class = $self->cust_class;
2421 ? $cust_class->categoryname
2427 Returns the customer class name, or the empty string if there is no customer
2434 my $cust_class = $self->cust_class;
2436 ? $cust_class->classname
2442 Returns the external tax status, as an FS::tax_status object, or the empty
2443 string if there is no tax status.
2449 if ( $self->taxstatusnum ) {
2450 qsearchs('tax_status', { 'taxstatusnum' => $self->taxstatusnum } );
2458 Returns the tax status code if there is one.
2464 my $tax_status = $self->tax_status;
2466 ? $tax_status->taxstatus
2470 =item BILLING METHODS
2472 Documentation on billing methods has been moved to
2473 L<FS::cust_main::Billing>.
2475 =item REALTIME BILLING METHODS
2477 Documentation on realtime billing methods has been moved to
2478 L<FS::cust_main::Billing_Realtime>.
2482 Removes the I<paycvv> field from the database directly.
2484 If there is an error, returns the error, otherwise returns false.
2486 DEPRECATED. Use L</remove_cvv_from_cust_payby> instead.
2491 die 'cust_main->remove_cvv deprecated';
2493 my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
2494 or return dbh->errstr;
2495 $sth->execute($self->custnum)
2496 or return $sth->errstr;
2503 Returns the total owed for this customer on all invoices
2504 (see L<FS::cust_bill/owed>).
2510 $self->total_owed_date(2145859200); #12/31/2037
2513 =item total_owed_date TIME
2515 Returns the total owed for this customer on all invoices with date earlier than
2516 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
2517 see L<Time::Local> and L<Date::Parse> for conversion functions.
2521 sub total_owed_date {
2525 my $custnum = $self->custnum;
2527 my $owed_sql = FS::cust_bill->owed_sql;
2530 SELECT SUM($owed_sql) FROM cust_bill
2531 WHERE custnum = $custnum
2535 sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2539 =item total_owed_pkgnum PKGNUM
2541 Returns the total owed on all invoices for this customer's specific package
2542 when using experimental package balances (see L<FS::cust_bill/owed_pkgnum>).
2546 sub total_owed_pkgnum {
2547 my( $self, $pkgnum ) = @_;
2548 $self->total_owed_date_pkgnum(2145859200, $pkgnum); #12/31/2037
2551 =item total_owed_date_pkgnum TIME PKGNUM
2553 Returns the total owed for this customer's specific package when using
2554 experimental package balances on all invoices with date earlier than
2555 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
2556 see L<Time::Local> and L<Date::Parse> for conversion functions.
2560 sub total_owed_date_pkgnum {
2561 my( $self, $time, $pkgnum ) = @_;
2564 foreach my $cust_bill (
2565 grep { $_->_date <= $time }
2566 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2568 $total_bill += $cust_bill->owed_pkgnum($pkgnum);
2570 sprintf( "%.2f", $total_bill );
2576 Returns the total amount of all payments.
2583 $total += $_->paid foreach $self->cust_pay;
2584 sprintf( "%.2f", $total );
2587 =item total_unapplied_credits
2589 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2590 customer. See L<FS::cust_credit/credited>.
2592 =item total_credited
2594 Old name for total_unapplied_credits. Don't use.
2598 sub total_credited {
2599 #carp "total_credited deprecated, use total_unapplied_credits";
2600 shift->total_unapplied_credits(@_);
2603 sub total_unapplied_credits {
2606 my $custnum = $self->custnum;
2608 my $unapplied_sql = FS::cust_credit->unapplied_sql;
2611 SELECT SUM($unapplied_sql) FROM cust_credit
2612 WHERE custnum = $custnum
2615 sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2619 =item total_unapplied_credits_pkgnum PKGNUM
2621 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2622 customer. See L<FS::cust_credit/credited>.
2626 sub total_unapplied_credits_pkgnum {
2627 my( $self, $pkgnum ) = @_;
2628 my $total_credit = 0;
2629 $total_credit += $_->credited foreach $self->cust_credit_pkgnum($pkgnum);
2630 sprintf( "%.2f", $total_credit );
2634 =item total_unapplied_payments
2636 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
2637 See L<FS::cust_pay/unapplied>.
2641 sub total_unapplied_payments {
2644 my $custnum = $self->custnum;
2646 my $unapplied_sql = FS::cust_pay->unapplied_sql;
2649 SELECT SUM($unapplied_sql) FROM cust_pay
2650 WHERE custnum = $custnum
2653 sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2657 =item total_unapplied_payments_pkgnum PKGNUM
2659 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer's
2660 specific package when using experimental package balances. See
2661 L<FS::cust_pay/unapplied>.
2665 sub total_unapplied_payments_pkgnum {
2666 my( $self, $pkgnum ) = @_;
2667 my $total_unapplied = 0;
2668 $total_unapplied += $_->unapplied foreach $self->cust_pay_pkgnum($pkgnum);
2669 sprintf( "%.2f", $total_unapplied );
2673 =item total_unapplied_refunds
2675 Returns the total unrefunded refunds (see L<FS::cust_refund>) for this
2676 customer. See L<FS::cust_refund/unapplied>.
2680 sub total_unapplied_refunds {
2682 my $custnum = $self->custnum;
2684 my $unapplied_sql = FS::cust_refund->unapplied_sql;
2687 SELECT SUM($unapplied_sql) FROM cust_refund
2688 WHERE custnum = $custnum
2691 sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2697 Returns the balance for this customer (total_owed plus total_unrefunded, minus
2698 total_unapplied_credits minus total_unapplied_payments).
2704 $self->balance_date_range;
2707 =item balance_date TIME
2709 Returns the balance for this customer, only considering invoices with date
2710 earlier than TIME (total_owed_date minus total_credited minus
2711 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
2712 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
2719 $self->balance_date_range(shift);
2722 =item balance_date_range [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
2724 Returns the balance for this customer, optionally considering invoices with
2725 date earlier than START_TIME, and not later than END_TIME
2726 (total_owed_date minus total_unapplied_credits minus total_unapplied_payments).
2728 Times are specified as SQL fragments or numeric
2729 UNIX timestamps; see L<perlfunc/"time">). Also see L<Time::Local> and
2730 L<Date::Parse> for conversion functions. The empty string can be passed
2731 to disable that time constraint completely.
2733 Accepts the same options as L<balance_date_sql>:
2737 =item unapplied_date
2739 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)
2743 An absolute cutoff time. Payments, credits, and refunds I<applied> after this
2744 time will be ignored. Note that START_TIME and END_TIME only limit the date
2745 range for invoices and I<unapplied> payments, credits, and refunds.
2751 sub balance_date_range {
2753 my $sql = 'SELECT SUM('. $self->balance_date_sql(@_).
2754 ') FROM cust_main WHERE custnum='. $self->custnum;
2755 sprintf( '%.2f', $self->scalar_sql($sql) || 0 );
2758 =item balance_pkgnum PKGNUM
2760 Returns the balance for this customer's specific package when using
2761 experimental package balances (total_owed plus total_unrefunded, minus
2762 total_unapplied_credits minus total_unapplied_payments)
2766 sub balance_pkgnum {
2767 my( $self, $pkgnum ) = @_;
2770 $self->total_owed_pkgnum($pkgnum)
2771 # n/a - refunds aren't part of pkg-balances since they don't apply to invoices
2772 # + $self->total_unapplied_refunds_pkgnum($pkgnum)
2773 - $self->total_unapplied_credits_pkgnum($pkgnum)
2774 - $self->total_unapplied_payments_pkgnum($pkgnum)
2780 Returns a hash of useful information for making a payment.
2790 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
2791 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
2792 'LECB' (Phone bill billing), 'BILL' (billing), or 'COMP' (free).
2796 For credit card transactions:
2808 For electronic check transactions:
2818 #XXX i need to be updated for 4.x+
2824 $return{balance} = $self->balance;
2826 $return{payname} = $self->payname
2827 || ( $self->first. ' '. $self->get('last') );
2829 $return{$_} = $self->bill_location->$_
2830 for qw(address1 address2 city state zip);
2832 $return{payby} = $self->payby;
2833 $return{stateid_state} = $self->stateid_state;
2835 if ( $self->payby =~ /^(CARD|DCRD)$/ ) {
2836 $return{card_type} = cardtype($self->payinfo);
2837 $return{payinfo} = $self->paymask;
2839 @return{'month', 'year'} = $self->paydate_monthyear;
2843 if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
2844 my ($payinfo1, $payinfo2) = split '@', $self->paymask;
2845 $return{payinfo1} = $payinfo1;
2846 $return{payinfo2} = $payinfo2;
2847 $return{paytype} = $self->paytype;
2848 $return{paystate} = $self->paystate;
2852 #doubleclick protection
2854 $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
2862 Returns the next payment expiration date for this customer. If they have no
2863 payment methods that will expire, returns 0.
2869 # filter out the ones that individually return 0, but then return 0 if
2870 # there are no results
2871 my @epochs = grep { $_ > 0 } map { $_->paydate_epoch } $self->cust_payby;
2872 min( @epochs ) || 0;
2875 =item paydate_epoch_sql
2877 Returns an SQL expression to get the next payment expiration date for a
2878 customer. Returns 2143260000 (2037-12-01) if there are no payment expiration
2879 dates, so that it's safe to test for "will it expire before date X" for any
2884 sub paydate_epoch_sql {
2886 my $paydate = FS::cust_payby->paydate_epoch_sql;
2887 "(SELECT COALESCE(MIN($paydate), 2143260000) FROM cust_payby WHERE cust_payby.custnum = cust_main.custnum)";
2891 my( $self, $taxname ) = @_;
2893 qsearchs( 'cust_main_exemption', { 'custnum' => $self->custnum,
2894 'taxname' => $taxname,
2899 =item cust_main_exemption
2901 =item invoicing_list
2903 Returns a list of email addresses (with svcnum entries expanded), and the word
2904 'POST' if the customer receives postal invoices.
2908 sub invoicing_list {
2909 my( $self, $arrayref ) = @_;
2912 warn "FS::cust_main::invoicing_list(ARRAY) is no longer supported.";
2915 my @emails = $self->invoicing_list_emailonly;
2916 push @emails, 'POST' if $self->get('postal_invoice');
2921 =item check_invoicing_list ARRAYREF
2923 Checks these arguements as valid input for the invoicing_list method. If there
2924 is an error, returns the error, otherwise returns false.
2928 sub check_invoicing_list {
2929 my( $self, $arrayref ) = @_;
2931 foreach my $address ( @$arrayref ) {
2933 if ($address eq 'FAX' and $self->getfield('fax') eq '') {
2934 return 'Can\'t add FAX invoice destination with a blank FAX number.';
2937 my $cust_main_invoice = new FS::cust_main_invoice ( {
2938 'custnum' => $self->custnum,
2941 my $error = $self->custnum
2942 ? $cust_main_invoice->check
2943 : $cust_main_invoice->checkdest
2945 return $error if $error;
2949 return "Email address required"
2950 if $conf->exists('cust_main-require_invoicing_list_email', $self->agentnum)
2951 && ! grep { $_ !~ /^([A-Z]+)$/ } @$arrayref;
2958 Returns the email addresses of all accounts provisioned for this customer.
2965 foreach my $cust_pkg ( $self->all_pkgs ) {
2966 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
2968 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2969 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2971 $list{$_}=1 foreach map { $_->email } @svc_acct;
2976 =item invoicing_list_addpost
2978 Adds postal invoicing to this customer. If this customer is already configured
2979 to receive postal invoices, does nothing.
2983 sub invoicing_list_addpost {
2985 if ( $self->get('postal_invoice') eq '' ) {
2986 $self->set('postal_invoice', 'Y');
2987 my $error = $self->replace;
2988 warn $error if $error; # should fail harder, but this is traditional
2992 =item invoicing_list_emailonly
2994 Returns the list of email invoice recipients (invoicing_list without non-email
2995 destinations such as POST and FAX).
2999 sub invoicing_list_emailonly {
3001 warn "$me invoicing_list_emailonly called"
3003 return () if !$self->custnum; # not yet inserted
3004 return map { $_->emailaddress }
3006 table => 'cust_contact',
3007 select => 'emailaddress',
3008 addl_from => ' JOIN contact USING (contactnum) '.
3009 ' JOIN contact_email USING (contactnum)',
3010 hashref => { 'custnum' => $self->custnum, },
3011 extra_sql => q( AND cust_contact.invoice_dest = 'Y'),
3015 =item invoicing_list_emailonly_scalar
3017 Returns the list of email invoice recipients (invoicing_list without non-email
3018 destinations such as POST and FAX) as a comma-separated scalar.
3022 sub invoicing_list_emailonly_scalar {
3024 warn "$me invoicing_list_emailonly_scalar called"
3026 join(', ', $self->invoicing_list_emailonly);
3029 =item contact_list [ CLASSNUM, DEST_FLAG... ]
3031 Returns a list of contacts (L<FS::contact> objects) for the customer.
3033 If no arguments are given, returns all contacts for the customer.
3035 Arguments may contain classnums. When classnums are specified, only
3036 contacts with a matching cust_contact.classnum are returned. When a
3037 classnum of 0 is given, contacts with a null classnum are also included.
3039 Arguments may also contain the dest flag names 'invoice' or 'message'.
3040 If given, contacts who's invoice_dest and/or message_dest flags are
3041 not set to 'Y' will be excluded.
3049 select => join(', ',(
3051 'cust_contact.invoice_dest',
3052 'cust_contact.message_dest',
3054 addl_from => ' JOIN cust_contact USING (contactnum)',
3055 extra_sql => ' WHERE cust_contact.custnum = '.$self->custnum,
3059 # Calling methods were relying on this method to use invoice_dest to
3060 # block e-mail messages. Depending on parameters, this may or may not
3061 # have actually happened.
3063 # The bug could cause this SQL to be used to filter e-mail addresses:
3066 # cust_contact.classnums IN (1,2,3)
3067 # OR cust_contact.invoice_dest = 'Y'
3070 # improperly including everybody with the opt-in flag AND everybody
3071 # in the contact classes
3073 # Possibility to introduce new bugs:
3074 # If callers of this method called it incorrectly, and didn't notice
3075 # because it seemed to send the e-mails they wanted.
3080 # cust_contact.classnum IN (1,2,3)
3082 # cust_contact.classnum IS NULL
3085 # cust_contact.invoice_dest = 'Y'
3087 # cust_contact.message_dest = 'Y'
3095 if ($_ eq 'invoice' || $_ eq 'message') {
3096 push @and_dest, " cust_contact.${_}_dest = 'Y' ";
3097 } elsif ($_ eq '0') {
3098 push @or_classnum, ' cust_contact.classnum IS NULL ';
3099 } elsif ( /^\d+$/ ) {
3100 push @classnums, $_;
3102 croak "bad classnum argument '$_'";
3106 push @or_classnum, 'cust_contact.classnum IN ('.join(',',@classnums).')'
3109 if (@or_classnum || @and_dest) { # catch, no arguments given
3110 $search->{extra_sql} .= ' AND ( ';
3113 $search->{extra_sql} .= ' ( ';
3114 $search->{extra_sql} .= join ' OR ', map {" $_ "} @or_classnum;
3115 $search->{extra_sql} .= ' ) ';
3116 $search->{extra_sql} .= ' AND ( ' if @and_dest;
3120 $search->{extra_sql} .= join ' OR ', map {" $_ "} @and_dest;
3121 $search->{extra_sql} .= ' ) ' if @or_classnum;
3124 $search->{extra_sql} .= ' ) ';
3126 warn "\$extra_sql: $search->{extra_sql} \n" if $DEBUG;
3132 =item contact_list_email [ CLASSNUM, ... ]
3134 Same as L</contact_list>, but returns email destinations instead of contact
3139 sub contact_list_email {
3141 my @contacts = $self->contact_list(@_);
3143 foreach my $contact (@contacts) {
3144 foreach my $contact_email ($contact->contact_email) {
3145 push @emails, Email::Address->new( $contact->firstlast,
3146 $contact_email->emailaddress
3153 =item contact_list_email_destinations
3155 Returns a list of emails and whether they receive invoices or messages destinations.
3156 { emailaddress => 'email.com', invoice => 'Y', message => '', }
3160 sub contact_list_email_destinations {
3162 warn "$me contact_list_email_destinations"
3164 return () if !$self->custnum; # not yet inserted
3167 table => 'cust_contact',
3168 select => 'emailaddress, cust_contact.invoice_dest as invoice, cust_contact.message_dest as message',
3169 addl_from => ' JOIN contact USING (contactnum) '.
3170 ' JOIN contact_email USING (contactnum)',
3171 hashref => { 'custnum' => $self->custnum, },
3172 order_by => 'ORDER BY custcontactnum DESC',
3177 =item contact_list_emailonly
3179 Returns an array of hashes containing the emails. Used for displaying contact email field in advanced customer reports.
3180 [ { data => 'email.com', }, ]
3184 sub contact_list_emailonly {
3186 warn "$me contact_list_emailonly called"
3189 foreach ($self->contact_list_email_destinations) {
3192 'data' => $_->emailaddress,
3195 push @emails, $data;
3200 =item contact_list_cust_invoice_only
3202 Returns an array of hashes containing cust_contact.invoice_dest. Does this email receive invoices. Used for displaying email Invoice field in advanced customer reports.
3203 [ { data => 'Yes', }, ]
3207 sub contact_list_cust_invoice_only {
3209 warn "$me contact_list_cust_invoice_only called"
3212 foreach ($self->contact_list_email_destinations) {
3213 my $invoice = $_->invoice ? 'Yes' : 'No';
3219 push @emails, $data;
3224 =item contact_list_cust_message_only
3226 Returns an array of hashes containing cust_contact.message_dest. Does this email receive message notifications. Used for displaying email Message field in advanced customer reports.
3227 [ { data => 'Yes', }, ]
3231 sub contact_list_cust_message_only {
3233 warn "$me contact_list_cust_message_only called"
3236 foreach ($self->contact_list_email_destinations) {
3237 my $message = $_->message ? 'Yes' : 'No';
3243 push @emails, $data;
3248 =item referral_custnum_cust_main
3250 Returns the customer who referred this customer (or the empty string, if
3251 this customer was not referred).
3253 Note the difference with referral_cust_main method: This method,
3254 referral_custnum_cust_main returns the single customer (if any) who referred
3255 this customer, while referral_cust_main returns an array of customers referred
3260 sub referral_custnum_cust_main {
3262 return '' unless $self->referral_custnum;
3263 qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3266 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
3268 Returns an array of customers referred by this customer (referral_custnum set
3269 to this custnum). If DEPTH is given, recurses up to the given depth, returning
3270 customers referred by customers referred by this customer and so on, inclusive.
3271 The default behavior is DEPTH 1 (no recursion).
3273 Note the difference with referral_custnum_cust_main method: This method,
3274 referral_cust_main, returns an array of customers referred BY this customer,
3275 while referral_custnum_cust_main returns the single customer (if any) who
3276 referred this customer.
3280 sub referral_cust_main {
3282 my $depth = @_ ? shift : 1;
3283 my $exclude = @_ ? shift : {};
3286 map { $exclude->{$_->custnum}++; $_; }
3287 grep { ! $exclude->{ $_->custnum } }
3288 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
3292 map { $_->referral_cust_main($depth-1, $exclude) }
3299 =item referral_cust_main_ncancelled
3301 Same as referral_cust_main, except only returns customers with uncancelled
3306 sub referral_cust_main_ncancelled {
3308 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
3311 =item referral_cust_pkg [ DEPTH ]
3313 Like referral_cust_main, except returns a flat list of all unsuspended (and
3314 uncancelled) packages for each customer. The number of items in this list may
3315 be useful for commission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
3319 sub referral_cust_pkg {
3321 my $depth = @_ ? shift : 1;
3323 map { $_->unsuspended_pkgs }
3324 grep { $_->unsuspended_pkgs }
3325 $self->referral_cust_main($depth);
3328 =item referring_cust_main
3330 Returns the single cust_main record for the customer who referred this customer
3331 (referral_custnum), or false.
3335 sub referring_cust_main {
3337 return '' unless $self->referral_custnum;
3338 qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3341 =item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
3343 Applies a credit to this customer. If there is an error, returns the error,
3344 otherwise returns false.
3346 REASON can be a text string, an FS::reason object, or a scalar reference to
3347 a reasonnum. If a text string, it will be automatically inserted as a new
3348 reason, and a 'reason_type' option must be passed to indicate the
3349 FS::reason_type for the new reason.
3351 An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
3352 Likewise for I<eventnum>, I<commission_agentnum>, I<commission_salesnum> and
3353 I<commission_pkgnum>.
3355 Any other options are passed to FS::cust_credit::insert.
3360 my( $self, $amount, $reason, %options ) = @_;
3362 my $cust_credit = new FS::cust_credit {
3363 'custnum' => $self->custnum,
3364 'amount' => $amount,
3367 if ( ref($reason) ) {
3369 if ( ref($reason) eq 'SCALAR' ) {
3370 $cust_credit->reasonnum( $$reason );
3372 $cust_credit->reasonnum( $reason->reasonnum );
3376 $cust_credit->set('reason', $reason)
3379 $cust_credit->$_( delete $options{$_} )
3380 foreach grep exists($options{$_}),
3381 qw( addlinfo eventnum ),
3382 map "commission_$_", qw( agentnum salesnum pkgnum );
3384 $cust_credit->insert(%options);
3388 =item charge HASHREF || AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
3390 Creates a one-time charge for this customer. If there is an error, returns
3391 the error, otherwise returns false.
3393 New-style, with a hashref of options:
3395 my $error = $cust_main->charge(
3399 'start_date' => str2time('7/4/2009'),
3400 'pkg' => 'Description',
3401 'comment' => 'Comment',
3402 'additional' => [], #extra invoice detail
3403 'classnum' => 1, #pkg_class
3405 'setuptax' => '', # or 'Y' for tax exempt
3407 'locationnum'=> 1234, # optional
3410 'taxclass' => 'Tax class',
3413 'taxproduct' => 2, #part_pkg_taxproduct
3414 'override' => {}, #XXX describe
3416 #will be filled in with the new object
3417 'cust_pkg_ref' => \$cust_pkg,
3419 #generate an invoice immediately
3421 'invoice_terms' => '', #with these terms
3427 my $error = $cust_main->charge( 54.32, 'Description', 'Comment', 'Tax class' );
3431 #super false laziness w/quotation::charge
3434 my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
3435 my ( $pkg, $comment, $additional );
3436 my ( $setuptax, $taxclass ); #internal taxes
3437 my ( $taxproduct, $override ); #vendor (CCH) taxes
3439 my $separate_bill = '';
3440 my $cust_pkg_ref = '';
3441 my ( $bill_now, $invoice_terms ) = ( 0, '' );
3443 my ( $discountnum, $discountnum_amount, $discountnum_percent ) = ( '','','' );
3444 if ( ref( $_[0] ) ) {
3445 $amount = $_[0]->{amount};
3446 $setup_cost = $_[0]->{setup_cost};
3447 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
3448 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
3449 $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
3450 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
3451 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
3452 : '$'. sprintf("%.2f",$amount);
3453 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
3454 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
3455 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
3456 $additional = $_[0]->{additional} || [];
3457 $taxproduct = $_[0]->{taxproductnum};
3458 $override = { '' => $_[0]->{tax_override} };
3459 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
3460 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
3461 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
3462 $locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
3463 $separate_bill = $_[0]->{separate_bill} || '';
3464 $discountnum = $_[0]->{setup_discountnum};
3465 $discountnum_amount = $_[0]->{setup_discountnum_amount};
3466 $discountnum_percent = $_[0]->{setup_discountnum_percent};
3472 $pkg = @_ ? shift : 'One-time charge';
3473 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
3475 $taxclass = @_ ? shift : '';
3479 local $SIG{HUP} = 'IGNORE';
3480 local $SIG{INT} = 'IGNORE';
3481 local $SIG{QUIT} = 'IGNORE';
3482 local $SIG{TERM} = 'IGNORE';
3483 local $SIG{TSTP} = 'IGNORE';
3484 local $SIG{PIPE} = 'IGNORE';
3486 my $oldAutoCommit = $FS::UID::AutoCommit;
3487 local $FS::UID::AutoCommit = 0;
3490 my $part_pkg = new FS::part_pkg ( {
3492 'comment' => $comment,
3496 'classnum' => ( $classnum ? $classnum : '' ),
3497 'setuptax' => $setuptax,
3498 'taxclass' => $taxclass,
3499 'taxproductnum' => $taxproduct,
3500 'setup_cost' => $setup_cost,
3503 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
3504 ( 0 .. @$additional - 1 )
3506 'additional_count' => scalar(@$additional),
3507 'setup_fee' => $amount,
3510 my $error = $part_pkg->insert( options => \%options,
3511 tax_overrides => $override,
3514 $dbh->rollback if $oldAutoCommit;
3518 my $pkgpart = $part_pkg->pkgpart;
3519 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
3520 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
3521 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
3522 $error = $type_pkgs->insert;
3524 $dbh->rollback if $oldAutoCommit;
3529 my $cust_pkg = new FS::cust_pkg ( {
3530 'custnum' => $self->custnum,
3531 'pkgpart' => $pkgpart,
3532 'quantity' => $quantity,
3533 'start_date' => $start_date,
3534 'no_auto' => $no_auto,
3535 'separate_bill' => $separate_bill,
3536 'locationnum' => $locationnum,
3537 'setup_discountnum' => $discountnum,
3538 'setup_discountnum_amount' => $discountnum_amount,
3539 'setup_discountnum_percent' => $discountnum_percent,
3542 $error = $cust_pkg->insert;
3544 $dbh->rollback if $oldAutoCommit;
3546 } elsif ( $cust_pkg_ref ) {
3547 ${$cust_pkg_ref} = $cust_pkg;
3551 my $error = $self->bill( 'invoice_terms' => $invoice_terms,
3552 'pkg_list' => [ $cust_pkg ],
3555 $dbh->rollback if $oldAutoCommit;
3560 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3565 #=item charge_postal_fee
3567 #Applies a one time charge this customer. If there is an error,
3568 #returns the error, returns the cust_pkg charge object or false
3569 #if there was no charge.
3573 # This should be a customer event. For that to work requires that bill
3574 # also be a customer event.
3576 sub charge_postal_fee {
3579 my $pkgpart = $conf->config('postal_invoice-fee_pkgpart', $self->agentnum);
3580 return '' unless ($pkgpart && grep { $_ eq 'POST' } $self->invoicing_list);
3582 my $cust_pkg = new FS::cust_pkg ( {
3583 'custnum' => $self->custnum,
3584 'pkgpart' => $pkgpart,
3588 my $error = $cust_pkg->insert;
3589 $error ? $error : $cust_pkg;
3592 =item num_cust_attachment_deleted
3594 Returns the number of deleted attachments for this customer (see
3595 L<FS::num_cust_attachment>).
3599 sub num_cust_attachments_deleted {
3602 " SELECT COUNT(*) FROM cust_attachment ".
3603 " WHERE custnum = ? AND disabled IS NOT NULL AND disabled > 0",
3610 Returns the most recent invnum (invoice number) for this customer.
3617 " SELECT MAX(invnum) FROM cust_bill WHERE custnum = ?",
3622 =item cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3624 Returns all the invoices (see L<FS::cust_bill>) for this customer.
3626 Optionally, a list or hashref of additional arguments to the qsearch call can
3633 my $opt = ref($_[0]) ? shift : { @_ };
3635 #return $self->num_cust_bill unless wantarray || keys %$opt;
3637 $opt->{'table'} = 'cust_bill';
3638 $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3639 $opt->{'hashref'}{'custnum'} = $self->custnum;
3640 $opt->{'order_by'} ||= 'ORDER BY _date ASC';
3642 map { $_ } #behavior of sort undefined in scalar context
3643 sort { $a->_date <=> $b->_date }
3647 =item open_cust_bill
3649 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
3654 sub open_cust_bill {
3658 'extra_sql' => ' AND '. FS::cust_bill->owed_sql. ' > 0',
3664 =item legacy_cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3666 Returns all the legacy invoices (see L<FS::legacy_cust_bill>) for this customer.
3670 sub legacy_cust_bill {
3673 #return $self->num_legacy_cust_bill unless wantarray;
3675 map { $_ } #behavior of sort undefined in scalar context
3676 sort { $a->_date <=> $b->_date }
3677 qsearch({ 'table' => 'legacy_cust_bill',
3678 'hashref' => { 'custnum' => $self->custnum, },
3679 'order_by' => 'ORDER BY _date ASC',
3683 =item cust_statement [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3685 Returns all the statements (see L<FS::cust_statement>) for this customer.
3687 Optionally, a list or hashref of additional arguments to the qsearch call can
3692 =item cust_bill_void
3694 Returns all the voided invoices (see L<FS::cust_bill_void>) for this customer.
3698 sub cust_bill_void {
3701 map { $_ } #return $self->num_cust_bill_void unless wantarray;
3702 sort { $a->_date <=> $b->_date }
3703 qsearch( 'cust_bill_void', { 'custnum' => $self->custnum } )
3706 sub cust_statement {
3708 my $opt = ref($_[0]) ? shift : { @_ };
3710 #return $self->num_cust_statement unless wantarray || keys %$opt;
3712 $opt->{'table'} = 'cust_statement';
3713 $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3714 $opt->{'hashref'}{'custnum'} = $self->custnum;
3715 $opt->{'order_by'} ||= 'ORDER BY _date ASC';
3717 map { $_ } #behavior of sort undefined in scalar context
3718 sort { $a->_date <=> $b->_date }
3722 =item svc_x SVCDB [ OPTION => VALUE | EXTRA_QSEARCH_PARAMS_HASHREF ]
3724 Returns all services of type SVCDB (such as 'svc_acct') for this customer.
3726 Optionally, a list or hashref of additional arguments to the qsearch call can
3727 be passed following the SVCDB.
3734 if ( ! $svcdb =~ /^svc_\w+$/ ) {
3735 warn "$me svc_x requires a svcdb";
3738 my $opt = ref($_[0]) ? shift : { @_ };
3740 $opt->{'table'} = $svcdb;
3741 $opt->{'addl_from'} =
3742 'LEFT JOIN cust_svc USING (svcnum) LEFT JOIN cust_pkg USING (pkgnum) '.
3743 ($opt->{'addl_from'} || '');
3745 my $custnum = $self->custnum;
3746 $custnum =~ /^\d+$/ or die "bad custnum '$custnum'";
3747 my $where = "cust_pkg.custnum = $custnum";
3749 my $extra_sql = $opt->{'extra_sql'} || '';
3750 if ( keys %{ $opt->{'hashref'} } ) {
3751 $extra_sql = " AND $where $extra_sql";
3754 if ( $opt->{'extra_sql'} =~ /^\s*where\s(.*)/si ) {
3755 $extra_sql = "WHERE $where AND $1";
3758 $extra_sql = "WHERE $where $extra_sql";
3761 $opt->{'extra_sql'} = $extra_sql;
3766 # required for use as an eventtable;
3769 $self->svc_x('svc_acct', @_);
3774 Returns all the credits (see L<FS::cust_credit>) for this customer.
3781 #return $self->num_cust_credit unless wantarray;
3783 map { $_ } #behavior of sort undefined in scalar context
3784 sort { $a->_date <=> $b->_date }
3785 qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
3788 =item cust_credit_pkgnum
3790 Returns all the credits (see L<FS::cust_credit>) for this customer's specific
3791 package when using experimental package balances.
3795 sub cust_credit_pkgnum {
3796 my( $self, $pkgnum ) = @_;
3797 map { $_ } #return $self->num_cust_credit_pkgnum($pkgnum) unless wantarray;
3798 sort { $a->_date <=> $b->_date }
3799 qsearch( 'cust_credit', { 'custnum' => $self->custnum,
3800 'pkgnum' => $pkgnum,
3805 =item cust_credit_void
3807 Returns all voided credits (see L<FS::cust_credit_void>) for this customer.
3811 sub cust_credit_void {
3814 sort { $a->_date <=> $b->_date }
3815 qsearch( 'cust_credit_void', { 'custnum' => $self->custnum } )
3820 Returns all the payments (see L<FS::cust_pay>) for this customer.
3826 my $opt = ref($_[0]) ? shift : { @_ };
3828 return $self->num_cust_pay unless wantarray || keys %$opt;
3830 $opt->{'table'} = 'cust_pay';
3831 $opt->{'hashref'}{'custnum'} = $self->custnum;
3833 map { $_ } #behavior of sort undefined in scalar context
3834 sort { $a->_date <=> $b->_date }
3841 Returns the number of payments (see L<FS::cust_pay>) for this customer. Also
3842 called automatically when the cust_pay method is used in a scalar context.
3848 my $sql = "SELECT COUNT(*) FROM cust_pay WHERE custnum = ?";
3849 my $sth = dbh->prepare($sql) or die dbh->errstr;
3850 $sth->execute($self->custnum) or die $sth->errstr;