91cdcda07b13a13fb39fcf3bc361c84f1274b3a8
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me $conf
5              $money_char $date_format $rdate_format $date_format_long );
6 use vars qw( $invoice_lines @buf ); #yuck
7 use Fcntl qw(:flock); #for spool_csv
8 use List::Util qw(min max);
9 use Date::Format;
10 use Text::Template 1.20;
11 use File::Temp 0.14;
12 use String::ShellQuote;
13 use HTML::Entities;
14 use Locale::Country;
15 use Storable qw( freeze thaw );
16 use FS::UID qw( datasrc );
17 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
18 use FS::Record qw( qsearch qsearchs dbh );
19 use FS::cust_main_Mixin;
20 use FS::cust_main;
21 use FS::cust_statement;
22 use FS::cust_bill_pkg;
23 use FS::cust_bill_pkg_display;
24 use FS::cust_bill_pkg_detail;
25 use FS::cust_credit;
26 use FS::cust_pay;
27 use FS::cust_pkg;
28 use FS::cust_credit_bill;
29 use FS::pay_batch;
30 use FS::cust_pay_batch;
31 use FS::cust_bill_event;
32 use FS::cust_event;
33 use FS::part_pkg;
34 use FS::cust_bill_pay;
35 use FS::cust_bill_pay_batch;
36 use FS::part_bill_event;
37 use FS::payby;
38 use FS::bill_batch;
39 use FS::cust_bill_batch;
40
41 @ISA = qw( FS::cust_main_Mixin FS::Record );
42
43 $DEBUG = 0;
44 $me = '[FS::cust_bill]';
45
46 #ask FS::UID to run this stuff for us later
47 FS::UID->install_callback( sub { 
48   $conf = new FS::Conf;
49   $money_char       = $conf->config('money_char')       || '$';  
50   $date_format      = $conf->config('date_format')      || '%x'; #/YY
51   $rdate_format     = $conf->config('date_format')      || '%m/%d/%Y';  #/YYYY
52   $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
53 } );
54
55 =head1 NAME
56
57 FS::cust_bill - Object methods for cust_bill records
58
59 =head1 SYNOPSIS
60
61   use FS::cust_bill;
62
63   $record = new FS::cust_bill \%hash;
64   $record = new FS::cust_bill { 'column' => 'value' };
65
66   $error = $record->insert;
67
68   $error = $new_record->replace($old_record);
69
70   $error = $record->delete;
71
72   $error = $record->check;
73
74   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
75
76   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
77
78   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
79
80   @cust_pay_objects = $cust_bill->cust_pay;
81
82   $tax_amount = $record->tax;
83
84   @lines = $cust_bill->print_text;
85   @lines = $cust_bill->print_text $time;
86
87 =head1 DESCRIPTION
88
89 An FS::cust_bill object represents an invoice; a declaration that a customer
90 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
91 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
92 following fields are currently supported:
93
94 Regular fields
95
96 =over 4
97
98 =item invnum - primary key (assigned automatically for new invoices)
99
100 =item custnum - customer (see L<FS::cust_main>)
101
102 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
103 L<Time::Local> and L<Date::Parse> for conversion functions.
104
105 =item charged - amount of this invoice
106
107 =item invoice_terms - optional terms override for this specific invoice
108
109 =back
110
111 Customer info at invoice generation time
112
113 =over 4
114
115 =item previous_balance
116
117 =item billing_balance
118
119 =back
120
121 Deprecated
122
123 =over 4
124
125 =item printed - deprecated
126
127 =back
128
129 Specific use cases
130
131 =over 4
132
133 =item closed - books closed flag, empty or `Y'
134
135 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
136
137 =item agent_invid - legacy invoice number
138
139 =back
140
141 =head1 METHODS
142
143 =over 4
144
145 =item new HASHREF
146
147 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
148 Invoices are normally created by calling the bill method of a customer object
149 (see L<FS::cust_main>).
150
151 =cut
152
153 sub table { 'cust_bill'; }
154
155 sub cust_linked { $_[0]->cust_main_custnum; } 
156 sub cust_unlinked_msg {
157   my $self = shift;
158   "WARNING: can't find cust_main.custnum ". $self->custnum.
159   ' (cust_bill.invnum '. $self->invnum. ')';
160 }
161
162 =item insert
163
164 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
165 returns the error, otherwise returns false.
166
167 =cut
168
169 sub insert {
170   my $self = shift;
171   warn "$me insert called\n" if $DEBUG;
172
173   local $SIG{HUP} = 'IGNORE';
174   local $SIG{INT} = 'IGNORE';
175   local $SIG{QUIT} = 'IGNORE';
176   local $SIG{TERM} = 'IGNORE';
177   local $SIG{TSTP} = 'IGNORE';
178   local $SIG{PIPE} = 'IGNORE';
179
180   my $oldAutoCommit = $FS::UID::AutoCommit;
181   local $FS::UID::AutoCommit = 0;
182   my $dbh = dbh;
183
184   my $error = $self->SUPER::insert;
185   if ( $error ) {
186     $dbh->rollback if $oldAutoCommit;
187     return $error;
188   }
189
190   if ( $self->get('cust_bill_pkg') ) {
191     foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
192       $cust_bill_pkg->invnum($self->invnum);
193       my $error = $cust_bill_pkg->insert;
194       if ( $error ) {
195         $dbh->rollback if $oldAutoCommit;
196         return "can't create invoice line item: $error";
197       }
198     }
199   }
200
201   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
202   '';
203
204 }
205
206 =item delete
207
208 This method now works but you probably shouldn't use it.  Instead, apply a
209 credit against the invoice.
210
211 Using this method to delete invoices outright is really, really bad.  There
212 would be no record you ever posted this invoice, and there are no check to
213 make sure charged = 0 or that there are no associated cust_bill_pkg records.
214
215 Really, don't use it.
216
217 =cut
218
219 sub delete {
220   my $self = shift;
221   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
222
223   local $SIG{HUP} = 'IGNORE';
224   local $SIG{INT} = 'IGNORE';
225   local $SIG{QUIT} = 'IGNORE';
226   local $SIG{TERM} = 'IGNORE';
227   local $SIG{TSTP} = 'IGNORE';
228   local $SIG{PIPE} = 'IGNORE';
229
230   my $oldAutoCommit = $FS::UID::AutoCommit;
231   local $FS::UID::AutoCommit = 0;
232   my $dbh = dbh;
233
234   foreach my $table (qw(
235     cust_bill_event
236     cust_event
237     cust_credit_bill
238     cust_bill_pay
239     cust_bill_pay
240     cust_credit_bill
241     cust_pay_batch
242     cust_bill_pay_batch
243     cust_bill_pkg
244   )) {
245
246     foreach my $linked ( $self->$table() ) {
247       my $error = $linked->delete;
248       if ( $error ) {
249         $dbh->rollback if $oldAutoCommit;
250         return $error;
251       }
252     }
253
254   }
255
256   my $error = $self->SUPER::delete(@_);
257   if ( $error ) {
258     $dbh->rollback if $oldAutoCommit;
259     return $error;
260   }
261
262   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
263
264   '';
265
266 }
267
268 =item replace [ OLD_RECORD ]
269
270 You can, but probably shouldn't modify invoices...
271
272 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
273 supplied, replaces this record.  If there is an error, returns the error,
274 otherwise returns false.
275
276 =cut
277
278 #replace can be inherited from Record.pm
279
280 # replace_check is now the preferred way to #implement replace data checks
281 # (so $object->replace() works without an argument)
282
283 sub replace_check {
284   my( $new, $old ) = ( shift, shift );
285   return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
286   #return "Can't change _date!" unless $old->_date eq $new->_date;
287   return "Can't change _date" unless $old->_date == $new->_date;
288   return "Can't change charged" unless $old->charged == $new->charged
289                                     || $old->charged == 0;
290
291   '';
292 }
293
294 =item check
295
296 Checks all fields to make sure this is a valid invoice.  If there is an error,
297 returns the error, otherwise returns false.  Called by the insert and replace
298 methods.
299
300 =cut
301
302 sub check {
303   my $self = shift;
304
305   my $error =
306     $self->ut_numbern('invnum')
307     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
308     || $self->ut_numbern('_date')
309     || $self->ut_money('charged')
310     || $self->ut_numbern('printed')
311     || $self->ut_enum('closed', [ '', 'Y' ])
312     || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
313     || $self->ut_numbern('agent_invid') #varchar?
314   ;
315   return $error if $error;
316
317   $self->_date(time) unless $self->_date;
318
319   $self->printed(0) if $self->printed eq '';
320
321   $self->SUPER::check;
322 }
323
324 =item display_invnum
325
326 Returns the displayed invoice number for this invoice: agent_invid if
327 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
328
329 =cut
330
331 sub display_invnum {
332   my $self = shift;
333   if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
334     return $self->agent_invid;
335   } else {
336     return $self->invnum;
337   }
338 }
339
340 =item previous
341
342 Returns a list consisting of the total previous balance for this customer, 
343 followed by the previous outstanding invoices (as FS::cust_bill objects also).
344
345 =cut
346
347 sub previous {
348   my $self = shift;
349   my $total = 0;
350   my @cust_bill = sort { $a->_date <=> $b->_date }
351     grep { $_->owed != 0 && $_->_date < $self->_date }
352       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
353   ;
354   foreach ( @cust_bill ) { $total += $_->owed; }
355   $total, @cust_bill;
356 }
357
358 =item cust_bill_pkg
359
360 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
361
362 =cut
363
364 sub cust_bill_pkg {
365   my $self = shift;
366   qsearch(
367     { 'table'    => 'cust_bill_pkg',
368       'hashref'  => { 'invnum' => $self->invnum },
369       'order_by' => 'ORDER BY billpkgnum',
370     }
371   );
372 }
373
374 =item cust_bill_pkg_pkgnum PKGNUM
375
376 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
377 specified pkgnum.
378
379 =cut
380
381 sub cust_bill_pkg_pkgnum {
382   my( $self, $pkgnum ) = @_;
383   qsearch(
384     { 'table'    => 'cust_bill_pkg',
385       'hashref'  => { 'invnum' => $self->invnum,
386                       'pkgnum' => $pkgnum,
387                     },
388       'order_by' => 'ORDER BY billpkgnum',
389     }
390   );
391 }
392
393 =item cust_pkg
394
395 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
396 this invoice.
397
398 =cut
399
400 sub cust_pkg {
401   my $self = shift;
402   my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
403                      $self->cust_bill_pkg;
404   my %saw = ();
405   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
406 }
407
408 =item no_auto
409
410 Returns true if any of the packages (or their definitions) corresponding to the
411 line items for this invoice have the no_auto flag set.
412
413 =cut
414
415 sub no_auto {
416   my $self = shift;
417   grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
418 }
419
420 =item open_cust_bill_pkg
421
422 Returns the open line items for this invoice.
423
424 Note that cust_bill_pkg with both setup and recur fees are returned as two
425 separate line items, each with only one fee.
426
427 =cut
428
429 # modeled after cust_main::open_cust_bill
430 sub open_cust_bill_pkg {
431   my $self = shift;
432
433   # grep { $_->owed > 0 } $self->cust_bill_pkg
434
435   my %other = ( 'recur' => 'setup',
436                 'setup' => 'recur', );
437   my @open = ();
438   foreach my $field ( qw( recur setup )) {
439     push @open, map  { $_->set( $other{$field}, 0 ); $_; }
440                 grep { $_->owed($field) > 0 }
441                 $self->cust_bill_pkg;
442   }
443
444   @open;
445 }
446
447 =item cust_bill_event
448
449 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
450
451 =cut
452
453 sub cust_bill_event {
454   my $self = shift;
455   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
456 }
457
458 =item num_cust_bill_event
459
460 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
461
462 =cut
463
464 sub num_cust_bill_event {
465   my $self = shift;
466   my $sql =
467     "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
468   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
469   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
470   $sth->fetchrow_arrayref->[0];
471 }
472
473 =item cust_event
474
475 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
476
477 =cut
478
479 #false laziness w/cust_pkg.pm
480 sub cust_event {
481   my $self = shift;
482   qsearch({
483     'table'     => 'cust_event',
484     'addl_from' => 'JOIN part_event USING ( eventpart )',
485     'hashref'   => { 'tablenum' => $self->invnum },
486     'extra_sql' => " AND eventtable = 'cust_bill' ",
487   });
488 }
489
490 =item num_cust_event
491
492 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
493
494 =cut
495
496 #false laziness w/cust_pkg.pm
497 sub num_cust_event {
498   my $self = shift;
499   my $sql =
500     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
501     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
502   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
503   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
504   $sth->fetchrow_arrayref->[0];
505 }
506
507 =item cust_main
508
509 Returns the customer (see L<FS::cust_main>) for this invoice.
510
511 =cut
512
513 sub cust_main {
514   my $self = shift;
515   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
516 }
517
518 =item cust_suspend_if_balance_over AMOUNT
519
520 Suspends the customer associated with this invoice if the total amount owed on
521 this invoice and all older invoices is greater than the specified amount.
522
523 Returns a list: an empty list on success or a list of errors.
524
525 =cut
526
527 sub cust_suspend_if_balance_over {
528   my( $self, $amount ) = ( shift, shift );
529   my $cust_main = $self->cust_main;
530   if ( $cust_main->total_owed_date($self->_date) < $amount ) {
531     return ();
532   } else {
533     $cust_main->suspend(@_);
534   }
535 }
536
537 =item cust_credit
538
539 Depreciated.  See the cust_credited method.
540
541  #Returns a list consisting of the total previous credited (see
542  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
543  #outstanding credits (FS::cust_credit objects).
544
545 =cut
546
547 sub cust_credit {
548   use Carp;
549   croak "FS::cust_bill->cust_credit depreciated; see ".
550         "FS::cust_bill->cust_credit_bill";
551   #my $self = shift;
552   #my $total = 0;
553   #my @cust_credit = sort { $a->_date <=> $b->_date }
554   #  grep { $_->credited != 0 && $_->_date < $self->_date }
555   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
556   #;
557   #foreach (@cust_credit) { $total += $_->credited; }
558   #$total, @cust_credit;
559 }
560
561 =item cust_pay
562
563 Depreciated.  See the cust_bill_pay method.
564
565 #Returns all payments (see L<FS::cust_pay>) for this invoice.
566
567 =cut
568
569 sub cust_pay {
570   use Carp;
571   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
572   #my $self = shift;
573   #sort { $a->_date <=> $b->_date }
574   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
575   #;
576 }
577
578 sub cust_pay_batch {
579   my $self = shift;
580   qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
581 }
582
583 sub cust_bill_pay_batch {
584   my $self = shift;
585   qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
586 }
587
588 =item cust_bill_pay
589
590 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
591
592 =cut
593
594 sub cust_bill_pay {
595   my $self = shift;
596   map { $_ } #return $self->num_cust_bill_pay unless wantarray;
597   sort { $a->_date <=> $b->_date }
598     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
599 }
600
601 =item cust_credited
602
603 =item cust_credit_bill
604
605 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
606
607 =cut
608
609 sub cust_credited {
610   my $self = shift;
611   map { $_ } #return $self->num_cust_credit_bill unless wantarray;
612   sort { $a->_date <=> $b->_date }
613     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
614   ;
615 }
616
617 sub cust_credit_bill {
618   shift->cust_credited(@_);
619 }
620
621 =item cust_bill_pay_pkgnum PKGNUM
622
623 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
624 with matching pkgnum.
625
626 =cut
627
628 sub cust_bill_pay_pkgnum {
629   my( $self, $pkgnum ) = @_;
630   map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
631   sort { $a->_date <=> $b->_date }
632     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
633                                 'pkgnum' => $pkgnum,
634                               }
635            );
636 }
637
638 =item cust_credited_pkgnum PKGNUM
639
640 =item cust_credit_bill_pkgnum PKGNUM
641
642 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
643 with matching pkgnum.
644
645 =cut
646
647 sub cust_credited_pkgnum {
648   my( $self, $pkgnum ) = @_;
649   map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
650   sort { $a->_date <=> $b->_date }
651     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
652                                    'pkgnum' => $pkgnum,
653                                  }
654            );
655 }
656
657 sub cust_credit_bill_pkgnum {
658   shift->cust_credited_pkgnum(@_);
659 }
660
661 =item tax
662
663 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
664
665 =cut
666
667 sub tax {
668   my $self = shift;
669   my $total = 0;
670   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
671                                              'pkgnum' => 0 } );
672   foreach (@taxlines) { $total += $_->setup; }
673   $total;
674 }
675
676 =item owed
677
678 Returns the amount owed (still outstanding) on this invoice, which is charged
679 minus all payment applications (see L<FS::cust_bill_pay>) and credit
680 applications (see L<FS::cust_credit_bill>).
681
682 =cut
683
684 sub owed {
685   my $self = shift;
686   my $balance = $self->charged;
687   $balance -= $_->amount foreach ( $self->cust_bill_pay );
688   $balance -= $_->amount foreach ( $self->cust_credited );
689   $balance = sprintf( "%.2f", $balance);
690   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
691   $balance;
692 }
693
694 sub owed_pkgnum {
695   my( $self, $pkgnum ) = @_;
696
697   #my $balance = $self->charged;
698   my $balance = 0;
699   $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
700
701   $balance -= $_->amount            for $self->cust_bill_pay_pkgnum($pkgnum);
702   $balance -= $_->amount            for $self->cust_credited_pkgnum($pkgnum);
703
704   $balance = sprintf( "%.2f", $balance);
705   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
706   $balance;
707 }
708
709 =item apply_payments_and_credits [ OPTION => VALUE ... ]
710
711 Applies unapplied payments and credits to this invoice.
712
713 A hash of optional arguments may be passed.  Currently "manual" is supported.
714 If true, a payment receipt is sent instead of a statement when
715 'payment_receipt_email' configuration option is set.
716
717 If there is an error, returns the error, otherwise returns false.
718
719 =cut
720
721 sub apply_payments_and_credits {
722   my( $self, %options ) = @_;
723
724   local $SIG{HUP} = 'IGNORE';
725   local $SIG{INT} = 'IGNORE';
726   local $SIG{QUIT} = 'IGNORE';
727   local $SIG{TERM} = 'IGNORE';
728   local $SIG{TSTP} = 'IGNORE';
729   local $SIG{PIPE} = 'IGNORE';
730
731   my $oldAutoCommit = $FS::UID::AutoCommit;
732   local $FS::UID::AutoCommit = 0;
733   my $dbh = dbh;
734
735   $self->select_for_update; #mutex
736
737   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
738   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
739
740   if ( $conf->exists('pkg-balances') ) {
741     # limit @payments & @credits to those w/ a pkgnum grepped from $self
742     my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
743     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
744     @credits  = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
745   }
746
747   while ( $self->owed > 0 and ( @payments || @credits ) ) {
748
749     my $app = '';
750     if ( @payments && @credits ) {
751
752       #decide which goes first by weight of top (unapplied) line item
753
754       my @open_lineitems = $self->open_cust_bill_pkg;
755
756       my $max_pay_weight =
757         max( map  { $_->part_pkg->pay_weight || 0 }
758              grep { $_ }
759              map  { $_->cust_pkg }
760                   @open_lineitems
761            );
762       my $max_credit_weight =
763         max( map  { $_->part_pkg->credit_weight || 0 }
764              grep { $_ } 
765              map  { $_->cust_pkg }
766                   @open_lineitems
767            );
768
769       #if both are the same... payments first?  it has to be something
770       if ( $max_pay_weight >= $max_credit_weight ) {
771         $app = 'pay';
772       } else {
773         $app = 'credit';
774       }
775     
776     } elsif ( @payments ) {
777       $app = 'pay';
778     } elsif ( @credits ) {
779       $app = 'credit';
780     } else {
781       die "guru meditation #12 and 35";
782     }
783
784     my $unapp_amount;
785     if ( $app eq 'pay' ) {
786
787       my $payment = shift @payments;
788       $unapp_amount = $payment->unapplied;
789       $app = new FS::cust_bill_pay { 'paynum'  => $payment->paynum };
790       $app->pkgnum( $payment->pkgnum )
791         if $conf->exists('pkg-balances') && $payment->pkgnum;
792
793     } elsif ( $app eq 'credit' ) {
794
795       my $credit = shift @credits;
796       $unapp_amount = $credit->credited;
797       $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
798       $app->pkgnum( $credit->pkgnum )
799         if $conf->exists('pkg-balances') && $credit->pkgnum;
800
801     } else {
802       die "guru meditation #12 and 35";
803     }
804
805     my $owed;
806     if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
807       warn "owed_pkgnum ". $app->pkgnum;
808       $owed = $self->owed_pkgnum($app->pkgnum);
809     } else {
810       $owed = $self->owed;
811     }
812     next unless $owed > 0;
813
814     warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
815     $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
816
817     $app->invnum( $self->invnum );
818
819     my $error = $app->insert(%options);
820     if ( $error ) {
821       $dbh->rollback if $oldAutoCommit;
822       return "Error inserting ". $app->table. " record: $error";
823     }
824     die $error if $error;
825
826   }
827
828   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
829   ''; #no error
830
831 }
832
833 =item generate_email OPTION => VALUE ...
834
835 Options:
836
837 =over 4
838
839 =item from
840
841 sender address, required
842
843 =item tempate
844
845 alternate template name, optional
846
847 =item print_text
848
849 text attachment arrayref, optional
850
851 =item subject
852
853 email subject, optional
854
855 =item notice_name
856
857 notice name instead of "Invoice", optional
858
859 =back
860
861 Returns an argument list to be passed to L<FS::Misc::send_email>.
862
863 =cut
864
865 use MIME::Entity;
866
867 sub generate_email {
868
869   my $self = shift;
870   my %args = @_;
871
872   my $me = '[FS::cust_bill::generate_email]';
873
874   my %return = (
875     'from'      => $args{'from'},
876     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
877   );
878
879   my %opt = (
880     'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
881     'template'      => $args{'template'},
882     'notice_name'   => ( $args{'notice_name'} || 'Invoice' ),
883   );
884
885   my $cust_main = $self->cust_main;
886
887   if (ref($args{'to'}) eq 'ARRAY') {
888     $return{'to'} = $args{'to'};
889   } else {
890     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
891                            $cust_main->invoicing_list
892                     ];
893   }
894
895   if ( $conf->exists('invoice_html') ) {
896
897     warn "$me creating HTML/text multipart message"
898       if $DEBUG;
899
900     $return{'nobody'} = 1;
901
902     my $alternative = build MIME::Entity
903       'Type'        => 'multipart/alternative',
904       'Encoding'    => '7bit',
905       'Disposition' => 'inline'
906     ;
907
908     my $data;
909     if ( $conf->exists('invoice_email_pdf')
910          and scalar($conf->config('invoice_email_pdf_note')) ) {
911
912       warn "$me using 'invoice_email_pdf_note' in multipart message"
913         if $DEBUG;
914       $data = [ map { $_ . "\n" }
915                     $conf->config('invoice_email_pdf_note')
916               ];
917
918     } else {
919
920       warn "$me not using 'invoice_email_pdf_note' in multipart message"
921         if $DEBUG;
922       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
923         $data = $args{'print_text'};
924       } else {
925         $data = [ $self->print_text(\%opt) ];
926       }
927
928     }
929
930     $alternative->attach(
931       'Type'        => 'text/plain',
932       #'Encoding'    => 'quoted-printable',
933       'Encoding'    => '7bit',
934       'Data'        => $data,
935       'Disposition' => 'inline',
936     );
937
938     $args{'from'} =~ /\@([\w\.\-]+)/;
939     my $from = $1 || 'example.com';
940     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
941
942     my $logo;
943     my $agentnum = $cust_main->agentnum;
944     if ( defined($args{'template'}) && length($args{'template'})
945          && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
946        )
947     {
948       $logo = 'logo_'. $args{'template'}. '.png';
949     } else {
950       $logo = "logo.png";
951     }
952     my $image_data = $conf->config_binary( $logo, $agentnum);
953
954     my $image = build MIME::Entity
955       'Type'       => 'image/png',
956       'Encoding'   => 'base64',
957       'Data'       => $image_data,
958       'Filename'   => 'logo.png',
959       'Content-ID' => "<$content_id>",
960     ;
961
962     $alternative->attach(
963       'Type'        => 'text/html',
964       'Encoding'    => 'quoted-printable',
965       'Data'        => [ '<html>',
966                          '  <head>',
967                          '    <title>',
968                          '      '. encode_entities($return{'subject'}), 
969                          '    </title>',
970                          '  </head>',
971                          '  <body bgcolor="#e8e8e8">',
972                          $self->print_html({ 'cid'=>$content_id, %opt }),
973                          '  </body>',
974                          '</html>',
975                        ],
976       'Disposition' => 'inline',
977       #'Filename'    => 'invoice.pdf',
978     );
979
980     my @otherparts = ();
981     if ( $cust_main->email_csv_cdr ) {
982
983       push @otherparts, build MIME::Entity
984         'Type'        => 'text/csv',
985         'Encoding'    => '7bit',
986         'Data'        => [ map { "$_\n" }
987                              $self->call_details('prepend_billed_number' => 1)
988                          ],
989         'Disposition' => 'attachment',
990         'Filename'    => 'usage-'. $self->invnum. '.csv',
991       ;
992
993     }
994
995     if ( $conf->exists('invoice_email_pdf') ) {
996
997       #attaching pdf too:
998       # multipart/mixed
999       #   multipart/related
1000       #     multipart/alternative
1001       #       text/plain
1002       #       text/html
1003       #     image/png
1004       #   application/pdf
1005
1006       my $related = build MIME::Entity 'Type'     => 'multipart/related',
1007                                        'Encoding' => '7bit';
1008
1009       #false laziness w/Misc::send_email
1010       $related->head->replace('Content-type',
1011         $related->mime_type.
1012         '; boundary="'. $related->head->multipart_boundary. '"'.
1013         '; type=multipart/alternative'
1014       );
1015
1016       $related->add_part($alternative);
1017
1018       $related->add_part($image);
1019
1020       my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1021
1022       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1023
1024     } else {
1025
1026       #no other attachment:
1027       # multipart/related
1028       #   multipart/alternative
1029       #     text/plain
1030       #     text/html
1031       #   image/png
1032
1033       $return{'content-type'} = 'multipart/related';
1034       $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1035       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1036       #$return{'disposition'} = 'inline';
1037
1038     }
1039   
1040   } else {
1041
1042     if ( $conf->exists('invoice_email_pdf') ) {
1043       warn "$me creating PDF attachment"
1044         if $DEBUG;
1045
1046       #mime parts arguments a la MIME::Entity->build().
1047       $return{'mimeparts'} = [
1048         { $self->mimebuild_pdf(\%opt) }
1049       ];
1050     }
1051   
1052     if ( $conf->exists('invoice_email_pdf')
1053          and scalar($conf->config('invoice_email_pdf_note')) ) {
1054
1055       warn "$me using 'invoice_email_pdf_note'"
1056         if $DEBUG;
1057       $return{'body'} = [ map { $_ . "\n" }
1058                               $conf->config('invoice_email_pdf_note')
1059                         ];
1060
1061     } else {
1062
1063       warn "$me not using 'invoice_email_pdf_note'"
1064         if $DEBUG;
1065       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1066         $return{'body'} = $args{'print_text'};
1067       } else {
1068         $return{'body'} = [ $self->print_text(\%opt) ];
1069       }
1070
1071     }
1072
1073   }
1074
1075   %return;
1076
1077 }
1078
1079 =item mimebuild_pdf
1080
1081 Returns a list suitable for passing to MIME::Entity->build(), representing
1082 this invoice as PDF attachment.
1083
1084 =cut
1085
1086 sub mimebuild_pdf {
1087   my $self = shift;
1088   (
1089     'Type'        => 'application/pdf',
1090     'Encoding'    => 'base64',
1091     'Data'        => [ $self->print_pdf(@_) ],
1092     'Disposition' => 'attachment',
1093     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
1094   );
1095 }
1096
1097 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1098
1099 Sends this invoice to the destinations configured for this customer: sends
1100 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
1101
1102 Options can be passed as a hashref (recommended) or as a list of up to 
1103 four values for templatename, agentnum, invoice_from and amount.
1104
1105 I<template>, if specified, is the name of a suffix for alternate invoices.
1106
1107 I<agentnum>, if specified, means that this invoice will only be sent for customers
1108 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
1109 single agent) or an arrayref of agentnums.
1110
1111 I<invoice_from>, if specified, overrides the default email invoice From: address.
1112
1113 I<amount>, if specified, only sends the invoice if the total amount owed on this
1114 invoice and all older invoices is greater than the specified amount.
1115
1116 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1117
1118 =cut
1119
1120 sub queueable_send {
1121   my %opt = @_;
1122
1123   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1124     or die "invalid invoice number: " . $opt{invnum};
1125
1126   my @args = ( $opt{template}, $opt{agentnum} );
1127   push @args, $opt{invoice_from}
1128     if exists($opt{invoice_from}) && $opt{invoice_from};
1129
1130   my $error = $self->send( @args );
1131   die $error if $error;
1132
1133 }
1134
1135 sub send {
1136   my $self = shift;
1137
1138   my( $template, $invoice_from, $notice_name );
1139   my $agentnums = '';
1140   my $balance_over = 0;
1141
1142   if ( ref($_[0]) ) {
1143     my $opt = shift;
1144     $template = $opt->{'template'} || '';
1145     if ( $agentnums = $opt->{'agentnum'} ) {
1146       $agentnums = [ $agentnums ] unless ref($agentnums);
1147     }
1148     $invoice_from = $opt->{'invoice_from'};
1149     $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1150     $notice_name = $opt->{'notice_name'};
1151   } else {
1152     $template = scalar(@_) ? shift : '';
1153     if ( scalar(@_) && $_[0]  ) {
1154       $agentnums = ref($_[0]) ? shift : [ shift ];
1155     }
1156     $invoice_from = shift if scalar(@_);
1157     $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1158   }
1159
1160   return 'N/A' unless ! $agentnums
1161                    or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1162
1163   return ''
1164     unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1165
1166   $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
1167                     $conf->config('invoice_from', $self->cust_main->agentnum );
1168
1169   my %opt = (
1170     'template'     => $template,
1171     'invoice_from' => $invoice_from,
1172     'notice_name'  => ( $notice_name || 'Invoice' ),
1173   );
1174
1175   my @invoicing_list = $self->cust_main->invoicing_list;
1176
1177   #$self->email_invoice(\%opt)
1178   $self->email(\%opt)
1179     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1180
1181   #$self->print_invoice(\%opt)
1182   $self->print(\%opt)
1183     if grep { $_ eq 'POST' } @invoicing_list; #postal
1184
1185   $self->fax_invoice(\%opt)
1186     if grep { $_ eq 'FAX' } @invoicing_list; #fax
1187
1188   '';
1189
1190 }
1191
1192 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] 
1193
1194 Emails this invoice.
1195
1196 Options can be passed as a hashref (recommended) or as a list of up to 
1197 two values for templatename and invoice_from.
1198
1199 I<template>, if specified, is the name of a suffix for alternate invoices.
1200
1201 I<invoice_from>, if specified, overrides the default email invoice From: address.
1202
1203 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1204
1205 =cut
1206
1207 sub queueable_email {
1208   my %opt = @_;
1209
1210   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1211     or die "invalid invoice number: " . $opt{invnum};
1212
1213   my @args = ( $opt{template} );
1214   push @args, $opt{invoice_from}
1215     if exists($opt{invoice_from}) && $opt{invoice_from};
1216
1217   my $error = $self->email( @args );
1218   die $error if $error;
1219
1220 }
1221
1222 #sub email_invoice {
1223 sub email {
1224   my $self = shift;
1225
1226   my( $template, $invoice_from, $notice_name );
1227   if ( ref($_[0]) ) {
1228     my $opt = shift;
1229     $template = $opt->{'template'} || '';
1230     $invoice_from = $opt->{'invoice_from'};
1231     $notice_name = $opt->{'notice_name'} || 'Invoice';
1232   } else {
1233     $template = scalar(@_) ? shift : '';
1234     $invoice_from = shift if scalar(@_);
1235     $notice_name = 'Invoice';
1236   }
1237
1238   $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
1239                     $conf->config('invoice_from', $self->cust_main->agentnum );
1240
1241   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
1242                             $self->cust_main->invoicing_list;
1243
1244   if ( ! @invoicing_list ) { #no recipients
1245     if ( $conf->exists('cust_bill-no_recipients-error') ) {
1246       die 'No recipients for customer #'. $self->custnum;
1247     } else {
1248       #default: better to notify this person than silence
1249       @invoicing_list = ($invoice_from);
1250     }
1251   }
1252
1253   my $subject = $self->email_subject($template);
1254
1255   my $error = send_email(
1256     $self->generate_email(
1257       'from'        => $invoice_from,
1258       'to'          => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1259       'subject'     => $subject,
1260       'template'    => $template,
1261       'notice_name' => $notice_name,
1262     )
1263   );
1264   die "can't email invoice: $error\n" if $error;
1265   #die "$error\n" if $error;
1266
1267 }
1268
1269 sub email_subject {
1270   my $self = shift;
1271
1272   #my $template = scalar(@_) ? shift : '';
1273   #per-template?
1274
1275   my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1276                 || 'Invoice';
1277
1278   my $cust_main = $self->cust_main;
1279   my $name = $cust_main->name;
1280   my $name_short = $cust_main->name_short;
1281   my $invoice_number = $self->invnum;
1282   my $invoice_date = $self->_date_pretty;
1283
1284   eval qq("$subject");
1285 }
1286
1287 =item lpr_data HASHREF | [ TEMPLATE ]
1288
1289 Returns the postscript or plaintext for this invoice as an arrayref.
1290
1291 Options can be passed as a hashref (recommended) or as a single optional value
1292 for template.
1293
1294 I<template>, if specified, is the name of a suffix for alternate invoices.
1295
1296 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1297
1298 =cut
1299
1300 sub lpr_data {
1301   my $self = shift;
1302   my( $template, $notice_name );
1303   if ( ref($_[0]) ) {
1304     my $opt = shift;
1305     $template = $opt->{'template'} || '';
1306     $notice_name = $opt->{'notice_name'} || 'Invoice';
1307   } else {
1308     $template = scalar(@_) ? shift : '';
1309     $notice_name = 'Invoice';
1310   }
1311
1312   my %opt = (
1313     'template'    => $template,
1314     'notice_name' => $notice_name,
1315   );
1316
1317   my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1318   [ $self->$method( \%opt ) ];
1319 }
1320
1321 =item print HASHREF | [ TEMPLATE ]
1322
1323 Prints this invoice.
1324
1325 Options can be passed as a hashref (recommended) or as a single optional
1326 value for template.
1327
1328 I<template>, if specified, is the name of a suffix for alternate invoices.
1329
1330 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1331
1332 =cut
1333
1334 #sub print_invoice {
1335 sub print {
1336   my $self = shift;
1337   my( $template, $notice_name );
1338   if ( ref($_[0]) ) {
1339     my $opt = shift;
1340     $template = $opt->{'template'} || '';
1341     $notice_name = $opt->{'notice_name'} || 'Invoice';
1342   } else {
1343     $template = scalar(@_) ? shift : '';
1344     $notice_name = 'Invoice';
1345   }
1346
1347   my %opt = (
1348     'template'    => $template,
1349     'notice_name' => $notice_name,
1350   );
1351
1352   if($conf->exists('invoice_print_pdf')) {
1353     # Add the invoice to the current batch.
1354     $self->batch_invoice(\%opt);
1355   }
1356   else {
1357     do_print $self->lpr_data(\%opt);
1358   }
1359 }
1360
1361 =item fax_invoice HASHREF | [ TEMPLATE ] 
1362
1363 Faxes this invoice.
1364
1365 Options can be passed as a hashref (recommended) or as a single optional
1366 value for template.
1367
1368 I<template>, if specified, is the name of a suffix for alternate invoices.
1369
1370 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1371
1372 =cut
1373
1374 sub fax_invoice {
1375   my $self = shift;
1376   my( $template, $notice_name );
1377   if ( ref($_[0]) ) {
1378     my $opt = shift;
1379     $template = $opt->{'template'} || '';
1380     $notice_name = $opt->{'notice_name'} || 'Invoice';
1381   } else {
1382     $template = scalar(@_) ? shift : '';
1383     $notice_name = 'Invoice';
1384   }
1385
1386   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1387     unless $conf->exists('invoice_latex');
1388
1389   my $dialstring = $self->cust_main->getfield('fax');
1390   #Check $dialstring?
1391
1392   my %opt = (
1393     'template'    => $template,
1394     'notice_name' => $notice_name,
1395   );
1396
1397   my $error = send_fax( 'docdata'    => $self->lpr_data(\%opt),
1398                         'dialstring' => $dialstring,
1399                       );
1400   die $error if $error;
1401
1402 }
1403
1404 =item batch_invoice [ HASHREF ]
1405
1406 Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
1407 isn't an open batch, one will be created.
1408
1409 =cut
1410
1411 sub batch_invoice {
1412   my ($self, $opt) = @_;
1413   my $batch = FS::bill_batch->get_open_batch;
1414   my $cust_bill_batch = FS::cust_bill_batch->new({
1415       batchnum => $batch->batchnum,
1416       invnum   => $self->invnum,
1417   });
1418   return $cust_bill_batch->insert($opt);
1419 }
1420
1421 =item ftp_invoice [ TEMPLATENAME ] 
1422
1423 Sends this invoice data via FTP.
1424
1425 TEMPLATENAME is unused?
1426
1427 =cut
1428
1429 sub ftp_invoice {
1430   my $self = shift;
1431   my $template = scalar(@_) ? shift : '';
1432
1433   $self->send_csv(
1434     'protocol'   => 'ftp',
1435     'server'     => $conf->config('cust_bill-ftpserver'),
1436     'username'   => $conf->config('cust_bill-ftpusername'),
1437     'password'   => $conf->config('cust_bill-ftppassword'),
1438     'dir'        => $conf->config('cust_bill-ftpdir'),
1439     'format'     => $conf->config('cust_bill-ftpformat'),
1440   );
1441 }
1442
1443 =item spool_invoice [ TEMPLATENAME ] 
1444
1445 Spools this invoice data (see L<FS::spool_csv>)
1446
1447 TEMPLATENAME is unused?
1448
1449 =cut
1450
1451 sub spool_invoice {
1452   my $self = shift;
1453   my $template = scalar(@_) ? shift : '';
1454
1455   $self->spool_csv(
1456     'format'       => $conf->config('cust_bill-spoolformat'),
1457     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1458   );
1459 }
1460
1461 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1462
1463 Like B<send>, but only sends the invoice if it is the newest open invoice for
1464 this customer.
1465
1466 =cut
1467
1468 sub send_if_newest {
1469   my $self = shift;
1470
1471   return ''
1472     if scalar(
1473                grep { $_->owed > 0 } 
1474                     qsearch('cust_bill', {
1475                       'custnum' => $self->custnum,
1476                       #'_date'   => { op=>'>', value=>$self->_date },
1477                       'invnum'  => { op=>'>', value=>$self->invnum },
1478                     } )
1479              );
1480     
1481   $self->send(@_);
1482 }
1483
1484 =item send_csv OPTION => VALUE, ...
1485
1486 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1487
1488 Options are:
1489
1490 protocol - currently only "ftp"
1491 server
1492 username
1493 password
1494 dir
1495
1496 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1497 and YYMMDDHHMMSS is a timestamp.
1498
1499 See L</print_csv> for a description of the output format.
1500
1501 =cut
1502
1503 sub send_csv {
1504   my($self, %opt) = @_;
1505
1506   #create file(s)
1507
1508   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1509   mkdir $spooldir, 0700 unless -d $spooldir;
1510
1511   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1512   my $file = "$spooldir/$tracctnum.csv";
1513   
1514   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1515
1516   open(CSV, ">$file") or die "can't open $file: $!";
1517   print CSV $header;
1518
1519   print CSV $detail;
1520
1521   close CSV;
1522
1523   my $net;
1524   if ( $opt{protocol} eq 'ftp' ) {
1525     eval "use Net::FTP;";
1526     die $@ if $@;
1527     $net = Net::FTP->new($opt{server}) or die @$;
1528   } else {
1529     die "unknown protocol: $opt{protocol}";
1530   }
1531
1532   $net->login( $opt{username}, $opt{password} )
1533     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1534
1535   $net->binary or die "can't set binary mode";
1536
1537   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1538
1539   $net->put($file) or die "can't put $file: $!";
1540
1541   $net->quit;
1542
1543   unlink $file;
1544
1545 }
1546
1547 =item spool_csv
1548
1549 Spools CSV invoice data.
1550
1551 Options are:
1552
1553 =over 4
1554
1555 =item format - 'default' or 'billco'
1556
1557 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
1558
1559 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1560
1561 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
1562
1563 =back
1564
1565 =cut
1566
1567 sub spool_csv {
1568   my($self, %opt) = @_;
1569
1570   my $cust_main = $self->cust_main;
1571
1572   if ( $opt{'dest'} ) {
1573     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1574                              $cust_main->invoicing_list;
1575     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1576                      || ! keys %invoicing_list;
1577   }
1578
1579   if ( $opt{'balanceover'} ) {
1580     return 'N/A'
1581       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1582   }
1583
1584   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1585   mkdir $spooldir, 0700 unless -d $spooldir;
1586
1587   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1588
1589   my $file =
1590     "$spooldir/".
1591     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1592     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1593     '.csv';
1594   
1595   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1596
1597   open(CSV, ">>$file") or die "can't open $file: $!";
1598   flock(CSV, LOCK_EX);
1599   seek(CSV, 0, 2);
1600
1601   print CSV $header;
1602
1603   if ( lc($opt{'format'}) eq 'billco' ) {
1604
1605     flock(CSV, LOCK_UN);
1606     close CSV;
1607
1608     $file =
1609       "$spooldir/".
1610       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1611       '-detail.csv';
1612
1613     open(CSV,">>$file") or die "can't open $file: $!";
1614     flock(CSV, LOCK_EX);
1615     seek(CSV, 0, 2);
1616   }
1617
1618   print CSV $detail;
1619
1620   flock(CSV, LOCK_UN);
1621   close CSV;
1622
1623   return '';
1624
1625 }
1626
1627 =item print_csv OPTION => VALUE, ...
1628
1629 Returns CSV data for this invoice.
1630
1631 Options are:
1632
1633 format - 'default' or 'billco'
1634
1635 Returns a list consisting of two scalars.  The first is a single line of CSV
1636 header information for this invoice.  The second is one or more lines of CSV
1637 detail information for this invoice.
1638
1639 If I<format> is not specified or "default", the fields of the CSV file are as
1640 follows:
1641
1642 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1643
1644 =over 4
1645
1646 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1647
1648 B<record_type> is C<cust_bill> for the initial header line only.  The
1649 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1650 fields are filled in.
1651
1652 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1653 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1654 are filled in.
1655
1656 =item invnum - invoice number
1657
1658 =item custnum - customer number
1659
1660 =item _date - invoice date
1661
1662 =item charged - total invoice amount
1663
1664 =item first - customer first name
1665
1666 =item last - customer first name
1667
1668 =item company - company name
1669
1670 =item address1 - address line 1
1671
1672 =item address2 - address line 1
1673
1674 =item city
1675
1676 =item state
1677
1678 =item zip
1679
1680 =item country
1681
1682 =item pkg - line item description
1683
1684 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1685
1686 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1687
1688 =item sdate - start date for recurring fee
1689
1690 =item edate - end date for recurring fee
1691
1692 =back
1693
1694 If I<format> is "billco", the fields of the header CSV file are as follows:
1695
1696   +-------------------------------------------------------------------+
1697   |                        FORMAT HEADER FILE                         |
1698   |-------------------------------------------------------------------|
1699   | Field | Description                   | Name       | Type | Width |
1700   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1701   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1702   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1703   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1704   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1705   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1706   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1707   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1708   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1709   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1710   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1711   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1712   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1713   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1714   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1715   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1716   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1717   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1718   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1719   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1720   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1721   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1722   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1723   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1724   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1725   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1726   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1727   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1728   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1729   +-------+-------------------------------+------------+------+-------+
1730
1731 If I<format> is "billco", the fields of the detail CSV file are as follows:
1732
1733                                   FORMAT FOR DETAIL FILE
1734         |                            |           |      |
1735   Field | Description                | Name      | Type | Width
1736   1     | N/A-Leave Empty            | RC        | CHAR |     2
1737   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1738   3     | Account Number             | TRACCTNUM | CHAR |    15
1739   4     | Invoice Number             | TRINVOICE | CHAR |    15
1740   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1741   6     | Transaction Detail         | DETAILS   | CHAR |   100
1742   7     | Amount                     | AMT       | NUM* |     9
1743   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1744   9     | Grouping Code              | GROUP     | CHAR |     2
1745   10    | User Defined               | ACCT CODE | CHAR |    15
1746
1747 =cut
1748
1749 sub print_csv {
1750   my($self, %opt) = @_;
1751   
1752   eval "use Text::CSV_XS";
1753   die $@ if $@;
1754
1755   my $cust_main = $self->cust_main;
1756
1757   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1758
1759   if ( lc($opt{'format'}) eq 'billco' ) {
1760
1761     my $taxtotal = 0;
1762     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1763
1764     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1765
1766     my( $previous_balance, @unused ) = $self->previous; #previous balance
1767
1768     my $pmt_cr_applied = 0;
1769     $pmt_cr_applied += $_->{'amount'}
1770       foreach ( $self->_items_payments, $self->_items_credits ) ;
1771
1772     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1773
1774     $csv->combine(
1775       '',                         #  1 | N/A-Leave Empty               CHAR   2
1776       '',                         #  2 | N/A-Leave Empty               CHAR  15
1777       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1778       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1779       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1780       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1781       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1782       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1783       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1784       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1785       '',                         # 10 | Ancillary Billing Information CHAR  30
1786       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1787       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1788
1789       # XXX ?
1790       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1791
1792       # XXX ?
1793       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1794
1795       $previous_balance,          # 15 | Previous Balance              NUM*   9
1796       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1797       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1798       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1799       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1800       '',                         # 20 | 30 Day Aging                  NUM*   9
1801       '',                         # 21 | 60 Day Aging                  NUM*   9
1802       '',                         # 22 | 90 Day Aging                  NUM*   9
1803       'N',                        # 23 | Y/N                           CHAR   1
1804       '',                         # 24 | Remittance automation         CHAR 100
1805       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1806       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1807       '0',                        # 27 | Federal Tax***                NUM*   9
1808       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1809       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1810     );
1811
1812   } else {
1813   
1814     $csv->combine(
1815       'cust_bill',
1816       $self->invnum,
1817       $self->custnum,
1818       time2str("%x", $self->_date),
1819       sprintf("%.2f", $self->charged),
1820       ( map { $cust_main->getfield($_) }
1821           qw( first last company address1 address2 city state zip country ) ),
1822       map { '' } (1..5),
1823     ) or die "can't create csv";
1824   }
1825
1826   my $header = $csv->string. "\n";
1827
1828   my $detail = '';
1829   if ( lc($opt{'format'}) eq 'billco' ) {
1830
1831     my $lineseq = 0;
1832     foreach my $item ( $self->_items_pkg ) {
1833
1834       $csv->combine(
1835         '',                     #  1 | N/A-Leave Empty            CHAR   2
1836         '',                     #  2 | N/A-Leave Empty            CHAR  15
1837         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1838         $self->invnum,          #  4 | Invoice Number             CHAR  15
1839         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1840         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1841         $item->{'amount'},      #  7 | Amount                     NUM*   9
1842         '',                     #  8 | Line Format Control**      CHAR   2
1843         '',                     #  9 | Grouping Code              CHAR   2
1844         '',                     # 10 | User Defined               CHAR  15
1845       );
1846
1847       $detail .= $csv->string. "\n";
1848
1849     }
1850
1851   } else {
1852
1853     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1854
1855       my($pkg, $setup, $recur, $sdate, $edate);
1856       if ( $cust_bill_pkg->pkgnum ) {
1857       
1858         ($pkg, $setup, $recur, $sdate, $edate) = (
1859           $cust_bill_pkg->part_pkg->pkg,
1860           ( $cust_bill_pkg->setup != 0
1861             ? sprintf("%.2f", $cust_bill_pkg->setup )
1862             : '' ),
1863           ( $cust_bill_pkg->recur != 0
1864             ? sprintf("%.2f", $cust_bill_pkg->recur )
1865             : '' ),
1866           ( $cust_bill_pkg->sdate 
1867             ? time2str("%x", $cust_bill_pkg->sdate)
1868             : '' ),
1869           ($cust_bill_pkg->edate 
1870             ?time2str("%x", $cust_bill_pkg->edate)
1871             : '' ),
1872         );
1873   
1874       } else { #pkgnum tax
1875         next unless $cust_bill_pkg->setup != 0;
1876         $pkg = $cust_bill_pkg->desc;
1877         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1878         ( $sdate, $edate ) = ( '', '' );
1879       }
1880   
1881       $csv->combine(
1882         'cust_bill_pkg',
1883         $self->invnum,
1884         ( map { '' } (1..11) ),
1885         ($pkg, $setup, $recur, $sdate, $edate)
1886       ) or die "can't create csv";
1887
1888       $detail .= $csv->string. "\n";
1889
1890     }
1891
1892   }
1893
1894   ( $header, $detail );
1895
1896 }
1897
1898 =item comp
1899
1900 Pays this invoice with a compliemntary payment.  If there is an error,
1901 returns the error, otherwise returns false.
1902
1903 =cut
1904
1905 sub comp {
1906   my $self = shift;
1907   my $cust_pay = new FS::cust_pay ( {
1908     'invnum'   => $self->invnum,
1909     'paid'     => $self->owed,
1910     '_date'    => '',
1911     'payby'    => 'COMP',
1912     'payinfo'  => $self->cust_main->payinfo,
1913     'paybatch' => '',
1914   } );
1915   $cust_pay->insert;
1916 }
1917
1918 =item realtime_card
1919
1920 Attempts to pay this invoice with a credit card payment via a
1921 Business::OnlinePayment realtime gateway.  See
1922 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1923 for supported processors.
1924
1925 =cut
1926
1927 sub realtime_card {
1928   my $self = shift;
1929   $self->realtime_bop( 'CC', @_ );
1930 }
1931
1932 =item realtime_ach
1933
1934 Attempts to pay this invoice with an electronic check (ACH) payment via a
1935 Business::OnlinePayment realtime gateway.  See
1936 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1937 for supported processors.
1938
1939 =cut
1940
1941 sub realtime_ach {
1942   my $self = shift;
1943   $self->realtime_bop( 'ECHECK', @_ );
1944 }
1945
1946 =item realtime_lec
1947
1948 Attempts to pay this invoice with phone bill (LEC) payment via a
1949 Business::OnlinePayment realtime gateway.  See
1950 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1951 for supported processors.
1952
1953 =cut
1954
1955 sub realtime_lec {
1956   my $self = shift;
1957   $self->realtime_bop( 'LEC', @_ );
1958 }
1959
1960 sub realtime_bop {
1961   my( $self, $method ) = @_;
1962
1963   my $cust_main = $self->cust_main;
1964   my $balance = $cust_main->balance;
1965   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1966   $amount = sprintf("%.2f", $amount);
1967   return "not run (balance $balance)" unless $amount > 0;
1968
1969   my $description = 'Internet Services';
1970   if ( $conf->exists('business-onlinepayment-description') ) {
1971     my $dtempl = $conf->config('business-onlinepayment-description');
1972
1973     my $agent_obj = $cust_main->agent
1974       or die "can't retreive agent for $cust_main (agentnum ".
1975              $cust_main->agentnum. ")";
1976     my $agent = $agent_obj->agent;
1977     my $pkgs = join(', ',
1978       map { $_->part_pkg->pkg }
1979         grep { $_->pkgnum } $self->cust_bill_pkg
1980     );
1981     $description = eval qq("$dtempl");
1982   }
1983
1984   $cust_main->realtime_bop($method, $amount,
1985     'description' => $description,
1986     'invnum'      => $self->invnum,
1987 #this didn't do what we want, it just calls apply_payments_and_credits
1988 #    'apply'       => 1,
1989     'apply_to_invoice' => 1,
1990  #what we want:
1991  #this changes application behavior: auto payments
1992                         #triggered against a specific invoice are now applied
1993                         #to that invoice instead of oldest open.
1994                         #seem okay to me...
1995   );
1996
1997 }
1998
1999 =item batch_card OPTION => VALUE...
2000
2001 Adds a payment for this invoice to the pending credit card batch (see
2002 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2003 runs the payment using a realtime gateway.
2004
2005 =cut
2006
2007 sub batch_card {
2008   my ($self, %options) = @_;
2009   my $cust_main = $self->cust_main;
2010
2011   $options{invnum} = $self->invnum;
2012   
2013   $cust_main->batch_card(%options);
2014 }
2015
2016 sub _agent_template {
2017   my $self = shift;
2018   $self->cust_main->agent_template;
2019 }
2020
2021 sub _agent_invoice_from {
2022   my $self = shift;
2023   $self->cust_main->agent_invoice_from;
2024 }
2025
2026 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2027
2028 Returns an text invoice, as a list of lines.
2029
2030 Options can be passed as a hashref (recommended) or as a list of time, template
2031 and then any key/value pairs for any other options.
2032
2033 I<time>, if specified, is used to control the printing of overdue messages.  The
2034 default is now.  It isn't the date of the invoice; that's the `_date' field.
2035 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2036 L<Time::Local> and L<Date::Parse> for conversion functions.
2037
2038 I<template>, if specified, is the name of a suffix for alternate invoices.
2039
2040 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2041
2042 =cut
2043
2044 sub print_text {
2045   my $self = shift;
2046   my( $today, $template, %opt );
2047   if ( ref($_[0]) ) {
2048     %opt = %{ shift() };
2049     $today = delete($opt{'time'}) || '';
2050     $template = delete($opt{template}) || '';
2051   } else {
2052     ( $today, $template, %opt ) = @_;
2053   }
2054
2055   my %params = ( 'format' => 'template' );
2056   $params{'time'} = $today if $today;
2057   $params{'template'} = $template if $template;
2058   $params{$_} = $opt{$_} 
2059     foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2060
2061   $self->print_generic( %params );
2062 }
2063
2064 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2065
2066 Internal method - returns a filename of a filled-in LaTeX template for this
2067 invoice (Note: add ".tex" to get the actual filename), and a filename of
2068 an associated logo (with the .eps extension included).
2069
2070 See print_ps and print_pdf for methods that return PostScript and PDF output.
2071
2072 Options can be passed as a hashref (recommended) or as a list of time, template
2073 and then any key/value pairs for any other options.
2074
2075 I<time>, if specified, is used to control the printing of overdue messages.  The
2076 default is now.  It isn't the date of the invoice; that's the `_date' field.
2077 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2078 L<Time::Local> and L<Date::Parse> for conversion functions.
2079
2080 I<template>, if specified, is the name of a suffix for alternate invoices.
2081
2082 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2083
2084 =cut
2085
2086 sub print_latex {
2087   my $self = shift;
2088   my( $today, $template, %opt );
2089   if ( ref($_[0]) ) {
2090     %opt = %{ shift() };
2091     $today = delete($opt{'time'}) || '';
2092     $template = delete($opt{template}) || '';
2093   } else {
2094     ( $today, $template, %opt ) = @_;
2095   }
2096
2097   my %params = ( 'format' => 'latex' );
2098   $params{'time'} = $today if $today;
2099   $params{'template'} = $template if $template;
2100   $params{$_} = $opt{$_} 
2101     foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2102
2103   $template ||= $self->_agent_template;
2104
2105   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2106   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2107                            DIR      => $dir,
2108                            SUFFIX   => '.eps',
2109                            UNLINK   => 0,
2110                          ) or die "can't open temp file: $!\n";
2111
2112   my $agentnum = $self->cust_main->agentnum;
2113
2114   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2115     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2116       or die "can't write temp file: $!\n";
2117   } else {
2118     print $lh $conf->config_binary('logo.eps', $agentnum)
2119       or die "can't write temp file: $!\n";
2120   }
2121   close $lh;
2122   $params{'logo_file'} = $lh->filename;
2123
2124   my @filled_in = $self->print_generic( %params );
2125   
2126   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2127                            DIR      => $dir,
2128                            SUFFIX   => '.tex',
2129                            UNLINK   => 0,
2130                          ) or die "can't open temp file: $!\n";
2131   print $fh join('', @filled_in );
2132   close $fh;
2133
2134   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2135   return ($1, $params{'logo_file'});
2136
2137 }
2138
2139 =item print_generic OPTION => VALUE ...
2140
2141 Internal method - returns a filled-in template for this invoice as a scalar.
2142
2143 See print_ps and print_pdf for methods that return PostScript and PDF output.
2144
2145 Non optional options include 
2146   format - latex, html, template
2147
2148 Optional options include
2149
2150 template - a value used as a suffix for a configuration template
2151
2152 time - a value used to control the printing of overdue messages.  The
2153 default is now.  It isn't the date of the invoice; that's the `_date' field.
2154 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2155 L<Time::Local> and L<Date::Parse> for conversion functions.
2156
2157 cid - 
2158
2159 unsquelch_cdr - overrides any per customer cdr squelching when true
2160
2161 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2162
2163 =cut
2164
2165 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
2166 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2167 # yes: fixed width (dot matrix) text printing will be borked
2168 sub print_generic {
2169
2170   my( $self, %params ) = @_;
2171   my $today = $params{today} ? $params{today} : time;
2172   warn "$me print_generic called on $self with suffix $params{template}\n"
2173     if $DEBUG;
2174
2175   my $format = $params{format};
2176   die "Unknown format: $format"
2177     unless $format =~ /^(latex|html|template)$/;
2178
2179   my $cust_main = $self->cust_main;
2180   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2181     unless $cust_main->payname
2182         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2183
2184   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
2185                      'html'     => [ '<%=', '%>' ],
2186                      'template' => [ '{', '}' ],
2187                    );
2188
2189   warn "$me print_generic creating template\n"
2190     if $DEBUG > 1;
2191
2192   #create the template
2193   my $template = $params{template} ? $params{template} : $self->_agent_template;
2194   my $templatefile = "invoice_$format";
2195   $templatefile .= "_$template"
2196     if length($template);
2197   my @invoice_template = map "$_\n", $conf->config($templatefile)
2198     or die "cannot load config data $templatefile";
2199
2200   my $old_latex = '';
2201   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2202     #change this to a die when the old code is removed
2203     warn "old-style invoice template $templatefile; ".
2204          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2205     $old_latex = 'true';
2206     @invoice_template = _translate_old_latex_format(@invoice_template);
2207   } 
2208
2209   warn "$me print_generic creating T:T object\n"
2210     if $DEBUG > 1;
2211
2212   my $text_template = new Text::Template(
2213     TYPE => 'ARRAY',
2214     SOURCE => \@invoice_template,
2215     DELIMITERS => $delimiters{$format},
2216   );
2217
2218   warn "$me print_generic compiling T:T object\n"
2219     if $DEBUG > 1;
2220
2221   $text_template->compile()
2222     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2223
2224
2225   # additional substitution could possibly cause breakage in existing templates
2226   my %convert_maps = ( 
2227     'latex' => {
2228                  'notes'         => sub { map "$_", @_ },
2229                  'footer'        => sub { map "$_", @_ },
2230                  'smallfooter'   => sub { map "$_", @_ },
2231                  'returnaddress' => sub { map "$_", @_ },
2232                  'coupon'        => sub { map "$_", @_ },
2233                  'summary'       => sub { map "$_", @_ },
2234                },
2235     'html'  => {
2236                  'notes' =>
2237                    sub {
2238                      map { 
2239                        s/%%(.*)$/<!-- $1 -->/g;
2240                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2241                        s/\\begin\{enumerate\}/<ol>/g;
2242                        s/\\item /  <li>/g;
2243                        s/\\end\{enumerate\}/<\/ol>/g;
2244                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2245                        s/\\\\\*/<br>/g;
2246                        s/\\dollar ?/\$/g;
2247                        s/\\#/#/g;
2248                        s/~/&nbsp;/g;
2249                        $_;
2250                      }  @_
2251                    },
2252                  'footer' =>
2253                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2254                  'smallfooter' =>
2255                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2256                  'returnaddress' =>
2257                    sub {
2258                      map { 
2259                        s/~/&nbsp;/g;
2260                        s/\\\\\*?\s*$/<BR>/;
2261                        s/\\hyphenation\{[\w\s\-]+}//;
2262                        s/\\([&])/$1/g;
2263                        $_;
2264                      }  @_
2265                    },
2266                  'coupon'        => sub { "" },
2267                  'summary'       => sub { "" },
2268                },
2269     'template' => {
2270                  'notes' =>
2271                    sub {
2272                      map { 
2273                        s/%%.*$//g;
2274                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2275                        s/\\begin\{enumerate\}//g;
2276                        s/\\item /  * /g;
2277                        s/\\end\{enumerate\}//g;
2278                        s/\\textbf\{(.*)\}/$1/g;
2279                        s/\\\\\*/ /;
2280                        s/\\dollar ?/\$/g;
2281                        $_;
2282                      }  @_
2283                    },
2284                  'footer' =>
2285                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2286                  'smallfooter' =>
2287                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2288                  'returnaddress' =>
2289                    sub {
2290                      map { 
2291                        s/~/ /g;
2292                        s/\\\\\*?\s*$/\n/;             # dubious
2293                        s/\\hyphenation\{[\w\s\-]+}//;
2294                        $_;
2295                      }  @_
2296                    },
2297                  'coupon'        => sub { "" },
2298                  'summary'       => sub { "" },
2299                },
2300   );
2301
2302
2303   # hashes for differing output formats
2304   my %nbsps = ( 'latex'    => '~',
2305                 'html'     => '',    # '&nbps;' would be nice
2306                 'template' => '',    # not used
2307               );
2308   my $nbsp = $nbsps{$format};
2309
2310   my %escape_functions = ( 'latex'    => \&_latex_escape,
2311                            'html'     => \&_html_escape_nbsp,#\&encode_entities,
2312                            'template' => sub { shift },
2313                          );
2314   my $escape_function = $escape_functions{$format};
2315   my $escape_function_nonbsp = ($format eq 'html')
2316                                  ? \&_html_escape : $escape_function;
2317
2318   my %date_formats = ( 'latex'    => $date_format_long,
2319                        'html'     => $date_format_long,
2320                        'template' => '%s',
2321                      );
2322   $date_formats{'html'} =~ s/ /&nbsp;/g;
2323
2324   my $date_format = $date_formats{$format};
2325
2326   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
2327                                                },
2328                              'html'     => sub { return '<b>'. shift(). '</b>'
2329                                                },
2330                              'template' => sub { shift },
2331                            );
2332   my $embolden_function = $embolden_functions{$format};
2333
2334   warn "$me generating template variables\n"
2335     if $DEBUG > 1;
2336
2337   # generate template variables
2338   my $returnaddress;
2339   if (
2340          defined( $conf->config_orbase( "invoice_${format}returnaddress",
2341                                         $template
2342                                       )
2343                 )
2344        && length( $conf->config_orbase( "invoice_${format}returnaddress",
2345                                         $template
2346                                       )
2347                 )
2348   ) {
2349
2350     $returnaddress = join("\n",
2351       $conf->config_orbase("invoice_${format}returnaddress", $template)
2352     );
2353
2354   } elsif ( grep /\S/,
2355             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2356
2357     my $convert_map = $convert_maps{$format}{'returnaddress'};
2358     $returnaddress =
2359       join( "\n",
2360             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2361                                                  $template
2362                                                )
2363                          )
2364           );
2365   } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2366
2367     my $convert_map = $convert_maps{$format}{'returnaddress'};
2368     $returnaddress = join( "\n", &$convert_map(
2369                                    map { s/( {2,})/'~' x length($1)/eg;
2370                                          s/$/\\\\\*/;
2371                                          $_
2372                                        }
2373                                      ( $conf->config('company_name', $self->cust_main->agentnum),
2374                                        $conf->config('company_address', $self->cust_main->agentnum),
2375                                      )
2376                                  )
2377                      );
2378
2379   } else {
2380
2381     my $warning = "Couldn't find a return address; ".
2382                   "do you need to set the company_address configuration value?";
2383     warn "$warning\n";
2384     $returnaddress = $nbsp;
2385     #$returnaddress = $warning;
2386
2387   }
2388
2389   warn "$me generating invoice data\n"
2390     if $DEBUG > 1;
2391
2392   my $agentnum = $self->cust_main->agentnum;
2393
2394   my %invoice_data = (
2395
2396     #invoice from info
2397     'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
2398     'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2399     'returnaddress'   => $returnaddress,
2400     'agent'           => &$escape_function($cust_main->agent->agent),
2401
2402     #invoice info
2403     'invnum'          => $self->invnum,
2404     'date'            => time2str($date_format, $self->_date),
2405     'today'           => time2str($date_format_long, $today),
2406     'terms'           => $self->terms,
2407     'template'        => $template, #params{'template'},
2408     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
2409     'current_charges' => sprintf("%.2f", $self->charged),
2410     'duedate'         => $self->due_date2str($rdate_format), #date_format?
2411
2412     #customer info
2413     'custnum'         => $cust_main->display_custnum,
2414     'agent_custid'    => &$escape_function($cust_main->agent_custid),
2415     ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2416       payname company address1 address2 city state zip fax
2417     )),
2418
2419     #global config
2420     'ship_enable'     => $conf->exists('invoice-ship_address'),
2421     'unitprices'      => $conf->exists('invoice-unitprice'),
2422     'smallernotes'    => $conf->exists('invoice-smallernotes'),
2423     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
2424     'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2425    
2426     #layout info -- would be fancy to calc some of this and bury the template
2427     #               here in the code
2428     'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2429     'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2430     'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
2431     'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2432     'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2433     'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2434     'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2435     'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2436     'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2437     'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2438
2439     # better hang on to conf_dir for a while (for old templates)
2440     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2441
2442     #these are only used when doing paged plaintext
2443     'page'            => 1,
2444     'total_pages'     => 1,
2445
2446   );
2447
2448   $invoice_data{finance_section} = '';
2449   if ( $conf->config('finance_pkgclass') ) {
2450     my $pkg_class =
2451       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2452     $invoice_data{finance_section} = $pkg_class->categoryname;
2453   } 
2454   $invoice_data{finance_amount} = '0.00';
2455   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2456
2457   my $countrydefault = $conf->config('countrydefault') || 'US';
2458   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2459   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2460     my $method = $prefix.$_;
2461     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2462   }
2463   $invoice_data{'ship_country'} = ''
2464     if ( $invoice_data{'ship_country'} eq $countrydefault );
2465   
2466   $invoice_data{'cid'} = $params{'cid'}
2467     if $params{'cid'};
2468
2469   if ( $cust_main->country eq $countrydefault ) {
2470     $invoice_data{'country'} = '';
2471   } else {
2472     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2473   }
2474
2475   my @address = ();
2476   $invoice_data{'address'} = \@address;
2477   push @address,
2478     $cust_main->payname.
2479       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2480         ? " (P.O. #". $cust_main->payinfo. ")"
2481         : ''
2482       )
2483   ;
2484   push @address, $cust_main->company
2485     if $cust_main->company;
2486   push @address, $cust_main->address1;
2487   push @address, $cust_main->address2
2488     if $cust_main->address2;
2489   push @address,
2490     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2491   push @address, $invoice_data{'country'}
2492     if $invoice_data{'country'};
2493   push @address, ''
2494     while (scalar(@address) < 5);
2495
2496   $invoice_data{'logo_file'} = $params{'logo_file'}
2497     if $params{'logo_file'};
2498
2499   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2500 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2501   #my $balance_due = $self->owed + $pr_total - $cr_total;
2502   my $balance_due = $self->owed + $pr_total;
2503   $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2504   $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2505   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2506   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2507
2508   my $summarypage = '';
2509   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2510     $summarypage = 1;
2511   }
2512   $invoice_data{'summarypage'} = $summarypage;
2513
2514   warn "$me substituting variables in notes, footer, smallfooter\n"
2515     if $DEBUG > 1;
2516
2517   foreach my $include (qw( notes footer smallfooter coupon )) {
2518
2519     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2520     my @inc_src;
2521
2522     if ( $conf->exists($inc_file, $agentnum)
2523          && length( $conf->config($inc_file, $agentnum) ) ) {
2524
2525       @inc_src = $conf->config($inc_file, $agentnum);
2526
2527     } else {
2528
2529       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2530
2531       my $convert_map = $convert_maps{$format}{$include};
2532
2533       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2534                        s/--\@\]/$delimiters{$format}[1]/g;
2535                        $_;
2536                      } 
2537                  &$convert_map( $conf->config($inc_file, $agentnum) );
2538
2539     }
2540
2541     my $inc_tt = new Text::Template (
2542       TYPE       => 'ARRAY',
2543       SOURCE     => [ map "$_\n", @inc_src ],
2544       DELIMITERS => $delimiters{$format},
2545     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2546
2547     unless ( $inc_tt->compile() ) {
2548       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2549       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2550       die $error;
2551     }
2552
2553     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2554
2555     $invoice_data{$include} =~ s/\n+$//
2556       if ($format eq 'latex');
2557   }
2558
2559   $invoice_data{'po_line'} =
2560     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2561       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2562       : $nbsp;
2563
2564   my %money_chars = ( 'latex'    => '',
2565                       'html'     => $conf->config('money_char') || '$',
2566                       'template' => '',
2567                     );
2568   my $money_char = $money_chars{$format};
2569
2570   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2571                             'html'     => $conf->config('money_char') || '$',
2572                             'template' => '',
2573                           );
2574   my $other_money_char = $other_money_chars{$format};
2575   $invoice_data{'dollar'} = $other_money_char;
2576
2577   my @detail_items = ();
2578   my @total_items = ();
2579   my @buf = ();
2580   my @sections = ();
2581
2582   $invoice_data{'detail_items'} = \@detail_items;
2583   $invoice_data{'total_items'} = \@total_items;
2584   $invoice_data{'buf'} = \@buf;
2585   $invoice_data{'sections'} = \@sections;
2586
2587   warn "$me generating sections\n"
2588     if $DEBUG > 1;
2589
2590   my $previous_section = { 'description' => 'Previous Charges',
2591                            'subtotal'    => $other_money_char.
2592                                             sprintf('%.2f', $pr_total),
2593                            'summarized'  => $summarypage ? 'Y' : '',
2594                          };
2595   $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '. 
2596     join(' / ', map { $cust_main->balance_date_range(@$_) }
2597                 $self->_prior_month30s
2598         )
2599     if $conf->exists('invoice_include_aging');
2600
2601   my $taxtotal = 0;
2602   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2603                       'subtotal'    => $taxtotal,   # adjusted below
2604                       'summarized'  => $summarypage ? 'Y' : '',
2605                     };
2606   my $tax_weight = _pkg_category($tax_section->{description})
2607                         ? _pkg_category($tax_section->{description})->weight
2608                         : 0;
2609   $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2610   $tax_section->{'sort_weight'} = $tax_weight;
2611
2612
2613   my $adjusttotal = 0;
2614   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2615                          'subtotal'    => 0,   # adjusted below
2616                          'summarized'  => $summarypage ? 'Y' : '',
2617                        };
2618   my $adjust_weight = _pkg_category($adjust_section->{description})
2619                         ? _pkg_category($adjust_section->{description})->weight
2620                         : 0;
2621   $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2622   $adjust_section->{'sort_weight'} = $adjust_weight;
2623
2624   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2625   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2626   $invoice_data{'multisection'} = $multisection;
2627   my $late_sections = [];
2628   my $extra_sections = [];
2629   my $extra_lines = ();
2630   if ( $multisection ) {
2631     ($extra_sections, $extra_lines) =
2632       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2633       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2634
2635     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2636
2637     push @detail_items, @$extra_lines if $extra_lines;
2638     push @sections,
2639       $self->_items_sections( $late_sections,      # this could stand a refactor
2640                               $summarypage,
2641                               $escape_function_nonbsp,
2642                               $extra_sections,
2643                               $format,             #bah
2644                             );
2645     if ($conf->exists('svc_phone_sections')) {
2646       my ($phone_sections, $phone_lines) =
2647         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2648       push @{$late_sections}, @$phone_sections;
2649       push @detail_items, @$phone_lines;
2650     }
2651   }else{
2652     push @sections, { 'description' => '', 'subtotal' => '' };
2653   }
2654
2655   unless (    $conf->exists('disable_previous_balance')
2656            || $conf->exists('previous_balance-summary_only')
2657          )
2658   {
2659
2660     warn "$me adding previous balances\n"
2661       if $DEBUG > 1;
2662
2663     foreach my $line_item ( $self->_items_previous ) {
2664
2665       my $detail = {
2666         ext_description => [],
2667       };
2668       $detail->{'ref'} = $line_item->{'pkgnum'};
2669       $detail->{'quantity'} = 1;
2670       $detail->{'section'} = $previous_section;
2671       $detail->{'description'} = &$escape_function($line_item->{'description'});
2672       if ( exists $line_item->{'ext_description'} ) {
2673         @{$detail->{'ext_description'}} = map {
2674           &$escape_function($_);
2675         } @{$line_item->{'ext_description'}};
2676       }
2677       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2678                             $line_item->{'amount'};
2679       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2680
2681       push @detail_items, $detail;
2682       push @buf, [ $detail->{'description'},
2683                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2684                  ];
2685     }
2686
2687   }
2688
2689   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2690     push @buf, ['','-----------'];
2691     push @buf, [ 'Total Previous Balance',
2692                  $money_char. sprintf("%10.2f", $pr_total) ];
2693     push @buf, ['',''];
2694   }
2695  
2696   if ( $conf->exists('svc_phone-did-summary') ) {
2697       warn "$me adding DID summary\n"
2698         if $DEBUG > 1;
2699
2700       my ($didsummary,$minutes) = $self->_did_summary;
2701       my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2702       push @detail_items, 
2703         { 'description' => $didsummary_desc,
2704             'ext_description' => [ $didsummary, $minutes ],
2705         }
2706         if !$multisection;
2707   }
2708
2709   foreach my $section (@sections, @$late_sections) {
2710
2711     warn "$me adding section \n". Dumper($section)
2712       if $DEBUG > 1;
2713
2714     # begin some normalization
2715     $section->{'subtotal'} = $section->{'amount'}
2716       if $multisection
2717          && !exists($section->{subtotal})
2718          && exists($section->{amount});
2719
2720     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2721       if ( $invoice_data{finance_section} &&
2722            $section->{'description'} eq $invoice_data{finance_section} );
2723
2724     $section->{'subtotal'} = $other_money_char.
2725                              sprintf('%.2f', $section->{'subtotal'})
2726       if $multisection;
2727
2728     # continue some normalization
2729     $section->{'amount'}   = $section->{'subtotal'}
2730       if $multisection;
2731
2732
2733     if ( $section->{'description'} ) {
2734       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2735                    [ '', '' ],
2736                  );
2737     }
2738
2739     warn "$me   setting options\n"
2740       if $DEBUG > 1;
2741
2742     my $multilocation = scalar($cust_main->cust_location); #too expensive?
2743     my %options = ();
2744     $options{'section'} = $section if $multisection;
2745     $options{'format'} = $format;
2746     $options{'escape_function'} = $escape_function;
2747     $options{'format_function'} = sub { () } unless $unsquelched;
2748     $options{'unsquelched'} = $unsquelched;
2749     $options{'summary_page'} = $summarypage;
2750     $options{'skip_usage'} =
2751       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2752     $options{'multilocation'} = $multilocation;
2753     $options{'multisection'} = $multisection;
2754
2755     warn "$me   searching for line items\n"
2756       if $DEBUG > 1;
2757
2758     foreach my $line_item ( $self->_items_pkg(%options) ) {
2759
2760       warn "$me     adding line item $line_item\n"
2761         if $DEBUG > 1;
2762
2763       my $detail = {
2764         ext_description => [],
2765       };
2766       $detail->{'ref'} = $line_item->{'pkgnum'};
2767       $detail->{'quantity'} = $line_item->{'quantity'};
2768       $detail->{'section'} = $section;
2769       $detail->{'description'} = &$escape_function($line_item->{'description'});
2770       if ( exists $line_item->{'ext_description'} ) {
2771         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2772       }
2773       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2774                               $line_item->{'amount'};
2775       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2776                                  $line_item->{'unit_amount'};
2777       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2778   
2779       push @detail_items, $detail;
2780       push @buf, ( [ $detail->{'description'},
2781                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2782                    ],
2783                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2784                  );
2785     }
2786
2787     if ( $section->{'description'} ) {
2788       push @buf, ( ['','-----------'],
2789                    [ $section->{'description'}. ' sub-total',
2790                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2791                    ],
2792                    [ '', '' ],
2793                    [ '', '' ],
2794                  );
2795     }
2796   
2797   }
2798   
2799   $invoice_data{current_less_finance} =
2800     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2801
2802   if ( $multisection && !$conf->exists('disable_previous_balance')
2803     || $conf->exists('previous_balance-summary_only') )
2804   {
2805     unshift @sections, $previous_section if $pr_total;
2806   }
2807
2808   warn "$me adding taxes\n"
2809     if $DEBUG > 1;
2810
2811   foreach my $tax ( $self->_items_tax ) {
2812
2813     $taxtotal += $tax->{'amount'};
2814
2815     my $description = &$escape_function( $tax->{'description'} );
2816     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2817
2818     if ( $multisection ) {
2819
2820       my $money = $old_latex ? '' : $money_char;
2821       push @detail_items, {
2822         ext_description => [],
2823         ref          => '',
2824         quantity     => '',
2825         description  => $description,
2826         amount       => $money. $amount,
2827         product_code => '',
2828         section      => $tax_section,
2829       };
2830
2831     } else {
2832
2833       push @total_items, {
2834         'total_item'   => $description,
2835         'total_amount' => $other_money_char. $amount,
2836       };
2837
2838     }
2839
2840     push @buf,[ $description,
2841                 $money_char. $amount,
2842               ];
2843
2844   }
2845   
2846   if ( $taxtotal ) {
2847     my $total = {};
2848     $total->{'total_item'} = 'Sub-total';
2849     $total->{'total_amount'} =
2850       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2851
2852     if ( $multisection ) {
2853       $tax_section->{'subtotal'} = $other_money_char.
2854                                    sprintf('%.2f', $taxtotal);
2855       $tax_section->{'pretotal'} = 'New charges sub-total '.
2856                                    $total->{'total_amount'};
2857       push @sections, $tax_section if $taxtotal;
2858     }else{
2859       unshift @total_items, $total;
2860     }
2861   }
2862   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2863
2864   push @buf,['','-----------'];
2865   push @buf,[( $conf->exists('disable_previous_balance') 
2866                ? 'Total Charges'
2867                : 'Total New Charges'
2868              ),
2869              $money_char. sprintf("%10.2f",$self->charged) ];
2870   push @buf,['',''];
2871
2872   {
2873     my $total = {};
2874     my $item = 'Total';
2875     $item = $conf->config('previous_balance-exclude_from_total')
2876          || 'Total New Charges'
2877       if $conf->exists('previous_balance-exclude_from_total');
2878     my $amount = $self->charged +
2879                    ( $conf->exists('disable_previous_balance') ||
2880                      $conf->exists('previous_balance-exclude_from_total')
2881                      ? 0
2882                      : $pr_total
2883                    );
2884     $total->{'total_item'} = &$embolden_function($item);
2885     $total->{'total_amount'} =
2886       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
2887     if ( $multisection ) {
2888       if ( $adjust_section->{'sort_weight'} ) {
2889         $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2890           sprintf("%.2f", ($self->billing_balance || 0) );
2891       } else {
2892         $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2893                                         sprintf('%.2f', $self->charged );
2894       } 
2895     }else{
2896       push @total_items, $total;
2897     }
2898     push @buf,['','-----------'];
2899     push @buf,[$item,
2900                $money_char.
2901                sprintf( '%10.2f', $amount )
2902               ];
2903     push @buf,['',''];
2904   }
2905   
2906   unless ( $conf->exists('disable_previous_balance') ) {
2907     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2908   
2909     # credits
2910     my $credittotal = 0;
2911     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2912
2913       my $total;
2914       $total->{'total_item'} = &$escape_function($credit->{'description'});
2915       $credittotal += $credit->{'amount'};
2916       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2917       $adjusttotal += $credit->{'amount'};
2918       if ( $multisection ) {
2919         my $money = $old_latex ? '' : $money_char;
2920         push @detail_items, {
2921           ext_description => [],
2922           ref          => '',
2923           quantity     => '',
2924           description  => &$escape_function($credit->{'description'}),
2925           amount       => $money. $credit->{'amount'},
2926           product_code => '',
2927           section      => $adjust_section,
2928         };
2929       } else {
2930         push @total_items, $total;
2931       }
2932
2933     }
2934     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2935
2936     #credits (again)
2937     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2938       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2939     }
2940
2941     # payments
2942     my $paymenttotal = 0;
2943     foreach my $payment ( $self->_items_payments ) {
2944       my $total = {};
2945       $total->{'total_item'} = &$escape_function($payment->{'description'});
2946       $paymenttotal += $payment->{'amount'};
2947       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2948       $adjusttotal += $payment->{'amount'};
2949       if ( $multisection ) {
2950         my $money = $old_latex ? '' : $money_char;
2951         push @detail_items, {
2952           ext_description => [],
2953           ref          => '',
2954           quantity     => '',
2955           description  => &$escape_function($payment->{'description'}),
2956           amount       => $money. $payment->{'amount'},
2957           product_code => '',
2958           section      => $adjust_section,
2959         };
2960       }else{
2961         push @total_items, $total;
2962       }
2963       push @buf, [ $payment->{'description'},
2964                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2965                  ];
2966     }
2967     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2968   
2969     if ( $multisection ) {
2970       $adjust_section->{'subtotal'} = $other_money_char.
2971                                       sprintf('%.2f', $adjusttotal);
2972       push @sections, $adjust_section
2973         unless $adjust_section->{sort_weight};
2974     }
2975
2976     { 
2977       my $total;
2978       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2979       $total->{'total_amount'} =
2980         &$embolden_function(
2981           $other_money_char. sprintf('%.2f', $summarypage 
2982                                                ? $self->charged +
2983                                                  $self->billing_balance
2984                                                : $self->owed + $pr_total
2985                                     )
2986         );
2987       if ( $multisection && !$adjust_section->{sort_weight} ) {
2988         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2989                                          $total->{'total_amount'};
2990       }else{
2991         push @total_items, $total;
2992       }
2993       push @buf,['','-----------'];
2994       push @buf,[$self->balance_due_msg, $money_char. 
2995         sprintf("%10.2f", $balance_due ) ];
2996     }
2997   }
2998
2999   if ( $multisection ) {
3000     if ($conf->exists('svc_phone_sections')) {
3001       my $total;
3002       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3003       $total->{'total_amount'} =
3004         &$embolden_function(
3005           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3006         );
3007       my $last_section = pop @sections;
3008       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3009                                      $total->{'total_amount'};
3010       push @sections, $last_section;
3011     }
3012     push @sections, @$late_sections
3013       if $unsquelched;
3014   }
3015
3016   my @includelist = ();
3017   push @includelist, 'summary' if $summarypage;
3018   foreach my $include ( @includelist ) {
3019
3020     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3021     my @inc_src;
3022
3023     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3024
3025       @inc_src = $conf->config($inc_file, $agentnum);
3026
3027     } else {
3028
3029       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3030
3031       my $convert_map = $convert_maps{$format}{$include};
3032
3033       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3034                        s/--\@\]/$delimiters{$format}[1]/g;
3035                        $_;
3036                      } 
3037                  &$convert_map( $conf->config($inc_file, $agentnum) );
3038
3039     }
3040
3041     my $inc_tt = new Text::Template (
3042       TYPE       => 'ARRAY',
3043       SOURCE     => [ map "$_\n", @inc_src ],
3044       DELIMITERS => $delimiters{$format},
3045     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3046
3047     unless ( $inc_tt->compile() ) {
3048       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3049       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3050       die $error;
3051     }
3052
3053     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3054
3055     $invoice_data{$include} =~ s/\n+$//
3056       if ($format eq 'latex');
3057   }
3058
3059   $invoice_lines = 0;
3060   my $wasfunc = 0;
3061   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3062     /invoice_lines\((\d*)\)/;
3063     $invoice_lines += $1 || scalar(@buf);
3064     $wasfunc=1;
3065   }
3066   die "no invoice_lines() functions in template?"
3067     if ( $format eq 'template' && !$wasfunc );
3068
3069   if ($format eq 'template') {
3070
3071     if ( $invoice_lines ) {
3072       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3073       $invoice_data{'total_pages'}++
3074         if scalar(@buf) % $invoice_lines;
3075     }
3076
3077     #setup subroutine for the template
3078     sub FS::cust_bill::_template::invoice_lines {
3079       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3080       map { 
3081         scalar(@FS::cust_bill::_template::buf)
3082           ? shift @FS::cust_bill::_template::buf
3083           : [ '', '' ];
3084       }
3085       ( 1 .. $lines );
3086     }
3087
3088     my $lines;
3089     my @collect;
3090     while (@buf) {
3091       push @collect, split("\n",
3092         $text_template->fill_in( HASH => \%invoice_data,
3093                                  PACKAGE => 'FS::cust_bill::_template'
3094                                )
3095       );
3096       $FS::cust_bill::_template::page++;
3097     }
3098     map "$_\n", @collect;
3099   }else{
3100     warn "filling in template for invoice ". $self->invnum. "\n"
3101       if $DEBUG;
3102     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3103       if $DEBUG > 1;
3104
3105     $text_template->fill_in(HASH => \%invoice_data);
3106   }
3107 }
3108
3109 # helper routine for generating date ranges
3110 sub _prior_month30s {
3111   my $self = shift;
3112   my @ranges = (
3113    [ 1,       2592000 ], # 0-30 days ago
3114    [ 2592000, 5184000 ], # 30-60 days ago
3115    [ 5184000, 7776000 ], # 60-90 days ago
3116    [ 7776000, 0       ], # 90+   days ago
3117   );
3118
3119   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3120           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3121       ] }
3122   @ranges;
3123 }
3124
3125 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3126
3127 Returns an postscript invoice, as a scalar.
3128
3129 Options can be passed as a hashref (recommended) or as a list of time, template
3130 and then any key/value pairs for any other options.
3131
3132 I<time> an optional value used to control the printing of overdue messages.  The
3133 default is now.  It isn't the date of the invoice; that's the `_date' field.
3134 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3135 L<Time::Local> and L<Date::Parse> for conversion functions.
3136
3137 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3138
3139 =cut
3140
3141 sub print_ps {
3142   my $self = shift;
3143
3144   my ($file, $lfile) = $self->print_latex(@_);
3145   my $ps = generate_ps($file);
3146   unlink($lfile);
3147
3148   $ps;
3149 }
3150
3151 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3152
3153 Returns an PDF invoice, as a scalar.
3154
3155 Options can be passed as a hashref (recommended) or as a list of time, template
3156 and then any key/value pairs for any other options.
3157
3158 I<time> an optional value used to control the printing of overdue messages.  The
3159 default is now.  It isn't the date of the invoice; that's the `_date' field.
3160 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3161 L<Time::Local> and L<Date::Parse> for conversion functions.
3162
3163 I<template>, if specified, is the name of a suffix for alternate invoices.
3164
3165 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3166
3167 =cut
3168
3169 sub print_pdf {
3170   my $self = shift;
3171
3172   my ($file, $lfile) = $self->print_latex(@_);
3173   my $pdf = generate_pdf($file);
3174   unlink($lfile);
3175
3176   $pdf;
3177 }
3178
3179 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3180
3181 Returns an HTML invoice, as a scalar.
3182
3183 I<time> an optional value used to control the printing of overdue messages.  The
3184 default is now.  It isn't the date of the invoice; that's the `_date' field.
3185 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3186 L<Time::Local> and L<Date::Parse> for conversion functions.
3187
3188 I<template>, if specified, is the name of a suffix for alternate invoices.
3189
3190 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3191
3192 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3193 when emailing the invoice as part of a multipart/related MIME email.
3194
3195 =cut
3196
3197 sub print_html {
3198   my $self = shift;
3199   my %params;
3200   if ( ref($_[0]) ) {
3201     %params = %{ shift() }; 
3202   }else{
3203     $params{'time'} = shift;
3204     $params{'template'} = shift;
3205     $params{'cid'} = shift;
3206   }
3207
3208   $params{'format'} = 'html';
3209
3210   $self->print_generic( %params );
3211 }
3212
3213 # quick subroutine for print_latex
3214 #
3215 # There are ten characters that LaTeX treats as special characters, which
3216 # means that they do not simply typeset themselves: 
3217 #      # $ % & ~ _ ^ \ { }
3218 #
3219 # TeX ignores blanks following an escaped character; if you want a blank (as
3220 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3221
3222 sub _latex_escape {
3223   my $value = shift;
3224   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3225   $value =~ s/([<>])/\$$1\$/g;
3226   $value;
3227 }
3228
3229 sub _html_escape {
3230   my $value = shift;
3231   encode_entities($value);
3232   $value;
3233 }
3234
3235 sub _html_escape_nbsp {
3236   my $value = _html_escape(shift);
3237   $value =~ s/ +/&nbsp;/g;
3238   $value;
3239 }
3240
3241 #utility methods for print_*
3242
3243 sub _translate_old_latex_format {
3244   warn "_translate_old_latex_format called\n"
3245     if $DEBUG; 
3246
3247   my @template = ();
3248   while ( @_ ) {
3249     my $line = shift;
3250   
3251     if ( $line =~ /^%%Detail\s*$/ ) {
3252   
3253       push @template, q![@--!,
3254                       q!  foreach my $_tr_line (@detail_items) {!,
3255                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3256                       q!      $_tr_line->{'description'} .= !, 
3257                       q!        "\\tabularnewline\n~~".!,
3258                       q!        join( "\\tabularnewline\n~~",!,
3259                       q!          @{$_tr_line->{'ext_description'}}!,
3260                       q!        );!,
3261                       q!    }!;
3262
3263       while ( ( my $line_item_line = shift )
3264               !~ /^%%EndDetail\s*$/                            ) {
3265         $line_item_line =~ s/'/\\'/g;    # nice LTS
3266         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3267         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3268         push @template, "    \$OUT .= '$line_item_line';";
3269       }
3270
3271       push @template, '}',
3272                       '--@]';
3273       #' doh, gvim
3274     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3275
3276       push @template, '[@--',
3277                       '  foreach my $_tr_line (@total_items) {';
3278
3279       while ( ( my $total_item_line = shift )
3280               !~ /^%%EndTotalDetails\s*$/                      ) {
3281         $total_item_line =~ s/'/\\'/g;    # nice LTS
3282         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3283         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3284         push @template, "    \$OUT .= '$total_item_line';";
3285       }
3286
3287       push @template, '}',
3288                       '--@]';
3289
3290     } else {
3291       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3292       push @template, $line;  
3293     }
3294   
3295   }
3296
3297   if ($DEBUG) {
3298     warn "$_\n" foreach @template;
3299   }
3300
3301   (@template);
3302 }
3303
3304 sub terms {
3305   my $self = shift;
3306
3307   #check for an invoice-specific override
3308   return $self->invoice_terms if $self->invoice_terms;
3309   
3310   #check for a customer- specific override
3311   my $cust_main = $self->cust_main;
3312   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3313
3314   #use configured default
3315   $conf->config('invoice_default_terms') || '';
3316 }
3317
3318 sub due_date {
3319   my $self = shift;
3320   my $duedate = '';
3321   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3322     $duedate = $self->_date() + ( $1 * 86400 );
3323   }
3324   $duedate;
3325 }
3326
3327 sub due_date2str {
3328   my $self = shift;
3329   $self->due_date ? time2str(shift, $self->due_date) : '';
3330 }
3331
3332 sub balance_due_msg {
3333   my $self = shift;
3334   my $msg = 'Balance Due';
3335   return $msg unless $self->terms;
3336   if ( $self->due_date ) {
3337     $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3338   } elsif ( $self->terms ) {
3339     $msg .= ' - '. $self->terms;
3340   }
3341   $msg;
3342 }
3343
3344 sub balance_due_date {
3345   my $self = shift;
3346   my $duedate = '';
3347   if (    $conf->exists('invoice_default_terms') 
3348        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3349     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3350   }
3351   $duedate;
3352 }
3353
3354 =item invnum_date_pretty
3355
3356 Returns a string with the invoice number and date, for example:
3357 "Invoice #54 (3/20/2008)"
3358
3359 =cut
3360
3361 sub invnum_date_pretty {
3362   my $self = shift;
3363   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3364 }
3365
3366 =item _date_pretty
3367
3368 Returns a string with the date, for example: "3/20/2008"
3369
3370 =cut
3371
3372 sub _date_pretty {
3373   my $self = shift;
3374   time2str($date_format, $self->_date);
3375 }
3376
3377 use vars qw(%pkg_category_cache);
3378 sub _items_sections {
3379   my $self = shift;
3380   my $late = shift;
3381   my $summarypage = shift;
3382   my $escape = shift;
3383   my $extra_sections = shift;
3384   my $format = shift;
3385
3386   my %subtotal = ();
3387   my %late_subtotal = ();
3388   my %not_tax = ();
3389
3390   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3391   {
3392
3393       my $usage = $cust_bill_pkg->usage;
3394
3395       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3396         next if ( $display->summary && $summarypage );
3397
3398         my $section = $display->section;
3399         my $type    = $display->type;
3400
3401         $not_tax{$section} = 1
3402           unless $cust_bill_pkg->pkgnum == 0;
3403
3404         if ( $display->post_total && !$summarypage ) {
3405           if (! $type || $type eq 'S') {
3406             $late_subtotal{$section} += $cust_bill_pkg->setup
3407               if $cust_bill_pkg->setup != 0;
3408           }
3409
3410           if (! $type) {
3411             $late_subtotal{$section} += $cust_bill_pkg->recur
3412               if $cust_bill_pkg->recur != 0;
3413           }
3414
3415           if ($type && $type eq 'R') {
3416             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3417               if $cust_bill_pkg->recur != 0;
3418           }
3419           
3420           if ($type && $type eq 'U') {
3421             $late_subtotal{$section} += $usage
3422               unless scalar(@$extra_sections);
3423           }
3424
3425         } else {
3426
3427           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3428
3429           if (! $type || $type eq 'S') {
3430             $subtotal{$section} += $cust_bill_pkg->setup
3431               if $cust_bill_pkg->setup != 0;
3432           }
3433
3434           if (! $type) {
3435             $subtotal{$section} += $cust_bill_pkg->recur
3436               if $cust_bill_pkg->recur != 0;
3437           }
3438
3439           if ($type && $type eq 'R') {
3440             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3441               if $cust_bill_pkg->recur != 0;
3442           }
3443           
3444           if ($type && $type eq 'U') {
3445             $subtotal{$section} += $usage
3446               unless scalar(@$extra_sections);
3447           }
3448
3449         }
3450
3451       }
3452
3453   }
3454
3455   %pkg_category_cache = ();
3456
3457   push @$late, map { { 'description' => &{$escape}($_),
3458                        'subtotal'    => $late_subtotal{$_},
3459                        'post_total'  => 1,
3460                        'sort_weight' => ( _pkg_category($_)
3461                                             ? _pkg_category($_)->weight
3462                                             : 0
3463                                        ),
3464                        ((_pkg_category($_) && _pkg_category($_)->condense)
3465                                            ? $self->_condense_section($format)
3466                                            : ()
3467                        ),
3468                    } }
3469                  sort _sectionsort keys %late_subtotal;
3470
3471   my @sections;
3472   if ( $summarypage ) {
3473     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3474                 map { $_->categoryname } qsearch('pkg_category', {});
3475     push @sections, '' if exists($subtotal{''});
3476   } else {
3477     @sections = keys %subtotal;
3478   }
3479
3480   my @early = map { { 'description' => &{$escape}($_),
3481                       'subtotal'    => $subtotal{$_},
3482                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3483                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3484                       'sort_weight' => ( _pkg_category($_)
3485                                            ? _pkg_category($_)->weight
3486                                            : 0
3487                                        ),
3488                        ((_pkg_category($_) && _pkg_category($_)->condense)
3489                                            ? $self->_condense_section($format)
3490                                            : ()
3491                        ),
3492                     }
3493                   } @sections;
3494   push @early, @$extra_sections if $extra_sections;
3495  
3496   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3497
3498 }
3499
3500 #helper subs for above
3501
3502 sub _sectionsort {
3503   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3504 }
3505
3506 sub _pkg_category {
3507   my $categoryname = shift;
3508   $pkg_category_cache{$categoryname} ||=
3509     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3510 }
3511
3512 my %condensed_format = (
3513   'label' => [ qw( Description Qty Amount ) ],
3514   'fields' => [
3515                 sub { shift->{description} },
3516                 sub { shift->{quantity} },
3517                 sub { my($href, %opt) = @_;
3518                       ($opt{dollar} || ''). $href->{amount};
3519                     },
3520               ],
3521   'align'  => [ qw( l r r ) ],
3522   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3523   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3524 );
3525
3526 sub _condense_section {
3527   my ( $self, $format ) = ( shift, shift );
3528   ( 'condensed' => 1,
3529     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3530       qw( description_generator
3531           header_generator
3532           total_generator
3533           total_line_generator
3534         )
3535   );
3536 }
3537
3538 sub _condensed_generator_defaults {
3539   my ( $self, $format ) = ( shift, shift );
3540   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3541 }
3542
3543 my %html_align = (
3544   'c' => 'center',
3545   'l' => 'left',
3546   'r' => 'right',
3547 );
3548
3549 sub _condensed_header_generator {
3550   my ( $self, $format ) = ( shift, shift );
3551
3552   my ( $f, $prefix, $suffix, $separator, $column ) =
3553     _condensed_generator_defaults($format);
3554
3555   if ($format eq 'latex') {
3556     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3557     $suffix = "\\\\\n\\hline";
3558     $separator = "&\n";
3559     $column =
3560       sub { my ($d,$a,$s,$w) = @_;
3561             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3562           };
3563   } elsif ( $format eq 'html' ) {
3564     $prefix = '<th></th>';
3565     $suffix = '';
3566     $separator = '';
3567     $column =
3568       sub { my ($d,$a,$s,$w) = @_;
3569             return qq!<th align="$html_align{$a}">$d</th>!;
3570       };
3571   }
3572
3573   sub {
3574     my @args = @_;
3575     my @result = ();
3576
3577     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3578       push @result,
3579         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3580     }
3581
3582     $prefix. join($separator, @result). $suffix;
3583   };
3584
3585 }
3586
3587 sub _condensed_description_generator {
3588   my ( $self, $format ) = ( shift, shift );
3589
3590   my ( $f, $prefix, $suffix, $separator, $column ) =
3591     _condensed_generator_defaults($format);
3592
3593   my $money_char = '$';
3594   if ($format eq 'latex') {
3595     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3596     $suffix = '\\\\';
3597     $separator = " & \n";
3598     $column =
3599       sub { my ($d,$a,$s,$w) = @_;
3600             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3601           };
3602     $money_char = '\\dollar';
3603   }elsif ( $format eq 'html' ) {
3604     $prefix = '"><td align="center"></td>';
3605     $suffix = '';
3606     $separator = '';
3607     $column =
3608       sub { my ($d,$a,$s,$w) = @_;
3609             return qq!<td align="$html_align{$a}">$d</td>!;
3610       };
3611     #$money_char = $conf->config('money_char') || '$';
3612     $money_char = '';  # this is madness
3613   }
3614
3615   sub {
3616     #my @args = @_;
3617     my $href = shift;
3618     my @result = ();
3619
3620     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3621       my $dollar = '';
3622       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3623       push @result,
3624         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3625                     map { $f->{$_}->[$i] } qw(align span width)
3626                   );
3627     }
3628
3629     $prefix. join( $separator, @result ). $suffix;
3630   };
3631
3632 }
3633
3634 sub _condensed_total_generator {
3635   my ( $self, $format ) = ( shift, shift );
3636
3637   my ( $f, $prefix, $suffix, $separator, $column ) =
3638     _condensed_generator_defaults($format);
3639   my $style = '';
3640
3641   if ($format eq 'latex') {
3642     $prefix = "& ";
3643     $suffix = "\\\\\n";
3644     $separator = " & \n";
3645     $column =
3646       sub { my ($d,$a,$s,$w) = @_;
3647             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3648           };
3649   }elsif ( $format eq 'html' ) {
3650     $prefix = '';
3651     $suffix = '';
3652     $separator = '';
3653     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3654     $column =
3655       sub { my ($d,$a,$s,$w) = @_;
3656             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3657       };
3658   }
3659
3660
3661   sub {
3662     my @args = @_;
3663     my @result = ();
3664
3665     #  my $r = &{$f->{fields}->[$i]}(@args);
3666     #  $r .= ' Total' unless $i;
3667
3668     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3669       push @result,
3670         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3671                     map { $f->{$_}->[$i] } qw(align span width)
3672                   );
3673     }
3674
3675     $prefix. join( $separator, @result ). $suffix;
3676   };
3677
3678 }
3679
3680 =item total_line_generator FORMAT
3681
3682 Returns a coderef used for generation of invoice total line items for this
3683 usage_class.  FORMAT is either html or latex
3684
3685 =cut
3686
3687 # should not be used: will have issues with hash element names (description vs
3688 # total_item and amount vs total_amount -- another array of functions?
3689
3690 sub _condensed_total_line_generator {
3691   my ( $self, $format ) = ( shift, shift );
3692
3693   my ( $f, $prefix, $suffix, $separator, $column ) =
3694     _condensed_generator_defaults($format);
3695   my $style = '';
3696
3697   if ($format eq 'latex') {
3698     $prefix = "& ";
3699     $suffix = "\\\\\n";
3700     $separator = " & \n";
3701     $column =
3702       sub { my ($d,$a,$s,$w) = @_;
3703             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3704           };
3705   }elsif ( $format eq 'html' ) {
3706     $prefix = '';
3707     $suffix = '';
3708     $separator = '';
3709     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3710     $column =
3711       sub { my ($d,$a,$s,$w) = @_;
3712             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3713       };
3714   }
3715
3716
3717   sub {
3718     my @args = @_;
3719     my @result = ();
3720
3721     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3722       push @result,
3723         &{$column}( &{$f->{fields}->[$i]}(@args),
3724                     map { $f->{$_}->[$i] } qw(align span width)
3725                   );
3726     }
3727
3728     $prefix. join( $separator, @result ). $suffix;
3729   };
3730
3731 }
3732
3733 #sub _items_extra_usage_sections {
3734 #  my $self = shift;
3735 #  my $escape = shift;
3736 #
3737 #  my %sections = ();
3738 #
3739 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
3740 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3741 #  {
3742 #    next unless $cust_bill_pkg->pkgnum > 0;
3743 #
3744 #    foreach my $section ( keys %usage_class ) {
3745 #
3746 #      my $usage = $cust_bill_pkg->usage($section);
3747 #
3748 #      next unless $usage && $usage > 0;
3749 #
3750 #      $sections{$section} ||= 0;
3751 #      $sections{$section} += $usage;
3752 #
3753 #    }
3754 #
3755 #  }
3756 #
3757 #  map { { 'description' => &{$escape}($_),
3758 #          'subtotal'    => $sections{$_},
3759 #          'summarized'  => '',
3760 #          'tax_section' => '',
3761 #        }
3762 #      }
3763 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3764 #
3765 #}
3766
3767 sub _items_extra_usage_sections {
3768   my $self = shift;
3769   my $escape = shift;
3770   my $format = shift;
3771
3772   my %sections = ();
3773   my %classnums = ();
3774   my %lines = ();
3775
3776   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3777   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3778     next unless $cust_bill_pkg->pkgnum > 0;
3779
3780     foreach my $classnum ( keys %usage_class ) {
3781       my $section = $usage_class{$classnum}->classname;
3782       $classnums{$section} = $classnum;
3783
3784       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3785         my $amount = $detail->amount;
3786         next unless $amount && $amount > 0;
3787  
3788         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3789         $sections{$section}{amount} += $amount;  #subtotal
3790         $sections{$section}{calls}++;
3791         $sections{$section}{duration} += $detail->duration;
3792
3793         my $desc = $detail->regionname; 
3794         my $description = $desc;
3795         $description = substr($desc, 0, 50). '...'
3796           if $format eq 'latex' && length($desc) > 50;
3797
3798         $lines{$section}{$desc} ||= {
3799           description     => &{$escape}($description),
3800           #pkgpart         => $part_pkg->pkgpart,
3801           pkgnum          => $cust_bill_pkg->pkgnum,
3802           ref             => '',
3803           amount          => 0,
3804           calls           => 0,
3805           duration        => 0,
3806           #unit_amount     => $cust_bill_pkg->unitrecur,
3807           quantity        => $cust_bill_pkg->quantity,
3808           product_code    => 'N/A',
3809           ext_description => [],
3810         };
3811
3812         $lines{$section}{$desc}{amount} += $amount;
3813         $lines{$section}{$desc}{calls}++;
3814         $lines{$section}{$desc}{duration} += $detail->duration;
3815
3816       }
3817     }
3818   }
3819
3820   my %sectionmap = ();
3821   foreach (keys %sections) {
3822     my $usage_class = $usage_class{$classnums{$_}};
3823     $sectionmap{$_} = { 'description' => &{$escape}($_),
3824                         'amount'    => $sections{$_}{amount},    #subtotal
3825                         'calls'       => $sections{$_}{calls},
3826                         'duration'    => $sections{$_}{duration},
3827                         'summarized'  => '',
3828                         'tax_section' => '',
3829                         'sort_weight' => $usage_class->weight,
3830                         ( $usage_class->format
3831                           ? ( map { $_ => $usage_class->$_($format) }
3832                               qw( description_generator header_generator total_generator total_line_generator )
3833                             )
3834                           : ()
3835                         ), 
3836                       };
3837   }
3838
3839   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3840                  values %sectionmap;
3841
3842   my @lines = ();
3843   foreach my $section ( keys %lines ) {
3844     foreach my $line ( keys %{$lines{$section}} ) {
3845       my $l = $lines{$section}{$line};
3846       $l->{section}     = $sectionmap{$section};
3847       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
3848       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3849       push @lines, $l;
3850     }
3851   }
3852
3853   return(\@sections, \@lines);
3854
3855 }
3856
3857 sub _did_summary {
3858     my $self = shift;
3859     my $end = $self->_date;
3860     my $start = $end - 2592000; # 30 days
3861     my $cust_main = $self->cust_main;
3862     my @pkgs = $cust_main->all_pkgs;
3863     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3864         = (0,0,0,0,0);
3865     my @seen = ();
3866     foreach my $pkg ( @pkgs ) {
3867         my @h_cust_svc = $pkg->h_cust_svc($end);
3868         foreach my $h_cust_svc ( @h_cust_svc ) {
3869             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3870             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3871
3872             my $inserted = $h_cust_svc->date_inserted;
3873             my $deleted = $h_cust_svc->date_deleted;
3874             my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3875             my $phone_deleted;
3876             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
3877             
3878 # DID either activated or ported in; cannot be both for same DID simultaneously
3879             if ($inserted >= $start && $inserted <= $end && $phone_inserted
3880                 && (!$phone_inserted->lnp_status 
3881                     || $phone_inserted->lnp_status eq ''
3882                     || $phone_inserted->lnp_status eq 'native')) {
3883                 $num_activated++;
3884             }
3885             else { # this one not so clean, should probably move to (h_)svc_phone
3886                  my $phone_portedin = qsearchs( 'h_svc_phone',
3887                       { 'svcnum' => $h_cust_svc->svcnum, 
3888                         'lnp_status' => 'portedin' },  
3889                       FS::h_svc_phone->sql_h_searchs($end),  
3890                     );
3891                  $num_portedin++ if $phone_portedin;
3892             }
3893
3894 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3895             if($deleted >= $start && $deleted <= $end && $phone_deleted
3896                 && (!$phone_deleted->lnp_status 
3897                     || $phone_deleted->lnp_status ne 'portingout')) {
3898                 $num_deactivated++;
3899             } 
3900             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
3901                 && $phone_deleted->lnp_status 
3902                 && $phone_deleted->lnp_status eq 'portingout') {
3903                 $num_portedout++;
3904             }
3905
3906             # increment usage minutes
3907             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
3908             foreach my $cdr ( @cdrs ) {
3909                 $minutes += $cdr->billsec/60;
3910             }
3911
3912             # don't look at this service again
3913             push @seen, $h_cust_svc->svcnum;
3914         }
3915     }
3916
3917     $minutes = sprintf("%d", $minutes);
3918     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
3919         . "$num_deactivated  Ported-Out: $num_portedout ",
3920             "Total Minutes: $minutes");
3921 }
3922
3923 sub _items_svc_phone_sections {
3924   my $self = shift;
3925   my $escape = shift;
3926   my $format = shift;
3927
3928   my %sections = ();
3929   my %classnums = ();
3930   my %lines = ();
3931
3932   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3933   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
3934
3935   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3936     next unless $cust_bill_pkg->pkgnum > 0;
3937
3938     my @header = $cust_bill_pkg->details_header;
3939     next unless scalar(@header);
3940
3941     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3942
3943       my $phonenum = $detail->phonenum;
3944       next unless $phonenum;
3945
3946       my $amount = $detail->amount;
3947       next unless $amount && $amount > 0;
3948
3949       $sections{$phonenum} ||= { 'amount'      => 0,
3950                                  'calls'       => 0,
3951                                  'duration'    => 0,
3952                                  'sort_weight' => -1,
3953                                  'phonenum'    => $phonenum,
3954                                 };
3955       $sections{$phonenum}{amount} += $amount;  #subtotal
3956       $sections{$phonenum}{calls}++;
3957       $sections{$phonenum}{duration} += $detail->duration;
3958
3959       my $desc = $detail->regionname; 
3960       my $description = $desc;
3961       $description = substr($desc, 0, 50). '...'
3962         if $format eq 'latex' && length($desc) > 50;
3963
3964       $lines{$phonenum}{$desc} ||= {
3965         description     => &{$escape}($description),
3966         #pkgpart         => $part_pkg->pkgpart,
3967         pkgnum          => '',
3968         ref             => '',
3969         amount          => 0,
3970         calls           => 0,
3971         duration        => 0,
3972         #unit_amount     => '',
3973         quantity        => '',
3974         product_code    => 'N/A',
3975         ext_description => [],
3976       };
3977
3978       $lines{$phonenum}{$desc}{amount} += $amount;
3979       $lines{$phonenum}{$desc}{calls}++;
3980       $lines{$phonenum}{$desc}{duration} += $detail->duration;
3981
3982       my $line = $usage_class{$detail->classnum}->classname;
3983       $sections{"$phonenum $line"} ||=
3984         { 'amount' => 0,
3985           'calls' => 0,
3986           'duration' => 0,
3987           'sort_weight' => $usage_class{$detail->classnum}->weight,
3988           'phonenum' => $phonenum,
3989           'header'  => [ @header ],
3990         };
3991       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
3992       $sections{"$phonenum $line"}{calls}++;
3993       $sections{"$phonenum $line"}{duration} += $detail->duration;
3994
3995       $lines{"$phonenum $line"}{$desc} ||= {
3996         description     => &{$escape}($description),
3997         #pkgpart         => $part_pkg->pkgpart,
3998         pkgnum          => '',
3999         ref             => '',
4000         amount          => 0,
4001         calls           => 0,
4002         duration        => 0,
4003         #unit_amount     => '',
4004         quantity        => '',
4005         product_code    => 'N/A',
4006         ext_description => [],
4007       };
4008
4009       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4010       $lines{"$phonenum $line"}{$desc}{calls}++;
4011       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4012       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4013            $detail->formatted('format' => $format);
4014
4015     }
4016   }
4017
4018   my %sectionmap = ();
4019   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4020   foreach ( keys %sections ) {
4021     my @header = @{ $sections{$_}{header} || [] };
4022     my $usage_simple =
4023       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4024     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4025     my $usage_class = $summary ? $simple : $usage_simple;
4026     my $ending = $summary ? ' usage charges' : '';
4027     my %gen_opt = ();
4028     unless ($summary) {
4029       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4030     }
4031     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4032                         'amount'    => $sections{$_}{amount},    #subtotal
4033                         'calls'       => $sections{$_}{calls},
4034                         'duration'    => $sections{$_}{duration},
4035                         'summarized'  => '',
4036                         'tax_section' => '',
4037                         'phonenum'    => $sections{$_}{phonenum},
4038                         'sort_weight' => $sections{$_}{sort_weight},
4039                         'post_total'  => $summary, #inspire pagebreak
4040                         (
4041                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4042                             qw( description_generator
4043                                 header_generator
4044                                 total_generator
4045                                 total_line_generator
4046                               )
4047                           )
4048                         ), 
4049                       };
4050   }
4051
4052   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4053                         $a->{sort_weight} <=> $b->{sort_weight}
4054                       }
4055                  values %sectionmap;
4056
4057   my @lines = ();
4058   foreach my $section ( keys %lines ) {
4059     foreach my $line ( keys %{$lines{$section}} ) {
4060       my $l = $lines{$section}{$line};
4061       $l->{section}     = $sectionmap{$section};
4062       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4063       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4064       push @lines, $l;
4065     }
4066   }
4067
4068   return(\@sections, \@lines);
4069
4070 }
4071
4072 sub _items {
4073   my $self = shift;
4074
4075   #my @display = scalar(@_)
4076   #              ? @_
4077   #              : qw( _items_previous _items_pkg );
4078   #              #: qw( _items_pkg );
4079   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4080   my @display = qw( _items_previous _items_pkg );
4081
4082   my @b = ();
4083   foreach my $display ( @display ) {
4084     push @b, $self->$display(@_);
4085   }
4086   @b;
4087 }
4088
4089 sub _items_previous {
4090   my $self = shift;
4091   my $cust_main = $self->cust_main;
4092   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4093   my @b = ();
4094   foreach ( @pr_cust_bill ) {
4095     my $date = $conf->exists('invoice_show_prior_due_date')
4096                ? 'due '. $_->due_date2str($date_format)
4097                : time2str($date_format, $_->_date);
4098     push @b, {
4099       'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4100       #'pkgpart'     => 'N/A',
4101       'pkgnum'      => 'N/A',
4102       'amount'      => sprintf("%.2f", $_->owed),
4103     };
4104   }
4105   @b;
4106
4107   #{
4108   #    'description'     => 'Previous Balance',
4109   #    #'pkgpart'         => 'N/A',
4110   #    'pkgnum'          => 'N/A',
4111   #    'amount'          => sprintf("%10.2f", $pr_total ),
4112   #    'ext_description' => [ map {
4113   #                                 "Invoice ". $_->invnum.
4114   #                                 " (". time2str("%x",$_->_date). ") ".
4115   #                                 sprintf("%10.2f", $_->owed)
4116   #                         } @pr_cust_bill ],
4117
4118   #};
4119 }
4120
4121 sub _items_pkg {
4122   my $self = shift;
4123   my %options = @_;
4124
4125   warn "$me _items_pkg searching for all package line items\n"
4126     if $DEBUG > 1;
4127
4128   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4129
4130   warn "$me _items_pkg filtering line items\n"
4131     if $DEBUG > 1;
4132   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4133
4134   if ($options{section} && $options{section}->{condensed}) {
4135
4136     warn "$me _items_pkg condensing section\n"
4137       if $DEBUG > 1;
4138
4139     my %itemshash = ();
4140     local $Storable::canonical = 1;
4141     foreach ( @items ) {
4142       my $item = { %$_ };
4143       delete $item->{ref};
4144       delete $item->{ext_description};
4145       my $key = freeze($item);
4146       $itemshash{$key} ||= 0;
4147       $itemshash{$key} ++; # += $item->{quantity};
4148     }
4149     @items = sort { $a->{description} cmp $b->{description} }
4150              map { my $i = thaw($_);
4151                    $i->{quantity} = $itemshash{$_};
4152                    $i->{amount} =
4153                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4154                    $i;
4155                  }
4156              keys %itemshash;
4157   }
4158
4159   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4160     if $DEBUG > 1;
4161
4162   @items;
4163 }
4164
4165 sub _taxsort {
4166   return 0 unless $a->itemdesc cmp $b->itemdesc;
4167   return -1 if $b->itemdesc eq 'Tax';
4168   return 1 if $a->itemdesc eq 'Tax';
4169   return -1 if $b->itemdesc eq 'Other surcharges';
4170   return 1 if $a->itemdesc eq 'Other surcharges';
4171   $a->itemdesc cmp $b->itemdesc;
4172 }
4173
4174 sub _items_tax {
4175   my $self = shift;
4176   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4177   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4178 }
4179
4180 sub _items_cust_bill_pkg {
4181   my $self = shift;
4182   my $cust_bill_pkg = shift;
4183   my %opt = @_;
4184
4185   my $format = $opt{format} || '';
4186   my $escape_function = $opt{escape_function} || sub { shift };
4187   my $format_function = $opt{format_function} || '';
4188   my $unsquelched = $opt{unsquelched} || '';
4189   my $section = $opt{section}->{description} if $opt{section};
4190   my $summary_page = $opt{summary_page} || '';
4191   my $multilocation = $opt{multilocation} || '';
4192   my $multisection = $opt{multisection} || '';
4193   my $discount_show_always = 0;
4194
4195   my @b = ();
4196   my ($s, $r, $u) = ( undef, undef, undef );
4197   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
4198   {
4199
4200     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4201                                 && $conf->exists('discount-show-always'));
4202
4203     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4204       if ( $_ && !$cust_bill_pkg->hidden ) {
4205         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4206         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4207         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4208         push @b, { %$_ }
4209           unless ( $_->{amount} == 0 && !$discount_show_always );
4210         $_ = undef;
4211       }
4212     }
4213
4214     foreach my $display ( grep { defined($section)
4215                                  ? $_->section eq $section
4216                                  : 1
4217                                }
4218                           #grep { !$_->summary || !$summary_page } # bunk!
4219                           grep { !$_->summary || $multisection }
4220                           $cust_bill_pkg->cust_bill_pkg_display
4221                         )
4222     {
4223
4224       my $type = $display->type;
4225
4226       my $desc = $cust_bill_pkg->desc;
4227       $desc = substr($desc, 0, 50). '...'
4228         if $format eq 'latex' && length($desc) > 50;
4229
4230       my %details_opt = ( 'format'          => $format,
4231                           'escape_function' => $escape_function,
4232                           'format_function' => $format_function,
4233                         );
4234
4235       if ( $cust_bill_pkg->pkgnum > 0 ) {
4236
4237         my $cust_pkg = $cust_bill_pkg->cust_pkg;
4238
4239         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4240
4241           my $description = $desc;
4242           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4243
4244           my @d = ();
4245           unless ( $cust_pkg->part_pkg->hide_svc_detail
4246                 || $cust_bill_pkg->hidden )
4247           {
4248
4249             push @d, map &{$escape_function}($_),
4250                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
4251               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4252
4253             if ( $multilocation ) {
4254               my $loc = $cust_pkg->location_label;
4255               $loc = substr($loc, 0, 50). '...'
4256                 if $format eq 'latex' && length($loc) > 50;
4257               push @d, &{$escape_function}($loc);
4258             }
4259
4260           }
4261
4262           push @d, $cust_bill_pkg->details(%details_opt)
4263             if $cust_bill_pkg->recur == 0;
4264
4265           if ( $cust_bill_pkg->hidden ) {
4266             $s->{amount}      += $cust_bill_pkg->setup;
4267             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4268             push @{ $s->{ext_description} }, @d;
4269           } else {
4270             $s = {
4271               description     => $description,
4272               #pkgpart         => $part_pkg->pkgpart,
4273               pkgnum          => $cust_bill_pkg->pkgnum,
4274               amount          => $cust_bill_pkg->setup,
4275               unit_amount     => $cust_bill_pkg->unitsetup,
4276               quantity        => $cust_bill_pkg->quantity,
4277               ext_description => \@d,
4278             };
4279           };
4280
4281         }
4282
4283         if ( ( $cust_bill_pkg->recur != 0  || $cust_bill_pkg->setup == 0 || 
4284                 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4285              ( !$type || $type eq 'R' || $type eq 'U' )
4286            )
4287         {
4288
4289           my $is_summary = $display->summary;
4290           my $description = ($is_summary && $type && $type eq 'U')
4291                             ? "Usage charges" : $desc;
4292
4293           unless ( $conf->exists('disable_line_item_date_ranges') ) {
4294             $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4295                             " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
4296           }
4297
4298           my @d = ();
4299
4300           #at least until cust_bill_pkg has "past" ranges in addition to
4301           #the "future" sdate/edate ones... see #3032
4302           my @dates = ( $self->_date );
4303           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4304           push @dates, $prev->sdate if $prev;
4305           push @dates, undef if !$prev;
4306
4307           unless ( $cust_pkg->part_pkg->hide_svc_detail
4308                 || $cust_bill_pkg->itemdesc
4309                 || $cust_bill_pkg->hidden
4310                 || $is_summary && $type && $type eq 'U' )
4311           {
4312
4313             push @d, map &{$escape_function}($_),
4314                          $cust_pkg->h_labels_short(@dates, 'I')
4315                                                    #$cust_bill_pkg->edate,
4316                                                    #$cust_bill_pkg->sdate)
4317               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4318
4319             if ( $multilocation ) {
4320               my $loc = $cust_pkg->location_label;
4321               $loc = substr($loc, 0, 50). '...'
4322                 if $format eq 'latex' && length($loc) > 50;
4323               push @d, &{$escape_function}($loc);
4324             }
4325
4326           }
4327
4328           push @d, $cust_bill_pkg->details(%details_opt)
4329             unless ($is_summary || $type && $type eq 'R');
4330   
4331           my $amount = 0;
4332           if (!$type) {
4333             $amount = $cust_bill_pkg->recur;
4334           }elsif($type eq 'R') {
4335             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4336           }elsif($type eq 'U') {
4337             $amount = $cust_bill_pkg->usage;
4338           }
4339   
4340           if ( !$type || $type eq 'R' ) {
4341
4342             if ( $cust_bill_pkg->hidden ) {
4343               $r->{amount}      += $amount;
4344               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4345               push @{ $r->{ext_description} }, @d;
4346             } else {
4347               $r = {
4348                 description     => $description,
4349                 #pkgpart         => $part_pkg->pkgpart,
4350                 pkgnum          => $cust_bill_pkg->pkgnum,
4351                 amount          => $amount,
4352                 unit_amount     => $cust_bill_pkg->unitrecur,
4353                 quantity        => $cust_bill_pkg->quantity,
4354                 ext_description => \@d,
4355               };
4356             }
4357
4358           } else {  # $type eq 'U'
4359
4360             if ( $cust_bill_pkg->hidden ) {
4361               $u->{amount}      += $amount;
4362               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4363               push @{ $u->{ext_description} }, @d;
4364             } else {
4365               $u = {
4366                 description     => $description,
4367                 #pkgpart         => $part_pkg->pkgpart,
4368                 pkgnum          => $cust_bill_pkg->pkgnum,
4369                 amount          => $amount,
4370                 unit_amount     => $cust_bill_pkg->unitrecur,
4371                 quantity        => $cust_bill_pkg->quantity,
4372                 ext_description => \@d,
4373               };
4374             }
4375
4376           }
4377
4378         } # recurring or usage with recurring charge
4379
4380       } else { #pkgnum tax or one-shot line item (??)
4381
4382         if ( $cust_bill_pkg->setup != 0 ) {
4383           push @b, {
4384             'description' => $desc,
4385             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
4386           };
4387         }
4388         if ( $cust_bill_pkg->recur != 0 ) {
4389           push @b, {
4390             'description' => "$desc (".
4391                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4392                              time2str($date_format, $cust_bill_pkg->edate). ')',
4393             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
4394           };
4395         }
4396
4397       }
4398
4399     }
4400
4401   }
4402
4403   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4404     if ( $_  ) {
4405       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4406       $_->{amount}      =~ s/^\-0\.00$/0.00/;
4407       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4408       push @b, { %$_ }
4409         unless ( $_->{amount} == 0 && !$discount_show_always );
4410     }
4411   }
4412
4413   @b;
4414
4415 }
4416
4417 sub _items_credits {
4418   my( $self, %opt ) = @_;
4419   my $trim_len = $opt{'trim_len'} || 60;
4420
4421   my @b;
4422   #credits
4423   foreach ( $self->cust_credited ) {
4424
4425     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4426
4427     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4428     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4429     $reason = " ($reason) " if $reason;
4430
4431     push @b, {
4432       #'description' => 'Credit ref\#'. $_->crednum.
4433       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
4434       #                 $reason,
4435       'description' => 'Credit applied '.
4436                        time2str($date_format,$_->cust_credit->_date). $reason,
4437       'amount'      => sprintf("%.2f",$_->amount),
4438     };
4439   }
4440
4441   @b;
4442
4443 }
4444
4445 sub _items_payments {
4446   my $self = shift;
4447
4448   my @b;
4449   #get & print payments
4450   foreach ( $self->cust_bill_pay ) {
4451
4452     #something more elaborate if $_->amount ne ->cust_pay->paid ?
4453
4454     push @b, {
4455       'description' => "Payment received ".
4456                        time2str($date_format,$_->cust_pay->_date ),
4457       'amount'      => sprintf("%.2f", $_->amount )
4458     };
4459   }
4460
4461   @b;
4462
4463 }
4464
4465 =item call_details [ OPTION => VALUE ... ]
4466
4467 Returns an array of CSV strings representing the call details for this invoice
4468 The only option available is the boolean prepend_billed_number
4469
4470 =cut
4471
4472 sub call_details {
4473   my ($self, %opt) = @_;
4474
4475   my $format_function = sub { shift };
4476
4477   if ($opt{prepend_billed_number}) {
4478     $format_function = sub {
4479       my $detail = shift;
4480       my $row = shift;
4481
4482       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4483       
4484     };
4485   }
4486
4487   my @details = map { $_->details( 'format_function' => $format_function,
4488                                    'escape_function' => sub{ return() },
4489                                  )
4490                     }
4491                   grep { $_->pkgnum }
4492                   $self->cust_bill_pkg;
4493   my $header = $details[0];
4494   ( $header, grep { $_ ne $header } @details );
4495 }
4496
4497
4498 =back
4499
4500 =head1 SUBROUTINES
4501
4502 =over 4
4503
4504 =item process_reprint
4505
4506 =cut
4507
4508 sub process_reprint {
4509   process_re_X('print', @_);
4510 }
4511
4512 =item process_reemail
4513
4514 =cut
4515
4516 sub process_reemail {
4517   process_re_X('email', @_);
4518 }
4519
4520 =item process_refax
4521
4522 =cut
4523
4524 sub process_refax {
4525   process_re_X('fax', @_);
4526 }
4527
4528 =item process_reftp
4529
4530 =cut
4531
4532 sub process_reftp {
4533   process_re_X('ftp', @_);
4534 }
4535
4536 =item respool
4537
4538 =cut
4539
4540 sub process_respool {
4541   process_re_X('spool', @_);
4542 }
4543
4544 use Storable qw(thaw);
4545 use Data::Dumper;
4546 use MIME::Base64;
4547 sub process_re_X {
4548   my( $method, $job ) = ( shift, shift );
4549   warn "$me process_re_X $method for job $job\n" if $DEBUG;
4550
4551   my $param = thaw(decode_base64(shift));
4552   warn Dumper($param) if $DEBUG;
4553
4554   re_X(
4555     $method,
4556     $job,
4557     %$param,
4558   );
4559
4560 }
4561
4562 sub re_X {
4563   my($method, $job, %param ) = @_;
4564   if ( $DEBUG ) {
4565     warn "re_X $method for job $job with param:\n".
4566          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
4567   }
4568
4569   #some false laziness w/search/cust_bill.html
4570   my $distinct = '';
4571   my $orderby = 'ORDER BY cust_bill._date';
4572
4573   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4574
4575   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4576      
4577   my @cust_bill = qsearch( {
4578     #'select'    => "cust_bill.*",
4579     'table'     => 'cust_bill',
4580     'addl_from' => $addl_from,
4581     'hashref'   => {},
4582     'extra_sql' => $extra_sql,
4583     'order_by'  => $orderby,
4584     'debug' => 1,
4585   } );
4586
4587   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4588
4589   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4590     if $DEBUG;
4591
4592   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4593   foreach my $cust_bill ( @cust_bill ) {
4594     $cust_bill->$method();
4595
4596     if ( $job ) { #progressbar foo
4597       $num++;
4598       if ( time - $min_sec > $last ) {
4599         my $error = $job->update_statustext(
4600           int( 100 * $num / scalar(@cust_bill) )
4601         );
4602         die $error if $error;
4603         $last = time;
4604       }
4605     }
4606
4607   }
4608
4609 }
4610
4611 =back
4612
4613 =head1 CLASS METHODS
4614
4615 =over 4
4616
4617 =item owed_sql
4618
4619 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4620
4621 =cut
4622
4623 sub owed_sql {
4624   my ($class, $start, $end) = @_;
4625   'charged - '. 
4626     $class->paid_sql($start, $end). ' - '. 
4627     $class->credited_sql($start, $end);
4628 }
4629
4630 =item net_sql
4631
4632 Returns an SQL fragment to retreive the net amount (charged minus credited).
4633
4634 =cut
4635
4636 sub net_sql {
4637   my ($class, $start, $end) = @_;
4638   'charged - '. $class->credited_sql($start, $end);
4639 }
4640
4641 =item paid_sql
4642
4643 Returns an SQL fragment to retreive the amount paid against this invoice.
4644
4645 =cut
4646
4647 sub paid_sql {
4648   my ($class, $start, $end) = @_;
4649   $start &&= "AND cust_bill_pay._date <= $start";
4650   $end   &&= "AND cust_bill_pay._date > $end";
4651   $start = '' unless defined($start);
4652   $end   = '' unless defined($end);
4653   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4654        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
4655 }
4656
4657 =item credited_sql
4658
4659 Returns an SQL fragment to retreive the amount credited against this invoice.
4660
4661 =cut
4662
4663 sub credited_sql {
4664   my ($class, $start, $end) = @_;
4665   $start &&= "AND cust_credit_bill._date <= $start";
4666   $end   &&= "AND cust_credit_bill._date >  $end";
4667   $start = '' unless defined($start);
4668   $end   = '' unless defined($end);
4669   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4670        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
4671 }
4672
4673 =item due_date_sql
4674
4675 Returns an SQL fragment to retrieve the due date of an invoice.
4676 Currently only supported on PostgreSQL.
4677
4678 =cut
4679
4680 sub due_date_sql {
4681 'COALESCE(
4682   SUBSTRING(
4683     COALESCE(
4684       cust_bill.invoice_terms,
4685       cust_main.invoice_terms,
4686       \''.($conf->config('invoice_default_terms') || '').'\'
4687     ), E\'Net (\\\\d+)\'
4688   )::INTEGER, 0
4689 ) * 86400 + cust_bill._date'
4690 }
4691
4692 =item search_sql_where HASHREF
4693
4694 Class method which returns an SQL WHERE fragment to search for parameters
4695 specified in HASHREF.  Valid parameters are
4696
4697 =over 4
4698
4699 =item _date
4700
4701 List reference of start date, end date, as UNIX timestamps.
4702
4703 =item invnum_min
4704
4705 =item invnum_max
4706
4707 =item agentnum
4708
4709 =item charged
4710
4711 List reference of charged limits (exclusive).
4712
4713 =item owed
4714
4715 List reference of charged limits (exclusive).
4716
4717 =item open
4718
4719 flag, return open invoices only
4720
4721 =item net
4722
4723 flag, return net invoices only
4724
4725 =item days
4726
4727 =item newest_percust
4728
4729 =back
4730
4731 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4732
4733 =cut
4734
4735 sub search_sql_where {
4736   my($class, $param) = @_;
4737   if ( $DEBUG ) {
4738     warn "$me search_sql_where called with params: \n".
4739          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
4740   }
4741
4742   my @search = ();
4743
4744   #agentnum
4745   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4746     push @search, "cust_main.agentnum = $1";
4747   }
4748
4749   #_date
4750   if ( $param->{_date} ) {
4751     my($beginning, $ending) = @{$param->{_date}};
4752
4753     push @search, "cust_bill._date >= $beginning",
4754                   "cust_bill._date <  $ending";
4755   }
4756
4757   #invnum
4758   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4759     push @search, "cust_bill.invnum >= $1";
4760   }
4761   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4762     push @search, "cust_bill.invnum <= $1";
4763   }
4764
4765   #charged
4766   if ( $param->{charged} ) {
4767     my @charged = ref($param->{charged})
4768                     ? @{ $param->{charged} }
4769                     : ($param->{charged});
4770
4771     push @search, map { s/^charged/cust_bill.charged/; $_; }
4772                       @charged;
4773   }
4774
4775   my $owed_sql = FS::cust_bill->owed_sql;
4776
4777   #owed
4778   if ( $param->{owed} ) {
4779     my @owed = ref($param->{owed})
4780                  ? @{ $param->{owed} }
4781                  : ($param->{owed});
4782     push @search, map { s/^owed/$owed_sql/; $_; }
4783                       @owed;
4784   }
4785
4786   #open/net flags
4787   push @search, "0 != $owed_sql"
4788     if $param->{'open'};
4789   push @search, '0 != '. FS::cust_bill->net_sql
4790     if $param->{'net'};
4791
4792   #days
4793   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4794     if $param->{'days'};
4795
4796   #newest_percust
4797   if ( $param->{'newest_percust'} ) {
4798
4799     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4800     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4801
4802     my @newest_where = map { my $x = $_;
4803                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
4804                              $x;
4805                            }
4806                            grep ! /^cust_main./, @search;
4807     my $newest_where = scalar(@newest_where)
4808                          ? ' AND '. join(' AND ', @newest_where)
4809                          : '';
4810
4811
4812     push @search, "cust_bill._date = (
4813       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4814         WHERE newest_cust_bill.custnum = cust_bill.custnum
4815           $newest_where
4816     )";
4817
4818   }
4819
4820   #agent virtualization
4821   my $curuser = $FS::CurrentUser::CurrentUser;
4822   if ( $curuser->username eq 'fs_queue'
4823        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4824     my $username = $1;
4825     my $newuser = qsearchs('access_user', {
4826       'username' => $username,
4827       'disabled' => '',
4828     } );
4829     if ( $newuser ) {
4830       $curuser = $newuser;
4831     } else {
4832       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4833     }
4834   }
4835   push @search, $curuser->agentnums_sql;
4836
4837   join(' AND ', @search );
4838
4839 }
4840
4841 =back
4842
4843 =head1 BUGS
4844
4845 The delete method.
4846
4847 =head1 SEE ALSO
4848
4849 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4850 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
4851 documentation.
4852
4853 =cut
4854
4855 1;
4856