enable cancel_pkgs to expire packages even if the reason is missing, #71623, from...
[freeside.git] / FS / FS / cust_main.pm
1 package FS::cust_main;
2 use base qw( FS::cust_main::Packages
3              FS::cust_main::Status
4              FS::cust_main::NationalID
5              FS::cust_main::Billing
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
12              FS::cust_main::Merge
13              FS::cust_main::API
14              FS::otaker_Mixin FS::cust_main_Mixin
15              FS::geocode_Mixin FS::Quotable_Mixin FS::Sales_Mixin
16              FS::o2m_Common
17              FS::Record
18            );
19
20 require 5.006;
21 use strict;
22 use Carp;
23 use Scalar::Util qw( blessed );
24 use Time::Local qw(timelocal);
25 use Data::Dumper;
26 use Tie::IxHash;
27 use Date::Format;
28 #use Date::Manip;
29 use File::Temp; #qw( tempfile );
30 use Business::CreditCard 0.28;
31 use List::Util qw(min);
32 use FS::UID qw( dbh driver_name );
33 use FS::Record qw( qsearchs qsearch dbdef regexp_sql );
34 use FS::Cursor;
35 use FS::Misc qw( generate_ps do_print money_pretty card_types );
36 use FS::Msgcat qw(gettext);
37 use FS::CurrentUser;
38 use FS::TicketSystem;
39 use FS::payby;
40 use FS::cust_pkg;
41 use FS::cust_svc;
42 use FS::cust_bill;
43 use FS::cust_bill_void;
44 use FS::legacy_cust_bill;
45 use FS::cust_pay;
46 use FS::cust_pay_pending;
47 use FS::cust_pay_void;
48 use FS::cust_pay_batch;
49 use FS::cust_credit;
50 use FS::cust_refund;
51 use FS::part_referral;
52 use FS::cust_main_county;
53 use FS::cust_location;
54 use FS::cust_class;
55 use FS::tax_status;
56 use FS::cust_main_exemption;
57 use FS::cust_tax_adjustment;
58 use FS::cust_tax_location;
59 use FS::agent_currency;
60 use FS::cust_main_invoice;
61 use FS::cust_tag;
62 use FS::prepay_credit;
63 use FS::queue;
64 use FS::part_pkg;
65 use FS::part_export;
66 #use FS::cust_event;
67 use FS::type_pkgs;
68 use FS::payment_gateway;
69 use FS::agent_payment_gateway;
70 use FS::banned_pay;
71 use FS::cust_main_note;
72 use FS::cust_attachment;
73 use FS::cust_contact;
74 use FS::Locales;
75 use FS::upgrade_journal;
76 use FS::sales;
77 use FS::cust_payby;
78 use FS::contact;
79 use FS::reason;
80
81 # 1 is mostly method/subroutine entry and options
82 # 2 traces progress of some operations
83 # 3 is even more information including possibly sensitive data
84 our $DEBUG = 0;
85 our $me = '[FS::cust_main]';
86
87 our $import = 0;
88 our $ignore_expired_card = 0;
89 our $ignore_banned_card = 0;
90 our $ignore_invalid_card = 0;
91
92 our $skip_fuzzyfiles = 0;
93
94 our $ucfirst_nowarn = 0;
95
96 #this info is in cust_payby as of 4.x
97 #this and the fields themselves can be removed in 5.x
98 our @encrypted_fields = ('payinfo', 'paycvv');
99 sub nohistory_fields { ('payinfo', 'paycvv'); }
100
101 our $conf;
102 our $default_agent_custid;
103 our $custnum_display_length;
104 #ask FS::UID to run this stuff for us later
105 #$FS::UID::callback{'FS::cust_main'} = sub { 
106 install_callback FS::UID sub { 
107   $conf = new FS::Conf;
108   $ignore_invalid_card    = $conf->exists('allow_invalid_cards');
109   $default_agent_custid   = $conf->exists('cust_main-default_agent_custid');
110   $custnum_display_length = $conf->config('cust_main-custnum-display_length');
111 };
112
113 sub _cache {
114   my $self = shift;
115   my ( $hashref, $cache ) = @_;
116   if ( exists $hashref->{'pkgnum'} ) {
117     #@{ $self->{'_pkgnum'} } = ();
118     my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
119     $self->{'_pkgnum'} = $subcache;
120     #push @{ $self->{'_pkgnum'} },
121     FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
122   }
123 }
124
125 =head1 NAME
126
127 FS::cust_main - Object methods for cust_main records
128
129 =head1 SYNOPSIS
130
131   use FS::cust_main;
132
133   $record = new FS::cust_main \%hash;
134   $record = new FS::cust_main { 'column' => 'value' };
135
136   $error = $record->insert;
137
138   $error = $new_record->replace($old_record);
139
140   $error = $record->delete;
141
142   $error = $record->check;
143
144   @cust_pkg = $record->all_pkgs;
145
146   @cust_pkg = $record->ncancelled_pkgs;
147
148   @cust_pkg = $record->suspended_pkgs;
149
150   $error = $record->bill;
151   $error = $record->bill %options;
152   $error = $record->bill 'time' => $time;
153
154   $error = $record->collect;
155   $error = $record->collect %options;
156   $error = $record->collect 'invoice_time'   => $time,
157                           ;
158
159 =head1 DESCRIPTION
160
161 An FS::cust_main object represents a customer.  FS::cust_main inherits from 
162 FS::Record.  The following fields are currently supported:
163
164 =over 4
165
166 =item custnum
167
168 Primary key (assigned automatically for new customers)
169
170 =item agentnum
171
172 Agent (see L<FS::agent>)
173
174 =item refnum
175
176 Advertising source (see L<FS::part_referral>)
177
178 =item first
179
180 First name
181
182 =item last
183
184 Last name
185
186 =item ss
187
188 Cocial security number (optional)
189
190 =item company
191
192 (optional)
193
194 =item daytime
195
196 phone (optional)
197
198 =item night
199
200 phone (optional)
201
202 =item fax
203
204 phone (optional)
205
206 =item mobile
207
208 phone (optional)
209
210 =item payby
211
212 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
213
214 =item payinfo
215
216 Payment Information (See L<FS::payinfo_Mixin> for data format)
217
218 =item paymask
219
220 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
221
222 =item paycvv
223
224 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
225
226 =item paydate
227
228 Expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
229
230 =item paystart_month
231
232 Start date month (maestro/solo cards only)
233
234 =item paystart_year
235
236 Start date year (maestro/solo cards only)
237
238 =item payissue
239
240 Issue number (maestro/solo cards only)
241
242 =item payname
243
244 Name on card or billing name
245
246 =item payip
247
248 IP address from which payment information was received
249
250 =item tax
251
252 Tax exempt, empty or `Y'
253
254 =item usernum
255
256 Order taker (see L<FS::access_user>)
257
258 =item comments
259
260 Comments (optional)
261
262 =item referral_custnum
263
264 Referring customer number
265
266 =item spool_cdr
267
268 Enable individual CDR spooling, empty or `Y'
269
270 =item dundate
271
272 A suggestion to events (see L<FS::part_bill_event">) to delay until this unix timestamp
273
274 =item squelch_cdr
275
276 Discourage individual CDR printing, empty or `Y'
277
278 =item edit_subject
279
280 Allow self-service editing of ticket subjects, empty or 'Y'
281
282 =item calling_list_exempt
283
284 Do not call, empty or 'Y'
285
286 =item invoice_ship_address
287
288 Display ship_address ("Service address") on invoices for this customer, empty or 'Y'
289
290 =back
291
292 =head1 METHODS
293
294 =over 4
295
296 =item new HASHREF
297
298 Creates a new customer.  To add the customer to the database, see L<"insert">.
299
300 Note that this stores the hash reference, not a distinct copy of the hash it
301 points to.  You can ask the object for a copy with the I<hash> method.
302
303 =cut
304
305 sub table { 'cust_main'; }
306
307 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
308
309 Adds this customer to the database.  If there is an error, returns the error,
310 otherwise returns false.
311
312 Usually the customer's location will not yet exist in the database, and
313 the C<bill_location> and C<ship_location> pseudo-fields must be set to 
314 uninserted L<FS::cust_location> objects.  These will be inserted and linked
315 (in both directions) to the new customer record.  If they're references 
316 to the same object, they will become the same location.
317
318 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
319 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
320 are inserted atomicly, or the transaction is rolled back.  Passing an empty
321 hash reference is equivalent to not supplying this parameter.  There should be
322 a better explanation of this, but until then, here's an example:
323
324   use Tie::RefHash;
325   tie %hash, 'Tie::RefHash'; #this part is important
326   %hash = (
327     $cust_pkg => [ $svc_acct ],
328     ...
329   );
330   $cust_main->insert( \%hash );
331
332 INVOICING_LIST_ARYREF: No longer supported.
333
334 Currently available options are: I<depend_jobnum>, I<noexport>,
335 I<tax_exemption>, I<prospectnum>, I<contact> and I<contact_params>.
336
337 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
338 on the supplied jobnum (they will not run until the specific job completes).
339 This can be used to defer provisioning until some action completes (such
340 as running the customer's credit card successfully).
341
342 The I<noexport> option is deprecated.  If I<noexport> is set true, no
343 provisioning jobs (exports) are scheduled.  (You can schedule them later with
344 the B<reexport> method.)
345
346 The I<tax_exemption> option can be set to an arrayref of tax names or a hashref
347 of tax names and exemption numbers.  FS::cust_main_exemption records will be
348 created and inserted.
349
350 If I<prospectnum> is set, moves contacts and locations from that prospect.
351
352 If I<contact> is set to an arrayref of FS::contact objects, those will be
353 inserted.
354
355 If I<contact_params> is set to a hashref of CGI parameters (and I<contact> is
356 unset), inserts those new contacts with this new customer.  Handles CGI
357 paramaters for an "m2" multiple entry field as passed by edit/cust_main.cgi
358
359 If I<cust_payby_params> is set to a hashref o fCGI parameters, inserts those
360 new stored payment records with this new customer.  Handles CGI parameters
361 for an "m2" multiple entry field as passed by edit/cust_main.cgi
362
363 =cut
364
365 sub insert {
366   my $self = shift;
367   my $cust_pkgs = @_ ? shift : {};
368   my $invoicing_list;
369   if ( $_[0] and ref($_[0]) eq 'ARRAY' ) {
370     warn "cust_main::insert using deprecated invoicing list argument";
371     $invoicing_list = shift;
372   }
373   my %options = @_;
374   warn "$me insert called with options ".
375        join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
376     if $DEBUG;
377
378   local $SIG{HUP} = 'IGNORE';
379   local $SIG{INT} = 'IGNORE';
380   local $SIG{QUIT} = 'IGNORE';
381   local $SIG{TERM} = 'IGNORE';
382   local $SIG{TSTP} = 'IGNORE';
383   local $SIG{PIPE} = 'IGNORE';
384
385   my $oldAutoCommit = $FS::UID::AutoCommit;
386   local $FS::UID::AutoCommit = 0;
387   my $dbh = dbh;
388
389   my $prepay_identifier = '';
390   my( $amount, $seconds, $upbytes, $downbytes, $totalbytes ) = (0, 0, 0, 0, 0);
391   my $payby = '';
392   if ( $self->payby eq 'PREPAY' ) {
393
394     $self->payby(''); #'BILL');
395     $prepay_identifier = $self->payinfo;
396     $self->payinfo('');
397
398     warn "  looking up prepaid card $prepay_identifier\n"
399       if $DEBUG > 1;
400
401     my $error = $self->get_prepay( $prepay_identifier,
402                                    'amount_ref'     => \$amount,
403                                    'seconds_ref'    => \$seconds,
404                                    'upbytes_ref'    => \$upbytes,
405                                    'downbytes_ref'  => \$downbytes,
406                                    'totalbytes_ref' => \$totalbytes,
407                                  );
408     if ( $error ) {
409       $dbh->rollback if $oldAutoCommit;
410       #return "error applying prepaid card (transaction rolled back): $error";
411       return $error;
412     }
413
414     $payby = 'PREP' if $amount;
415
416   } elsif ( $self->payby =~ /^(CASH|WEST|MCRD|MCHK|PPAL)$/ ) {
417
418     $payby = $1;
419     $self->payby(''); #'BILL');
420     $amount = $self->paid;
421
422   }
423
424   # insert locations
425   foreach my $l (qw(bill_location ship_location)) {
426
427     my $loc = delete $self->hashref->{$l} or next;
428
429     if ( !$loc->locationnum ) {
430       # warn the location that we're going to insert it with no custnum
431       $loc->set(custnum_pending => 1);
432       warn "  inserting $l\n"
433         if $DEBUG > 1;
434       my $error = $loc->insert;
435       if ( $error ) {
436         $dbh->rollback if $oldAutoCommit;
437         my $label = $l eq 'ship_location' ? 'service' : 'billing';
438         return "$error (in $label location)";
439       }
440
441     } elsif ( $loc->prospectnum ) {
442
443       $loc->prospectnum('');
444       $loc->set(custnum_pending => 1);
445       my $error = $loc->replace;
446       if ( $error ) {
447         $dbh->rollback if $oldAutoCommit;
448         my $label = $l eq 'ship_location' ? 'service' : 'billing';
449         return "$error (moving $label location)";
450       }
451
452     } elsif ( ($loc->custnum || 0) > 0 ) {
453       # then it somehow belongs to another customer--shouldn't happen
454       $dbh->rollback if $oldAutoCommit;
455       return "$l belongs to customer ".$loc->custnum;
456     }
457     # else it already belongs to this customer 
458     # (happens when ship_location is identical to bill_location)
459
460     $self->set($l.'num', $loc->locationnum);
461
462     if ( $self->get($l.'num') eq '' ) {
463       $dbh->rollback if $oldAutoCommit;
464       return "$l not set";
465     }
466   }
467
468   warn "  inserting $self\n"
469     if $DEBUG > 1;
470
471   $self->signupdate(time) unless $self->signupdate;
472
473   $self->auto_agent_custid()
474     if $conf->config('cust_main-auto_agent_custid') && ! $self->agent_custid;
475
476   my $error =  $self->check_payinfo_cardtype
477             || $self->SUPER::insert;
478   if ( $error ) {
479     $dbh->rollback if $oldAutoCommit;
480     #return "inserting cust_main record (transaction rolled back): $error";
481     return $error;
482   }
483
484   # now set cust_location.custnum
485   foreach my $l (qw(bill_location ship_location)) {
486     warn "  setting $l.custnum\n"
487       if $DEBUG > 1;
488     my $loc = $self->$l or next;
489     unless ( $loc->custnum ) {
490       $loc->set(custnum => $self->custnum);
491       $error ||= $loc->replace;
492     }
493
494     if ( $error ) {
495       $dbh->rollback if $oldAutoCommit;
496       return "error setting $l custnum: $error";
497     }
498   }
499
500   warn "  setting customer tags\n"
501     if $DEBUG > 1;
502
503   foreach my $tagnum ( @{ $self->tagnum || [] } ) {
504     my $cust_tag = new FS::cust_tag { 'tagnum'  => $tagnum,
505                                       'custnum' => $self->custnum };
506     my $error = $cust_tag->insert;
507     if ( $error ) {
508       $dbh->rollback if $oldAutoCommit;
509       return $error;
510     }
511   }
512
513   my $prospectnum = delete $options{'prospectnum'};
514   if ( $prospectnum ) {
515
516     warn "  moving contacts and locations from prospect $prospectnum\n"
517       if $DEBUG > 1;
518
519     my $prospect_main =
520       qsearchs('prospect_main', { 'prospectnum' => $prospectnum } );
521     unless ( $prospect_main ) {
522       $dbh->rollback if $oldAutoCommit;
523       return "Unknown prospectnum $prospectnum";
524     }
525     $prospect_main->custnum($self->custnum);
526     $prospect_main->disabled('Y');
527     my $error = $prospect_main->replace;
528     if ( $error ) {
529       $dbh->rollback if $oldAutoCommit;
530       return $error;
531     }
532
533     foreach my $prospect_contact ( $prospect_main->prospect_contact ) {
534       my $cust_contact = new FS::cust_contact {
535         'custnum' => $self->custnum,
536         'invoice_dest' => 'Y', # invoice_dest currently not set for prospect contacts
537         map { $_ => $prospect_contact->$_() } qw( contactnum classnum comment )
538       };
539       my $error =  $cust_contact->insert
540                 || $prospect_contact->delete;
541       if ( $error ) {
542         $dbh->rollback if $oldAutoCommit;
543         return $error;
544       }
545     }
546
547     my @cust_location = $prospect_main->cust_location;
548     my @qual = $prospect_main->qual;
549
550     foreach my $r ( @cust_location, @qual ) {
551       $r->prospectnum('');
552       $r->custnum($self->custnum);
553       my $error = $r->replace;
554       if ( $error ) {
555         $dbh->rollback if $oldAutoCommit;
556         return $error;
557       }
558     }
559     # since we set invoice_dest on all migrated prospect contacts (for now),
560     # don't process invoicing_list.
561     delete $options{'invoicing_list'};
562     $invoicing_list = undef;
563   }
564
565   warn "  setting contacts\n"
566     if $DEBUG > 1;
567
568   $invoicing_list ||= $options{'invoicing_list'};
569   if ( $invoicing_list ) {
570
571     $invoicing_list = [ $invoicing_list ] if !ref($invoicing_list);
572
573     my $email = '';
574     foreach my $dest (@$invoicing_list ) {
575       if ($dest eq 'POST') {
576         $self->set('postal_invoice', 'Y');
577       } else {
578
579         my $contact_email = qsearchs('contact_email', { emailaddress => $dest });
580         if ( $contact_email ) {
581           my $cust_contact = FS::cust_contact->new({
582               contactnum    => $contact_email->contactnum,
583               custnum       => $self->custnum,
584           });
585           $cust_contact->set('invoice_dest', 'Y');
586           my $error = $cust_contact->insert;
587           if ( $error ) {
588             $dbh->rollback if $oldAutoCommit;
589             return "$error (linking to email address $dest)";
590           }
591
592         } else {
593           # this email address is not yet linked to any contact
594           $email .= ',' if length($email);
595           $email .= $dest;
596         }
597       }
598     }
599
600     my $contact = FS::contact->new({
601       'custnum'       => $self->get('custnum'),
602       'last'          => $self->get('last'),
603       'first'         => $self->get('first'),
604       'emailaddress'  => $email,
605       'invoice_dest'  => 'Y', # yes, you can set this via the contact
606     });
607     my $error = $contact->insert;
608     if ( $error ) {
609       $dbh->rollback if $oldAutoCommit;
610       return $error;
611     }
612
613   }
614
615   if ( my $contact = delete $options{'contact'} ) {
616
617     foreach my $c ( @$contact ) {
618       $c->custnum($self->custnum);
619       my $error = $c->insert;
620       if ( $error ) {
621         $dbh->rollback if $oldAutoCommit;
622         return $error;
623       }
624
625     }
626
627   } elsif ( my $contact_params = delete $options{'contact_params'} ) {
628
629     my $error = $self->process_o2m( 'table'  => 'contact',
630                                     'fields' => FS::contact->cgi_contact_fields,
631                                     'params' => $contact_params,
632                                   );
633     if ( $error ) {
634       $dbh->rollback if $oldAutoCommit;
635       return $error;
636     }
637   }
638
639   warn "  setting cust_payby\n"
640     if $DEBUG > 1;
641
642   if ( $options{cust_payby} ) {
643
644     foreach my $cust_payby ( @{ $options{cust_payby} } ) {
645       $cust_payby->custnum($self->custnum);
646       my $error = $cust_payby->insert;
647       if ( $error ) {
648         $dbh->rollback if $oldAutoCommit;
649         return $error;
650       }
651     }
652
653   } elsif ( my $cust_payby_params = delete $options{'cust_payby_params'} ) {
654
655     my $error = $self->process_o2m(
656       'table'         => 'cust_payby',
657       'fields'        => FS::cust_payby->cgi_cust_payby_fields,
658       'params'        => $cust_payby_params,
659       'hash_callback' => \&FS::cust_payby::cgi_hash_callback,
660     );
661     if ( $error ) {
662       $dbh->rollback if $oldAutoCommit;
663       return $error;
664     }
665
666   }
667
668   warn "  setting cust_main_exemption\n"
669     if $DEBUG > 1;
670
671   my $tax_exemption = delete $options{'tax_exemption'};
672   if ( $tax_exemption ) {
673
674     $tax_exemption = { map { $_ => '' } @$tax_exemption }
675       if ref($tax_exemption) eq 'ARRAY';
676
677     foreach my $taxname ( keys %$tax_exemption ) {
678       my $cust_main_exemption = new FS::cust_main_exemption {
679         'custnum'       => $self->custnum,
680         'taxname'       => $taxname,
681         'exempt_number' => $tax_exemption->{$taxname},
682       };
683       my $error = $cust_main_exemption->insert;
684       if ( $error ) {
685         $dbh->rollback if $oldAutoCommit;
686         return "inserting cust_main_exemption (transaction rolled back): $error";
687       }
688     }
689   }
690
691   warn "  ordering packages\n"
692     if $DEBUG > 1;
693
694   $error = $self->order_pkgs( $cust_pkgs,
695                               %options,
696                               'seconds_ref'    => \$seconds,
697                               'upbytes_ref'    => \$upbytes,
698                               'downbytes_ref'  => \$downbytes,
699                               'totalbytes_ref' => \$totalbytes,
700                             );
701   if ( $error ) {
702     $dbh->rollback if $oldAutoCommit;
703     return $error;
704   }
705
706   if ( $seconds ) {
707     $dbh->rollback if $oldAutoCommit;
708     return "No svc_acct record to apply pre-paid time";
709   }
710   if ( $upbytes || $downbytes || $totalbytes ) {
711     $dbh->rollback if $oldAutoCommit;
712     return "No svc_acct record to apply pre-paid data";
713   }
714
715   if ( $amount ) {
716     warn "  inserting initial $payby payment of $amount\n"
717       if $DEBUG > 1;
718     $error = $self->insert_cust_pay($payby, $amount, $prepay_identifier);
719     if ( $error ) {
720       $dbh->rollback if $oldAutoCommit;
721       return "inserting payment (transaction rolled back): $error";
722     }
723   }
724
725   unless ( $import || $skip_fuzzyfiles ) {
726     warn "  queueing fuzzyfiles update\n"
727       if $DEBUG > 1;
728     $error = $self->queue_fuzzyfiles_update;
729     if ( $error ) {
730       $dbh->rollback if $oldAutoCommit;
731       return "updating fuzzy search cache: $error";
732     }
733   }
734
735   # FS::geocode_Mixin::after_insert or something?
736   if ( $conf->config('tax_district_method') and !$import ) {
737     # if anything non-empty, try to look it up
738     my $queue = new FS::queue {
739       'job'     => 'FS::geocode_Mixin::process_district_update',
740       'custnum' => $self->custnum,
741     };
742     my $error = $queue->insert( ref($self), $self->custnum );
743     if ( $error ) {
744       $dbh->rollback if $oldAutoCommit;
745       return "queueing tax district update: $error";
746     }
747   }
748
749   # cust_main exports!
750   warn "  exporting\n" if $DEBUG > 1;
751
752   my $export_args = $options{'export_args'} || [];
753
754   my @part_export =
755     map qsearch( 'part_export', {exportnum=>$_} ),
756       $conf->config('cust_main-exports'); #, $agentnum
757
758   foreach my $part_export ( @part_export ) {
759     my $error = $part_export->export_insert($self, @$export_args);
760     if ( $error ) {
761       $dbh->rollback if $oldAutoCommit;
762       return "exporting to ". $part_export->exporttype.
763              " (transaction rolled back): $error";
764     }
765   }
766
767   #foreach my $depend_jobnum ( @$depend_jobnums ) {
768   #    warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
769   #      if $DEBUG;
770   #    foreach my $jobnum ( @jobnums ) {
771   #      my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
772   #      warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
773   #        if $DEBUG;
774   #      my $error = $queue->depend_insert($depend_jobnum);
775   #      if ( $error ) {
776   #        $dbh->rollback if $oldAutoCommit;
777   #        return "error queuing job dependancy: $error";
778   #      }
779   #    }
780   #  }
781   #
782   #}
783   #
784   #if ( exists $options{'jobnums'} ) {
785   #  push @{ $options{'jobnums'} }, @jobnums;
786   #}
787
788   warn "  insert complete; committing transaction\n"
789     if $DEBUG > 1;
790
791   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
792   '';
793
794 }
795
796 use File::CounterFile;
797 sub auto_agent_custid {
798   my $self = shift;
799
800   my $format = $conf->config('cust_main-auto_agent_custid');
801   my $agent_custid;
802   if ( $format eq '1YMMXXXXXXXX' ) {
803
804     my $counter = new File::CounterFile 'cust_main.agent_custid';
805     $counter->lock;
806
807     my $ym = 100000000000 + time2str('%y%m00000000', time);
808     if ( $ym > $counter->value ) {
809       $counter->{'value'} = $agent_custid = $ym;
810       $counter->{'updated'} = 1;
811     } else {
812       $agent_custid = $counter->inc;
813     }
814
815     $counter->unlock;
816
817   } else {
818     die "Unknown cust_main-auto_agent_custid format: $format";
819   }
820
821   $self->agent_custid($agent_custid);
822
823 }
824
825 =item PACKAGE METHODS
826
827 Documentation on customer package methods has been moved to
828 L<FS::cust_main::Packages>.
829
830 =item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF, UPBYTEREF, DOWNBYTEREF ]
831
832 Recharges this (existing) customer with the specified prepaid card (see
833 L<FS::prepay_credit>), specified either by I<identifier> or as an
834 FS::prepay_credit object.  If there is an error, returns the error, otherwise
835 returns false.
836
837 Optionally, five scalar references can be passed as well.  They will have their
838 values filled in with the amount, number of seconds, and number of upload,
839 download, and total bytes applied by this prepaid card.
840
841 =cut
842
843 #the ref bullshit here should be refactored like get_prepay.  MyAccount.pm is
844 #the only place that uses these args
845 sub recharge_prepay { 
846   my( $self, $prepay_credit, $amountref, $secondsref, 
847       $upbytesref, $downbytesref, $totalbytesref ) = @_;
848
849   local $SIG{HUP} = 'IGNORE';
850   local $SIG{INT} = 'IGNORE';
851   local $SIG{QUIT} = 'IGNORE';
852   local $SIG{TERM} = 'IGNORE';
853   local $SIG{TSTP} = 'IGNORE';
854   local $SIG{PIPE} = 'IGNORE';
855
856   my $oldAutoCommit = $FS::UID::AutoCommit;
857   local $FS::UID::AutoCommit = 0;
858   my $dbh = dbh;
859
860   my( $amount, $seconds, $upbytes, $downbytes, $totalbytes) = ( 0, 0, 0, 0, 0 );
861
862   my $error = $self->get_prepay( $prepay_credit,
863                                  'amount_ref'     => \$amount,
864                                  'seconds_ref'    => \$seconds,
865                                  'upbytes_ref'    => \$upbytes,
866                                  'downbytes_ref'  => \$downbytes,
867                                  'totalbytes_ref' => \$totalbytes,
868                                )
869            || $self->increment_seconds($seconds)
870            || $self->increment_upbytes($upbytes)
871            || $self->increment_downbytes($downbytes)
872            || $self->increment_totalbytes($totalbytes)
873            || $self->insert_cust_pay_prepay( $amount,
874                                              ref($prepay_credit)
875                                                ? $prepay_credit->identifier
876                                                : $prepay_credit
877                                            );
878
879   if ( $error ) {
880     $dbh->rollback if $oldAutoCommit;
881     return $error;
882   }
883
884   if ( defined($amountref)  ) { $$amountref  = $amount;  }
885   if ( defined($secondsref) ) { $$secondsref = $seconds; }
886   if ( defined($upbytesref) ) { $$upbytesref = $upbytes; }
887   if ( defined($downbytesref) ) { $$downbytesref = $downbytes; }
888   if ( defined($totalbytesref) ) { $$totalbytesref = $totalbytes; }
889
890   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
891   '';
892
893 }
894
895 =item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , OPTION => VALUE ... ]
896
897 Looks up and deletes a prepaid card (see L<FS::prepay_credit>),
898 specified either by I<identifier> or as an FS::prepay_credit object.
899
900 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
901 incremented by the values of the prepaid card.
902
903 If the prepaid card specifies an I<agentnum> (see L<FS::agent>), it is used to
904 check or set this customer's I<agentnum>.
905
906 If there is an error, returns the error, otherwise returns false.
907
908 =cut
909
910
911 sub get_prepay {
912   my( $self, $prepay_credit, %opt ) = @_;
913
914   local $SIG{HUP} = 'IGNORE';
915   local $SIG{INT} = 'IGNORE';
916   local $SIG{QUIT} = 'IGNORE';
917   local $SIG{TERM} = 'IGNORE';
918   local $SIG{TSTP} = 'IGNORE';
919   local $SIG{PIPE} = 'IGNORE';
920
921   my $oldAutoCommit = $FS::UID::AutoCommit;
922   local $FS::UID::AutoCommit = 0;
923   my $dbh = dbh;
924
925   unless ( ref($prepay_credit) ) {
926
927     my $identifier = $prepay_credit;
928
929     $prepay_credit = qsearchs(
930       'prepay_credit',
931       { 'identifier' => $identifier },
932       '',
933       'FOR UPDATE'
934     );
935
936     unless ( $prepay_credit ) {
937       $dbh->rollback if $oldAutoCommit;
938       return "Invalid prepaid card: ". $identifier;
939     }
940
941   }
942
943   if ( $prepay_credit->agentnum ) {
944     if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
945       $dbh->rollback if $oldAutoCommit;
946       return "prepaid card not valid for agent ". $self->agentnum;
947     }
948     $self->agentnum($prepay_credit->agentnum);
949   }
950
951   my $error = $prepay_credit->delete;
952   if ( $error ) {
953     $dbh->rollback if $oldAutoCommit;
954     return "removing prepay_credit (transaction rolled back): $error";
955   }
956
957   ${ $opt{$_.'_ref'} } += $prepay_credit->$_()
958     for grep $opt{$_.'_ref'}, qw( amount seconds upbytes downbytes totalbytes );
959
960   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
961   '';
962
963 }
964
965 =item increment_upbytes SECONDS
966
967 Updates this customer's single or primary account (see L<FS::svc_acct>) by
968 the specified number of upbytes.  If there is an error, returns the error,
969 otherwise returns false.
970
971 =cut
972
973 sub increment_upbytes {
974   _increment_column( shift, 'upbytes', @_);
975 }
976
977 =item increment_downbytes SECONDS
978
979 Updates this customer's single or primary account (see L<FS::svc_acct>) by
980 the specified number of downbytes.  If there is an error, returns the error,
981 otherwise returns false.
982
983 =cut
984
985 sub increment_downbytes {
986   _increment_column( shift, 'downbytes', @_);
987 }
988
989 =item increment_totalbytes SECONDS
990
991 Updates this customer's single or primary account (see L<FS::svc_acct>) by
992 the specified number of totalbytes.  If there is an error, returns the error,
993 otherwise returns false.
994
995 =cut
996
997 sub increment_totalbytes {
998   _increment_column( shift, 'totalbytes', @_);
999 }
1000
1001 =item increment_seconds SECONDS
1002
1003 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1004 the specified number of seconds.  If there is an error, returns the error,
1005 otherwise returns false.
1006
1007 =cut
1008
1009 sub increment_seconds {
1010   _increment_column( shift, 'seconds', @_);
1011 }
1012
1013 =item _increment_column AMOUNT
1014
1015 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1016 the specified number of seconds or bytes.  If there is an error, returns
1017 the error, otherwise returns false.
1018
1019 =cut
1020
1021 sub _increment_column {
1022   my( $self, $column, $amount ) = @_;
1023   warn "$me increment_column called: $column, $amount\n"
1024     if $DEBUG;
1025
1026   return '' unless $amount;
1027
1028   my @cust_pkg = grep { $_->part_pkg->svcpart('svc_acct') }
1029                       $self->ncancelled_pkgs;
1030
1031   if ( ! @cust_pkg ) {
1032     return 'No packages with primary or single services found'.
1033            ' to apply pre-paid time';
1034   } elsif ( scalar(@cust_pkg) > 1 ) {
1035     #maybe have a way to specify the package/account?
1036     return 'Multiple packages found to apply pre-paid time';
1037   }
1038
1039   my $cust_pkg = $cust_pkg[0];
1040   warn "  found package pkgnum ". $cust_pkg->pkgnum. "\n"
1041     if $DEBUG > 1;
1042
1043   my @cust_svc =
1044     $cust_pkg->cust_svc( $cust_pkg->part_pkg->svcpart('svc_acct') );
1045
1046   if ( ! @cust_svc ) {
1047     return 'No account found to apply pre-paid time';
1048   } elsif ( scalar(@cust_svc) > 1 ) {
1049     return 'Multiple accounts found to apply pre-paid time';
1050   }
1051   
1052   my $svc_acct = $cust_svc[0]->svc_x;
1053   warn "  found service svcnum ". $svc_acct->pkgnum.
1054        ' ('. $svc_acct->email. ")\n"
1055     if $DEBUG > 1;
1056
1057   $column = "increment_$column";
1058   $svc_acct->$column($amount);
1059
1060 }
1061
1062 =item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
1063
1064 Inserts a prepayment in the specified amount for this customer.  An optional
1065 second argument can specify the prepayment identifier for tracking purposes.
1066 If there is an error, returns the error, otherwise returns false.
1067
1068 =cut
1069
1070 sub insert_cust_pay_prepay {
1071   shift->insert_cust_pay('PREP', @_);
1072 }
1073
1074 =item insert_cust_pay_cash AMOUNT [ PAYINFO ]
1075
1076 Inserts a cash payment in the specified amount for this customer.  An optional
1077 second argument can specify the payment identifier for tracking purposes.
1078 If there is an error, returns the error, otherwise returns false.
1079
1080 =cut
1081
1082 sub insert_cust_pay_cash {
1083   shift->insert_cust_pay('CASH', @_);
1084 }
1085
1086 =item insert_cust_pay_west AMOUNT [ PAYINFO ]
1087
1088 Inserts a Western Union payment in the specified amount for this customer.  An
1089 optional second argument can specify the prepayment identifier for tracking
1090 purposes.  If there is an error, returns the error, otherwise returns false.
1091
1092 =cut
1093
1094 sub insert_cust_pay_west {
1095   shift->insert_cust_pay('WEST', @_);
1096 }
1097
1098 sub insert_cust_pay {
1099   my( $self, $payby, $amount ) = splice(@_, 0, 3);
1100   my $payinfo = scalar(@_) ? shift : '';
1101
1102   my $cust_pay = new FS::cust_pay {
1103     'custnum' => $self->custnum,
1104     'paid'    => sprintf('%.2f', $amount),
1105     #'_date'   => #date the prepaid card was purchased???
1106     'payby'   => $payby,
1107     'payinfo' => $payinfo,
1108   };
1109   $cust_pay->insert;
1110
1111 }
1112
1113 =item delete [ OPTION => VALUE ... ]
1114
1115 This deletes the customer.  If there is an error, returns the error, otherwise
1116 returns false.
1117
1118 This will completely remove all traces of the customer record.  This is not
1119 what you want when a customer cancels service; for that, cancel all of the
1120 customer's packages (see L</cancel>).
1121
1122 If the customer has any uncancelled packages, you need to pass a new (valid)
1123 customer number for those packages to be transferred to, as the "new_customer"
1124 option.  Cancelled packages will be deleted.  Did I mention that this is NOT
1125 what you want when a customer cancels service and that you really should be
1126 looking at L<FS::cust_pkg/cancel>?  
1127
1128 You can't delete a customer with invoices (see L<FS::cust_bill>),
1129 statements (see L<FS::cust_statement>), credits (see L<FS::cust_credit>),
1130 payments (see L<FS::cust_pay>) or refunds (see L<FS::cust_refund>), unless you
1131 set the "delete_financials" option to a true value.
1132
1133 =cut
1134
1135 sub delete {
1136   my( $self, %opt ) = @_;
1137
1138   local $SIG{HUP} = 'IGNORE';
1139   local $SIG{INT} = 'IGNORE';
1140   local $SIG{QUIT} = 'IGNORE';
1141   local $SIG{TERM} = 'IGNORE';
1142   local $SIG{TSTP} = 'IGNORE';
1143   local $SIG{PIPE} = 'IGNORE';
1144
1145   my $oldAutoCommit = $FS::UID::AutoCommit;
1146   local $FS::UID::AutoCommit = 0;
1147   my $dbh = dbh;
1148
1149   if ( qsearch('agent', { 'agent_custnum' => $self->custnum } ) ) {
1150      $dbh->rollback if $oldAutoCommit;
1151      return "Can't delete a master agent customer";
1152   }
1153
1154   #use FS::access_user
1155   if ( qsearch('access_user', { 'user_custnum' => $self->custnum } ) ) {
1156      $dbh->rollback if $oldAutoCommit;
1157      return "Can't delete a master employee customer";
1158   }
1159
1160   tie my %financial_tables, 'Tie::IxHash',
1161     'cust_bill'      => 'invoices',
1162     'cust_statement' => 'statements',
1163     'cust_credit'    => 'credits',
1164     'cust_pay'       => 'payments',
1165     'cust_refund'    => 'refunds',
1166   ;
1167    
1168   foreach my $table ( keys %financial_tables ) {
1169
1170     my @records = $self->$table();
1171
1172     if ( @records && ! $opt{'delete_financials'} ) {
1173       $dbh->rollback if $oldAutoCommit;
1174       return "Can't delete a customer with ". $financial_tables{$table};
1175     }
1176
1177     foreach my $record ( @records ) {
1178       my $error = $record->delete;
1179       if ( $error ) {
1180         $dbh->rollback if $oldAutoCommit;
1181         return "Error deleting ". $financial_tables{$table}. ": $error\n";
1182       }
1183     }
1184
1185   }
1186
1187   my @cust_pkg = $self->ncancelled_pkgs;
1188   if ( @cust_pkg ) {
1189     my $new_custnum = $opt{'new_custnum'};
1190     unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
1191       $dbh->rollback if $oldAutoCommit;
1192       return "Invalid new customer number: $new_custnum";
1193     }
1194     foreach my $cust_pkg ( @cust_pkg ) {
1195       my %hash = $cust_pkg->hash;
1196       $hash{'custnum'} = $new_custnum;
1197       my $new_cust_pkg = new FS::cust_pkg ( \%hash );
1198       my $error = $new_cust_pkg->replace($cust_pkg,
1199                                          options => { $cust_pkg->options },
1200                                         );
1201       if ( $error ) {
1202         $dbh->rollback if $oldAutoCommit;
1203         return $error;
1204       }
1205     }
1206   }
1207   my @cancelled_cust_pkg = $self->all_pkgs;
1208   foreach my $cust_pkg ( @cancelled_cust_pkg ) {
1209     my $error = $cust_pkg->delete;
1210     if ( $error ) {
1211       $dbh->rollback if $oldAutoCommit;
1212       return $error;
1213     }
1214   }
1215
1216   #cust_tax_adjustment in financials?
1217   #cust_pay_pending?  ouch
1218   foreach my $table (qw(
1219     cust_main_invoice cust_main_exemption cust_tag cust_attachment contact
1220     cust_payby cust_location cust_main_note cust_tax_adjustment
1221     cust_pay_void cust_pay_batch queue cust_tax_exempt
1222   )) {
1223     foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
1224       my $error = $record->delete;
1225       if ( $error ) {
1226         $dbh->rollback if $oldAutoCommit;
1227         return $error;
1228       }
1229     }
1230   }
1231
1232   my $sth = $dbh->prepare(
1233     'UPDATE cust_main SET referral_custnum = NULL WHERE referral_custnum = ?'
1234   ) or do {
1235     my $errstr = $dbh->errstr;
1236     $dbh->rollback if $oldAutoCommit;
1237     return $errstr;
1238   };
1239   $sth->execute($self->custnum) or do {
1240     my $errstr = $sth->errstr;
1241     $dbh->rollback if $oldAutoCommit;
1242     return $errstr;
1243   };
1244
1245   #tickets
1246
1247   my $ticket_dbh = '';
1248   if ($conf->config('ticket_system') eq 'RT_Internal') {
1249     $ticket_dbh = $dbh;
1250   } elsif ($conf->config('ticket_system') eq 'RT_External') {
1251     my ($datasrc, $user, $pass) = $conf->config('ticket_system-rt_external_datasrc');
1252     $ticket_dbh = DBI->connect($datasrc, $user, $pass, { 'ChopBlanks' => 1 });
1253       #or die "RT_External DBI->connect error: $DBI::errstr\n";
1254   }
1255
1256   if ( $ticket_dbh ) {
1257
1258     my $ticket_sth = $ticket_dbh->prepare(
1259       'DELETE FROM Links WHERE Target = ?'
1260     ) or do {
1261       my $errstr = $ticket_dbh->errstr;
1262       $dbh->rollback if $oldAutoCommit;
1263       return $errstr;
1264     };
1265     $ticket_sth->execute('freeside://freeside/cust_main/'.$self->custnum)
1266       or do {
1267         my $errstr = $ticket_sth->errstr;
1268         $dbh->rollback if $oldAutoCommit;
1269         return $errstr;
1270       };
1271
1272     #check and see if the customer is the only link on the ticket, and
1273     #if so, set the ticket to deleted status in RT?
1274     #maybe someday, for now this will at least fix tickets not displaying
1275
1276   }
1277
1278   #delete the customer record
1279
1280   my $error = $self->SUPER::delete;
1281   if ( $error ) {
1282     $dbh->rollback if $oldAutoCommit;
1283     return $error;
1284   }
1285
1286   # cust_main exports!
1287
1288   #my $export_args = $options{'export_args'} || [];
1289
1290   my @part_export =
1291     map qsearch( 'part_export', {exportnum=>$_} ),
1292       $conf->config('cust_main-exports'); #, $agentnum
1293
1294   foreach my $part_export ( @part_export ) {
1295     my $error = $part_export->export_delete( $self ); #, @$export_args);
1296     if ( $error ) {
1297       $dbh->rollback if $oldAutoCommit;
1298       return "exporting to ". $part_export->exporttype.
1299              " (transaction rolled back): $error";
1300     }
1301   }
1302
1303   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1304   '';
1305
1306 }
1307
1308 =item replace [ OLD_RECORD ] [ INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
1309
1310 Replaces the OLD_RECORD with this one in the database.  If there is an error,
1311 returns the error, otherwise returns false.
1312
1313 To change the customer's address, set the pseudo-fields C<bill_location> and
1314 C<ship_location>.  The address will still only change if at least one of the
1315 address fields differs from the existing values.
1316
1317 INVOICING_LIST_ARYREF: If you pass an arrayref to this method, it will be
1318 set as the contact email address for a default contact with the same name as
1319 the customer.
1320
1321 Currently available options are: I<tax_exemption>, I<cust_payby_params>, 
1322 I<contact_params>, I<invoicing_list>.
1323
1324 The I<tax_exemption> option can be set to an arrayref of tax names or a hashref
1325 of tax names and exemption numbers.  FS::cust_main_exemption records will be
1326 deleted and inserted as appropriate.
1327
1328 I<cust_payby_params> and I<contact_params> can be hashrefs of named parameter
1329 groups (describing the customer's payment methods and contacts, respectively)
1330 in the style supported by L<FS::o2m_Common/process_o2m>. See L<FS::cust_payby>
1331 and L<FS::contact> for the fields these can contain.
1332
1333 I<invoicing_list> is a synonym for the INVOICING_LIST_ARYREF parameter, and
1334 should be used instead if possible.
1335
1336 =cut
1337
1338 sub replace {
1339   my $self = shift;
1340
1341   my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
1342               ? shift
1343               : $self->replace_old;
1344
1345   my @param = @_;
1346
1347   warn "$me replace called\n"
1348     if $DEBUG;
1349
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');
1355
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 );
1360
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 );
1365
1366   if (    $self->payby =~ /^(CARD|DCRD)$/
1367        && $old->payinfo ne $self->payinfo
1368        && $old->paymask ne $self->paymask )
1369   {
1370     my $error = $self->check_payinfo_cardtype;
1371     return $error if $error;
1372   }
1373
1374   return "Invoicing locale is required"
1375     if $old->locale
1376     && ! $self->locale
1377     && $conf->exists('cust_main-require_locale');
1378
1379   local $SIG{HUP} = 'IGNORE';
1380   local $SIG{INT} = 'IGNORE';
1381   local $SIG{QUIT} = 'IGNORE';
1382   local $SIG{TERM} = 'IGNORE';
1383   local $SIG{TSTP} = 'IGNORE';
1384   local $SIG{PIPE} = 'IGNORE';
1385
1386   my $oldAutoCommit = $FS::UID::AutoCommit;
1387   local $FS::UID::AutoCommit = 0;
1388   my $dbh = dbh;
1389
1390   for my $l (qw(bill_location ship_location)) {
1391     #my $old_loc = $old->$l;
1392     my $new_loc = $self->$l or next;
1393
1394     # find the existing location if there is one
1395     $new_loc->set('custnum' => $self->custnum);
1396     my $error = $new_loc->find_or_insert;
1397     if ( $error ) {
1398       $dbh->rollback if $oldAutoCommit;
1399       return $error;
1400     }
1401     $self->set($l.'num', $new_loc->locationnum);
1402   } #for $l
1403
1404   my $invoicing_list;
1405   if ( @param && ref($param[0]) eq 'ARRAY' ) { # INVOICING_LIST_ARYREF
1406     warn "cust_main::replace: using deprecated invoicing list argument";
1407     $invoicing_list = shift @param;
1408   }
1409
1410   my %options = @param;
1411
1412   $invoicing_list ||= $options{invoicing_list};
1413
1414   my @contacts = map { $_->contact } $self->cust_contact;
1415   # find a contact that matches the customer's name
1416   my ($implicit_contact) = grep { $_->first eq $old->get('first')
1417                               and $_->last  eq $old->get('last') }
1418                             @contacts;
1419   $implicit_contact ||= FS::contact->new({
1420       'custnum'       => $self->custnum,
1421       'locationnum'   => $self->get('bill_locationnum'),
1422   });
1423
1424   # for any of these that are already contact emails, link to the existing
1425   # contact
1426   if ( $invoicing_list ) {
1427     my $email = '';
1428
1429     # kind of like process_m2m on these, except:
1430     # - the other side is two tables in a join
1431     # - and we might have to create new contact_emails
1432     # - and possibly a new contact
1433     # 
1434     # Find existing invoice emails that aren't on the implicit contact.
1435     # Any of these that are not on the new invoicing list will be removed.
1436     my %old_email_cust_contact;
1437     foreach my $cust_contact ($self->cust_contact) {
1438       next if !$cust_contact->invoice_dest;
1439       next if $cust_contact->contactnum == ($implicit_contact->contactnum || 0);
1440
1441       foreach my $contact_email ($cust_contact->contact->contact_email) {
1442         $old_email_cust_contact{ $contact_email->emailaddress } = $cust_contact;
1443       }
1444     }
1445
1446     foreach my $dest (@$invoicing_list) {
1447
1448       if ($dest eq 'POST') {
1449
1450         $self->set('postal_invoice', 'Y');
1451
1452       } elsif ( exists($old_email_cust_contact{$dest}) ) {
1453
1454         delete $old_email_cust_contact{$dest}; # don't need to remove it, then
1455
1456       } else {
1457
1458         # See if it belongs to some other contact; if so, link it.
1459         my $contact_email = qsearchs('contact_email', { emailaddress => $dest });
1460         if ( $contact_email
1461              and $contact_email->contactnum != ($implicit_contact->contactnum || 0) ) {
1462           my $cust_contact = qsearchs('cust_contact', {
1463               contactnum  => $contact_email->contactnum,
1464               custnum     => $self->custnum,
1465           }) || FS::cust_contact->new({
1466               contactnum    => $contact_email->contactnum,
1467               custnum       => $self->custnum,
1468           });
1469           $cust_contact->set('invoice_dest', 'Y');
1470           my $error = $cust_contact->custcontactnum ?
1471                         $cust_contact->replace : $cust_contact->insert;
1472           if ( $error ) {
1473             $dbh->rollback if $oldAutoCommit;
1474             return "$error (linking to email address $dest)";
1475           }
1476
1477         } else {
1478           # This email address is not yet linked to any contact, so it will
1479           # be added to the implicit contact.
1480           $email .= ',' if length($email);
1481           $email .= $dest;
1482         }
1483       }
1484     }
1485
1486     foreach my $remove_dest (keys %old_email_cust_contact) {
1487       my $cust_contact = $old_email_cust_contact{$remove_dest};
1488       # These were not in the list of requested destinations, so take them off.
1489       $cust_contact->set('invoice_dest', '');
1490       my $error = $cust_contact->replace;
1491       if ( $error ) {
1492         $dbh->rollback if $oldAutoCommit;
1493         return "$error (unlinking email address $remove_dest)";
1494       }
1495     }
1496
1497     # make sure it keeps up with the changed customer name, if any
1498     $implicit_contact->set('last', $self->get('last'));
1499     $implicit_contact->set('first', $self->get('first'));
1500     $implicit_contact->set('emailaddress', $email);
1501     $implicit_contact->set('invoice_dest', 'Y');
1502     $implicit_contact->set('custnum', $self->custnum);
1503
1504     my $error;
1505     if ( $implicit_contact->contactnum ) {
1506       $error = $implicit_contact->replace;
1507     } elsif ( length($email) ) { # don't create a new contact if not needed
1508       $error = $implicit_contact->insert;
1509     }
1510
1511     if ( $error ) {
1512       $dbh->rollback if $oldAutoCommit;
1513       return "$error (adding email address $email)";
1514     }
1515
1516   }
1517
1518   # replace the customer record
1519   my $error = $self->SUPER::replace($old);
1520
1521   if ( $error ) {
1522     $dbh->rollback if $oldAutoCommit;
1523     return $error;
1524   }
1525
1526   # now move packages to the new service location
1527   $self->set('ship_location', ''); #flush cache
1528   if ( $old->ship_locationnum and # should only be null during upgrade...
1529        $old->ship_locationnum != $self->ship_locationnum ) {
1530     $error = $old->ship_location->move_to($self->ship_location);
1531     if ( $error ) {
1532       $dbh->rollback if $oldAutoCommit;
1533       return $error;
1534     }
1535   }
1536   # don't move packages based on the billing location, but 
1537   # disable it if it's no longer in use
1538   if ( $old->bill_locationnum and
1539        $old->bill_locationnum != $self->bill_locationnum ) {
1540     $error = $old->bill_location->disable_if_unused;
1541     if ( $error ) {
1542       $dbh->rollback if $oldAutoCommit;
1543       return $error;
1544     }
1545   }
1546
1547   if ( $self->exists('tagnum') ) { #so we don't delete these on edit by accident
1548
1549     #this could be more efficient than deleting and re-inserting, if it matters
1550     foreach my $cust_tag (qsearch('cust_tag', {'custnum'=>$self->custnum} )) {
1551       my $error = $cust_tag->delete;
1552       if ( $error ) {
1553         $dbh->rollback if $oldAutoCommit;
1554         return $error;
1555       }
1556     }
1557     foreach my $tagnum ( @{ $self->tagnum || [] } ) {
1558       my $cust_tag = new FS::cust_tag { 'tagnum'  => $tagnum,
1559                                         'custnum' => $self->custnum };
1560       my $error = $cust_tag->insert;
1561       if ( $error ) {
1562         $dbh->rollback if $oldAutoCommit;
1563         return $error;
1564       }
1565     }
1566
1567   }
1568
1569   my $tax_exemption = delete $options{'tax_exemption'};
1570   if ( $tax_exemption ) {
1571
1572     $tax_exemption = { map { $_ => '' } @$tax_exemption }
1573       if ref($tax_exemption) eq 'ARRAY';
1574
1575     my %cust_main_exemption =
1576       map { $_->taxname => $_ }
1577           qsearch('cust_main_exemption', { 'custnum' => $old->custnum } );
1578
1579     foreach my $taxname ( keys %$tax_exemption ) {
1580
1581       if ( $cust_main_exemption{$taxname} && 
1582            $cust_main_exemption{$taxname}->exempt_number eq $tax_exemption->{$taxname}
1583          )
1584       {
1585         delete $cust_main_exemption{$taxname};
1586         next;
1587       }
1588
1589       my $cust_main_exemption = new FS::cust_main_exemption {
1590         'custnum'       => $self->custnum,
1591         'taxname'       => $taxname,
1592         'exempt_number' => $tax_exemption->{$taxname},
1593       };
1594       my $error = $cust_main_exemption->insert;
1595       if ( $error ) {
1596         $dbh->rollback if $oldAutoCommit;
1597         return "inserting cust_main_exemption (transaction rolled back): $error";
1598       }
1599     }
1600
1601     foreach my $cust_main_exemption ( values %cust_main_exemption ) {
1602       my $error = $cust_main_exemption->delete;
1603       if ( $error ) {
1604         $dbh->rollback if $oldAutoCommit;
1605         return "deleting cust_main_exemption (transaction rolled back): $error";
1606       }
1607     }
1608
1609   }
1610
1611   if ( my $cust_payby_params = delete $options{'cust_payby_params'} ) {
1612
1613     my $error = $self->process_o2m(
1614       'table'         => 'cust_payby',
1615       'fields'        => FS::cust_payby->cgi_cust_payby_fields,
1616       'params'        => $cust_payby_params,
1617       'hash_callback' => \&FS::cust_payby::cgi_hash_callback,
1618     );
1619     if ( $error ) {
1620       $dbh->rollback if $oldAutoCommit;
1621       return $error;
1622     }
1623
1624   }
1625
1626   if ( my $contact_params = delete $options{'contact_params'} ) {
1627
1628     # this can potentially replace contacts that were created by the
1629     # invoicing list argument, but the UI shouldn't allow both of them
1630     # to be specified
1631
1632     my $error = $self->process_o2m(
1633       'table'         => 'contact',
1634       'fields'        => FS::contact->cgi_contact_fields,
1635       'params'        => $contact_params,
1636     );
1637     if ( $error ) {
1638       $dbh->rollback if $oldAutoCommit;
1639       return $error;
1640     }
1641
1642   }
1643
1644   unless ( $import || $skip_fuzzyfiles ) {
1645     $error = $self->queue_fuzzyfiles_update;
1646     if ( $error ) {
1647       $dbh->rollback if $oldAutoCommit;
1648       return "updating fuzzy search cache: $error";
1649     }
1650   }
1651
1652   # tax district update in cust_location
1653
1654   # cust_main exports!
1655
1656   my $export_args = $options{'export_args'} || [];
1657
1658   my @part_export =
1659     map qsearch( 'part_export', {exportnum=>$_} ),
1660       $conf->config('cust_main-exports'); #, $agentnum
1661
1662   foreach my $part_export ( @part_export ) {
1663     my $error = $part_export->export_replace( $self, $old, @$export_args);
1664     if ( $error ) {
1665       $dbh->rollback if $oldAutoCommit;
1666       return "exporting to ". $part_export->exporttype.
1667              " (transaction rolled back): $error";
1668     }
1669   }
1670
1671   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1672   '';
1673
1674 }
1675
1676 =item queue_fuzzyfiles_update
1677
1678 Used by insert & replace to update the fuzzy search cache
1679
1680 =cut
1681
1682 use FS::cust_main::Search;
1683 sub queue_fuzzyfiles_update {
1684   my $self = shift;
1685
1686   local $SIG{HUP} = 'IGNORE';
1687   local $SIG{INT} = 'IGNORE';
1688   local $SIG{QUIT} = 'IGNORE';
1689   local $SIG{TERM} = 'IGNORE';
1690   local $SIG{TSTP} = 'IGNORE';
1691   local $SIG{PIPE} = 'IGNORE';
1692
1693   my $oldAutoCommit = $FS::UID::AutoCommit;
1694   local $FS::UID::AutoCommit = 0;
1695   my $dbh = dbh;
1696
1697   foreach my $field ( 'first', 'last', 'company', 'ship_company' ) {
1698     my $queue = new FS::queue { 
1699       'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
1700     };
1701     my @args = "cust_main.$field", $self->get($field);
1702     my $error = $queue->insert( @args );
1703     if ( $error ) {
1704       $dbh->rollback if $oldAutoCommit;
1705       return "queueing job (transaction rolled back): $error";
1706     }
1707   }
1708
1709   my @locations = ();
1710   push @locations, $self->bill_location if $self->bill_locationnum;
1711   push @locations, $self->ship_location if @locations && $self->has_ship_address;
1712   foreach my $location (@locations) {
1713     my $queue = new FS::queue { 
1714       'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
1715     };
1716     my @args = 'cust_location.address1', $location->address1;
1717     my $error = $queue->insert( @args );
1718     if ( $error ) {
1719       $dbh->rollback if $oldAutoCommit;
1720       return "queueing job (transaction rolled back): $error";
1721     }
1722   }
1723
1724   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1725   '';
1726
1727 }
1728
1729 =item check
1730
1731 Checks all fields to make sure this is a valid customer record.  If there is
1732 an error, returns the error, otherwise returns false.  Called by the insert
1733 and replace methods.
1734
1735 =cut
1736
1737 sub check {
1738   my $self = shift;
1739
1740   warn "$me check BEFORE: \n". $self->_dump
1741     if $DEBUG > 2;
1742
1743   my $error =
1744     $self->ut_numbern('custnum')
1745     || $self->ut_number('agentnum')
1746     || $self->ut_textn('agent_custid')
1747     || $self->ut_number('refnum')
1748     || $self->ut_foreign_keyn('bill_locationnum', 'cust_location','locationnum')
1749     || $self->ut_foreign_keyn('ship_locationnum', 'cust_location','locationnum')
1750     || $self->ut_foreign_keyn('classnum', 'cust_class', 'classnum')
1751     || $self->ut_foreign_keyn('salesnum', 'sales', 'salesnum')
1752     || $self->ut_foreign_keyn('taxstatusnum', 'tax_status', 'taxstatusnum')
1753     || $self->ut_textn('custbatch')
1754     || $self->ut_name('last')
1755     || $self->ut_name('first')
1756     || $self->ut_snumbern('signupdate')
1757     || $self->ut_snumbern('birthdate')
1758     || $self->ut_namen('spouse_last')
1759     || $self->ut_namen('spouse_first')
1760     || $self->ut_snumbern('spouse_birthdate')
1761     || $self->ut_snumbern('anniversary_date')
1762     || $self->ut_textn('company')
1763     || $self->ut_textn('ship_company')
1764     || $self->ut_anything('comments')
1765     || $self->ut_numbern('referral_custnum')
1766     || $self->ut_textn('stateid')
1767     || $self->ut_textn('stateid_state')
1768     || $self->ut_textn('invoice_terms')
1769     || $self->ut_floatn('cdr_termination_percentage')
1770     || $self->ut_floatn('credit_limit')
1771     || $self->ut_numbern('billday')
1772     || $self->ut_numbern('prorate_day')
1773     || $self->ut_flag('edit_subject')
1774     || $self->ut_flag('calling_list_exempt')
1775     || $self->ut_flag('invoice_noemail')
1776     || $self->ut_flag('message_noemail')
1777     || $self->ut_enum('locale', [ '', FS::Locales->locales ])
1778     || $self->ut_currencyn('currency')
1779     || $self->ut_textn('po_number')
1780     || $self->ut_enum('complimentary', [ '', 'Y' ])
1781     || $self->ut_flag('invoice_ship_address')
1782     || $self->ut_flag('invoice_dest')
1783   ;
1784
1785   foreach (qw(company ship_company)) {
1786     my $company = $self->get($_);
1787     $company =~ s/^\s+//; 
1788     $company =~ s/\s+$//; 
1789     $company =~ s/\s+/ /g;
1790     $self->set($_, $company);
1791   }
1792
1793   #barf.  need message catalogs.  i18n.  etc.
1794   $error .= "Please select an advertising source."
1795     if $error =~ /^Illegal or empty \(numeric\) refnum: /;
1796   return $error if $error;
1797
1798   my $agent = qsearchs( 'agent', { 'agentnum' => $self->agentnum } )
1799     or return "Unknown agent";
1800
1801   if ( $self->currency ) {
1802     my $agent_currency = qsearchs( 'agent_currency', {
1803       'agentnum' => $agent->agentnum,
1804       'currency' => $self->currency,
1805     })
1806       or return "Agent ". $agent->agent.
1807                 " not permitted to offer ".  $self->currency. " invoicing";
1808   }
1809
1810   return "Unknown refnum"
1811     unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
1812
1813   return "Unknown referring custnum: ". $self->referral_custnum
1814     unless ! $self->referral_custnum 
1815            || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
1816
1817   if ( $self->ss eq '' ) {
1818     $self->ss('');
1819   } else {
1820     my $ss = $self->ss;
1821     $ss =~ s/\D//g;
1822     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
1823       or return "Illegal social security number: ". $self->ss;
1824     $self->ss("$1-$2-$3");
1825   }
1826
1827   #turn off invoice_ship_address if ship & bill are the same
1828   if ($self->bill_locationnum eq $self->ship_locationnum) {
1829     $self->invoice_ship_address('');
1830   }
1831
1832   # cust_main_county verification now handled by cust_location check
1833
1834   $error =
1835        $self->ut_phonen('daytime', $self->country)
1836     || $self->ut_phonen('night',   $self->country)
1837     || $self->ut_phonen('fax',     $self->country)
1838     || $self->ut_phonen('mobile',  $self->country)
1839   ;
1840   return $error if $error;
1841
1842   if ( $conf->exists('cust_main-require_phone', $self->agentnum)
1843        && ! $import
1844        && ! length($self->daytime) && ! length($self->night) && ! length($self->mobile)
1845      ) {
1846
1847     my $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/
1848                           ? 'Day Phone'
1849                           : FS::Msgcat::_gettext('daytime');
1850     my $night_label = FS::Msgcat::_gettext('night') =~ /^(night)?$/
1851                         ? 'Night Phone'
1852                         : FS::Msgcat::_gettext('night');
1853
1854     my $mobile_label = FS::Msgcat::_gettext('mobile') =~ /^(mobile)?$/
1855                         ? 'Mobile Phone'
1856                         : FS::Msgcat::_gettext('mobile');
1857
1858     return "$daytime_label, $night_label or $mobile_label is required"
1859   
1860   }
1861
1862   return "Please select an invoicing locale"
1863     if ! $self->locale
1864     && ! $self->custnum
1865     && $conf->exists('cust_main-require_locale');
1866
1867   foreach my $flag (qw( tax spool_cdr squelch_cdr archived email_csv_cdr )) {
1868     $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
1869     $self->$flag($1);
1870   }
1871
1872   $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
1873
1874   warn "$me check AFTER: \n". $self->_dump
1875     if $DEBUG > 2;
1876
1877   $self->SUPER::check;
1878 }
1879
1880 sub check_payinfo_cardtype {
1881   my $self = shift;
1882
1883   return '' unless $self->payby =~ /^(CARD|DCRD)$/;
1884
1885   my $payinfo = $self->payinfo;
1886   $payinfo =~ s/\D//g;
1887
1888   return '' if $payinfo =~ /^99\d{14}$/; #token
1889
1890   my %bop_card_types = map { $_=>1 } values %{ card_types() };
1891   my $cardtype = cardtype($payinfo);
1892
1893   return "$cardtype not accepted" unless $bop_card_types{$cardtype};
1894
1895   '';
1896
1897 }
1898
1899 =item replace_check
1900
1901 Additional checks for replace only.
1902
1903 =cut
1904
1905 sub replace_check {
1906   my ($new,$old) = @_;
1907   #preserve old value if global config is set
1908   if ($old && $conf->exists('invoice-ship_address')) {
1909     $new->invoice_ship_address($old->invoice_ship_address);
1910   }
1911   return '';
1912 }
1913
1914 =item addr_fields 
1915
1916 Returns a list of fields which have ship_ duplicates.
1917
1918 =cut
1919
1920 sub addr_fields {
1921   qw( last first company
1922       locationname
1923       address1 address2 city county state zip country
1924       latitude longitude
1925       daytime night fax mobile
1926     );
1927 }
1928
1929 =item has_ship_address
1930
1931 Returns true if this customer record has a separate shipping address.
1932
1933 =cut
1934
1935 sub has_ship_address {
1936   my $self = shift;
1937   $self->bill_locationnum != $self->ship_locationnum;
1938 }
1939
1940 =item location_hash
1941
1942 Returns a list of key/value pairs, with the following keys: address1, 
1943 adddress2, city, county, state, zip, country, district, and geocode.  The 
1944 shipping address is used if present.
1945
1946 =cut
1947
1948 sub location_hash {
1949   my $self = shift;
1950   $self->ship_location->location_hash;
1951 }
1952
1953 =item cust_location
1954
1955 Returns all locations (see L<FS::cust_location>) for this customer.
1956
1957 =cut
1958
1959 sub cust_location {
1960   my $self = shift;
1961   qsearch({
1962     'table'   => 'cust_location',
1963     'hashref' => { 'custnum'     => $self->custnum,
1964                    'prospectnum' => '',
1965                  },
1966     'order_by' => 'ORDER BY country, LOWER(state), LOWER(city), LOWER(county), LOWER(address1), LOWER(address2)',
1967   });
1968 }
1969
1970 =item cust_contact
1971
1972 Returns all contact associations (see L<FS::cust_contact>) for this customer.
1973
1974 =cut
1975
1976 sub cust_contact {
1977   my $self = shift;
1978   qsearch('cust_contact', { 'custnum' => $self->custnum } );
1979 }
1980
1981 =item cust_payby PAYBY
1982
1983 Returns all payment methods (see L<FS::cust_payby>) for this customer.
1984
1985 If one or more PAYBY are specified, returns only payment methods for specified PAYBY.
1986 Does not validate PAYBY.
1987
1988 =cut
1989
1990 sub cust_payby {
1991   my $self = shift;
1992   my @payby = @_;
1993   my $search = {
1994     'table'    => 'cust_payby',
1995     'hashref'  => { 'custnum' => $self->custnum },
1996     'order_by' => "ORDER BY payby IN ('CARD','CHEK') DESC, weight ASC",
1997   };
1998   $search->{'extra_sql'} = ' AND payby IN ( '.
1999                                join(',', map dbh->quote($_), @payby).
2000                              ' ) '
2001     if @payby;
2002
2003   qsearch($search);
2004 }
2005
2006 =item has_cust_payby_auto
2007
2008 Returns true if customer has an automatic payment method ('CARD' or 'CHEK')
2009
2010 =cut
2011
2012 sub has_cust_payby_auto {
2013   my $self = shift;
2014   scalar( qsearch({ 
2015     'table'     => 'cust_payby',
2016     'hashref'   => { 'custnum' => $self->custnum, },
2017     'extra_sql' => " AND payby IN ( 'CARD', 'CHEK' ) ",
2018     'order_by'  => 'LIMIT 1',
2019   }) );
2020
2021 }
2022
2023 =item unsuspend
2024
2025 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
2026 and L<FS::cust_pkg>) for this customer, except those on hold.
2027
2028 Returns a list: an empty list on success or a list of errors.
2029
2030 =cut
2031
2032 sub unsuspend {
2033   my $self = shift;
2034   grep { ($_->get('setup')) && $_->unsuspend } $self->suspended_pkgs;
2035 }
2036
2037 =item release_hold
2038
2039 Unsuspends all suspended packages in the on-hold state (those without setup 
2040 dates) for this customer. 
2041
2042 =cut
2043
2044 sub release_hold {
2045   my $self = shift;
2046   grep { (!$_->setup) && $_->unsuspend } $self->suspended_pkgs;
2047 }
2048
2049 =item suspend
2050
2051 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
2052
2053 Returns a list: an empty list on success or a list of errors.
2054
2055 =cut
2056
2057 sub suspend {
2058   my $self = shift;
2059   grep { $_->suspend(@_) } $self->unsuspended_pkgs;
2060 }
2061
2062 =item suspend_if_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2063
2064 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
2065 PKGPARTs (see L<FS::part_pkg>).  Preferred usage is to pass a hashref instead
2066 of a list of pkgparts; the hashref has the following keys:
2067
2068 =over 4
2069
2070 =item pkgparts - listref of pkgparts
2071
2072 =item (other options are passed to the suspend method)
2073
2074 =back
2075
2076
2077 Returns a list: an empty list on success or a list of errors.
2078
2079 =cut
2080
2081 sub suspend_if_pkgpart {
2082   my $self = shift;
2083   my (@pkgparts, %opt);
2084   if (ref($_[0]) eq 'HASH'){
2085     @pkgparts = @{$_[0]{pkgparts}};
2086     %opt      = %{$_[0]};
2087   }else{
2088     @pkgparts = @_;
2089   }
2090   grep { $_->suspend(%opt) }
2091     grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
2092       $self->unsuspended_pkgs;
2093 }
2094
2095 =item suspend_unless_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2096
2097 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
2098 given PKGPARTs (see L<FS::part_pkg>).  Preferred usage is to pass a hashref
2099 instead of a list of pkgparts; the hashref has the following keys:
2100
2101 =over 4
2102
2103 =item pkgparts - listref of pkgparts
2104
2105 =item (other options are passed to the suspend method)
2106
2107 =back
2108
2109 Returns a list: an empty list on success or a list of errors.
2110
2111 =cut
2112
2113 sub suspend_unless_pkgpart {
2114   my $self = shift;
2115   my (@pkgparts, %opt);
2116   if (ref($_[0]) eq 'HASH'){
2117     @pkgparts = @{$_[0]{pkgparts}};
2118     %opt      = %{$_[0]};
2119   }else{
2120     @pkgparts = @_;
2121   }
2122   grep { $_->suspend(%opt) }
2123     grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
2124       $self->unsuspended_pkgs;
2125 }
2126
2127 =item cancel [ OPTION => VALUE ... ]
2128
2129 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
2130 The cancellation time will be now.
2131
2132 =back
2133
2134 Always returns a list: an empty list on success or a list of errors.
2135
2136 =cut
2137
2138 sub cancel {
2139   my $self = shift;
2140   my %opt = @_;
2141   warn "$me cancel called on customer ". $self->custnum. " with options ".
2142        join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
2143     if $DEBUG;
2144   my @pkgs = $self->ncancelled_pkgs;
2145
2146   $self->cancel_pkgs( %opt, 'cust_pkg' => \@pkgs );
2147 }
2148
2149 =item cancel_pkgs OPTIONS
2150
2151 Cancels a specified list of packages. OPTIONS can include:
2152
2153 =over 4
2154
2155 =item cust_pkg - an arrayref of the packages. Required.
2156
2157 =item time - the cancellation time, used to calculate final bills and
2158 unused-time credits if any. Will be passed through to the bill() and
2159 FS::cust_pkg::cancel() methods.
2160
2161 =item quiet - can be set true to supress email cancellation notices.
2162
2163 =item reason - can be set to a cancellation reason (see L<FS:reason>), either a
2164 reasonnum of an existing reason, or passing a hashref will create a new reason.
2165 The hashref should have the following keys:
2166 typenum - Reason type (see L<FS::reason_type>)
2167 reason - Text of the new reason.
2168
2169 =item cust_pkg_reason - can be an arrayref of L<FS::cust_pkg_reason> objects
2170 for the individual packages, parallel to the C<cust_pkg> argument. The
2171 reason and reason_otaker arguments will be taken from those objects.
2172
2173 =item ban - can be set true to ban this customer's credit card or ACH information, if present.
2174
2175 =item nobill - can be set true to skip billing if it might otherwise be done.
2176
2177 =cut
2178
2179 sub cancel_pkgs {
2180   my( $self, %opt ) = @_;
2181
2182   # we're going to cancel services, which is not reversible
2183   die "cancel_pkgs cannot be run inside a transaction"
2184     if $FS::UID::AutoCommit == 0;
2185
2186   local $FS::UID::AutoCommit = 0;
2187
2188   return ( 'access denied' )
2189     unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
2190
2191   if ( $opt{'ban'} ) {
2192
2193     foreach my $cust_payby ( $self->cust_payby ) {
2194
2195       #well, if they didn't get decrypted on search, then we don't have to 
2196       # try again... queue a job for the server that does have decryption
2197       # capability if we're in a paranoid multi-server implementation?
2198       return ( "Can't (yet) ban encrypted credit cards" )
2199         if $cust_payby->is_encrypted($cust_payby->payinfo);
2200
2201       my $ban = new FS::banned_pay $cust_payby->_new_banned_pay_hashref;
2202       my $error = $ban->insert;
2203       if ($error) {
2204         dbh->rollback;
2205         return ( $error );
2206       }
2207
2208     }
2209
2210   }
2211
2212   my @pkgs = @{ delete $opt{'cust_pkg'} };
2213   my $cancel_time = $opt{'time'} || time;
2214
2215   # bill all packages first, so we don't lose usage, service counts for
2216   # bulk billing, etc.
2217   if ( !$opt{nobill} && $conf->exists('bill_usage_on_cancel') ) {
2218     $opt{nobill} = 1;
2219     my $error = $self->bill( 'pkg_list' => [ @pkgs ],
2220                              'cancel'   => 1,
2221                              'time'     => $cancel_time );
2222     if ($error) {
2223       warn "Error billing during cancel, custnum ". $self->custnum. ": $error";
2224       dbh->rollback;
2225       return ( "Error billing during cancellation: $error" );
2226     }
2227   }
2228   dbh->commit;
2229
2230   $FS::UID::AutoCommit = 1;
2231   my @errors;
2232   # now cancel all services, the same way we would for individual packages.
2233   # if any of them fail, cancel the rest anyway.
2234   my @cust_svc = map { $_->cust_svc } @pkgs;
2235   my @sorted_cust_svc =
2236     map  { $_->[0] }
2237     sort { $a->[1] <=> $b->[1] }
2238     map  { [ $_, $_->svc_x ? $_->svc_x->table_info->{'cancel_weight'} : -1 ]; } @cust_svc
2239   ;
2240   warn "$me removing ".scalar(@sorted_cust_svc)." service(s) for customer ".
2241     $self->custnum."\n"
2242     if $DEBUG;
2243   foreach my $cust_svc (@sorted_cust_svc) {
2244     my $part_svc = $cust_svc->part_svc;
2245     next if ( defined($part_svc) and $part_svc->preserve );
2246     my $error = $cust_svc->cancel; # immediate cancel, no date option
2247     push @errors, $error if $error;
2248   }
2249   if (@errors) {
2250     return @errors;
2251   }
2252
2253   warn "$me cancelling ". scalar(@pkgs) ." package(s) for customer ".
2254     $self->custnum. "\n"
2255     if $DEBUG;
2256
2257   my @cprs;
2258   if ($opt{'cust_pkg_reason'}) {
2259     @cprs = @{ delete $opt{'cust_pkg_reason'} };
2260   }
2261   my $null_reason;
2262   foreach (@pkgs) {
2263     my %lopt = %opt;
2264     if (@cprs) {
2265       my $cpr = shift @cprs;
2266       if ( $cpr ) {
2267         $lopt{'reason'}        = $cpr->reasonnum;
2268         $lopt{'reason_otaker'} = $cpr->otaker;
2269       } else {
2270         warn "no reason found when canceling package ".$_->pkgnum."\n";
2271         # we're not actually required to pass a reason to cust_pkg::cancel,
2272         # but if we're getting to this point, something has gone awry.
2273         $null_reason ||= FS::reason->new_or_existing(
2274           reason  => 'unknown reason',
2275           type    => 'Cancel Reason',
2276           class   => 'C',
2277         );
2278         $lopt{'reason'} = $null_reason->reasonnum;
2279         $lopt{'reason_otaker'} = $FS::CurrentUser::CurrentUser->username;
2280       }
2281     }
2282     my $error = $_->cancel(%lopt);
2283     push @errors, 'pkgnum '.$_->pkgnum.': '.$error if $error;
2284   }
2285
2286   return @errors;
2287 }
2288
2289 sub _banned_pay_hashref {
2290   die 'cust_main->_banned_pay_hashref deprecated';
2291
2292   my $self = shift;
2293
2294   my %payby2ban = (
2295     'CARD' => 'CARD',
2296     'DCRD' => 'CARD',
2297     'CHEK' => 'CHEK',
2298     'DCHK' => 'CHEK'
2299   );
2300
2301   {
2302     'payby'   => $payby2ban{$self->payby},
2303     'payinfo' => $self->payinfo,
2304     #don't ever *search* on reason! #'reason'  =>
2305   };
2306 }
2307
2308 =item notes
2309
2310 Returns all notes (see L<FS::cust_main_note>) for this customer.
2311
2312 =cut
2313
2314 sub notes {
2315   my($self,$orderby_classnum) = (shift,shift);
2316   my $orderby = "sticky DESC, _date DESC";
2317   $orderby = "classnum ASC, $orderby" if $orderby_classnum;
2318   qsearch( 'cust_main_note',
2319            { 'custnum' => $self->custnum },
2320            '',
2321            "ORDER BY $orderby",
2322          );
2323 }
2324
2325 =item agent
2326
2327 Returns the agent (see L<FS::agent>) for this customer.
2328
2329 =item agent_name
2330
2331 Returns the agent name (see L<FS::agent>) for this customer.
2332
2333 =cut
2334
2335 sub agent_name {
2336   my $self = shift;
2337   $self->agent->agent;
2338 }
2339
2340 =item cust_tag
2341
2342 Returns any tags associated with this customer, as FS::cust_tag objects,
2343 or an empty list if there are no tags.
2344
2345 =item part_tag
2346
2347 Returns any tags associated with this customer, as FS::part_tag objects,
2348 or an empty list if there are no tags.
2349
2350 =cut
2351
2352 sub part_tag {
2353   my $self = shift;
2354   map $_->part_tag, $self->cust_tag; 
2355 }
2356
2357
2358 =item cust_class
2359
2360 Returns the customer class, as an FS::cust_class object, or the empty string
2361 if there is no customer class.
2362
2363 =item categoryname 
2364
2365 Returns the customer category name, or the empty string if there is no customer
2366 category.
2367
2368 =cut
2369
2370 sub categoryname {
2371   my $self = shift;
2372   my $cust_class = $self->cust_class;
2373   $cust_class
2374     ? $cust_class->categoryname
2375     : '';
2376 }
2377
2378 =item classname 
2379
2380 Returns the customer class name, or the empty string if there is no customer
2381 class.
2382
2383 =cut
2384
2385 sub classname {
2386   my $self = shift;
2387   my $cust_class = $self->cust_class;
2388   $cust_class
2389     ? $cust_class->classname
2390     : '';
2391 }
2392
2393 =item tax_status
2394
2395 Returns the external tax status, as an FS::tax_status object, or the empty 
2396 string if there is no tax status.
2397
2398 =cut
2399
2400 sub tax_status {
2401   my $self = shift;
2402   if ( $self->taxstatusnum ) {
2403     qsearchs('tax_status', { 'taxstatusnum' => $self->taxstatusnum } );
2404   } else {
2405     return '';
2406   } 
2407 }
2408
2409 =item taxstatus
2410
2411 Returns the tax status code if there is one.
2412
2413 =cut
2414
2415 sub taxstatus {
2416   my $self = shift;
2417   my $tax_status = $self->tax_status;
2418   $tax_status
2419     ? $tax_status->taxstatus
2420     : '';
2421 }
2422
2423 =item BILLING METHODS
2424
2425 Documentation on billing methods has been moved to
2426 L<FS::cust_main::Billing>.
2427
2428 =item REALTIME BILLING METHODS
2429
2430 Documentation on realtime billing methods has been moved to
2431 L<FS::cust_main::Billing_Realtime>.
2432
2433 =item remove_cvv
2434
2435 Removes the I<paycvv> field from the database directly.
2436
2437 If there is an error, returns the error, otherwise returns false.
2438
2439 DEPRECATED.  Use L</remove_cvv_from_cust_payby> instead.
2440
2441 =cut
2442
2443 sub remove_cvv {
2444   die 'cust_main->remove_cvv deprecated';
2445   my $self = shift;
2446   my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
2447     or return dbh->errstr;
2448   $sth->execute($self->custnum)
2449     or return $sth->errstr;
2450   $self->paycvv('');
2451   '';
2452 }
2453
2454 =item total_owed
2455
2456 Returns the total owed for this customer on all invoices
2457 (see L<FS::cust_bill/owed>).
2458
2459 =cut
2460
2461 sub total_owed {
2462   my $self = shift;
2463   $self->total_owed_date(2145859200); #12/31/2037
2464 }
2465
2466 =item total_owed_date TIME
2467
2468 Returns the total owed for this customer on all invoices with date earlier than
2469 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
2470 see L<Time::Local> and L<Date::Parse> for conversion functions.
2471
2472 =cut
2473
2474 sub total_owed_date {
2475   my $self = shift;
2476   my $time = shift;
2477
2478   my $custnum = $self->custnum;
2479
2480   my $owed_sql = FS::cust_bill->owed_sql;
2481
2482   my $sql = "
2483     SELECT SUM($owed_sql) FROM cust_bill
2484       WHERE custnum = $custnum
2485         AND _date <= $time
2486   ";
2487
2488   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2489
2490 }
2491
2492 =item total_owed_pkgnum PKGNUM
2493
2494 Returns the total owed on all invoices for this customer's specific package
2495 when using experimental package balances (see L<FS::cust_bill/owed_pkgnum>).
2496
2497 =cut
2498
2499 sub total_owed_pkgnum {
2500   my( $self, $pkgnum ) = @_;
2501   $self->total_owed_date_pkgnum(2145859200, $pkgnum); #12/31/2037
2502 }
2503
2504 =item total_owed_date_pkgnum TIME PKGNUM
2505
2506 Returns the total owed for this customer's specific package when using
2507 experimental package balances on all invoices with date earlier than
2508 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
2509 see L<Time::Local> and L<Date::Parse> for conversion functions.
2510
2511 =cut
2512
2513 sub total_owed_date_pkgnum {
2514   my( $self, $time, $pkgnum ) = @_;
2515
2516   my $total_bill = 0;
2517   foreach my $cust_bill (
2518     grep { $_->_date <= $time }
2519       qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2520   ) {
2521     $total_bill += $cust_bill->owed_pkgnum($pkgnum);
2522   }
2523   sprintf( "%.2f", $total_bill );
2524
2525 }
2526
2527 =item total_paid
2528
2529 Returns the total amount of all payments.
2530
2531 =cut
2532
2533 sub total_paid {
2534   my $self = shift;
2535   my $total = 0;
2536   $total += $_->paid foreach $self->cust_pay;
2537   sprintf( "%.2f", $total );
2538 }
2539
2540 =item total_unapplied_credits
2541
2542 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2543 customer.  See L<FS::cust_credit/credited>.
2544
2545 =item total_credited
2546
2547 Old name for total_unapplied_credits.  Don't use.
2548
2549 =cut
2550
2551 sub total_credited {
2552   #carp "total_credited deprecated, use total_unapplied_credits";
2553   shift->total_unapplied_credits(@_);
2554 }
2555
2556 sub total_unapplied_credits {
2557   my $self = shift;
2558
2559   my $custnum = $self->custnum;
2560
2561   my $unapplied_sql = FS::cust_credit->unapplied_sql;
2562
2563   my $sql = "
2564     SELECT SUM($unapplied_sql) FROM cust_credit
2565       WHERE custnum = $custnum
2566   ";
2567
2568   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2569
2570 }
2571
2572 =item total_unapplied_credits_pkgnum PKGNUM
2573
2574 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2575 customer.  See L<FS::cust_credit/credited>.
2576
2577 =cut
2578
2579 sub total_unapplied_credits_pkgnum {
2580   my( $self, $pkgnum ) = @_;
2581   my $total_credit = 0;
2582   $total_credit += $_->credited foreach $self->cust_credit_pkgnum($pkgnum);
2583   sprintf( "%.2f", $total_credit );
2584 }
2585
2586
2587 =item total_unapplied_payments
2588
2589 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
2590 See L<FS::cust_pay/unapplied>.
2591
2592 =cut
2593
2594 sub total_unapplied_payments {
2595   my $self = shift;
2596
2597   my $custnum = $self->custnum;
2598
2599   my $unapplied_sql = FS::cust_pay->unapplied_sql;
2600
2601   my $sql = "
2602     SELECT SUM($unapplied_sql) FROM cust_pay
2603       WHERE custnum = $custnum
2604   ";
2605
2606   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2607
2608 }
2609
2610 =item total_unapplied_payments_pkgnum PKGNUM
2611
2612 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer's
2613 specific package when using experimental package balances.  See
2614 L<FS::cust_pay/unapplied>.
2615
2616 =cut
2617
2618 sub total_unapplied_payments_pkgnum {
2619   my( $self, $pkgnum ) = @_;
2620   my $total_unapplied = 0;
2621   $total_unapplied += $_->unapplied foreach $self->cust_pay_pkgnum($pkgnum);
2622   sprintf( "%.2f", $total_unapplied );
2623 }
2624
2625
2626 =item total_unapplied_refunds
2627
2628 Returns the total unrefunded refunds (see L<FS::cust_refund>) for this
2629 customer.  See L<FS::cust_refund/unapplied>.
2630
2631 =cut
2632
2633 sub total_unapplied_refunds {
2634   my $self = shift;
2635   my $custnum = $self->custnum;
2636
2637   my $unapplied_sql = FS::cust_refund->unapplied_sql;
2638
2639   my $sql = "
2640     SELECT SUM($unapplied_sql) FROM cust_refund
2641       WHERE custnum = $custnum
2642   ";
2643
2644   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2645
2646 }
2647
2648 =item balance
2649
2650 Returns the balance for this customer (total_owed plus total_unrefunded, minus
2651 total_unapplied_credits minus total_unapplied_payments).
2652
2653 =cut
2654
2655 sub balance {
2656   my $self = shift;
2657   $self->balance_date_range;
2658 }
2659
2660 =item balance_date TIME
2661
2662 Returns the balance for this customer, only considering invoices with date
2663 earlier than TIME (total_owed_date minus total_credited minus
2664 total_unapplied_payments).  TIME is specified as a UNIX timestamp; see
2665 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
2666 functions.
2667
2668 =cut
2669
2670 sub balance_date {
2671   my $self = shift;
2672   $self->balance_date_range(shift);
2673 }
2674
2675 =item balance_date_range [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
2676
2677 Returns the balance for this customer, optionally considering invoices with
2678 date earlier than START_TIME, and not later than END_TIME
2679 (total_owed_date minus total_unapplied_credits minus total_unapplied_payments).
2680
2681 Times are specified as SQL fragments or numeric
2682 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
2683 L<Date::Parse> for conversion functions.  The empty string can be passed
2684 to disable that time constraint completely.
2685
2686 Accepts the same options as L<balance_date_sql>:
2687
2688 =over 4
2689
2690 =item unapplied_date
2691
2692 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)
2693
2694 =item cutoff
2695
2696 An absolute cutoff time.  Payments, credits, and refunds I<applied> after this 
2697 time will be ignored.  Note that START_TIME and END_TIME only limit the date 
2698 range for invoices and I<unapplied> payments, credits, and refunds.
2699
2700 =back
2701
2702 =cut
2703
2704 sub balance_date_range {
2705   my $self = shift;
2706   my $sql = 'SELECT SUM('. $self->balance_date_sql(@_).
2707             ') FROM cust_main WHERE custnum='. $self->custnum;
2708   sprintf( '%.2f', $self->scalar_sql($sql) || 0 );
2709 }
2710
2711 =item balance_pkgnum PKGNUM
2712
2713 Returns the balance for this customer's specific package when using
2714 experimental package balances (total_owed plus total_unrefunded, minus
2715 total_unapplied_credits minus total_unapplied_payments)
2716
2717 =cut
2718
2719 sub balance_pkgnum {
2720   my( $self, $pkgnum ) = @_;
2721
2722   sprintf( "%.2f",
2723       $self->total_owed_pkgnum($pkgnum)
2724 # n/a - refunds aren't part of pkg-balances since they don't apply to invoices
2725 #    + $self->total_unapplied_refunds_pkgnum($pkgnum)
2726     - $self->total_unapplied_credits_pkgnum($pkgnum)
2727     - $self->total_unapplied_payments_pkgnum($pkgnum)
2728   );
2729 }
2730
2731 =item payment_info
2732
2733 Returns a hash of useful information for making a payment.
2734
2735 =over 4
2736
2737 =item balance
2738
2739 Current balance.
2740
2741 =item payby
2742
2743 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
2744 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
2745 'LECB' (Phone bill billing), 'BILL' (billing), or 'COMP' (free).
2746
2747 =back
2748
2749 For credit card transactions:
2750
2751 =over 4
2752
2753 =item card_type 1
2754
2755 =item payname
2756
2757 Exact name on card
2758
2759 =back
2760
2761 For electronic check transactions:
2762
2763 =over 4
2764
2765 =item stateid_state
2766
2767 =back
2768
2769 =cut
2770
2771 #XXX i need to be updated for 4.x+
2772 sub payment_info {
2773   my $self = shift;
2774
2775   my %return = ();
2776
2777   $return{balance} = $self->balance;
2778
2779   $return{payname} = $self->payname
2780                      || ( $self->first. ' '. $self->get('last') );
2781
2782   $return{$_} = $self->bill_location->$_
2783     for qw(address1 address2 city state zip);
2784
2785   $return{payby} = $self->payby;
2786   $return{stateid_state} = $self->stateid_state;
2787
2788   if ( $self->payby =~ /^(CARD|DCRD)$/ ) {
2789     $return{card_type} = cardtype($self->payinfo);
2790     $return{payinfo} = $self->paymask;
2791
2792     @return{'month', 'year'} = $self->paydate_monthyear;
2793
2794   }
2795
2796   if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
2797     my ($payinfo1, $payinfo2) = split '@', $self->paymask;
2798     $return{payinfo1} = $payinfo1;
2799     $return{payinfo2} = $payinfo2;
2800     $return{paytype}  = $self->paytype;
2801     $return{paystate} = $self->paystate;
2802
2803   }
2804
2805   #doubleclick protection
2806   my $_date = time;
2807   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
2808
2809   %return;
2810
2811 }
2812
2813 =item paydate_epoch
2814
2815 Returns the next payment expiration date for this customer. If they have no
2816 payment methods that will expire, returns 0.
2817
2818 =cut
2819
2820 sub paydate_epoch {
2821   my $self = shift;
2822   # filter out the ones that individually return 0, but then return 0 if
2823   # there are no results
2824   my @epochs = grep { $_ > 0 } map { $_->paydate_epoch } $self->cust_payby;
2825   min( @epochs ) || 0;
2826 }
2827
2828 =item paydate_epoch_sql
2829
2830 Returns an SQL expression to get the next payment expiration date for a
2831 customer. Returns 2143260000 (2037-12-01) if there are no payment expiration
2832 dates, so that it's safe to test for "will it expire before date X" for any
2833 date up to then.
2834
2835 =cut
2836
2837 sub paydate_epoch_sql {
2838   my $class = shift;
2839   my $paydate = FS::cust_payby->paydate_epoch_sql;
2840   "(SELECT COALESCE(MIN($paydate), 2143260000) FROM cust_payby WHERE cust_payby.custnum = cust_main.custnum)";
2841 }
2842
2843 sub tax_exemption {
2844   my( $self, $taxname ) = @_;
2845
2846   qsearchs( 'cust_main_exemption', { 'custnum' => $self->custnum,
2847                                      'taxname' => $taxname,
2848                                    },
2849           );
2850 }
2851
2852 =item cust_main_exemption
2853
2854 =item invoicing_list
2855
2856 Returns a list of email addresses (with svcnum entries expanded), and the word
2857 'POST' if the customer receives postal invoices.
2858
2859 =cut
2860
2861 sub invoicing_list {
2862   my( $self, $arrayref ) = @_;
2863
2864   if ( $arrayref ) {
2865     warn "FS::cust_main::invoicing_list(ARRAY) is no longer supported.";
2866   }
2867   
2868   my @emails = $self->invoicing_list_emailonly;
2869   push @emails, 'POST' if $self->get('postal_invoice');
2870
2871   @emails;
2872 }
2873
2874 =item check_invoicing_list ARRAYREF
2875
2876 Checks these arguements as valid input for the invoicing_list method.  If there
2877 is an error, returns the error, otherwise returns false.
2878
2879 =cut
2880
2881 sub check_invoicing_list {
2882   my( $self, $arrayref ) = @_;
2883
2884   foreach my $address ( @$arrayref ) {
2885
2886     if ($address eq 'FAX' and $self->getfield('fax') eq '') {
2887       return 'Can\'t add FAX invoice destination with a blank FAX number.';
2888     }
2889
2890     my $cust_main_invoice = new FS::cust_main_invoice ( {
2891       'custnum' => $self->custnum,
2892       'dest'    => $address,
2893     } );
2894     my $error = $self->custnum
2895                 ? $cust_main_invoice->check
2896                 : $cust_main_invoice->checkdest
2897     ;
2898     return $error if $error;
2899
2900   }
2901
2902   return "Email address required"
2903     if $conf->exists('cust_main-require_invoicing_list_email', $self->agentnum)
2904     && ! grep { $_ !~ /^([A-Z]+)$/ } @$arrayref;
2905
2906   '';
2907 }
2908
2909 =item all_emails
2910
2911 Returns the email addresses of all accounts provisioned for this customer.
2912
2913 =cut
2914
2915 sub all_emails {
2916   my $self = shift;
2917   my %list;
2918   foreach my $cust_pkg ( $self->all_pkgs ) {
2919     my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
2920     my @svc_acct =
2921       map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2922         grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2923           @cust_svc;
2924     $list{$_}=1 foreach map { $_->email } @svc_acct;
2925   }
2926   keys %list;
2927 }
2928
2929 =item invoicing_list_addpost
2930
2931 Adds postal invoicing to this customer.  If this customer is already configured
2932 to receive postal invoices, does nothing.
2933
2934 =cut
2935
2936 sub invoicing_list_addpost {
2937   my $self = shift;
2938   if ( $self->get('postal_invoice') eq '' ) {
2939     $self->set('postal_invoice', 'Y');
2940     my $error = $self->replace;
2941     warn $error if $error; # should fail harder, but this is traditional
2942   }
2943 }
2944
2945 =item invoicing_list_emailonly
2946
2947 Returns the list of email invoice recipients (invoicing_list without non-email
2948 destinations such as POST and FAX).
2949
2950 =cut
2951
2952 sub invoicing_list_emailonly {
2953   my $self = shift;
2954   warn "$me invoicing_list_emailonly called"
2955     if $DEBUG;
2956   return () if !$self->custnum; # not yet inserted
2957   return map { $_->emailaddress }
2958     qsearch({
2959         table     => 'cust_contact',
2960         select    => 'emailaddress',
2961         addl_from => ' JOIN contact USING (contactnum) '.
2962                      ' JOIN contact_email USING (contactnum)',
2963         hashref   => { 'custnum' => $self->custnum, },
2964         extra_sql => q( AND cust_contact.invoice_dest = 'Y'),
2965     });
2966 }
2967
2968 =item invoicing_list_emailonly_scalar
2969
2970 Returns the list of email invoice recipients (invoicing_list without non-email
2971 destinations such as POST and FAX) as a comma-separated scalar.
2972
2973 =cut
2974
2975 sub invoicing_list_emailonly_scalar {
2976   my $self = shift;
2977   warn "$me invoicing_list_emailonly_scalar called"
2978     if $DEBUG;
2979   join(', ', $self->invoicing_list_emailonly);
2980 }
2981
2982 =item contact_list [ CLASSNUM, ... ]
2983
2984 Returns a list of contacts (L<FS::contact> objects) for the customer. If
2985 a list of contact classnums is given, returns only contacts in those
2986 classes. If the pseudo-classnum 'invoice' is given, returns contacts that
2987 are marked as invoice destinations. If '0' is given, also returns contacts
2988 with no class.
2989
2990 If no arguments are given, returns all contacts for the customer.
2991
2992 =cut
2993
2994 sub contact_list {
2995   my $self = shift;
2996   my $search = {
2997     table       => 'contact',
2998     select      => 'contact.*, cust_contact.invoice_dest',
2999     addl_from   => ' JOIN cust_contact USING (contactnum)',
3000     extra_sql   => ' WHERE cust_contact.custnum = '.$self->custnum,
3001   };
3002
3003   my @orwhere;
3004   my @classnums;
3005   foreach (@_) {
3006     if ( $_ eq 'invoice' ) {
3007       push @orwhere, 'cust_contact.invoice_dest = \'Y\'';
3008     } elsif ( $_ eq '0' ) {
3009       push @orwhere, 'cust_contact.classnum is null';
3010     } elsif ( /^\d+$/ ) {
3011       push @classnums, $_;
3012     } else {
3013       die "bad classnum argument '$_'";
3014     }
3015   }
3016
3017   if (@classnums) {
3018     push @orwhere, 'cust_contact.classnum IN ('.join(',', @classnums).')';
3019   }
3020   if (@orwhere) {
3021     $search->{extra_sql} .= ' AND (' .
3022                             join(' OR ', map "( $_ )", @orwhere) .
3023                             ')';
3024   }
3025
3026   qsearch($search);
3027 }
3028
3029 =item contact_list_email [ CLASSNUM, ... ]
3030
3031 Same as L</contact_list>, but returns email destinations instead of contact
3032 objects.
3033
3034 =cut
3035
3036 sub contact_list_email {
3037   my $self = shift;
3038   my @contacts = $self->contact_list(@_);
3039   my @emails;
3040   foreach my $contact (@contacts) {
3041     foreach my $contact_email ($contact->contact_email) {
3042       push @emails,
3043         $contact->firstlast . ' <' . $contact_email->emailaddress . '>';
3044     }
3045   }
3046   @emails;
3047 }
3048
3049 =item referral_custnum_cust_main
3050
3051 Returns the customer who referred this customer (or the empty string, if
3052 this customer was not referred).
3053
3054 Note the difference with referral_cust_main method: This method,
3055 referral_custnum_cust_main returns the single customer (if any) who referred
3056 this customer, while referral_cust_main returns an array of customers referred
3057 BY this customer.
3058
3059 =cut
3060
3061 sub referral_custnum_cust_main {
3062   my $self = shift;
3063   return '' unless $self->referral_custnum;
3064   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3065 }
3066
3067 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
3068
3069 Returns an array of customers referred by this customer (referral_custnum set
3070 to this custnum).  If DEPTH is given, recurses up to the given depth, returning
3071 customers referred by customers referred by this customer and so on, inclusive.
3072 The default behavior is DEPTH 1 (no recursion).
3073
3074 Note the difference with referral_custnum_cust_main method: This method,
3075 referral_cust_main, returns an array of customers referred BY this customer,
3076 while referral_custnum_cust_main returns the single customer (if any) who
3077 referred this customer.
3078
3079 =cut
3080
3081 sub referral_cust_main {
3082   my $self = shift;
3083   my $depth = @_ ? shift : 1;
3084   my $exclude = @_ ? shift : {};
3085
3086   my @cust_main =
3087     map { $exclude->{$_->custnum}++; $_; }
3088       grep { ! $exclude->{ $_->custnum } }
3089         qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
3090
3091   if ( $depth > 1 ) {
3092     push @cust_main,
3093       map { $_->referral_cust_main($depth-1, $exclude) }
3094         @cust_main;
3095   }
3096
3097   @cust_main;
3098 }
3099
3100 =item referral_cust_main_ncancelled
3101
3102 Same as referral_cust_main, except only returns customers with uncancelled
3103 packages.
3104
3105 =cut
3106
3107 sub referral_cust_main_ncancelled {
3108   my $self = shift;
3109   grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
3110 }
3111
3112 =item referral_cust_pkg [ DEPTH ]
3113
3114 Like referral_cust_main, except returns a flat list of all unsuspended (and
3115 uncancelled) packages for each customer.  The number of items in this list may
3116 be useful for commission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
3117
3118 =cut
3119
3120 sub referral_cust_pkg {
3121   my $self = shift;
3122   my $depth = @_ ? shift : 1;
3123
3124   map { $_->unsuspended_pkgs }
3125     grep { $_->unsuspended_pkgs }
3126       $self->referral_cust_main($depth);
3127 }
3128
3129 =item referring_cust_main
3130
3131 Returns the single cust_main record for the customer who referred this customer
3132 (referral_custnum), or false.
3133
3134 =cut
3135
3136 sub referring_cust_main {
3137   my $self = shift;
3138   return '' unless $self->referral_custnum;
3139   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3140 }
3141
3142 =item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
3143
3144 Applies a credit to this customer.  If there is an error, returns the error,
3145 otherwise returns false.
3146
3147 REASON can be a text string, an FS::reason object, or a scalar reference to
3148 a reasonnum.  If a text string, it will be automatically inserted as a new
3149 reason, and a 'reason_type' option must be passed to indicate the
3150 FS::reason_type for the new reason.
3151
3152 An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
3153 Likewise for I<eventnum>, I<commission_agentnum>, I<commission_salesnum> and
3154 I<commission_pkgnum>.
3155
3156 Any other options are passed to FS::cust_credit::insert.
3157
3158 =cut
3159
3160 sub credit {
3161   my( $self, $amount, $reason, %options ) = @_;
3162
3163   my $cust_credit = new FS::cust_credit {
3164     'custnum' => $self->custnum,
3165     'amount'  => $amount,
3166   };
3167
3168   if ( ref($reason) ) {
3169
3170     if ( ref($reason) eq 'SCALAR' ) {
3171       $cust_credit->reasonnum( $$reason );
3172     } else {
3173       $cust_credit->reasonnum( $reason->reasonnum );
3174     }
3175
3176   } else {
3177     $cust_credit->set('reason', $reason)
3178   }
3179
3180   $cust_credit->$_( delete $options{$_} )
3181     foreach grep exists($options{$_}),
3182               qw( addlinfo eventnum ),
3183               map "commission_$_", qw( agentnum salesnum pkgnum );
3184
3185   $cust_credit->insert(%options);
3186
3187 }
3188
3189 =item charge HASHREF || AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
3190
3191 Creates a one-time charge for this customer.  If there is an error, returns
3192 the error, otherwise returns false.
3193
3194 New-style, with a hashref of options:
3195
3196   my $error = $cust_main->charge(
3197                                   {
3198                                     'amount'     => 54.32,
3199                                     'quantity'   => 1,
3200                                     'start_date' => str2time('7/4/2009'),
3201                                     'pkg'        => 'Description',
3202                                     'comment'    => 'Comment',
3203                                     'additional' => [], #extra invoice detail
3204                                     'classnum'   => 1,  #pkg_class
3205
3206                                     'setuptax'   => '', # or 'Y' for tax exempt
3207
3208                                     'locationnum'=> 1234, # optional
3209
3210                                     #internal taxation
3211                                     'taxclass'   => 'Tax class',
3212
3213                                     #vendor taxation
3214                                     'taxproduct' => 2,  #part_pkg_taxproduct
3215                                     'override'   => {}, #XXX describe
3216
3217                                     #will be filled in with the new object
3218                                     'cust_pkg_ref' => \$cust_pkg,
3219
3220                                     #generate an invoice immediately
3221                                     'bill_now' => 0,
3222                                     'invoice_terms' => '', #with these terms
3223                                   }
3224                                 );
3225
3226 Old-style:
3227
3228   my $error = $cust_main->charge( 54.32, 'Description', 'Comment', 'Tax class' );
3229
3230 =cut
3231
3232 #super false laziness w/quotation::charge
3233 sub charge {
3234   my $self = shift;
3235   my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
3236   my ( $pkg, $comment, $additional );
3237   my ( $setuptax, $taxclass );   #internal taxes
3238   my ( $taxproduct, $override ); #vendor (CCH) taxes
3239   my $no_auto = '';
3240   my $separate_bill = '';
3241   my $cust_pkg_ref = '';
3242   my ( $bill_now, $invoice_terms ) = ( 0, '' );
3243   my $locationnum;
3244   if ( ref( $_[0] ) ) {
3245     $amount     = $_[0]->{amount};
3246     $setup_cost = $_[0]->{setup_cost};
3247     $quantity   = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
3248     $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
3249     $no_auto    = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
3250     $pkg        = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
3251     $comment    = exists($_[0]->{comment}) ? $_[0]->{comment}
3252                                            : '$'. sprintf("%.2f",$amount);
3253     $setuptax   = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
3254     $taxclass   = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
3255     $classnum   = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
3256     $additional = $_[0]->{additional} || [];
3257     $taxproduct = $_[0]->{taxproductnum};
3258     $override   = { '' => $_[0]->{tax_override} };
3259     $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
3260     $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
3261     $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
3262     $locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
3263     $separate_bill = $_[0]->{separate_bill} || '';
3264   } else { # yuck
3265     $amount     = shift;
3266     $setup_cost = '';
3267     $quantity   = 1;
3268     $start_date = '';
3269     $pkg        = @_ ? shift : 'One-time charge';
3270     $comment    = @_ ? shift : '$'. sprintf("%.2f",$amount);
3271     $setuptax   = '';
3272     $taxclass   = @_ ? shift : '';
3273     $additional = [];
3274   }
3275
3276   local $SIG{HUP} = 'IGNORE';
3277   local $SIG{INT} = 'IGNORE';
3278   local $SIG{QUIT} = 'IGNORE';
3279   local $SIG{TERM} = 'IGNORE';
3280   local $SIG{TSTP} = 'IGNORE';
3281   local $SIG{PIPE} = 'IGNORE';
3282
3283   my $oldAutoCommit = $FS::UID::AutoCommit;
3284   local $FS::UID::AutoCommit = 0;
3285   my $dbh = dbh;
3286
3287   my $part_pkg = new FS::part_pkg ( {
3288     'pkg'           => $pkg,
3289     'comment'       => $comment,
3290     'plan'          => 'flat',
3291     'freq'          => 0,
3292     'disabled'      => 'Y',
3293     'classnum'      => ( $classnum ? $classnum : '' ),
3294     'setuptax'      => $setuptax,
3295     'taxclass'      => $taxclass,
3296     'taxproductnum' => $taxproduct,
3297     'setup_cost'    => $setup_cost,
3298   } );
3299
3300   my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
3301                         ( 0 .. @$additional - 1 )
3302                   ),
3303                   'additional_count' => scalar(@$additional),
3304                   'setup_fee' => $amount,
3305                 );
3306
3307   my $error = $part_pkg->insert( options       => \%options,
3308                                  tax_overrides => $override,
3309                                );
3310   if ( $error ) {
3311     $dbh->rollback if $oldAutoCommit;
3312     return $error;
3313   }
3314
3315   my $pkgpart = $part_pkg->pkgpart;
3316   my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
3317   unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
3318     my $type_pkgs = new FS::type_pkgs \%type_pkgs;
3319     $error = $type_pkgs->insert;
3320     if ( $error ) {
3321       $dbh->rollback if $oldAutoCommit;
3322       return $error;
3323     }
3324   }
3325
3326   my $cust_pkg = new FS::cust_pkg ( {
3327     'custnum'    => $self->custnum,
3328     'pkgpart'    => $pkgpart,
3329     'quantity'   => $quantity,
3330     'start_date' => $start_date,
3331     'no_auto'    => $no_auto,
3332     'separate_bill' => $separate_bill,
3333     'locationnum'=> $locationnum,
3334   } );
3335
3336   $error = $cust_pkg->insert;
3337   if ( $error ) {
3338     $dbh->rollback if $oldAutoCommit;
3339     return $error;
3340   } elsif ( $cust_pkg_ref ) {
3341     ${$cust_pkg_ref} = $cust_pkg;
3342   }
3343
3344   if ( $bill_now ) {
3345     my $error = $self->bill( 'invoice_terms' => $invoice_terms,
3346                              'pkg_list'      => [ $cust_pkg ],
3347                            );
3348     if ( $error ) {
3349       $dbh->rollback if $oldAutoCommit;
3350       return $error;
3351     }   
3352   }
3353
3354   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3355   return '';
3356
3357 }
3358
3359 #=item charge_postal_fee
3360 #
3361 #Applies a one time charge this customer.  If there is an error,
3362 #returns the error, returns the cust_pkg charge object or false
3363 #if there was no charge.
3364 #
3365 #=cut
3366 #
3367 # This should be a customer event.  For that to work requires that bill
3368 # also be a customer event.
3369
3370 sub charge_postal_fee {
3371   my $self = shift;
3372
3373   my $pkgpart = $conf->config('postal_invoice-fee_pkgpart', $self->agentnum);
3374   return '' unless ($pkgpart && grep { $_ eq 'POST' } $self->invoicing_list);
3375
3376   my $cust_pkg = new FS::cust_pkg ( {
3377     'custnum'  => $self->custnum,
3378     'pkgpart'  => $pkgpart,
3379     'quantity' => 1,
3380   } );
3381
3382   my $error = $cust_pkg->insert;
3383   $error ? $error : $cust_pkg;
3384 }
3385
3386 =item cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3387
3388 Returns all the invoices (see L<FS::cust_bill>) for this customer.
3389
3390 Optionally, a list or hashref of additional arguments to the qsearch call can
3391 be passed.
3392
3393 =cut
3394
3395 sub cust_bill {
3396   my $self = shift;
3397   my $opt = ref($_[0]) ? shift : { @_ };
3398
3399   #return $self->num_cust_bill unless wantarray || keys %$opt;
3400
3401   $opt->{'table'} = 'cust_bill';
3402   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3403   $opt->{'hashref'}{'custnum'} = $self->custnum;
3404   $opt->{'order_by'} ||= 'ORDER BY _date ASC';
3405
3406   map { $_ } #behavior of sort undefined in scalar context
3407     sort { $a->_date <=> $b->_date }
3408       qsearch($opt);
3409 }
3410
3411 =item open_cust_bill
3412
3413 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
3414 customer.
3415
3416 =cut
3417
3418 sub open_cust_bill {
3419   my $self = shift;
3420
3421   $self->cust_bill(
3422     'extra_sql' => ' AND '. FS::cust_bill->owed_sql. ' > 0',
3423     #@_
3424   );
3425
3426 }
3427
3428 =item legacy_cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3429
3430 Returns all the legacy invoices (see L<FS::legacy_cust_bill>) for this customer.
3431
3432 =cut
3433
3434 sub legacy_cust_bill {
3435   my $self = shift;
3436
3437   #return $self->num_legacy_cust_bill unless wantarray;
3438
3439   map { $_ } #behavior of sort undefined in scalar context
3440     sort { $a->_date <=> $b->_date }
3441       qsearch({ 'table'    => 'legacy_cust_bill',
3442                 'hashref'  => { 'custnum' => $self->custnum, },
3443                 'order_by' => 'ORDER BY _date ASC',
3444              });
3445 }
3446
3447 =item cust_statement [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3448
3449 Returns all the statements (see L<FS::cust_statement>) for this customer.
3450
3451 Optionally, a list or hashref of additional arguments to the qsearch call can
3452 be passed.
3453
3454 =cut
3455
3456 =item cust_bill_void
3457
3458 Returns all the voided invoices (see L<FS::cust_bill_void>) for this customer.
3459
3460 =cut
3461
3462 sub cust_bill_void {
3463   my $self = shift;
3464
3465   map { $_ } #return $self->num_cust_bill_void unless wantarray;
3466   sort { $a->_date <=> $b->_date }
3467     qsearch( 'cust_bill_void', { 'custnum' => $self->custnum } )
3468 }
3469
3470 sub cust_statement {
3471   my $self = shift;
3472   my $opt = ref($_[0]) ? shift : { @_ };
3473
3474   #return $self->num_cust_statement unless wantarray || keys %$opt;
3475
3476   $opt->{'table'} = 'cust_statement';
3477   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3478   $opt->{'hashref'}{'custnum'} = $self->custnum;
3479   $opt->{'order_by'} ||= 'ORDER BY _date ASC';
3480
3481   map { $_ } #behavior of sort undefined in scalar context
3482     sort { $a->_date <=> $b->_date }
3483       qsearch($opt);
3484 }
3485
3486 =item svc_x SVCDB [ OPTION => VALUE | EXTRA_QSEARCH_PARAMS_HASHREF ]
3487
3488 Returns all services of type SVCDB (such as 'svc_acct') for this customer.  
3489
3490 Optionally, a list or hashref of additional arguments to the qsearch call can 
3491 be passed following the SVCDB.
3492
3493 =cut
3494
3495 sub svc_x {
3496   my $self = shift;
3497   my $svcdb = shift;
3498   if ( ! $svcdb =~ /^svc_\w+$/ ) {
3499     warn "$me svc_x requires a svcdb";
3500     return;
3501   }
3502   my $opt = ref($_[0]) ? shift : { @_ };
3503
3504   $opt->{'table'} = $svcdb;
3505   $opt->{'addl_from'} = 
3506     'LEFT JOIN cust_svc USING (svcnum) LEFT JOIN cust_pkg USING (pkgnum) '.
3507     ($opt->{'addl_from'} || '');
3508
3509   my $custnum = $self->custnum;
3510   $custnum =~ /^\d+$/ or die "bad custnum '$custnum'";
3511   my $where = "cust_pkg.custnum = $custnum";
3512
3513   my $extra_sql = $opt->{'extra_sql'} || '';
3514   if ( keys %{ $opt->{'hashref'} } ) {
3515     $extra_sql = " AND $where $extra_sql";
3516   }
3517   else {
3518     if ( $opt->{'extra_sql'} =~ /^\s*where\s(.*)/si ) {
3519       $extra_sql = "WHERE $where AND $1";
3520     }
3521     else {
3522       $extra_sql = "WHERE $where $extra_sql";
3523     }
3524   }
3525   $opt->{'extra_sql'} = $extra_sql;
3526
3527   qsearch($opt);
3528 }
3529
3530 # required for use as an eventtable; 
3531 sub svc_acct {
3532   my $self = shift;
3533   $self->svc_x('svc_acct', @_);
3534 }
3535
3536 =item cust_credit
3537
3538 Returns all the credits (see L<FS::cust_credit>) for this customer.
3539
3540 =cut
3541
3542 sub cust_credit {
3543   my $self = shift;
3544
3545   #return $self->num_cust_credit unless wantarray;
3546
3547   map { $_ } #behavior of sort undefined in scalar context
3548     sort { $a->_date <=> $b->_date }
3549       qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
3550 }
3551
3552 =item cust_credit_pkgnum
3553
3554 Returns all the credits (see L<FS::cust_credit>) for this customer's specific
3555 package when using experimental package balances.
3556
3557 =cut
3558
3559 sub cust_credit_pkgnum {
3560   my( $self, $pkgnum ) = @_;
3561   map { $_ } #return $self->num_cust_credit_pkgnum($pkgnum) unless wantarray;
3562   sort { $a->_date <=> $b->_date }
3563     qsearch( 'cust_credit', { 'custnum' => $self->custnum,
3564                               'pkgnum'  => $pkgnum,
3565                             }
3566     );
3567 }
3568
3569 =item cust_credit_void
3570
3571 Returns all voided credits (see L<FS::cust_credit_void>) for this customer.
3572
3573 =cut
3574
3575 sub cust_credit_void {
3576   my $self = shift;
3577   map { $_ }
3578   sort { $a->_date <=> $b->_date }
3579     qsearch( 'cust_credit_void', { 'custnum' => $self->custnum } )
3580 }
3581
3582 =item cust_pay
3583
3584 Returns all the payments (see L<FS::cust_pay>) for this customer.
3585
3586 =cut
3587
3588 sub cust_pay {
3589   my $self = shift;
3590   my $opt = ref($_[0]) ? shift : { @_ };
3591
3592   return $self->num_cust_pay unless wantarray || keys %$opt;
3593
3594   $opt->{'table'} = 'cust_pay';
3595   $opt->{'hashref'}{'custnum'} = $self->custnum;
3596
3597   map { $_ } #behavior of sort undefined in scalar context
3598     sort { $a->_date <=> $b->_date }
3599       qsearch($opt);
3600
3601 }
3602
3603 =item num_cust_pay
3604
3605 Returns the number of payments (see L<FS::cust_pay>) for this customer.  Also
3606 called automatically when the cust_pay method is used in a scalar context.
3607
3608 =cut
3609
3610 sub num_cust_pay {
3611   my $self = shift;
3612   my $sql = "SELECT COUNT(*) FROM cust_pay WHERE custnum = ?";
3613   my $sth = dbh->prepare($sql) or die dbh->errstr;
3614   $sth->execute($self->custnum) or die $sth->errstr;
3615   $sth->fetchrow_arrayref->[0];
3616 }
3617
3618 =item unapplied_cust_pay
3619
3620 Returns all the unapplied payments (see L<FS::cust_pay>) for this customer.
3621
3622 =cut
3623
3624 sub unapplied_cust_pay {
3625   my $self = shift;
3626
3627   $self->cust_pay(
3628     'extra_sql' => ' AND '. FS::cust_pay->unapplied_sql. ' > 0',
3629     #@_
3630   );
3631
3632 }
3633
3634 =item cust_pay_pkgnum
3635
3636 Returns all the payments (see L<FS::cust_pay>) for this customer's specific
3637 package when using experimental package balances.
3638
3639 =cut
3640
3641 sub cust_pay_pkgnum {
3642   my( $self, $pkgnum ) = @_;
3643   map { $_ } #return $self->num_cust_pay_pkgnum($pkgnum) unless wantarray;
3644   sort { $a->_date <=> $b->_date }
3645     qsearch( 'cust_pay', { 'custnum' => $self->custnum,
3646                            'pkgnum'  => $pkgnum,
3647                          }
3648     );
3649 }
3650
3651 =item cust_pay_void
3652
3653 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
3654
3655 =cut
3656
3657 sub cust_pay_void {
3658   my $self = shift;
3659   map { $_ } #return $self->num_cust_pay_void unless wantarray;
3660   sort { $a->_date <=> $b->_date }
3661     qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
3662 }
3663
3664 =item cust_pay_pending
3665
3666 Returns all pending payments (see L<FS::cust_pay_pending>) for this customer
3667 (without status "done").
3668
3669 =cut
3670
3671 sub cust_pay_pending {
3672   my $self = shift;
3673   return $self->num_cust_pay_pending unless wantarray;
3674   sort { $a->_date <=> $b->_date }
3675     qsearch( 'cust_pay_pending', {
3676                                    'custnum' => $self->custnum,
3677                                    'status'  => { op=>'!=', value=>'done' },
3678                                  },
3679            );
3680 }
3681
3682 =item cust_pay_pending_attempt
3683
3684 Returns all payment attempts / declined payments for this customer, as pending
3685 payments objects (see L<FS::cust_pay_pending>), with status "done" but without
3686 a corresponding payment (see L<FS::cust_pay>).
3687
3688 =cut
3689
3690 sub cust_pay_pending_attempt {
3691   my $self = shift;
3692   return $self->num_cust_pay_pending_attempt unless wantarray;
3693   sort { $a->_date <=> $b->_date }
3694     qsearch( 'cust_pay_pending', {
3695                                    'custnum' => $self->custnum,
3696                                    'status'  => 'done',
3697                                    'paynum'  => '',
3698                                  },
3699            );
3700 }
3701
3702 =item num_cust_pay_pending
3703
3704 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
3705 customer (without status "done").  Also called automatically when the
3706 cust_pay_pending method is used in a scalar context.
3707
3708 =cut
3709
3710 sub num_cust_pay_pending {
3711   my $self = shift;
3712   $self->scalar_sql(
3713     " SELECT COUNT(*) FROM cust_pay_pending ".
3714       " WHERE custnum = ? AND status != 'done' ",
3715     $self->custnum
3716   );
3717 }
3718
3719 =item num_cust_pay_pending_attempt
3720
3721 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
3722 customer, with status "done" but without a corresp.  Also called automatically when the
3723 cust_pay_pending method is used in a scalar context.
3724
3725 =cut
3726
3727 sub num_cust_pay_pending_attempt {
3728   my $self = shift;
3729   $self->scalar_sql(
3730     " SELECT COUNT(*) FROM cust_pay_pending ".
3731       " WHERE custnum = ? AND status = 'done' AND paynum IS NULL",
3732     $self->custnum
3733   );
3734 }
3735
3736 =item cust_refund
3737
3738 Returns all the refunds (see L<FS::cust_refund>) for this customer.
3739
3740 =cut
3741
3742 sub cust_refund {
3743   my $self = shift;
3744   map { $_ } #return $self->num_cust_refund unless wantarray;
3745   sort { $a->_date <=> $b->_date }
3746     qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
3747 }
3748
3749 =item display_custnum
3750
3751 Returns the displayed customer number for this customer: agent_custid if
3752 cust_main-default_agent_custid is set and it has a value, custnum otherwise.
3753
3754 =cut
3755
3756 sub display_custnum {
3757   my $self = shift;
3758
3759   return $self->agent_custid
3760     if $default_agent_custid && $self->agent_custid;
3761
3762   my $prefix = $conf->config('cust_main-custnum-display_prefix', $self->agentnum) || '';
3763
3764   if ( $prefix ) {
3765     return $prefix . 
3766            sprintf('%0'.($custnum_display_length||8).'d', $self->custnum)
3767   } elsif ( $custnum_display_length ) {
3768     return sprintf('%0'.$custnum_display_length.'d', $self->custnum);
3769   } else {
3770     return $self->custnum;
3771   }
3772 }
3773
3774 =item name
3775
3776 Returns a name string for this customer, either "Company (Last, First)" or
3777 "Last, First".
3778
3779 =cut
3780
3781 sub name {
3782   my $self = shift;
3783   my $name = $self->contact;
3784   $name = $self->company. " ($name)" if $self->company;
3785   $name;
3786 }
3787
3788 =item service_contact
3789
3790 Returns the L<FS::contact> object for this customer that has the 'Service'
3791 contact class, or undef if there is no such contact.  Deprecated; don't use
3792 this in new code.
3793
3794 =cut
3795
3796 sub service_contact {
3797   my $self = shift;
3798   if ( !exists($self->{service_contact}) ) {
3799     my $classnum = $self->scalar_sql(
3800       'SELECT classnum FROM contact_class WHERE classname = \'Service\''
3801     ) || 0; #if it's zero, qsearchs will return nothing
3802     my $cust_contact = qsearchs('cust_contact', { 
3803         'classnum' => $classnum,
3804         'custnum'  => $self->custnum,
3805     });
3806     $self->{service_contact} = $cust_contact->contact if $cust_contact;
3807   }
3808   $self->{service_contact};
3809 }
3810
3811 =item ship_name
3812
3813 Returns a name string for this (service/shipping) contact, either
3814 "Company (Last, First)" or "Last, First".
3815
3816 =cut
3817
3818 sub ship_name {
3819   my $self = shift;
3820
3821   my $name = $self->ship_contact;
3822   $name = $self->company. " ($name)" if $self->company;
3823   $name;
3824 }
3825
3826 =item name_short
3827
3828 Returns a name string for this customer, either "Company" or "First Last".
3829
3830 =cut
3831
3832 sub name_short {
3833   my $self = shift;
3834   $self->company !~ /^\s*$/ ? $self->company : $self->contact_firstlast;
3835 }
3836
3837 =item ship_name_short
3838
3839 Returns a name string for this (service/shipping) contact, either "Company"
3840 or "First Last".
3841
3842 =cut
3843
3844 sub ship_name_short {
3845   my $self = shift;
3846   $self->service_contact 
3847     ? $self->ship_contact_firstlast 
3848     : $self->name_short
3849 }
3850
3851 =item contact
3852
3853 Returns this customer's full (billing) contact name only, "Last, First"
3854
3855 =cut
3856
3857 sub contact {
3858   my $self = shift;
3859   $self->get('last'). ', '. $self->first;
3860 }
3861
3862 =item ship_contact
3863
3864 Returns this customer's full (shipping) contact name only, "Last, First"
3865
3866 =cut
3867
3868 sub ship_contact {
3869   my $self = shift;
3870   my $contact = $self->service_contact || $self;
3871   $contact->get('last') . ', ' . $contact->get('first');
3872 }
3873
3874 =item contact_firstlast
3875
3876 Returns this customers full (billing) contact name only, "First Last".
3877
3878 =cut
3879
3880 sub contact_firstlast {
3881   my $self = shift;
3882   $self->first. ' '. $self->get('last');
3883 }
3884
3885 =item ship_contact_firstlast
3886
3887 Returns this customer's full (shipping) contact name only, "First Last".
3888
3889 =cut
3890
3891 sub ship_contact_firstlast {
3892   my $self = shift;
3893   my $contact = $self->service_contact || $self;
3894   $contact->get('first') . ' '. $contact->get('last');
3895 }
3896
3897 sub bill_country_full {
3898   my $self = shift;
3899   $self->bill_location->country_full;
3900 }
3901
3902 sub ship_country_full {
3903   my $self = shift;
3904   $self->ship_location->country_full;
3905 }
3906
3907 =item county_state_county [ PREFIX ]
3908
3909 Returns a string consisting of just the county, state and country.
3910
3911 =cut
3912
3913 sub county_state_country {
3914   my $self = shift;
3915   my $locationnum;
3916   if ( @_ && $_[0] && $self->has_ship_address ) {
3917     $locationnum = $self->ship_locationnum;
3918   } else {
3919     $locationnum = $self->bill_locationnum;
3920   }
3921   my $cust_location = qsearchs('cust_location', { locationnum=>$locationnum });
3922   $cust_location->county_state_country;
3923 }
3924
3925 =item geocode DATA_VENDOR
3926
3927 Returns a value for the customer location as encoded by DATA_VENDOR.
3928 Currently this only makes sense for "CCH" as DATA_VENDOR.
3929
3930 =cut
3931
3932 =item cust_status
3933
3934 =item status
3935
3936 Returns a status string for this customer, currently:
3937
3938 =over 4
3939
3940 =item prospect
3941
3942 No packages have ever been ordered.  Displayed as "No packages".
3943
3944 =item ordered
3945
3946 Recurring packages all are new (not yet billed).
3947
3948 =item active
3949
3950 One or more recurring packages is active.
3951
3952 =item inactive
3953
3954 No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled).
3955
3956 =item suspended
3957
3958 All non-cancelled recurring packages are suspended.
3959
3960 =item cancelled
3961
3962 All recurring packages are cancelled.
3963
3964 =back
3965
3966 Behavior of inactive vs. cancelled edge cases can be adjusted with the
3967 cust_main-status_module configuration option.
3968
3969 =cut
3970
3971 sub status { shift->cust_status(@_); }
3972
3973 sub cust_status {
3974   my $self = shift;
3975   return $self->hashref->{cust_status} if $self->hashref->{cust_status};
3976   for my $status ( FS::cust_main->statuses() ) {
3977     my $method = $status.'_sql';
3978     my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
3979     my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
3980     $sth->execute( ($self->custnum) x $numnum )
3981       or die "Error executing 'SELECT $sql': ". $sth->errstr;
3982     if ( $sth->fetchrow_arrayref->[0] ) {
3983       $self->hashref->{cust_status} = $status;
3984       return $status;
3985     }
3986   }
3987 }
3988
3989 =item is_status_delay_cancel
3990
3991 Returns true if customer status is 'suspended'
3992 and all suspended cust_pkg return true for
3993 cust_pkg->is_status_delay_cancel.
3994
3995 This is not a real status, this only meant for hacking display 
3996 values, because otherwise treating the customer as suspended is 
3997 really the whole point of the delay_cancel option.
3998
3999 =cut
4000
4001 sub is_status_delay_cancel {
4002   my ($self) = @_;
4003   return 0 unless $self->status eq 'suspended';
4004   foreach my $cust_pkg ($self->ncancelled_pkgs) {
4005     return 0 unless $cust_pkg->is_status_delay_cancel;
4006   }
4007   return 1;
4008 }
4009
4010 =item ucfirst_cust_status
4011
4012 =item ucfirst_status
4013
4014 Deprecated, use the cust_status_label method instead.
4015
4016 Returns the status with the first character capitalized.
4017
4018 =cut
4019
4020 sub ucfirst_status {
4021   carp "ucfirst_status deprecated, use cust_status_label" unless $ucfirst_nowarn;
4022   local($ucfirst_nowarn) = 1;
4023   shift->ucfirst_cust_status(@_);
4024 }
4025
4026 sub ucfirst_cust_status {
4027   carp "ucfirst_cust_status deprecated, use cust_status_label" unless $ucfirst_nowarn;
4028   my $self = shift;
4029   ucfirst($self->cust_status);
4030 }
4031
4032 =item cust_status_label
4033
4034 =item status_label
4035
4036 Returns the display label for this status.
4037
4038 =cut
4039
4040 sub status_label { shift->cust_status_label(@_); }
4041
4042 sub cust_status_label {
4043   my $self = shift;
4044   __PACKAGE__->statuslabels->{$self->cust_status};
4045 }
4046
4047 =item statuscolor
4048
4049 Returns a hex triplet color string for this customer's status.
4050
4051 =cut
4052
4053 sub statuscolor { shift->cust_statuscolor(@_); }
4054
4055 sub cust_statuscolor {
4056   my $self = shift;
4057   __PACKAGE__->statuscolors->{$self->cust_status};
4058 }
4059
4060 =item tickets [ STATUS ]
4061
4062 Returns an array of hashes representing the customer's RT tickets.
4063
4064 An optional status (or arrayref or hashref of statuses) may be specified.
4065
4066 =cut
4067
4068 sub tickets {
4069   my $self = shift;
4070   my $status = ( @_ && $_[0] ) ? shift : '';
4071
4072   my $num = $conf->config('cust_main-max_tickets') || 10;
4073   my @tickets = ();
4074
4075   if ( $conf->config('ticket_system') ) {
4076     unless ( $conf->config('ticket_system-custom_priority_field') ) {
4077
4078       @tickets = @{ FS::TicketSystem->customer_tickets( $self->custnum,
4079                                                         $num,
4080                                                         undef,
4081                                                         $status,
4082                                                       )
4083                   };
4084
4085     } else {
4086
4087       foreach my $priority (
4088         $conf->config('ticket_system-custom_priority_field-values'), ''
4089       ) {
4090         last if scalar(@tickets) >= $num;
4091         push @tickets, 
4092           @{ FS::TicketSystem->customer_tickets( $self->custnum,
4093                                                  $num - scalar(@tickets),
4094                                                  $priority,
4095                                                  $status,
4096                                                )
4097            };
4098       }
4099     }
4100   }
4101   (@tickets);
4102 }
4103
4104 =item appointments [ STATUS ]
4105
4106 Returns an array of hashes representing the customer's RT tickets which
4107 are appointments.
4108
4109 =cut
4110
4111 sub appointments {
4112   my $self = shift;
4113   my $status = ( @_ && $_[0] ) ? shift : '';
4114
4115   return () unless $conf->config('ticket_system');
4116
4117   my $queueid = $conf->config('ticket_system-appointment-queueid');
4118
4119   @{ FS::TicketSystem->customer_tickets( $self->custnum,
4120                                          99,
4121                                          undef,
4122                                          $status,
4123                                          $queueid,
4124                                        )
4125   };
4126 }
4127
4128 # Return services representing svc_accts in customer support packages
4129 sub support_services {
4130   my $self = shift;
4131   my %packages = map { $_ => 1 } $conf->config('support_packages');
4132
4133   grep { $_->pkg_svc && $_->pkg_svc->primary_svc eq 'Y' }
4134     grep { $_->part_svc->svcdb eq 'svc_acct' }
4135     map { $_->cust_svc }
4136     grep { exists $packages{ $_->pkgpart } }
4137     $self->ncancelled_pkgs;
4138
4139 }
4140
4141 # Return a list of latitude/longitude for one of the services (if any)
4142 sub service_coordinates {
4143   my $self = shift;
4144
4145   my @svc_X = 
4146     grep { $_->latitude && $_->longitude }
4147     map { $_->svc_x }
4148     map { $_->cust_svc }
4149     $self->ncancelled_pkgs;
4150
4151   scalar(@svc_X) ? ( $svc_X[0]->latitude, $svc_X[0]->longitude ) : ()
4152 }
4153
4154 =item masked FIELD
4155
4156 Returns a masked version of the named field
4157
4158 =cut
4159
4160 sub masked {
4161 my ($self,$field) = @_;
4162
4163 # Show last four
4164
4165 'x'x(length($self->getfield($field))-4).
4166   substr($self->getfield($field), (length($self->getfield($field))-4));
4167
4168 }
4169
4170 =item payment_history
4171
4172 Returns an array of hashrefs standardizing information from cust_bill, cust_pay,
4173 cust_credit and cust_refund objects.  Each hashref has the following fields:
4174
4175 I<type> - one of 'Line item', 'Invoice', 'Payment', 'Credit', 'Refund' or 'Previous'
4176
4177 I<date> - value of _date field, unix timestamp
4178
4179 I<date_pretty> - user-friendly date
4180
4181 I<description> - user-friendly description of item
4182
4183 I<amount> - impact of item on user's balance 
4184 (positive for Invoice/Refund/Line item, negative for Payment/Credit.)
4185 Not to be confused with the native 'amount' field in cust_credit, see below.
4186
4187 I<amount_pretty> - includes money char
4188
4189 I<balance> - customer balance, chronologically as of this item
4190
4191 I<balance_pretty> - includes money char
4192
4193 I<charged> - amount charged for cust_bill (Invoice or Line item) records, undef for other types
4194
4195 I<paid> - amount paid for cust_pay records, undef for other types
4196
4197 I<credit> - amount credited for cust_credit records, undef for other types.
4198 Literally the 'amount' field from cust_credit, renamed here to avoid confusion.
4199
4200 I<refund> - amount refunded for cust_refund records, undef for other types
4201
4202 The four table-specific keys always have positive values, whether they reflect charges or payments.
4203
4204 The following options may be passed to this method:
4205
4206 I<line_items> - if true, returns charges ('Line item') rather than invoices
4207
4208 I<start_date> - unix timestamp, only include records on or after.
4209 If specified, an item of type 'Previous' will also be included.
4210 It does not have table-specific fields.
4211
4212 I<end_date> - unix timestamp, only include records before
4213
4214 I<reverse_sort> - order from newest to oldest (default is oldest to newest)
4215
4216 I<conf> - optional already-loaded FS::Conf object.
4217
4218 =cut
4219
4220 # Caution: this gets used by FS::ClientAPI::MyAccount::billing_history,
4221 # and also for sending customer statements, which should both be kept customer-friendly.
4222 # If you add anything that shouldn't be passed on through the API or exposed 
4223 # to customers, add a new option to include it, don't include it by default
4224 sub payment_history {
4225   my $self = shift;
4226   my $opt = ref($_[0]) ? $_[0] : { @_ };
4227
4228   my $conf = $$opt{'conf'} || new FS::Conf;
4229   my $money_char = $conf->config("money_char") || '$',
4230
4231   #first load entire history, 
4232   #need previous to calculate previous balance
4233   #loading after end_date shouldn't hurt too much?
4234   my @history = ();
4235   if ( $$opt{'line_items'} ) {
4236
4237     foreach my $cust_bill ( $self->cust_bill ) {
4238
4239       push @history, {
4240         'type'        => 'Line item',
4241         'description' => $_->desc( $self->locale ).
4242                            ( $_->sdate && $_->edate
4243                                ? ' '. time2str('%d-%b-%Y', $_->sdate).
4244                                  ' To '. time2str('%d-%b-%Y', $_->edate)
4245                                : ''
4246                            ),
4247         'amount'      => sprintf('%.2f', $_->setup + $_->recur ),
4248         'charged'     => sprintf('%.2f', $_->setup + $_->recur ),
4249         'date'        => $cust_bill->_date,
4250         'date_pretty' => $self->time2str_local('short', $cust_bill->_date ),
4251       }
4252         foreach $cust_bill->cust_bill_pkg;
4253
4254     }
4255
4256   } else {
4257
4258     push @history, {
4259                      'type'        => 'Invoice',
4260                      'description' => 'Invoice #'. $_->display_invnum,
4261                      'amount'      => sprintf('%.2f', $_->charged ),
4262                      'charged'     => sprintf('%.2f', $_->charged ),
4263                      'date'        => $_->_date,
4264                      'date_pretty' => $self->time2str_local('short', $_->_date ),
4265                    }
4266       foreach $self->cust_bill;
4267
4268   }
4269
4270   push @history, {
4271                    'type'        => 'Payment',
4272                    'description' => 'Payment', #XXX type
4273                    'amount'      => sprintf('%.2f', 0 - $_->paid ),
4274                    'paid'        => sprintf('%.2f', $_->paid ),
4275                    'date'        => $_->_date,
4276                    'date_pretty' => $self->time2str_local('short', $_->_date ),
4277                  }
4278     foreach $self->cust_pay;
4279
4280   push @history, {
4281                    'type'        => 'Credit',
4282                    'description' => 'Credit', #more info?
4283                    'amount'      => sprintf('%.2f', 0 -$_->amount ),
4284                    'credit'      => sprintf('%.2f', $_->amount ),
4285                    'date'        => $_->_date,
4286                    'date_pretty' => $self->time2str_local('short', $_->_date ),
4287                  }
4288     foreach $self->cust_credit;
4289
4290   push @history, {
4291                    'type'        => 'Refund',
4292                    'description' => 'Refund', #more info?  type, like payment?
4293                    'amount'      => $_->refund,
4294                    'refund'      => $_->refund,
4295                    'date'        => $_->_date,
4296                    'date_pretty' => $self->time2str_local('short', $_->_date ),
4297                  }
4298     foreach $self->cust_refund;
4299
4300   #put it all in chronological order
4301   @history = sort { $a->{'date'} <=> $b->{'date'} } @history;
4302
4303   #calculate balance, filter items outside date range
4304   my $previous = 0;
4305   my $balance = 0;
4306   my @out = ();
4307   foreach my $item (@history) {
4308     last if $$opt{'end_date'} && ($$item{'date'} >= $$opt{'end_date'});
4309     $balance += $$item{'amount'};
4310     if ($$opt{'start_date'} && ($$item{'date'} < $$opt{'start_date'})) {
4311       $previous += $$item{'amount'};
4312       next;
4313     }
4314     $$item{'balance'} = sprintf("%.2f",$balance);
4315     foreach my $key ( qw(amount balance) ) {
4316       $$item{$key.'_pretty'} = money_pretty($$item{$key});
4317     }
4318     push(@out,$item);
4319   }
4320
4321   # start with previous balance, if there was one
4322   if ($previous) {
4323     my $item = {
4324       'type'        => 'Previous',
4325       'description' => 'Previous balance',
4326       'amount'      => sprintf("%.2f",$previous),
4327       'balance'     => sprintf("%.2f",$previous),
4328       'date'        => $$opt{'start_date'},
4329       'date_pretty' => $self->time2str_local('short', $$opt{'start_date'} ),
4330     };
4331     #false laziness with above
4332     foreach my $key ( qw(amount balance) ) {
4333       $$item{$key.'_pretty'} = $$item{$key};
4334       $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/;
4335     }
4336     unshift(@out,$item);
4337   }
4338
4339   @out = reverse @history if $$opt{'reverse_sort'};
4340
4341   return @out;
4342 }
4343
4344 =item save_cust_payby
4345
4346 Saves a new cust_payby for this customer, replacing an existing entry only
4347 in select circumstances.  Does not validate input.
4348
4349 If auto is specified, marks this as the customer's primary method, or the 
4350 specified weight.  Existing payment methods have their weight incremented as
4351 appropriate.
4352
4353 If bill_location is specified with auto, also sets location in cust_main.
4354
4355 Will not insert complete duplicates of existing records, or records in which the
4356 only difference from an existing record is to turn off automatic payment (will
4357 return without error.)  Will replace existing records in which the only difference 
4358 is to add a value to a previously empty preserved field and/or turn on automatic payment.
4359 Fields marked as preserved are optional, and existing values will not be overwritten with 
4360 blanks when replacing.
4361
4362 Accepts the following named parameters:
4363
4364 =over 4
4365
4366 =item payment_payby
4367
4368 either CARD or CHEK
4369
4370 =item auto
4371
4372 save as an automatic payment type (CARD/CHEK if true, DCRD/DCHK if false)
4373
4374 =item weight
4375
4376 optional, set higher than 1 for secondary, etc.
4377
4378 =item payinfo
4379
4380 required
4381
4382 =item paymask
4383
4384 optional, but should be specified for anything that might be tokenized, will be preserved when replacing
4385
4386 =item payname
4387
4388 required
4389
4390 =item payip
4391
4392 optional, will be preserved when replacing
4393
4394 =item paydate
4395
4396 CARD only, required
4397
4398 =item bill_location
4399
4400 CARD only, required, FS::cust_location object
4401
4402 =item paystart_month
4403
4404 CARD only, optional, will be preserved when replacing
4405
4406 =item paystart_year
4407
4408 CARD only, optional, will be preserved when replacing
4409
4410 =item payissue
4411
4412 CARD only, optional, will be preserved when replacing
4413
4414 =item paycvv
4415
4416 CARD only, only used if conf cvv-save is set appropriately
4417
4418 =item paytype
4419
4420 CHEK only
4421
4422 =item paystate
4423
4424 CHEK only
4425
4426 =back
4427
4428 =cut
4429
4430 #The code for this option is in place, but it's not currently used
4431 #
4432 # =item replace
4433 #
4434 # existing cust_payby object to be replaced (must match custnum)
4435
4436 # stateid/stateid_state/ss are not currently supported in cust_payby,
4437 # might not even work properly in 4.x, but will need to work here if ever added
4438
4439 sub save_cust_payby {
4440   my $self = shift;
4441   my %opt = @_;
4442
4443   my $old = $opt{'replace'};
4444   my $new = new FS::cust_payby { $old ? $old->hash : () };
4445   return "Customer number does not match" if $new->custnum and $new->custnum != $self->custnum;
4446   $new->set( 'custnum' => $self->custnum );
4447
4448   my $payby = $opt{'payment_payby'};
4449   return "Bad payby" unless grep(/^$payby$/,('CARD','CHEK'));
4450
4451   # don't allow turning off auto when replacing
4452   $opt{'auto'} ||= 1 if $old and $old->payby !~ /^D/;
4453
4454   my @check_existing; # payby relevant to this payment_payby
4455
4456   # set payby based on auto
4457   if ( $payby eq 'CARD' ) { 
4458     $new->set( 'payby' => ( $opt{'auto'} ? 'CARD' : 'DCRD' ) );
4459     @check_existing = qw( CARD DCRD );
4460   } elsif ( $payby eq 'CHEK' ) {
4461     $new->set( 'payby' => ( $opt{'auto'} ? 'CHEK' : 'DCHK' ) );
4462     @check_existing = qw( CHEK DCHK );
4463   }
4464
4465   $new->set( 'weight' => $opt{'auto'} ? $opt{'weight'} : '' );
4466
4467   # basic fields
4468   $new->payinfo($opt{'payinfo'}); # sets default paymask, but not if it's already tokenized
4469   $new->paymask($opt{'paymask'}) if $opt{'paymask'}; # in case it's been tokenized, override with loaded paymask
4470   $new->set( 'payname' => $opt{'payname'} );
4471   $new->set( 'payip' => $opt{'payip'} ); # will be preserved below
4472
4473   my $conf = new FS::Conf;
4474
4475   # compare to FS::cust_main::realtime_bop - check both to make sure working correctly
4476   if ( $payby eq 'CARD' &&
4477        ( (grep { $_ eq cardtype($opt{'payinfo'}) } $conf->config('cvv-save')) 
4478          || $conf->exists('business-onlinepayment-verification') 
4479        )
4480   ) {
4481     $new->set( 'paycvv' => $opt{'paycvv'} );
4482   } else {
4483     $new->set( 'paycvv' => '');
4484   }
4485
4486   local $SIG{HUP} = 'IGNORE';
4487   local $SIG{INT} = 'IGNORE';
4488   local $SIG{QUIT} = 'IGNORE';
4489   local $SIG{TERM} = 'IGNORE';
4490   local $SIG{TSTP} = 'IGNORE';
4491   local $SIG{PIPE} = 'IGNORE';
4492
4493   my $oldAutoCommit = $FS::UID::AutoCommit;
4494   local $FS::UID::AutoCommit = 0;
4495   my $dbh = dbh;
4496
4497   # set fields specific to payment_payby
4498   if ( $payby eq 'CARD' ) {
4499     if ($opt{'bill_location'}) {
4500       $opt{'bill_location'}->set('custnum' => $self->custnum);
4501       my $error = $opt{'bill_location'}->find_or_insert;
4502       if ( $error ) {
4503         $dbh->rollback if $oldAutoCommit;
4504         return $error;
4505       }
4506       $new->set( 'locationnum' => $opt{'bill_location'}->locationnum );
4507     }
4508     foreach my $field ( qw( paydate paystart_month paystart_year payissue ) ) {
4509       $new->set( $field => $opt{$field} );
4510     }
4511   } else {
4512     foreach my $field ( qw(paytype paystate) ) {
4513       $new->set( $field => $opt{$field} );
4514     }
4515   }
4516
4517   # other cust_payby to compare this to
4518   my @existing = $self->cust_payby(@check_existing);
4519
4520   # fields that can overwrite blanks with values, but not values with blanks
4521   my @preserve = qw( paymask locationnum paystart_month paystart_year payissue payip );
4522
4523   my $skip_cust_payby = 0; # true if we don't need to save or reweight cust_payby
4524   unless ($old) {
4525     # generally, we don't want to overwrite existing cust_payby with this,
4526     # but we can replace if we're only marking it auto or adding a preserved field
4527     # and we can avoid saving a total duplicate or merely turning off auto
4528 PAYBYLOOP:
4529     foreach my $cust_payby (@existing) {
4530       # check fields that absolutely should not change
4531       foreach my $field ($new->fields) {
4532         next if grep(/^$field$/, qw( custpaybynum payby weight ) );
4533         next if grep(/^$field$/, @preserve );
4534         next PAYBYLOOP unless $new->get($field) eq $cust_payby->get($field);
4535       }
4536       # now check fields that can replace if one value is blank
4537       my $replace = 0;
4538       foreach my $field (@preserve) {
4539         if (
4540           ( $new->get($field) and !$cust_payby->get($field) ) or
4541           ( $cust_payby->get($field) and !$new->get($field) )
4542         ) {
4543           # prevention of overwriting values with blanks happens farther below
4544           $replace = 1;
4545         } elsif ( $new->get($field) ne $cust_payby->get($field) ) {
4546           next PAYBYLOOP;
4547         }
4548       }
4549       unless ( $replace ) {
4550         # nearly identical, now check weight
4551         if ($new->get('weight') eq $cust_payby->get('weight') or !$new->get('weight')) {
4552           # ignore identical cust_payby, and ignore attempts to turn off auto
4553           # no need to save or re-weight cust_payby (but still need to update/commit $self)
4554           $skip_cust_payby = 1;
4555           last PAYBYLOOP;
4556         }
4557         # otherwise, only change is to mark this as primary
4558       }
4559       # if we got this far, we're definitely replacing
4560       $old = $cust_payby;
4561       last PAYBYLOOP;
4562     } #PAYBYLOOP
4563   }
4564
4565   if ($old) {
4566     $new->set( 'custpaybynum' => $old->custpaybynum );
4567     # don't turn off automatic payment (but allow it to be turned on)
4568     if ($new->payby =~ /^D/ and $new->payby ne $old->payby) {
4569       $opt{'auto'} = 1;
4570       $new->set( 'payby' => $old->payby );
4571       $new->set( 'weight' => 1 );
4572     }
4573     # make sure we're not overwriting values with blanks
4574     foreach my $field (@preserve) {
4575       if ( $old->get($field) and !$new->get($field) ) {
4576         $new->set( $field => $old->get($field) );
4577       }
4578     }
4579   }
4580
4581   # only overwrite cust_main bill_location if auto
4582   if ($opt{'auto'} && $opt{'bill_location'}) {
4583     $self->set('bill_location' => $opt{'bill_location'});
4584     my $error = $self->replace;
4585     if ( $error ) {
4586       $dbh->rollback if $oldAutoCommit;
4587       return $error;
4588     }
4589   }
4590
4591   # done with everything except reweighting and saving cust_payby
4592   # still need to commit changes to cust_main and cust_location
4593   if ($skip_cust_payby) {
4594     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4595     return '';
4596   }
4597
4598   # re-weight existing primary cust_pay for this payby
4599   if ($opt{'auto'}) {
4600     foreach my $cust_payby (@existing) {
4601       # relies on cust_payby return order
4602       last unless $cust_payby->payby !~ /^D/;
4603       last if $cust_payby->weight > 1;
4604       next if $new->custpaybynum eq $cust_payby->custpaybynum;
4605       next if $cust_payby->weight < ($opt{'weight'} || 1);
4606       $cust_payby->weight( $cust_payby->weight + 1 );
4607       my $error = $cust_payby->replace;
4608       if ( $error ) {
4609         $dbh->rollback if $oldAutoCommit;
4610         return "Error reweighting cust_payby: $error";
4611       }
4612     }
4613   }
4614
4615   # finally, save cust_payby
4616   my $error = $old ? $new->replace($old) : $new->insert;
4617   if ( $error ) {
4618     $dbh->rollback if $oldAutoCommit;
4619     return $error;
4620   }
4621
4622   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4623   '';
4624
4625 }
4626
4627 =item remove_cvv_from_cust_payby PAYINFO
4628
4629 Removes paycvv from associated cust_payby with matching PAYINFO.
4630
4631 =cut
4632
4633 sub remove_cvv_from_cust_payby {
4634   my ($self,$payinfo) = @_;
4635
4636   my $oldAutoCommit = $FS::UID::AutoCommit;
4637   local $FS::UID::AutoCommit = 0;
4638   my $dbh = dbh;
4639
4640   foreach my $cust_payby ( qsearch('cust_payby',{ custnum => $self->custnum }) ) {
4641     next unless $cust_payby->payinfo eq $payinfo; # can't qsearch on payinfo
4642     $cust_payby->paycvv('');
4643     my $error = $cust_payby->replace;
4644     if ($error) {
4645       $dbh->rollback if $oldAutoCommit;
4646       return $error;
4647     }
4648   }
4649
4650   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4651   '';
4652 }
4653
4654 =back
4655
4656 =head1 CLASS METHODS
4657
4658 =over 4
4659
4660 =item statuses
4661
4662 Class method that returns the list of possible status strings for customers
4663 (see L<the status method|/status>).  For example:
4664
4665   @statuses = FS::cust_main->statuses();
4666
4667 =cut
4668
4669 sub statuses {
4670   my $self = shift;
4671   keys %{ $self->statuscolors };
4672 }
4673
4674 =item cust_status_sql
4675
4676 Returns an SQL fragment to determine the status of a cust_main record, as a 
4677 string.
4678
4679 =cut
4680
4681 sub cust_status_sql {
4682   my $sql = 'CASE';
4683   for my $status ( FS::cust_main->statuses() ) {
4684     my $method = $status.'_sql';
4685     $sql .= ' WHEN ('.FS::cust_main->$method.") THEN '$status'";
4686   }
4687   $sql .= ' END';
4688   return $sql;
4689 }
4690
4691
4692 =item prospect_sql
4693
4694 Returns an SQL expression identifying prospective cust_main records (customers
4695 with no packages ever ordered)
4696
4697 =cut
4698
4699 use vars qw($select_count_pkgs);
4700 $select_count_pkgs =
4701   "SELECT COUNT(*) FROM cust_pkg
4702     WHERE cust_pkg.custnum = cust_main.custnum";
4703
4704 sub select_count_pkgs_sql {
4705   $select_count_pkgs;
4706 }
4707
4708 sub prospect_sql {
4709   " 0 = ( $select_count_pkgs ) ";
4710 }
4711
4712 =item ordered_sql
4713
4714 Returns an SQL expression identifying ordered cust_main records (customers with
4715 no active packages, but recurring packages not yet setup or one time charges
4716 not yet billed).
4717
4718 =cut
4719
4720 sub ordered_sql {
4721   FS::cust_main->none_active_sql.
4722   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->not_yet_billed_sql. " ) ";
4723 }
4724
4725 =item active_sql
4726
4727 Returns an SQL expression identifying active cust_main records (customers with
4728 active recurring packages).
4729
4730 =cut
4731
4732 sub active_sql {
4733   " 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
4734 }
4735
4736 =item none_active_sql
4737
4738 Returns an SQL expression identifying cust_main records with no active
4739 recurring packages.  This includes customers of status prospect, ordered,
4740 inactive, and suspended.
4741
4742 =cut
4743
4744 sub none_active_sql {
4745   " 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
4746 }
4747
4748 =item inactive_sql
4749
4750 Returns an SQL expression identifying inactive cust_main records (customers with
4751 no active recurring packages, but otherwise unsuspended/uncancelled).
4752
4753 =cut
4754
4755 sub inactive_sql {
4756   FS::cust_main->none_active_sql.
4757   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " ) ";
4758 }
4759
4760 =item susp_sql
4761 =item suspended_sql
4762
4763 Returns an SQL expression identifying suspended cust_main records.
4764
4765 =cut
4766
4767
4768 sub suspended_sql { susp_sql(@_); }
4769 sub susp_sql {
4770   FS::cust_main->none_active_sql.
4771   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " ) ";
4772 }
4773
4774 =item cancel_sql
4775 =item cancelled_sql
4776
4777 Returns an SQL expression identifying cancelled cust_main records.
4778
4779 =cut
4780
4781 sub cancel_sql { shift->cancelled_sql(@_); }
4782
4783 =item uncancel_sql
4784 =item uncancelled_sql
4785
4786 Returns an SQL expression identifying un-cancelled cust_main records.
4787
4788 =cut
4789
4790 sub uncancelled_sql { uncancel_sql(@_); }
4791 sub uncancel_sql {
4792   my $self = shift;
4793   "( NOT (".$self->cancelled_sql.") )"; #sensitive to cust_main-status_module
4794 }
4795
4796 =item balance_sql
4797
4798 Returns an SQL fragment to retreive the balance.
4799
4800 =cut
4801
4802 sub balance_sql { "
4803     ( SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill
4804         WHERE cust_bill.custnum   = cust_main.custnum     )
4805   - ( SELECT COALESCE( SUM(paid),    0 ) FROM cust_pay
4806         WHERE cust_pay.custnum    = cust_main.custnum     )
4807   - ( SELECT COALESCE( SUM(amount),  0 ) FROM cust_credit
4808         WHERE cust_credit.custnum = cust_main.custnum     )
4809   + ( SELECT COALESCE( SUM(refund),  0 ) FROM cust_refund
4810         WHERE cust_refund.custnum = cust_main.custnum     )
4811 "; }
4812
4813 =item balance_date_sql [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
4814
4815 Returns an SQL fragment to retreive the balance for this customer, optionally
4816 considering invoices with date earlier than START_TIME, and not
4817 later than END_TIME (total_owed_date minus total_unapplied_credits minus
4818 total_unapplied_payments).
4819
4820 Times are specified as SQL fragments or numeric
4821 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
4822 L<Date::Parse> for conversion functions.  The empty string can be passed
4823 to disable that time constraint completely.
4824
4825 Available options are:
4826
4827 =over 4
4828
4829 =item unapplied_date
4830
4831 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)
4832
4833 =item total
4834
4835 (unused.  obsolete?)
4836 set to true to remove all customer comparison clauses, for totals
4837
4838 =item where
4839
4840 (unused.  obsolete?)
4841 WHERE clause hashref (elements "AND"ed together) (typically used with the total option)
4842
4843 =item join
4844
4845 (unused.  obsolete?)
4846 JOIN clause (typically used with the total option)
4847
4848 =item cutoff
4849
4850 An absolute cutoff time.  Payments, credits, and refunds I<applied> after this 
4851 time will be ignored.  Note that START_TIME and END_TIME only limit the date 
4852 range for invoices and I<unapplied> payments, credits, and refunds.
4853
4854 =back
4855
4856 =cut
4857
4858 sub balance_date_sql {
4859   my( $class, $start, $end, %opt ) = @_;
4860
4861   my $cutoff = $opt{'cutoff'};
4862
4863   my $owed         = FS::cust_bill->owed_sql($cutoff);
4864   my $unapp_refund = FS::cust_refund->unapplied_sql($cutoff);
4865   my $unapp_credit = FS::cust_credit->unapplied_sql($cutoff);
4866   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
4867
4868   my $j = $opt{'join'} || '';
4869
4870   my $owed_wh   = $class->_money_table_where( 'cust_bill',   $start,$end,%opt );
4871   my $refund_wh = $class->_money_table_where( 'cust_refund', $start,$end,%opt );
4872   my $credit_wh = $class->_money_table_where( 'cust_credit', $start,$end,%opt );
4873   my $pay_wh    = $class->_money_table_where( 'cust_pay',    $start,$end,%opt );
4874
4875   "   ( SELECT COALESCE(SUM($owed),         0) FROM cust_bill   $j $owed_wh   )
4876     + ( SELECT COALESCE(SUM($unapp_refund), 0) FROM cust_refund $j $refund_wh )
4877     - ( SELECT COALESCE(SUM($unapp_credit), 0) FROM cust_credit $j $credit_wh )
4878     - ( SELECT COALESCE(SUM($unapp_pay),    0) FROM cust_pay    $j $pay_wh    )
4879   ";
4880
4881 }
4882
4883 =item unapplied_payments_date_sql START_TIME [ END_TIME ]
4884
4885 Returns an SQL fragment to retreive the total unapplied payments for this
4886 customer, only considering payments with date earlier than START_TIME, and
4887 optionally not later than END_TIME.
4888
4889 Times are specified as SQL fragments or numeric
4890 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
4891 L<Date::Parse> for conversion functions.  The empty string can be passed
4892 to disable that time constraint completely.
4893
4894 Available options are:
4895
4896 =cut
4897
4898 sub unapplied_payments_date_sql {
4899   my( $class, $start, $end, %opt ) = @_;
4900
4901   my $cutoff = $opt{'cutoff'};
4902
4903   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
4904
4905   my $pay_where = $class->_money_table_where( 'cust_pay', $start, $end,
4906                                                           'unapplied_date'=>1 );
4907
4908   " ( SELECT COALESCE(SUM($unapp_pay), 0) FROM cust_pay $pay_where ) ";
4909 }
4910
4911 =item _money_table_where TABLE START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
4912
4913 Helper method for balance_date_sql; name (and usage) subject to change
4914 (suggestions welcome).
4915
4916 Returns a WHERE clause for the specified monetary TABLE (cust_bill,
4917 cust_refund, cust_credit or cust_pay).
4918
4919 If TABLE is "cust_bill" or the unapplied_date option is true, only
4920 considers records with date earlier than START_TIME, and optionally not
4921 later than END_TIME .
4922
4923 =cut
4924
4925 sub _money_table_where {
4926   my( $class, $table, $start, $end, %opt ) = @_;
4927
4928   my @where = ();
4929   push @where, "cust_main.custnum = $table.custnum" unless $opt{'total'};
4930   if ( $table eq 'cust_bill' || $opt{'unapplied_date'} ) {
4931     push @where, "$table._date <= $start" if defined($start) && length($start);
4932     push @where, "$table._date >  $end"   if defined($end)   && length($end);
4933   }
4934   push @where, @{$opt{'where'}} if $opt{'where'};
4935   my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where ) : '';
4936
4937   $where;
4938
4939 }
4940
4941 #for dyanmic FS::$table->search in httemplate/misc/email_customers.html
4942 use FS::cust_main::Search;
4943 sub search {
4944   my $class = shift;
4945   FS::cust_main::Search->search(@_);
4946 }
4947
4948 =back
4949
4950 =head1 SUBROUTINES
4951
4952 =over 4
4953
4954 =item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
4955
4956 Generates a templated notification to the customer (see L<Text::Template>).
4957
4958 OPTIONS is a hash and may include
4959
4960 I<extra_fields> - a hashref of name/value pairs which will be substituted
4961    into the template.  These values may override values mentioned below
4962    and those from the customer record.
4963
4964 I<template_text> - if present, ignores TEMPLATE_NAME and uses the provided text
4965
4966 The following variables are available in the template instead of or in addition
4967 to the fields of the customer record.
4968
4969 I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or company_address
4970
4971 =cut
4972
4973 # a lot like cust_bill::print_latex
4974 sub generate_letter {
4975   my ($self, $template, %options) = @_;
4976
4977   warn "Template $template does not exist" && return
4978     unless $conf->exists($template) || $options{'template_text'};
4979
4980   my $template_source = $options{'template_text'} 
4981                         ? [ $options{'template_text'} ] 
4982                         : [ map "$_\n", $conf->config($template) ];
4983
4984   my $letter_template = new Text::Template
4985                         ( TYPE       => 'ARRAY',
4986                           SOURCE     => $template_source,
4987                           DELIMITERS => [ '[@--', '--@]' ],
4988                         )
4989     or die "can't create new Text::Template object: Text::Template::ERROR";
4990
4991   $letter_template->compile()
4992     or die "can't compile template: Text::Template::ERROR";
4993
4994   my %letter_data = map { $_ => $self->$_ } $self->fields;
4995
4996   for (keys %{$options{extra_fields}}){
4997     $letter_data{$_} = $options{extra_fields}->{$_};
4998   }
4999
5000   unless(exists($letter_data{returnaddress})){
5001     my $retadd = join("\n", $conf->config_orbase( 'invoice_latexreturnaddress',
5002                                                   $self->agent_template)
5003                      );
5004     if ( length($retadd) ) {
5005       $letter_data{returnaddress} = $retadd;
5006     } elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) {
5007       $letter_data{returnaddress} =
5008         join( "\n", map { s/( {2,})/'~' x length($1)/eg;
5009                           s/$/\\\\\*/;
5010                           $_;
5011                         }
5012                     ( $conf->config('company_name', $self->agentnum),
5013                       $conf->config('company_address', $self->agentnum),
5014                     )
5015         );
5016     } else {
5017       $letter_data{returnaddress} = '~';
5018     }
5019   }
5020
5021   $letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
5022
5023   $letter_data{company_name} = $conf->config('company_name', $self->agentnum);
5024
5025   my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
5026
5027   my $lh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
5028                            DIR      => $dir,
5029                            SUFFIX   => '.eps',
5030                            UNLINK   => 0,
5031                          ) or die "can't open temp file: $!\n";
5032   print $lh $conf->config_binary('logo.eps', $self->agentnum)
5033     or die "can't write temp file: $!\n";
5034   close $lh;
5035   $letter_data{'logo_file'} = $lh->filename;
5036
5037   my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
5038                            DIR      => $dir,
5039                            SUFFIX   => '.tex',
5040                            UNLINK   => 0,
5041                          ) or die "can't open temp file: $!\n";
5042
5043   $letter_template->fill_in( OUTPUT => $fh, HASH => \%letter_data );
5044   close $fh;
5045   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
5046   return ($1, $letter_data{'logo_file'});
5047
5048 }
5049
5050 =item print_ps TEMPLATE 
5051
5052 Returns an postscript letter filled in from TEMPLATE, as a scalar.
5053
5054 =cut
5055
5056 sub print_ps {
5057   my $self = shift;
5058   my($file, $lfile) = $self->generate_letter(@_);
5059   my $ps = FS::Misc::generate_ps($file);
5060   unlink($file.'.tex');
5061   unlink($lfile);
5062
5063   $ps;
5064 }
5065
5066 =item print TEMPLATE
5067
5068 Prints the filled in template.
5069
5070 TEMPLATE is the name of a L<Text::Template> to fill in and print.
5071
5072 =cut
5073
5074 sub queueable_print {
5075   my %opt = @_;
5076
5077   my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
5078     or die "invalid customer number: " . $opt{custnum};
5079
5080 #do not backport this change to 3.x
5081 #  my $error = $self->print( { 'template' => $opt{template} } );
5082   my $error = $self->print( $opt{'template'} );
5083   die $error if $error;
5084 }
5085
5086 sub print {
5087   my ($self, $template) = (shift, shift);
5088   do_print(
5089     [ $self->print_ps($template) ],
5090     'agentnum' => $self->agentnum,
5091   );
5092 }
5093
5094 #these three subs should just go away once agent stuff is all config overrides
5095
5096 sub agent_template {
5097   my $self = shift;
5098   $self->_agent_plandata('agent_templatename');
5099 }
5100
5101 sub agent_invoice_from {
5102   my $self = shift;
5103   $self->_agent_plandata('agent_invoice_from');
5104 }
5105
5106 sub _agent_plandata {
5107   my( $self, $option ) = @_;
5108
5109   #yuck.  this whole thing needs to be reconciled better with 1.9's idea of
5110   #agent-specific Conf
5111
5112   use FS::part_event::Condition;
5113   
5114   my $agentnum = $self->agentnum;
5115
5116   my $regexp = regexp_sql();
5117
5118   my $part_event_option =
5119     qsearchs({
5120       'select'    => 'part_event_option.*',
5121       'table'     => 'part_event_option',
5122       'addl_from' => q{
5123         LEFT JOIN part_event USING ( eventpart )
5124         LEFT JOIN part_event_option AS peo_agentnum
5125           ON ( part_event.eventpart = peo_agentnum.eventpart
5126                AND peo_agentnum.optionname = 'agentnum'
5127                AND peo_agentnum.optionvalue }. $regexp. q{ '(^|,)}. $agentnum. q{(,|$)'
5128              )
5129         LEFT JOIN part_event_condition
5130           ON ( part_event.eventpart = part_event_condition.eventpart
5131                AND part_event_condition.conditionname = 'cust_bill_age'
5132              )
5133         LEFT JOIN part_event_condition_option
5134           ON ( part_event_condition.eventconditionnum = part_event_condition_option.eventconditionnum
5135                AND part_event_condition_option.optionname = 'age'
5136              )
5137       },
5138       #'hashref'   => { 'optionname' => $option },
5139       #'hashref'   => { 'part_event_option.optionname' => $option },
5140       'extra_sql' =>
5141         " WHERE part_event_option.optionname = ". dbh->quote($option).
5142         " AND action = 'cust_bill_send_agent' ".
5143         " AND ( disabled IS NULL OR disabled != 'Y' ) ".
5144         " AND peo_agentnum.optionname = 'agentnum' ".
5145         " AND ( agentnum IS NULL OR agentnum = $agentnum ) ".
5146         " ORDER BY
5147            CASE WHEN part_event_condition_option.optionname IS NULL
5148            THEN -1
5149            ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
5150         " END
5151           , part_event.weight".
5152         " LIMIT 1"
5153     });
5154     
5155   unless ( $part_event_option ) {
5156     return $self->agent->invoice_template || ''
5157       if $option eq 'agent_templatename';
5158     return '';
5159   }
5160
5161   $part_event_option->optionvalue;
5162
5163 }
5164
5165 sub process_o2m_qsearch {
5166   my $self = shift;
5167   my $table = shift;
5168   return qsearch($table, @_) unless $table eq 'contact';
5169
5170   my $hashref = shift;
5171   my %hash = %$hashref;
5172   ( my $custnum = delete $hash{'custnum'} ) =~ /^(\d+)$/
5173     or die 'guru meditation #4343';
5174
5175   qsearch({ 'table'     => 'contact',
5176             'addl_from' => 'LEFT JOIN cust_contact USING ( contactnum )',
5177             'hashref'   => \%hash,
5178             'extra_sql' => ( keys %hash ? ' AND ' : ' WHERE ' ).
5179                            " cust_contact.custnum = $custnum "
5180          });                
5181 }
5182
5183 sub process_o2m_qsearchs {
5184   my $self = shift;
5185   my $table = shift;
5186   return qsearchs($table, @_) unless $table eq 'contact';
5187
5188   my $hashref = shift;
5189   my %hash = %$hashref;
5190   ( my $custnum = delete $hash{'custnum'} ) =~ /^(\d+)$/
5191     or die 'guru meditation #2121';
5192
5193   qsearchs({ 'table'     => 'contact',
5194              'addl_from' => 'LEFT JOIN cust_contact USING ( contactnum )',
5195              'hashref'   => \%hash,
5196              'extra_sql' => ( keys %hash ? ' AND ' : ' WHERE ' ).
5197                             " cust_contact.custnum = $custnum "
5198           });                
5199 }
5200
5201 =item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
5202
5203 Subroutine (not a method), designed to be called from the queue.
5204
5205 Takes a list of options and values.
5206
5207 Pulls up the customer record via the custnum option and calls bill_and_collect.
5208
5209 =cut
5210
5211 sub queued_bill {
5212   my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_;
5213
5214   my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
5215   warn 'bill_and_collect custnum#'. $cust_main->custnum. "\n";#log custnum w/pid
5216
5217   #without this errors don't get rolled back
5218   $args{'fatal'} = 1; # runs from job queue, will be caught
5219
5220   $cust_main->bill_and_collect( %args );
5221 }
5222
5223 =item queued_collect 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
5224
5225 Like queued_bill, but instead of C<bill_and_collect>, just runs the 
5226 C<collect> part.  This is used in batch tax calculation, where invoice 
5227 generation and collection events have to be completely separated.
5228
5229 =cut
5230
5231 sub queued_collect {
5232   my (%args) = @_;
5233   my $cust_main = FS::cust_main->by_key($args{'custnum'});
5234   
5235   $cust_main->collect(%args);
5236 }
5237
5238 sub process_bill_and_collect {
5239   my $job = shift;
5240   my $param = shift;
5241   my $cust_main = qsearchs( 'cust_main', { custnum => $param->{'custnum'} } )
5242       or die "custnum '$param->{custnum}' not found!\n";
5243   $param->{'job'}   = $job;
5244   $param->{'fatal'} = 1; # runs from job queue, will be caught
5245   $param->{'retry'} = 1;
5246
5247   $cust_main->bill_and_collect( %$param );
5248 }
5249
5250 #starting to take quite a while for big dbs
5251 #   (JRNL: journaled so it only happens once per database)
5252 # - seq scan of h_cust_main (yuck), but not going to index paycvv, so
5253 # JRNL seq scan of cust_main on signupdate... index signupdate?  will that help?
5254 # JRNL seq scan of cust_main on paydate... index on substrings?  maybe set an
5255 # JRNL seq scan of cust_main on payinfo.. certainly not going toi ndex that...
5256 # JRNL leading/trailing spaces in first, last, company
5257 # JRNL migrate to cust_payby
5258 # - otaker upgrade?  journal and call it good?  (double check to make sure
5259 #    we're not still setting otaker here)
5260 #
5261 #only going to get worse with new location stuff...
5262
5263 sub _upgrade_data { #class method
5264   my ($class, %opts) = @_;
5265
5266   my @statements = ();
5267
5268   #this seems to be the only expensive one.. why does it take so long?
5269   unless ( FS::upgrade_journal->is_done('cust_main__signupdate') ) {
5270     push @statements,
5271       'UPDATE cust_main SET signupdate = (SELECT signupdate FROM h_cust_main WHERE signupdate IS NOT NULL AND h_cust_main.custnum = cust_main.custnum ORDER BY historynum DESC LIMIT 1) WHERE signupdate IS NULL';
5272     FS::upgrade_journal->set_done('cust_main__signupdate');
5273   }
5274
5275   my $t = time;
5276   foreach my $sql ( @statements ) {
5277     my $sth = dbh->prepare($sql) or die dbh->errstr;
5278     $sth->execute or die $sth->errstr;
5279     #warn ( (time - $t). " seconds\n" );
5280     #$t = time;
5281   }
5282
5283   local($ignore_expired_card) = 1;
5284   local($ignore_banned_card) = 1;
5285   local($skip_fuzzyfiles) = 1;
5286   local($import) = 1; #prevent automatic geocoding (need its own variable?)
5287
5288   FS::cust_main::Location->_upgrade_data(%opts);
5289
5290   unless ( FS::upgrade_journal->is_done('cust_main__trimspaces') ) {
5291
5292     foreach my $cust_main ( qsearch({
5293       'table'     => 'cust_main', 
5294       'hashref'   => {},
5295       'extra_sql' => 'WHERE '.
5296                        join(' OR ',
5297                          map "$_ LIKE ' %' OR $_ LIKE '% ' OR $_ LIKE '%  %'",
5298                            qw( first last company )
5299                        ),
5300     }) ) {
5301       my $error = $cust_main->replace;
5302       die $error if $error;
5303     }
5304
5305     FS::upgrade_journal->set_done('cust_main__trimspaces');
5306
5307   }
5308
5309   $class->_upgrade_otaker(%opts);
5310
5311 }
5312
5313 =back
5314
5315 =head1 BUGS
5316
5317 The delete method.
5318
5319 The delete method should possibly take an FS::cust_main object reference
5320 instead of a scalar customer number.
5321
5322 Bill and collect options should probably be passed as references instead of a
5323 list.
5324
5325 There should probably be a configuration file with a list of allowed credit
5326 card types.
5327
5328 No multiple currency support (probably a larger project than just this module).
5329
5330 Birthdates rely on negative epoch values.
5331
5332 B<collect> I<invoice_time> should be renamed I<time>, like B<bill>.
5333
5334 =head1 SEE ALSO
5335
5336 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
5337 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
5338 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
5339
5340 =cut
5341
5342 1;
5343