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