ef0a2984e04f6524d0e5726676dd13be3f533715
[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   warn "$me generating template variables\n"
2406     if $DEBUG > 1;
2407
2408   # generate template variables
2409   my $returnaddress;
2410   if (
2411          defined( $conf->config_orbase( "invoice_${format}returnaddress",
2412                                         $template
2413                                       )
2414                 )
2415        && length( $conf->config_orbase( "invoice_${format}returnaddress",
2416                                         $template
2417                                       )
2418                 )
2419   ) {
2420
2421     $returnaddress = join("\n",
2422       $conf->config_orbase("invoice_${format}returnaddress", $template)
2423     );
2424
2425   } elsif ( grep /\S/,
2426             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2427
2428     my $convert_map = $convert_maps{$format}{'returnaddress'};
2429     $returnaddress =
2430       join( "\n",
2431             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2432                                                  $template
2433                                                )
2434                          )
2435           );
2436   } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2437
2438     my $convert_map = $convert_maps{$format}{'returnaddress'};
2439     $returnaddress = join( "\n", &$convert_map(
2440                                    map { s/( {2,})/'~' x length($1)/eg;
2441                                          s/$/\\\\\*/;
2442                                          $_
2443                                        }
2444                                      ( $conf->config('company_name', $self->cust_main->agentnum),
2445                                        $conf->config('company_address', $self->cust_main->agentnum),
2446                                      )
2447                                  )
2448                      );
2449
2450   } else {
2451
2452     my $warning = "Couldn't find a return address; ".
2453                   "do you need to set the company_address configuration value?";
2454     warn "$warning\n";
2455     $returnaddress = $nbsp;
2456     #$returnaddress = $warning;
2457
2458   }
2459
2460   warn "$me generating invoice data\n"
2461     if $DEBUG > 1;
2462
2463   my $agentnum = $self->cust_main->agentnum;
2464
2465   my %invoice_data = (
2466
2467     #invoice from info
2468     'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
2469     'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2470     'returnaddress'   => $returnaddress,
2471     'agent'           => &$escape_function($cust_main->agent->agent),
2472
2473     #invoice info
2474     'invnum'          => $self->invnum,
2475     'date'            => time2str($date_format, $self->_date),
2476     'today'           => time2str($date_format_long, $today),
2477     'terms'           => $self->terms,
2478     'template'        => $template, #params{'template'},
2479     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
2480     'current_charges' => sprintf("%.2f", $self->charged),
2481     'duedate'         => $self->due_date2str($rdate_format), #date_format?
2482
2483     #customer info
2484     'custnum'         => $cust_main->display_custnum,
2485     'agent_custid'    => &$escape_function($cust_main->agent_custid),
2486     ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2487       payname company address1 address2 city state zip fax
2488     )),
2489
2490     #global config
2491     'ship_enable'     => $conf->exists('invoice-ship_address'),
2492     'unitprices'      => $conf->exists('invoice-unitprice'),
2493     'smallernotes'    => $conf->exists('invoice-smallernotes'),
2494     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
2495     'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2496    
2497     #layout info -- would be fancy to calc some of this and bury the template
2498     #               here in the code
2499     'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2500     'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2501     'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
2502     'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2503     'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2504     'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2505     'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2506     'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2507     'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2508     'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2509
2510     # better hang on to conf_dir for a while (for old templates)
2511     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2512
2513     #these are only used when doing paged plaintext
2514     'page'            => 1,
2515     'total_pages'     => 1,
2516
2517   );
2518   
2519   my $min_sdate = 999999999999;
2520   my $max_edate = 0;
2521   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2522     next unless $cust_bill_pkg->pkgnum > 0;
2523     $min_sdate = $cust_bill_pkg->sdate
2524       if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2525     $max_edate = $cust_bill_pkg->edate
2526       if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2527   }
2528
2529   $invoice_data{'bill_period'} = '';
2530   $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate) 
2531     . " to " . time2str('%e %h', $max_edate)
2532     if ($max_edate != 0 && $min_sdate != 999999999999);
2533
2534   $invoice_data{finance_section} = '';
2535   if ( $conf->config('finance_pkgclass') ) {
2536     my $pkg_class =
2537       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2538     $invoice_data{finance_section} = $pkg_class->categoryname;
2539   } 
2540   $invoice_data{finance_amount} = '0.00';
2541   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2542
2543   my $countrydefault = $conf->config('countrydefault') || 'US';
2544   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2545   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2546     my $method = $prefix.$_;
2547     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2548   }
2549   $invoice_data{'ship_country'} = ''
2550     if ( $invoice_data{'ship_country'} eq $countrydefault );
2551   
2552   $invoice_data{'cid'} = $params{'cid'}
2553     if $params{'cid'};
2554
2555   if ( $cust_main->country eq $countrydefault ) {
2556     $invoice_data{'country'} = '';
2557   } else {
2558     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2559   }
2560
2561   my @address = ();
2562   $invoice_data{'address'} = \@address;
2563   push @address,
2564     $cust_main->payname.
2565       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2566         ? " (P.O. #". $cust_main->payinfo. ")"
2567         : ''
2568       )
2569   ;
2570   push @address, $cust_main->company
2571     if $cust_main->company;
2572   push @address, $cust_main->address1;
2573   push @address, $cust_main->address2
2574     if $cust_main->address2;
2575   push @address,
2576     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2577   push @address, $invoice_data{'country'}
2578     if $invoice_data{'country'};
2579   push @address, ''
2580     while (scalar(@address) < 5);
2581
2582   $invoice_data{'logo_file'} = $params{'logo_file'}
2583     if $params{'logo_file'};
2584   $invoice_data{'barcode_file'} = $params{'barcode_file'}
2585     if $params{'barcode_file'};
2586   $invoice_data{'barcode_img'} = $params{'barcode_img'}
2587     if $params{'barcode_img'};
2588   $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2589     if $params{'barcode_cid'};
2590
2591   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2592 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2593   #my $balance_due = $self->owed + $pr_total - $cr_total;
2594   my $balance_due = $self->owed + $pr_total;
2595   $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2596   $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2597   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2598   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2599
2600   my $summarypage = '';
2601   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2602     $summarypage = 1;
2603   }
2604   $invoice_data{'summarypage'} = $summarypage;
2605
2606   warn "$me substituting variables in notes, footer, smallfooter\n"
2607     if $DEBUG > 1;
2608
2609   foreach my $include (qw( notes footer smallfooter coupon )) {
2610
2611     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2612     my @inc_src;
2613
2614     if ( $conf->exists($inc_file, $agentnum)
2615          && length( $conf->config($inc_file, $agentnum) ) ) {
2616
2617       @inc_src = $conf->config($inc_file, $agentnum);
2618
2619     } else {
2620
2621       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2622
2623       my $convert_map = $convert_maps{$format}{$include};
2624
2625       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2626                        s/--\@\]/$delimiters{$format}[1]/g;
2627                        $_;
2628                      } 
2629                  &$convert_map( $conf->config($inc_file, $agentnum) );
2630
2631     }
2632
2633     my $inc_tt = new Text::Template (
2634       TYPE       => 'ARRAY',
2635       SOURCE     => [ map "$_\n", @inc_src ],
2636       DELIMITERS => $delimiters{$format},
2637     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2638
2639     unless ( $inc_tt->compile() ) {
2640       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2641       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2642       die $error;
2643     }
2644
2645     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2646
2647     $invoice_data{$include} =~ s/\n+$//
2648       if ($format eq 'latex');
2649   }
2650
2651   $invoice_data{'po_line'} =
2652     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2653       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2654       : $nbsp;
2655
2656   my %money_chars = ( 'latex'    => '',
2657                       'html'     => $conf->config('money_char') || '$',
2658                       'template' => '',
2659                     );
2660   my $money_char = $money_chars{$format};
2661
2662   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2663                             'html'     => $conf->config('money_char') || '$',
2664                             'template' => '',
2665                           );
2666   my $other_money_char = $other_money_chars{$format};
2667   $invoice_data{'dollar'} = $other_money_char;
2668
2669   my @detail_items = ();
2670   my @total_items = ();
2671   my @buf = ();
2672   my @sections = ();
2673
2674   $invoice_data{'detail_items'} = \@detail_items;
2675   $invoice_data{'total_items'} = \@total_items;
2676   $invoice_data{'buf'} = \@buf;
2677   $invoice_data{'sections'} = \@sections;
2678
2679   warn "$me generating sections\n"
2680     if $DEBUG > 1;
2681
2682   my $previous_section = { 'description' => 'Previous Charges',
2683                            'subtotal'    => $other_money_char.
2684                                             sprintf('%.2f', $pr_total),
2685                            'summarized'  => $summarypage ? 'Y' : '',
2686                          };
2687   $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '. 
2688     join(' / ', map { $cust_main->balance_date_range(@$_) }
2689                 $self->_prior_month30s
2690         )
2691     if $conf->exists('invoice_include_aging');
2692
2693   my $taxtotal = 0;
2694   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2695                       'subtotal'    => $taxtotal,   # adjusted below
2696                       'summarized'  => $summarypage ? 'Y' : '',
2697                     };
2698   my $tax_weight = _pkg_category($tax_section->{description})
2699                         ? _pkg_category($tax_section->{description})->weight
2700                         : 0;
2701   $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2702   $tax_section->{'sort_weight'} = $tax_weight;
2703
2704
2705   my $adjusttotal = 0;
2706   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2707                          'subtotal'    => 0,   # adjusted below
2708                          'summarized'  => $summarypage ? 'Y' : '',
2709                        };
2710   my $adjust_weight = _pkg_category($adjust_section->{description})
2711                         ? _pkg_category($adjust_section->{description})->weight
2712                         : 0;
2713   $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2714   $adjust_section->{'sort_weight'} = $adjust_weight;
2715
2716   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2717   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2718   $invoice_data{'multisection'} = $multisection;
2719   my $late_sections = [];
2720   my $extra_sections = [];
2721   my $extra_lines = ();
2722   if ( $multisection ) {
2723     ($extra_sections, $extra_lines) =
2724       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2725       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2726
2727     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2728
2729     push @detail_items, @$extra_lines if $extra_lines;
2730     push @sections,
2731       $self->_items_sections( $late_sections,      # this could stand a refactor
2732                               $summarypage,
2733                               $escape_function_nonbsp,
2734                               $extra_sections,
2735                               $format,             #bah
2736                             );
2737     if ($conf->exists('svc_phone_sections')) {
2738       my ($phone_sections, $phone_lines) =
2739         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2740       push @{$late_sections}, @$phone_sections;
2741       push @detail_items, @$phone_lines;
2742     }
2743   }else{
2744     push @sections, { 'description' => '', 'subtotal' => '' };
2745   }
2746
2747   unless (    $conf->exists('disable_previous_balance')
2748            || $conf->exists('previous_balance-summary_only')
2749          )
2750   {
2751
2752     warn "$me adding previous balances\n"
2753       if $DEBUG > 1;
2754
2755     foreach my $line_item ( $self->_items_previous ) {
2756
2757       my $detail = {
2758         ext_description => [],
2759       };
2760       $detail->{'ref'} = $line_item->{'pkgnum'};
2761       $detail->{'quantity'} = 1;
2762       $detail->{'section'} = $previous_section;
2763       $detail->{'description'} = &$escape_function($line_item->{'description'});
2764       if ( exists $line_item->{'ext_description'} ) {
2765         @{$detail->{'ext_description'}} = map {
2766           &$escape_function($_);
2767         } @{$line_item->{'ext_description'}};
2768       }
2769       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2770                             $line_item->{'amount'};
2771       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2772
2773       push @detail_items, $detail;
2774       push @buf, [ $detail->{'description'},
2775                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2776                  ];
2777     }
2778
2779   }
2780
2781   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2782     push @buf, ['','-----------'];
2783     push @buf, [ 'Total Previous Balance',
2784                  $money_char. sprintf("%10.2f", $pr_total) ];
2785     push @buf, ['',''];
2786   }
2787  
2788   if ( $conf->exists('svc_phone-did-summary') ) {
2789       warn "$me adding DID summary\n"
2790         if $DEBUG > 1;
2791
2792       my ($didsummary,$minutes) = $self->_did_summary;
2793       my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2794       push @detail_items, 
2795         { 'description' => $didsummary_desc,
2796             'ext_description' => [ $didsummary, $minutes ],
2797         }
2798         if !$multisection;
2799   }
2800
2801   foreach my $section (@sections, @$late_sections) {
2802
2803     warn "$me adding section \n". Dumper($section)
2804       if $DEBUG > 1;
2805
2806     # begin some normalization
2807     $section->{'subtotal'} = $section->{'amount'}
2808       if $multisection
2809          && !exists($section->{subtotal})
2810          && exists($section->{amount});
2811
2812     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2813       if ( $invoice_data{finance_section} &&
2814            $section->{'description'} eq $invoice_data{finance_section} );
2815
2816     $section->{'subtotal'} = $other_money_char.
2817                              sprintf('%.2f', $section->{'subtotal'})
2818       if $multisection;
2819
2820     # continue some normalization
2821     $section->{'amount'}   = $section->{'subtotal'}
2822       if $multisection;
2823
2824
2825     if ( $section->{'description'} ) {
2826       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2827                    [ '', '' ],
2828                  );
2829     }
2830
2831     warn "$me   setting options\n"
2832       if $DEBUG > 1;
2833
2834     my $multilocation = scalar($cust_main->cust_location); #too expensive?
2835     my %options = ();
2836     $options{'section'} = $section if $multisection;
2837     $options{'format'} = $format;
2838     $options{'escape_function'} = $escape_function;
2839     $options{'format_function'} = sub { () } unless $unsquelched;
2840     $options{'unsquelched'} = $unsquelched;
2841     $options{'summary_page'} = $summarypage;
2842     $options{'skip_usage'} =
2843       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2844     $options{'multilocation'} = $multilocation;
2845     $options{'multisection'} = $multisection;
2846
2847     warn "$me   searching for line items\n"
2848       if $DEBUG > 1;
2849
2850     foreach my $line_item ( $self->_items_pkg(%options) ) {
2851
2852       warn "$me     adding line item $line_item\n"
2853         if $DEBUG > 1;
2854
2855       my $detail = {
2856         ext_description => [],
2857       };
2858       $detail->{'ref'} = $line_item->{'pkgnum'};
2859       $detail->{'quantity'} = $line_item->{'quantity'};
2860       $detail->{'section'} = $section;
2861       $detail->{'description'} = &$escape_function($line_item->{'description'});
2862       if ( exists $line_item->{'ext_description'} ) {
2863         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2864       }
2865       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2866                               $line_item->{'amount'};
2867       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2868                                  $line_item->{'unit_amount'};
2869       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2870   
2871       push @detail_items, $detail;
2872       push @buf, ( [ $detail->{'description'},
2873                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2874                    ],
2875                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2876                  );
2877     }
2878
2879     if ( $section->{'description'} ) {
2880       push @buf, ( ['','-----------'],
2881                    [ $section->{'description'}. ' sub-total',
2882                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2883                    ],
2884                    [ '', '' ],
2885                    [ '', '' ],
2886                  );
2887     }
2888   
2889   }
2890   
2891   $invoice_data{current_less_finance} =
2892     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2893
2894   if ( $multisection && !$conf->exists('disable_previous_balance')
2895     || $conf->exists('previous_balance-summary_only') )
2896   {
2897     unshift @sections, $previous_section if $pr_total;
2898   }
2899
2900   warn "$me adding taxes\n"
2901     if $DEBUG > 1;
2902
2903   foreach my $tax ( $self->_items_tax ) {
2904
2905     $taxtotal += $tax->{'amount'};
2906
2907     my $description = &$escape_function( $tax->{'description'} );
2908     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2909
2910     if ( $multisection ) {
2911
2912       my $money = $old_latex ? '' : $money_char;
2913       push @detail_items, {
2914         ext_description => [],
2915         ref          => '',
2916         quantity     => '',
2917         description  => $description,
2918         amount       => $money. $amount,
2919         product_code => '',
2920         section      => $tax_section,
2921       };
2922
2923     } else {
2924
2925       push @total_items, {
2926         'total_item'   => $description,
2927         'total_amount' => $other_money_char. $amount,
2928       };
2929
2930     }
2931
2932     push @buf,[ $description,
2933                 $money_char. $amount,
2934               ];
2935
2936   }
2937   
2938   if ( $taxtotal ) {
2939     my $total = {};
2940     $total->{'total_item'} = 'Sub-total';
2941     $total->{'total_amount'} =
2942       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2943
2944     if ( $multisection ) {
2945       $tax_section->{'subtotal'} = $other_money_char.
2946                                    sprintf('%.2f', $taxtotal);
2947       $tax_section->{'pretotal'} = 'New charges sub-total '.
2948                                    $total->{'total_amount'};
2949       push @sections, $tax_section if $taxtotal;
2950     }else{
2951       unshift @total_items, $total;
2952     }
2953   }
2954   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2955
2956   push @buf,['','-----------'];
2957   push @buf,[( $conf->exists('disable_previous_balance') 
2958                ? 'Total Charges'
2959                : 'Total New Charges'
2960              ),
2961              $money_char. sprintf("%10.2f",$self->charged) ];
2962   push @buf,['',''];
2963
2964   {
2965     my $total = {};
2966     my $item = 'Total';
2967     $item = $conf->config('previous_balance-exclude_from_total')
2968          || 'Total New Charges'
2969       if $conf->exists('previous_balance-exclude_from_total');
2970     my $amount = $self->charged +
2971                    ( $conf->exists('disable_previous_balance') ||
2972                      $conf->exists('previous_balance-exclude_from_total')
2973                      ? 0
2974                      : $pr_total
2975                    );
2976     $total->{'total_item'} = &$embolden_function($item);
2977     $total->{'total_amount'} =
2978       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
2979     if ( $multisection ) {
2980       if ( $adjust_section->{'sort_weight'} ) {
2981         $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2982           sprintf("%.2f", ($self->billing_balance || 0) );
2983       } else {
2984         $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2985                                         sprintf('%.2f', $self->charged );
2986       } 
2987     }else{
2988       push @total_items, $total;
2989     }
2990     push @buf,['','-----------'];
2991     push @buf,[$item,
2992                $money_char.
2993                sprintf( '%10.2f', $amount )
2994               ];
2995     push @buf,['',''];
2996   }
2997   
2998   unless ( $conf->exists('disable_previous_balance') ) {
2999     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3000   
3001     # credits
3002     my $credittotal = 0;
3003     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3004
3005       my $total;
3006       $total->{'total_item'} = &$escape_function($credit->{'description'});
3007       $credittotal += $credit->{'amount'};
3008       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3009       $adjusttotal += $credit->{'amount'};
3010       if ( $multisection ) {
3011         my $money = $old_latex ? '' : $money_char;
3012         push @detail_items, {
3013           ext_description => [],
3014           ref          => '',
3015           quantity     => '',
3016           description  => &$escape_function($credit->{'description'}),
3017           amount       => $money. $credit->{'amount'},
3018           product_code => '',
3019           section      => $adjust_section,
3020         };
3021       } else {
3022         push @total_items, $total;
3023       }
3024
3025     }
3026     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3027
3028     #credits (again)
3029     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3030       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3031     }
3032
3033     # payments
3034     my $paymenttotal = 0;
3035     foreach my $payment ( $self->_items_payments ) {
3036       my $total = {};
3037       $total->{'total_item'} = &$escape_function($payment->{'description'});
3038       $paymenttotal += $payment->{'amount'};
3039       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3040       $adjusttotal += $payment->{'amount'};
3041       if ( $multisection ) {
3042         my $money = $old_latex ? '' : $money_char;
3043         push @detail_items, {
3044           ext_description => [],
3045           ref          => '',
3046           quantity     => '',
3047           description  => &$escape_function($payment->{'description'}),
3048           amount       => $money. $payment->{'amount'},
3049           product_code => '',
3050           section      => $adjust_section,
3051         };
3052       }else{
3053         push @total_items, $total;
3054       }
3055       push @buf, [ $payment->{'description'},
3056                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
3057                  ];
3058     }
3059     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3060   
3061     if ( $multisection ) {
3062       $adjust_section->{'subtotal'} = $other_money_char.
3063                                       sprintf('%.2f', $adjusttotal);
3064       push @sections, $adjust_section
3065         unless $adjust_section->{sort_weight};
3066     }
3067
3068     { 
3069       my $total;
3070       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3071       $total->{'total_amount'} =
3072         &$embolden_function(
3073           $other_money_char. sprintf('%.2f', $summarypage 
3074                                                ? $self->charged +
3075                                                  $self->billing_balance
3076                                                : $self->owed + $pr_total
3077                                     )
3078         );
3079       if ( $multisection && !$adjust_section->{sort_weight} ) {
3080         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3081                                          $total->{'total_amount'};
3082       }else{
3083         push @total_items, $total;
3084       }
3085       push @buf,['','-----------'];
3086       push @buf,[$self->balance_due_msg, $money_char. 
3087         sprintf("%10.2f", $balance_due ) ];
3088     }
3089   }
3090
3091   if ( $multisection ) {
3092     if ($conf->exists('svc_phone_sections')) {
3093       my $total;
3094       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3095       $total->{'total_amount'} =
3096         &$embolden_function(
3097           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3098         );
3099       my $last_section = pop @sections;
3100       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3101                                      $total->{'total_amount'};
3102       push @sections, $last_section;
3103     }
3104     push @sections, @$late_sections
3105       if $unsquelched;
3106   }
3107
3108   my @includelist = ();
3109   push @includelist, 'summary' if $summarypage;
3110   foreach my $include ( @includelist ) {
3111
3112     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3113     my @inc_src;
3114
3115     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3116
3117       @inc_src = $conf->config($inc_file, $agentnum);
3118
3119     } else {
3120
3121       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3122
3123       my $convert_map = $convert_maps{$format}{$include};
3124
3125       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3126                        s/--\@\]/$delimiters{$format}[1]/g;
3127                        $_;
3128                      } 
3129                  &$convert_map( $conf->config($inc_file, $agentnum) );
3130
3131     }
3132
3133     my $inc_tt = new Text::Template (
3134       TYPE       => 'ARRAY',
3135       SOURCE     => [ map "$_\n", @inc_src ],
3136       DELIMITERS => $delimiters{$format},
3137     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3138
3139     unless ( $inc_tt->compile() ) {
3140       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3141       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3142       die $error;
3143     }
3144
3145     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3146
3147     $invoice_data{$include} =~ s/\n+$//
3148       if ($format eq 'latex');
3149   }
3150
3151   $invoice_lines = 0;
3152   my $wasfunc = 0;
3153   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3154     /invoice_lines\((\d*)\)/;
3155     $invoice_lines += $1 || scalar(@buf);
3156     $wasfunc=1;
3157   }
3158   die "no invoice_lines() functions in template?"
3159     if ( $format eq 'template' && !$wasfunc );
3160
3161   if ($format eq 'template') {
3162
3163     if ( $invoice_lines ) {
3164       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3165       $invoice_data{'total_pages'}++
3166         if scalar(@buf) % $invoice_lines;
3167     }
3168
3169     #setup subroutine for the template
3170     sub FS::cust_bill::_template::invoice_lines {
3171       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3172       map { 
3173         scalar(@FS::cust_bill::_template::buf)
3174           ? shift @FS::cust_bill::_template::buf
3175           : [ '', '' ];
3176       }
3177       ( 1 .. $lines );
3178     }
3179
3180     my $lines;
3181     my @collect;
3182     while (@buf) {
3183       push @collect, split("\n",
3184         $text_template->fill_in( HASH => \%invoice_data,
3185                                  PACKAGE => 'FS::cust_bill::_template'
3186                                )
3187       );
3188       $FS::cust_bill::_template::page++;
3189     }
3190     map "$_\n", @collect;
3191   }else{
3192     warn "filling in template for invoice ". $self->invnum. "\n"
3193       if $DEBUG;
3194     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3195       if $DEBUG > 1;
3196
3197     $text_template->fill_in(HASH => \%invoice_data);
3198   }
3199 }
3200
3201 # helper routine for generating date ranges
3202 sub _prior_month30s {
3203   my $self = shift;
3204   my @ranges = (
3205    [ 1,       2592000 ], # 0-30 days ago
3206    [ 2592000, 5184000 ], # 30-60 days ago
3207    [ 5184000, 7776000 ], # 60-90 days ago
3208    [ 7776000, 0       ], # 90+   days ago
3209   );
3210
3211   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3212           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3213       ] }
3214   @ranges;
3215 }
3216
3217 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3218
3219 Returns an postscript invoice, as a scalar.
3220
3221 Options can be passed as a hashref (recommended) or as a list of time, template
3222 and then any key/value pairs for any other options.
3223
3224 I<time> an optional value used to control the printing of overdue messages.  The
3225 default is now.  It isn't the date of the invoice; that's the `_date' field.
3226 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3227 L<Time::Local> and L<Date::Parse> for conversion functions.
3228
3229 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3230
3231 =cut
3232
3233 sub print_ps {
3234   my $self = shift;
3235
3236   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3237   my $ps = generate_ps($file);
3238   unlink($logofile);
3239   unlink($barcodefile);
3240
3241   $ps;
3242 }
3243
3244 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3245
3246 Returns an PDF invoice, as a scalar.
3247
3248 Options can be passed as a hashref (recommended) or as a list of time, template
3249 and then any key/value pairs for any other options.
3250
3251 I<time> an optional value used to control the printing of overdue messages.  The
3252 default is now.  It isn't the date of the invoice; that's the `_date' field.
3253 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3254 L<Time::Local> and L<Date::Parse> for conversion functions.
3255
3256 I<template>, if specified, is the name of a suffix for alternate invoices.
3257
3258 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3259
3260 =cut
3261
3262 sub print_pdf {
3263   my $self = shift;
3264
3265   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3266   my $pdf = generate_pdf($file);
3267   unlink($logofile);
3268   unlink($barcodefile);
3269
3270   $pdf;
3271 }
3272
3273 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3274
3275 Returns an HTML invoice, as a scalar.
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 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3287 when emailing the invoice as part of a multipart/related MIME email.
3288
3289 =cut
3290
3291 sub print_html {
3292   my $self = shift;
3293   my %params;
3294   if ( ref($_[0]) ) {
3295     %params = %{ shift() }; 
3296   }else{
3297     $params{'time'} = shift;
3298     $params{'template'} = shift;
3299     $params{'cid'} = shift;
3300   }
3301
3302   $params{'format'} = 'html';
3303   
3304   $self->print_generic( %params );
3305 }
3306
3307 # quick subroutine for print_latex
3308 #
3309 # There are ten characters that LaTeX treats as special characters, which
3310 # means that they do not simply typeset themselves: 
3311 #      # $ % & ~ _ ^ \ { }
3312 #
3313 # TeX ignores blanks following an escaped character; if you want a blank (as
3314 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3315
3316 sub _latex_escape {
3317   my $value = shift;
3318   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3319   $value =~ s/([<>])/\$$1\$/g;
3320   $value;
3321 }
3322
3323 sub _html_escape {
3324   my $value = shift;
3325   encode_entities($value);
3326   $value;
3327 }
3328
3329 sub _html_escape_nbsp {
3330   my $value = _html_escape(shift);
3331   $value =~ s/ +/&nbsp;/g;
3332   $value;
3333 }
3334
3335 #utility methods for print_*
3336
3337 sub _translate_old_latex_format {
3338   warn "_translate_old_latex_format called\n"
3339     if $DEBUG; 
3340
3341   my @template = ();
3342   while ( @_ ) {
3343     my $line = shift;
3344   
3345     if ( $line =~ /^%%Detail\s*$/ ) {
3346   
3347       push @template, q![@--!,
3348                       q!  foreach my $_tr_line (@detail_items) {!,
3349                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3350                       q!      $_tr_line->{'description'} .= !, 
3351                       q!        "\\tabularnewline\n~~".!,
3352                       q!        join( "\\tabularnewline\n~~",!,
3353                       q!          @{$_tr_line->{'ext_description'}}!,
3354                       q!        );!,
3355                       q!    }!;
3356
3357       while ( ( my $line_item_line = shift )
3358               !~ /^%%EndDetail\s*$/                            ) {
3359         $line_item_line =~ s/'/\\'/g;    # nice LTS
3360         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3361         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3362         push @template, "    \$OUT .= '$line_item_line';";
3363       }
3364
3365       push @template, '}',
3366                       '--@]';
3367       #' doh, gvim
3368     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3369
3370       push @template, '[@--',
3371                       '  foreach my $_tr_line (@total_items) {';
3372
3373       while ( ( my $total_item_line = shift )
3374               !~ /^%%EndTotalDetails\s*$/                      ) {
3375         $total_item_line =~ s/'/\\'/g;    # nice LTS
3376         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3377         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3378         push @template, "    \$OUT .= '$total_item_line';";
3379       }
3380
3381       push @template, '}',
3382                       '--@]';
3383
3384     } else {
3385       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3386       push @template, $line;  
3387     }
3388   
3389   }
3390
3391   if ($DEBUG) {
3392     warn "$_\n" foreach @template;
3393   }
3394
3395   (@template);
3396 }
3397
3398 sub terms {
3399   my $self = shift;
3400
3401   #check for an invoice-specific override
3402   return $self->invoice_terms if $self->invoice_terms;
3403   
3404   #check for a customer- specific override
3405   my $cust_main = $self->cust_main;
3406   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3407
3408   #use configured default
3409   $conf->config('invoice_default_terms') || '';
3410 }
3411
3412 sub due_date {
3413   my $self = shift;
3414   my $duedate = '';
3415   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3416     $duedate = $self->_date() + ( $1 * 86400 );
3417   }
3418   $duedate;
3419 }
3420
3421 sub due_date2str {
3422   my $self = shift;
3423   $self->due_date ? time2str(shift, $self->due_date) : '';
3424 }
3425
3426 sub balance_due_msg {
3427   my $self = shift;
3428   my $msg = 'Balance Due';
3429   return $msg unless $self->terms;
3430   if ( $self->due_date ) {
3431     $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3432   } elsif ( $self->terms ) {
3433     $msg .= ' - '. $self->terms;
3434   }
3435   $msg;
3436 }
3437
3438 sub balance_due_date {
3439   my $self = shift;
3440   my $duedate = '';
3441   if (    $conf->exists('invoice_default_terms') 
3442        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3443     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3444   }
3445   $duedate;
3446 }
3447
3448 =item invnum_date_pretty
3449
3450 Returns a string with the invoice number and date, for example:
3451 "Invoice #54 (3/20/2008)"
3452
3453 =cut
3454
3455 sub invnum_date_pretty {
3456   my $self = shift;
3457   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3458 }
3459
3460 =item _date_pretty
3461
3462 Returns a string with the date, for example: "3/20/2008"
3463
3464 =cut
3465
3466 sub _date_pretty {
3467   my $self = shift;
3468   time2str($date_format, $self->_date);
3469 }
3470
3471 use vars qw(%pkg_category_cache);
3472 sub _items_sections {
3473   my $self = shift;
3474   my $late = shift;
3475   my $summarypage = shift;
3476   my $escape = shift;
3477   my $extra_sections = shift;
3478   my $format = shift;
3479
3480   my %subtotal = ();
3481   my %late_subtotal = ();
3482   my %not_tax = ();
3483
3484   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3485   {
3486
3487       my $usage = $cust_bill_pkg->usage;
3488
3489       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3490         next if ( $display->summary && $summarypage );
3491
3492         my $section = $display->section;
3493         my $type    = $display->type;
3494
3495         $not_tax{$section} = 1
3496           unless $cust_bill_pkg->pkgnum == 0;
3497
3498         if ( $display->post_total && !$summarypage ) {
3499           if (! $type || $type eq 'S') {
3500             $late_subtotal{$section} += $cust_bill_pkg->setup
3501               if $cust_bill_pkg->setup != 0;
3502           }
3503
3504           if (! $type) {
3505             $late_subtotal{$section} += $cust_bill_pkg->recur
3506               if $cust_bill_pkg->recur != 0;
3507           }
3508
3509           if ($type && $type eq 'R') {
3510             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3511               if $cust_bill_pkg->recur != 0;
3512           }
3513           
3514           if ($type && $type eq 'U') {
3515             $late_subtotal{$section} += $usage
3516               unless scalar(@$extra_sections);
3517           }
3518
3519         } else {
3520
3521           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3522
3523           if (! $type || $type eq 'S') {
3524             $subtotal{$section} += $cust_bill_pkg->setup
3525               if $cust_bill_pkg->setup != 0;
3526           }
3527
3528           if (! $type) {
3529             $subtotal{$section} += $cust_bill_pkg->recur
3530               if $cust_bill_pkg->recur != 0;
3531           }
3532
3533           if ($type && $type eq 'R') {
3534             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3535               if $cust_bill_pkg->recur != 0;
3536           }
3537           
3538           if ($type && $type eq 'U') {
3539             $subtotal{$section} += $usage
3540               unless scalar(@$extra_sections);
3541           }
3542
3543         }
3544
3545       }
3546
3547   }
3548
3549   %pkg_category_cache = ();
3550
3551   push @$late, map { { 'description' => &{$escape}($_),
3552                        'subtotal'    => $late_subtotal{$_},
3553                        'post_total'  => 1,
3554                        'sort_weight' => ( _pkg_category($_)
3555                                             ? _pkg_category($_)->weight
3556                                             : 0
3557                                        ),
3558                        ((_pkg_category($_) && _pkg_category($_)->condense)
3559                                            ? $self->_condense_section($format)
3560                                            : ()
3561                        ),
3562                    } }
3563                  sort _sectionsort keys %late_subtotal;
3564
3565   my @sections;
3566   if ( $summarypage ) {
3567     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3568                 map { $_->categoryname } qsearch('pkg_category', {});
3569     push @sections, '' if exists($subtotal{''});
3570   } else {
3571     @sections = keys %subtotal;
3572   }
3573
3574   my @early = map { { 'description' => &{$escape}($_),
3575                       'subtotal'    => $subtotal{$_},
3576                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3577                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3578                       'sort_weight' => ( _pkg_category($_)
3579                                            ? _pkg_category($_)->weight
3580                                            : 0
3581                                        ),
3582                        ((_pkg_category($_) && _pkg_category($_)->condense)
3583                                            ? $self->_condense_section($format)
3584                                            : ()
3585                        ),
3586                     }
3587                   } @sections;
3588   push @early, @$extra_sections if $extra_sections;
3589  
3590   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3591
3592 }
3593
3594 #helper subs for above
3595
3596 sub _sectionsort {
3597   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3598 }
3599
3600 sub _pkg_category {
3601   my $categoryname = shift;
3602   $pkg_category_cache{$categoryname} ||=
3603     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3604 }
3605
3606 my %condensed_format = (
3607   'label' => [ qw( Description Qty Amount ) ],
3608   'fields' => [
3609                 sub { shift->{description} },
3610                 sub { shift->{quantity} },
3611                 sub { my($href, %opt) = @_;
3612                       ($opt{dollar} || ''). $href->{amount};
3613                     },
3614               ],
3615   'align'  => [ qw( l r r ) ],
3616   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3617   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3618 );
3619
3620 sub _condense_section {
3621   my ( $self, $format ) = ( shift, shift );
3622   ( 'condensed' => 1,
3623     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3624       qw( description_generator
3625           header_generator
3626           total_generator
3627           total_line_generator
3628         )
3629   );
3630 }
3631
3632 sub _condensed_generator_defaults {
3633   my ( $self, $format ) = ( shift, shift );
3634   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3635 }
3636
3637 my %html_align = (
3638   'c' => 'center',
3639   'l' => 'left',
3640   'r' => 'right',
3641 );
3642
3643 sub _condensed_header_generator {
3644   my ( $self, $format ) = ( shift, shift );
3645
3646   my ( $f, $prefix, $suffix, $separator, $column ) =
3647     _condensed_generator_defaults($format);
3648
3649   if ($format eq 'latex') {
3650     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3651     $suffix = "\\\\\n\\hline";
3652     $separator = "&\n";
3653     $column =
3654       sub { my ($d,$a,$s,$w) = @_;
3655             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3656           };
3657   } elsif ( $format eq 'html' ) {
3658     $prefix = '<th></th>';
3659     $suffix = '';
3660     $separator = '';
3661     $column =
3662       sub { my ($d,$a,$s,$w) = @_;
3663             return qq!<th align="$html_align{$a}">$d</th>!;
3664       };
3665   }
3666
3667   sub {
3668     my @args = @_;
3669     my @result = ();
3670
3671     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3672       push @result,
3673         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3674     }
3675
3676     $prefix. join($separator, @result). $suffix;
3677   };
3678
3679 }
3680
3681 sub _condensed_description_generator {
3682   my ( $self, $format ) = ( shift, shift );
3683
3684   my ( $f, $prefix, $suffix, $separator, $column ) =
3685     _condensed_generator_defaults($format);
3686
3687   my $money_char = '$';
3688   if ($format eq 'latex') {
3689     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3690     $suffix = '\\\\';
3691     $separator = " & \n";
3692     $column =
3693       sub { my ($d,$a,$s,$w) = @_;
3694             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3695           };
3696     $money_char = '\\dollar';
3697   }elsif ( $format eq 'html' ) {
3698     $prefix = '"><td align="center"></td>';
3699     $suffix = '';
3700     $separator = '';
3701     $column =
3702       sub { my ($d,$a,$s,$w) = @_;
3703             return qq!<td align="$html_align{$a}">$d</td>!;
3704       };
3705     #$money_char = $conf->config('money_char') || '$';
3706     $money_char = '';  # this is madness
3707   }
3708
3709   sub {
3710     #my @args = @_;
3711     my $href = shift;
3712     my @result = ();
3713
3714     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3715       my $dollar = '';
3716       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3717       push @result,
3718         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3719                     map { $f->{$_}->[$i] } qw(align span width)
3720                   );
3721     }
3722
3723     $prefix. join( $separator, @result ). $suffix;
3724   };
3725
3726 }
3727
3728 sub _condensed_total_generator {
3729   my ( $self, $format ) = ( shift, shift );
3730
3731   my ( $f, $prefix, $suffix, $separator, $column ) =
3732     _condensed_generator_defaults($format);
3733   my $style = '';
3734
3735   if ($format eq 'latex') {
3736     $prefix = "& ";
3737     $suffix = "\\\\\n";
3738     $separator = " & \n";
3739     $column =
3740       sub { my ($d,$a,$s,$w) = @_;
3741             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3742           };
3743   }elsif ( $format eq 'html' ) {
3744     $prefix = '';
3745     $suffix = '';
3746     $separator = '';
3747     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3748     $column =
3749       sub { my ($d,$a,$s,$w) = @_;
3750             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3751       };
3752   }
3753
3754
3755   sub {
3756     my @args = @_;
3757     my @result = ();
3758
3759     #  my $r = &{$f->{fields}->[$i]}(@args);
3760     #  $r .= ' Total' unless $i;
3761
3762     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3763       push @result,
3764         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3765                     map { $f->{$_}->[$i] } qw(align span width)
3766                   );
3767     }
3768
3769     $prefix. join( $separator, @result ). $suffix;
3770   };
3771
3772 }
3773
3774 =item total_line_generator FORMAT
3775
3776 Returns a coderef used for generation of invoice total line items for this
3777 usage_class.  FORMAT is either html or latex
3778
3779 =cut
3780
3781 # should not be used: will have issues with hash element names (description vs
3782 # total_item and amount vs total_amount -- another array of functions?
3783
3784 sub _condensed_total_line_generator {
3785   my ( $self, $format ) = ( shift, shift );
3786
3787   my ( $f, $prefix, $suffix, $separator, $column ) =
3788     _condensed_generator_defaults($format);
3789   my $style = '';
3790
3791   if ($format eq 'latex') {
3792     $prefix = "& ";
3793     $suffix = "\\\\\n";
3794     $separator = " & \n";
3795     $column =
3796       sub { my ($d,$a,$s,$w) = @_;
3797             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3798           };
3799   }elsif ( $format eq 'html' ) {
3800     $prefix = '';
3801     $suffix = '';
3802     $separator = '';
3803     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3804     $column =
3805       sub { my ($d,$a,$s,$w) = @_;
3806             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3807       };
3808   }
3809
3810
3811   sub {
3812     my @args = @_;
3813     my @result = ();
3814
3815     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3816       push @result,
3817         &{$column}( &{$f->{fields}->[$i]}(@args),
3818                     map { $f->{$_}->[$i] } qw(align span width)
3819                   );
3820     }
3821
3822     $prefix. join( $separator, @result ). $suffix;
3823   };
3824
3825 }
3826
3827 #sub _items_extra_usage_sections {
3828 #  my $self = shift;
3829 #  my $escape = shift;
3830 #
3831 #  my %sections = ();
3832 #
3833 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
3834 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3835 #  {
3836 #    next unless $cust_bill_pkg->pkgnum > 0;
3837 #
3838 #    foreach my $section ( keys %usage_class ) {
3839 #
3840 #      my $usage = $cust_bill_pkg->usage($section);
3841 #
3842 #      next unless $usage && $usage > 0;
3843 #
3844 #      $sections{$section} ||= 0;
3845 #      $sections{$section} += $usage;
3846 #
3847 #    }
3848 #
3849 #  }
3850 #
3851 #  map { { 'description' => &{$escape}($_),
3852 #          'subtotal'    => $sections{$_},
3853 #          'summarized'  => '',
3854 #          'tax_section' => '',
3855 #        }
3856 #      }
3857 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3858 #
3859 #}
3860
3861 sub _items_extra_usage_sections {
3862   my $self = shift;
3863   my $escape = shift;
3864   my $format = shift;
3865
3866   my %sections = ();
3867   my %classnums = ();
3868   my %lines = ();
3869
3870   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3871   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3872     next unless $cust_bill_pkg->pkgnum > 0;
3873
3874     foreach my $classnum ( keys %usage_class ) {
3875       my $section = $usage_class{$classnum}->classname;
3876       $classnums{$section} = $classnum;
3877
3878       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3879         my $amount = $detail->amount;
3880         next unless $amount && $amount > 0;
3881  
3882         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3883         $sections{$section}{amount} += $amount;  #subtotal
3884         $sections{$section}{calls}++;
3885         $sections{$section}{duration} += $detail->duration;
3886
3887         my $desc = $detail->regionname; 
3888         my $description = $desc;
3889         $description = substr($desc, 0, 50). '...'
3890           if $format eq 'latex' && length($desc) > 50;
3891
3892         $lines{$section}{$desc} ||= {
3893           description     => &{$escape}($description),
3894           #pkgpart         => $part_pkg->pkgpart,
3895           pkgnum          => $cust_bill_pkg->pkgnum,
3896           ref             => '',
3897           amount          => 0,
3898           calls           => 0,
3899           duration        => 0,
3900           #unit_amount     => $cust_bill_pkg->unitrecur,
3901           quantity        => $cust_bill_pkg->quantity,
3902           product_code    => 'N/A',
3903           ext_description => [],
3904         };
3905
3906         $lines{$section}{$desc}{amount} += $amount;
3907         $lines{$section}{$desc}{calls}++;
3908         $lines{$section}{$desc}{duration} += $detail->duration;
3909
3910       }
3911     }
3912   }
3913
3914   my %sectionmap = ();
3915   foreach (keys %sections) {
3916     my $usage_class = $usage_class{$classnums{$_}};
3917     $sectionmap{$_} = { 'description' => &{$escape}($_),
3918                         'amount'    => $sections{$_}{amount},    #subtotal
3919                         'calls'       => $sections{$_}{calls},
3920                         'duration'    => $sections{$_}{duration},
3921                         'summarized'  => '',
3922                         'tax_section' => '',
3923                         'sort_weight' => $usage_class->weight,
3924                         ( $usage_class->format
3925                           ? ( map { $_ => $usage_class->$_($format) }
3926                               qw( description_generator header_generator total_generator total_line_generator )
3927                             )
3928                           : ()
3929                         ), 
3930                       };
3931   }
3932
3933   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3934                  values %sectionmap;
3935
3936   my @lines = ();
3937   foreach my $section ( keys %lines ) {
3938     foreach my $line ( keys %{$lines{$section}} ) {
3939       my $l = $lines{$section}{$line};
3940       $l->{section}     = $sectionmap{$section};
3941       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
3942       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3943       push @lines, $l;
3944     }
3945   }
3946
3947   return(\@sections, \@lines);
3948
3949 }
3950
3951 sub _did_summary {
3952     my $self = shift;
3953     my $end = $self->_date;
3954     my $start = $end - 2592000; # 30 days
3955     my $cust_main = $self->cust_main;
3956     my @pkgs = $cust_main->all_pkgs;
3957     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3958         = (0,0,0,0,0);
3959     my @seen = ();
3960     foreach my $pkg ( @pkgs ) {
3961         my @h_cust_svc = $pkg->h_cust_svc($end);
3962         foreach my $h_cust_svc ( @h_cust_svc ) {
3963             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3964             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3965
3966             my $inserted = $h_cust_svc->date_inserted;
3967             my $deleted = $h_cust_svc->date_deleted;
3968             my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3969             my $phone_deleted;
3970             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
3971             
3972 # DID either activated or ported in; cannot be both for same DID simultaneously
3973             if ($inserted >= $start && $inserted <= $end && $phone_inserted
3974                 && (!$phone_inserted->lnp_status 
3975                     || $phone_inserted->lnp_status eq ''
3976                     || $phone_inserted->lnp_status eq 'native')) {
3977                 $num_activated++;
3978             }
3979             else { # this one not so clean, should probably move to (h_)svc_phone
3980                  my $phone_portedin = qsearchs( 'h_svc_phone',
3981                       { 'svcnum' => $h_cust_svc->svcnum, 
3982                         'lnp_status' => 'portedin' },  
3983                       FS::h_svc_phone->sql_h_searchs($end),  
3984                     );
3985                  $num_portedin++ if $phone_portedin;
3986             }
3987
3988 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3989             if($deleted >= $start && $deleted <= $end && $phone_deleted
3990                 && (!$phone_deleted->lnp_status 
3991                     || $phone_deleted->lnp_status ne 'portingout')) {
3992                 $num_deactivated++;
3993             } 
3994             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
3995                 && $phone_deleted->lnp_status 
3996                 && $phone_deleted->lnp_status eq 'portingout') {
3997                 $num_portedout++;
3998             }
3999
4000             # increment usage minutes
4001             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4002             foreach my $cdr ( @cdrs ) {
4003                 $minutes += $cdr->billsec/60;
4004             }
4005
4006             # don't look at this service again
4007             push @seen, $h_cust_svc->svcnum;
4008         }
4009     }
4010
4011     $minutes = sprintf("%d", $minutes);
4012     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
4013         . "$num_deactivated  Ported-Out: $num_portedout ",
4014             "Total Minutes: $minutes");
4015 }
4016
4017 sub _items_svc_phone_sections {
4018   my $self = shift;
4019   my $escape = shift;
4020   my $format = shift;
4021
4022   my %sections = ();
4023   my %classnums = ();
4024   my %lines = ();
4025
4026   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4027   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4028
4029   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4030     next unless $cust_bill_pkg->pkgnum > 0;
4031
4032     my @header = $cust_bill_pkg->details_header;
4033     next unless scalar(@header);
4034
4035     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4036
4037       my $phonenum = $detail->phonenum;
4038       next unless $phonenum;
4039
4040       my $amount = $detail->amount;
4041       next unless $amount && $amount > 0;
4042
4043       $sections{$phonenum} ||= { 'amount'      => 0,
4044                                  'calls'       => 0,
4045                                  'duration'    => 0,
4046                                  'sort_weight' => -1,
4047                                  'phonenum'    => $phonenum,
4048                                 };
4049       $sections{$phonenum}{amount} += $amount;  #subtotal
4050       $sections{$phonenum}{calls}++;
4051       $sections{$phonenum}{duration} += $detail->duration;
4052
4053       my $desc = $detail->regionname; 
4054       my $description = $desc;
4055       $description = substr($desc, 0, 50). '...'
4056         if $format eq 'latex' && length($desc) > 50;
4057
4058       $lines{$phonenum}{$desc} ||= {
4059         description     => &{$escape}($description),
4060         #pkgpart         => $part_pkg->pkgpart,
4061         pkgnum          => '',
4062         ref             => '',
4063         amount          => 0,
4064         calls           => 0,
4065         duration        => 0,
4066         #unit_amount     => '',
4067         quantity        => '',
4068         product_code    => 'N/A',
4069         ext_description => [],
4070       };
4071
4072       $lines{$phonenum}{$desc}{amount} += $amount;
4073       $lines{$phonenum}{$desc}{calls}++;
4074       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4075
4076       my $line = $usage_class{$detail->classnum}->classname;
4077       $sections{"$phonenum $line"} ||=
4078         { 'amount' => 0,
4079           'calls' => 0,
4080           'duration' => 0,
4081           'sort_weight' => $usage_class{$detail->classnum}->weight,
4082           'phonenum' => $phonenum,
4083           'header'  => [ @header ],
4084         };
4085       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4086       $sections{"$phonenum $line"}{calls}++;
4087       $sections{"$phonenum $line"}{duration} += $detail->duration;
4088
4089       $lines{"$phonenum $line"}{$desc} ||= {
4090         description     => &{$escape}($description),
4091         #pkgpart         => $part_pkg->pkgpart,
4092         pkgnum          => '',
4093         ref             => '',
4094         amount          => 0,
4095         calls           => 0,
4096         duration        => 0,
4097         #unit_amount     => '',
4098         quantity        => '',
4099         product_code    => 'N/A',
4100         ext_description => [],
4101       };
4102
4103       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4104       $lines{"$phonenum $line"}{$desc}{calls}++;
4105       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4106       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4107            $detail->formatted('format' => $format);
4108
4109     }
4110   }
4111
4112   my %sectionmap = ();
4113   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4114   foreach ( keys %sections ) {
4115     my @header = @{ $sections{$_}{header} || [] };
4116     my $usage_simple =
4117       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4118     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4119     my $usage_class = $summary ? $simple : $usage_simple;
4120     my $ending = $summary ? ' usage charges' : '';
4121     my %gen_opt = ();
4122     unless ($summary) {
4123       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4124     }
4125     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4126                         'amount'    => $sections{$_}{amount},    #subtotal
4127                         'calls'       => $sections{$_}{calls},
4128                         'duration'    => $sections{$_}{duration},
4129                         'summarized'  => '',
4130                         'tax_section' => '',
4131                         'phonenum'    => $sections{$_}{phonenum},
4132                         'sort_weight' => $sections{$_}{sort_weight},
4133                         'post_total'  => $summary, #inspire pagebreak
4134                         (
4135                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4136                             qw( description_generator
4137                                 header_generator
4138                                 total_generator
4139                                 total_line_generator
4140                               )
4141                           )
4142                         ), 
4143                       };
4144   }
4145
4146   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4147                         $a->{sort_weight} <=> $b->{sort_weight}
4148                       }
4149                  values %sectionmap;
4150
4151   my @lines = ();
4152   foreach my $section ( keys %lines ) {
4153     foreach my $line ( keys %{$lines{$section}} ) {
4154       my $l = $lines{$section}{$line};
4155       $l->{section}     = $sectionmap{$section};
4156       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4157       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4158       push @lines, $l;
4159     }
4160   }
4161   
4162   if($conf->exists('phone_usage_class_summary')) { 
4163       # this only works with Latex
4164       my @newlines;
4165       my @newsections;
4166
4167       # after this, we'll have only two sections per DID:
4168       # Calls Summary and Calls Detail
4169       foreach my $section ( @sections ) {
4170         if($section->{'post_total'}) {
4171             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4172             $section->{'total_line_generator'} = sub { '' };
4173             $section->{'total_generator'} = sub { '' };
4174             $section->{'header_generator'} = sub { '' };
4175             $section->{'description_generator'} = '';
4176             push @newsections, $section;
4177             my %calls_detail = %$section;
4178             $calls_detail{'post_total'} = '';
4179             $calls_detail{'sort_weight'} = '';
4180             $calls_detail{'description_generator'} = sub { '' };
4181             $calls_detail{'header_generator'} = sub {
4182                 return ' & Date/Time & Called Number & Duration & Price'
4183                     if $format eq 'latex';
4184                 '';
4185             };
4186             $calls_detail{'description'} = 'Calls Detail: '
4187                                                     . $section->{'phonenum'};
4188             push @newsections, \%calls_detail;  
4189         }
4190       }
4191
4192       # after this, each usage class is collapsed/summarized into a single
4193       # line under the Calls Summary section
4194       foreach my $newsection ( @newsections ) {
4195         if($newsection->{'post_total'}) { # this means Calls Summary
4196             foreach my $section ( @sections ) {
4197                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
4198                                 && !$section->{'post_total'});
4199                 my $newdesc = $section->{'description'};
4200                 my $tn = $section->{'phonenum'};
4201                 $newdesc =~ s/$tn//g;
4202                 my $line = {  ext_description => [],
4203                               pkgnum => '',
4204                               ref => '',
4205                               quantity => '',
4206                               calls => $section->{'calls'},
4207                               section => $newsection,
4208                               duration => $section->{'duration'},
4209                               description => $newdesc,
4210                               amount => sprintf("%.2f",$section->{'amount'}),
4211                               product_code => 'N/A',
4212                             };
4213                 push @newlines, $line;
4214             }
4215         }
4216       }
4217
4218       # after this, Calls Details is populated with all CDRs
4219       foreach my $newsection ( @newsections ) {
4220         if(!$newsection->{'post_total'}) { # this means Calls Details
4221             foreach my $line ( @lines ) {
4222                 next unless (scalar(@{$line->{'ext_description'}}) &&
4223                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4224                             );
4225                 my @extdesc = @{$line->{'ext_description'}};
4226                 my @newextdesc;
4227                 foreach my $extdesc ( @extdesc ) {
4228                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4229                     push @newextdesc, $extdesc;
4230                 }
4231                 $line->{'ext_description'} = \@newextdesc;
4232                 $line->{'section'} = $newsection;
4233                 push @newlines, $line;
4234             }
4235         }
4236       }
4237
4238       return(\@newsections, \@newlines);
4239   }
4240
4241   return(\@sections, \@lines);
4242
4243 }
4244
4245 sub _items {
4246   my $self = shift;
4247
4248   #my @display = scalar(@_)
4249   #              ? @_
4250   #              : qw( _items_previous _items_pkg );
4251   #              #: qw( _items_pkg );
4252   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4253   my @display = qw( _items_previous _items_pkg );
4254
4255   my @b = ();
4256   foreach my $display ( @display ) {
4257     push @b, $self->$display(@_);
4258   }
4259   @b;
4260 }
4261
4262 sub _items_previous {
4263   my $self = shift;
4264   my $cust_main = $self->cust_main;
4265   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4266   my @b = ();
4267   foreach ( @pr_cust_bill ) {
4268     my $date = $conf->exists('invoice_show_prior_due_date')
4269                ? 'due '. $_->due_date2str($date_format)
4270                : time2str($date_format, $_->_date);
4271     push @b, {
4272       'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4273       #'pkgpart'     => 'N/A',
4274       'pkgnum'      => 'N/A',
4275       'amount'      => sprintf("%.2f", $_->owed),
4276     };
4277   }
4278   @b;
4279
4280   #{
4281   #    'description'     => 'Previous Balance',
4282   #    #'pkgpart'         => 'N/A',
4283   #    'pkgnum'          => 'N/A',
4284   #    'amount'          => sprintf("%10.2f", $pr_total ),
4285   #    'ext_description' => [ map {
4286   #                                 "Invoice ". $_->invnum.
4287   #                                 " (". time2str("%x",$_->_date). ") ".
4288   #                                 sprintf("%10.2f", $_->owed)
4289   #                         } @pr_cust_bill ],
4290
4291   #};
4292 }
4293
4294 sub _items_pkg {
4295   my $self = shift;
4296   my %options = @_;
4297
4298   warn "$me _items_pkg searching for all package line items\n"
4299     if $DEBUG > 1;
4300
4301   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4302
4303   warn "$me _items_pkg filtering line items\n"
4304     if $DEBUG > 1;
4305   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4306
4307   if ($options{section} && $options{section}->{condensed}) {
4308
4309     warn "$me _items_pkg condensing section\n"
4310       if $DEBUG > 1;
4311
4312     my %itemshash = ();
4313     local $Storable::canonical = 1;
4314     foreach ( @items ) {
4315       my $item = { %$_ };
4316       delete $item->{ref};
4317       delete $item->{ext_description};
4318       my $key = freeze($item);
4319       $itemshash{$key} ||= 0;
4320       $itemshash{$key} ++; # += $item->{quantity};
4321     }
4322     @items = sort { $a->{description} cmp $b->{description} }
4323              map { my $i = thaw($_);
4324                    $i->{quantity} = $itemshash{$_};
4325                    $i->{amount} =
4326                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4327                    $i;
4328                  }
4329              keys %itemshash;
4330   }
4331
4332   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4333     if $DEBUG > 1;
4334
4335   @items;
4336 }
4337
4338 sub _taxsort {
4339   return 0 unless $a->itemdesc cmp $b->itemdesc;
4340   return -1 if $b->itemdesc eq 'Tax';
4341   return 1 if $a->itemdesc eq 'Tax';
4342   return -1 if $b->itemdesc eq 'Other surcharges';
4343   return 1 if $a->itemdesc eq 'Other surcharges';
4344   $a->itemdesc cmp $b->itemdesc;
4345 }
4346
4347 sub _items_tax {
4348   my $self = shift;
4349   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4350   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4351 }
4352
4353 sub _items_cust_bill_pkg {
4354   my $self = shift;
4355   my $cust_bill_pkgs = shift;
4356   my %opt = @_;
4357
4358   my $format = $opt{format} || '';
4359   my $escape_function = $opt{escape_function} || sub { shift };
4360   my $format_function = $opt{format_function} || '';
4361   my $unsquelched = $opt{unsquelched} || '';
4362   my $section = $opt{section}->{description} if $opt{section};
4363   my $summary_page = $opt{summary_page} || '';
4364   my $multilocation = $opt{multilocation} || '';
4365   my $multisection = $opt{multisection} || '';
4366   my $discount_show_always = 0;
4367
4368   my @b = ();
4369   my ($s, $r, $u) = ( undef, undef, undef );
4370   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4371   {
4372
4373     warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4374       if $DEBUG > 1;
4375
4376     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4377                                 && $conf->exists('discount-show-always'));
4378
4379     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4380       if ( $_ && !$cust_bill_pkg->hidden ) {
4381         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4382         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4383         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4384         push @b, { %$_ }
4385           unless ( $_->{amount} == 0 && !$discount_show_always );
4386         $_ = undef;
4387       }
4388     }
4389
4390     foreach my $display ( grep { defined($section)
4391                                  ? $_->section eq $section
4392                                  : 1
4393                                }
4394                           #grep { !$_->summary || !$summary_page } # bunk!
4395                           grep { !$_->summary || $multisection }
4396                           $cust_bill_pkg->cust_bill_pkg_display
4397                         )
4398     {
4399
4400       warn "$me _items_cust_bill_pkg considering display item $display\n"
4401         if $DEBUG > 1;
4402
4403       my $type = $display->type;
4404
4405       my $desc = $cust_bill_pkg->desc;
4406       $desc = substr($desc, 0, 50). '...'
4407         if $format eq 'latex' && length($desc) > 50;
4408
4409       my %details_opt = ( 'format'          => $format,
4410                           'escape_function' => $escape_function,
4411                           'format_function' => $format_function,
4412                         );
4413
4414       if ( $cust_bill_pkg->pkgnum > 0 ) {
4415
4416         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4417           if $DEBUG > 1;
4418  
4419         my $cust_pkg = $cust_bill_pkg->cust_pkg;
4420
4421         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4422
4423           warn "$me _items_cust_bill_pkg adding setup\n"
4424             if $DEBUG > 1;
4425
4426           my $description = $desc;
4427           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4428
4429           my @d = ();
4430           unless ( $cust_pkg->part_pkg->hide_svc_detail
4431                 || $cust_bill_pkg->hidden )
4432           {
4433
4434             push @d, map &{$escape_function}($_),
4435                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
4436               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4437
4438             if ( $multilocation ) {
4439               my $loc = $cust_pkg->location_label;
4440               $loc = substr($loc, 0, 50). '...'
4441                 if $format eq 'latex' && length($loc) > 50;
4442               push @d, &{$escape_function}($loc);
4443             }
4444
4445           }
4446
4447           push @d, $cust_bill_pkg->details(%details_opt)
4448             if $cust_bill_pkg->recur == 0;
4449
4450           if ( $cust_bill_pkg->hidden ) {
4451             $s->{amount}      += $cust_bill_pkg->setup;
4452             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4453             push @{ $s->{ext_description} }, @d;
4454           } else {
4455             $s = {
4456               description     => $description,
4457               #pkgpart         => $part_pkg->pkgpart,
4458               pkgnum          => $cust_bill_pkg->pkgnum,
4459               amount          => $cust_bill_pkg->setup,
4460               unit_amount     => $cust_bill_pkg->unitsetup,
4461               quantity        => $cust_bill_pkg->quantity,
4462               ext_description => \@d,
4463             };
4464           };
4465
4466         }
4467
4468         if ( ( $cust_bill_pkg->recur != 0  || $cust_bill_pkg->setup == 0 || 
4469                 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4470              ( !$type || $type eq 'R' || $type eq 'U' )
4471            )
4472         {
4473
4474           warn "$me _items_cust_bill_pkg adding recur/usage\n"
4475             if $DEBUG > 1;
4476
4477           my $is_summary = $display->summary;
4478           my $description = ($is_summary && $type && $type eq 'U')
4479                             ? "Usage charges" : $desc;
4480
4481           $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4482                           " - ". time2str($date_format, $cust_bill_pkg->edate).
4483                           ")"
4484             unless $conf->exists('disable_line_item_date_ranges');
4485
4486           my @d = ();
4487
4488           #at least until cust_bill_pkg has "past" ranges in addition to
4489           #the "future" sdate/edate ones... see #3032
4490           my @dates = ( $self->_date );
4491           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4492           push @dates, $prev->sdate if $prev;
4493           push @dates, undef if !$prev;
4494
4495           unless ( $cust_pkg->part_pkg->hide_svc_detail
4496                 || $cust_bill_pkg->itemdesc
4497                 || $cust_bill_pkg->hidden
4498                 || $is_summary && $type && $type eq 'U' )
4499           {
4500
4501             warn "$me _items_cust_bill_pkg adding service details\n"
4502               if $DEBUG > 1;
4503
4504             push @d, map &{$escape_function}($_),
4505                          $cust_pkg->h_labels_short(@dates, 'I')
4506                                                    #$cust_bill_pkg->edate,
4507                                                    #$cust_bill_pkg->sdate)
4508               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4509
4510             warn "$me _items_cust_bill_pkg done adding service details\n"
4511               if $DEBUG > 1;
4512
4513             if ( $multilocation ) {
4514               my $loc = $cust_pkg->location_label;
4515               $loc = substr($loc, 0, 50). '...'
4516                 if $format eq 'latex' && length($loc) > 50;
4517               push @d, &{$escape_function}($loc);
4518             }
4519
4520           }
4521
4522           warn "$me _items_cust_bill_pkg adding details\n"
4523             if $DEBUG > 1;
4524
4525           push @d, $cust_bill_pkg->details(%details_opt)
4526             unless $is_summary; # || ($type && $type eq 'R');
4527
4528           warn "$me _items_cust_bill_pkg calculating amount\n"
4529             if $DEBUG > 1;
4530   
4531           my $amount = 0;
4532           if (!$type) {
4533             $amount = $cust_bill_pkg->recur;
4534           }elsif($type eq 'R') {
4535             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4536           }elsif($type eq 'U') {
4537             $amount = $cust_bill_pkg->usage;
4538           }
4539   
4540           if ( !$type || $type eq 'R' ) {
4541
4542             warn "$me _items_cust_bill_pkg adding recur\n"
4543               if $DEBUG > 1;
4544
4545             if ( $cust_bill_pkg->hidden ) {
4546               $r->{amount}      += $amount;
4547               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4548               push @{ $r->{ext_description} }, @d;
4549             } else {
4550               $r = {
4551                 description     => $description,
4552                 #pkgpart         => $part_pkg->pkgpart,
4553                 pkgnum          => $cust_bill_pkg->pkgnum,
4554                 amount          => $amount,
4555                 unit_amount     => $cust_bill_pkg->unitrecur,
4556                 quantity        => $cust_bill_pkg->quantity,
4557                 ext_description => \@d,
4558               };
4559             }
4560
4561           } else {  # $type eq 'U'
4562
4563             warn "$me _items_cust_bill_pkg adding usage\n"
4564               if $DEBUG > 1;
4565
4566             if ( $cust_bill_pkg->hidden ) {
4567               $u->{amount}      += $amount;
4568               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4569               push @{ $u->{ext_description} }, @d;
4570             } else {
4571               $u = {
4572                 description     => $description,
4573                 #pkgpart         => $part_pkg->pkgpart,
4574                 pkgnum          => $cust_bill_pkg->pkgnum,
4575                 amount          => $amount,
4576                 unit_amount     => $cust_bill_pkg->unitrecur,
4577                 quantity        => $cust_bill_pkg->quantity,
4578                 ext_description => \@d,
4579               };
4580             }
4581
4582           }
4583
4584         } # recurring or usage with recurring charge
4585
4586       } else { #pkgnum tax or one-shot line item (??)
4587
4588         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4589           if $DEBUG > 1;
4590
4591         if ( $cust_bill_pkg->setup != 0 ) {
4592           push @b, {
4593             'description' => $desc,
4594             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
4595           };
4596         }
4597         if ( $cust_bill_pkg->recur != 0 ) {
4598           push @b, {
4599             'description' => "$desc (".
4600                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4601                              time2str($date_format, $cust_bill_pkg->edate). ')',
4602             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
4603           };
4604         }
4605
4606       }
4607
4608     }
4609
4610   }
4611
4612   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4613     if $DEBUG > 1;
4614
4615   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4616     if ( $_  ) {
4617       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4618       $_->{amount}      =~ s/^\-0\.00$/0.00/;
4619       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4620       push @b, { %$_ }
4621         unless ( $_->{amount} == 0 && !$discount_show_always );
4622     }
4623   }
4624
4625   @b;
4626
4627 }
4628
4629 sub _items_credits {
4630   my( $self, %opt ) = @_;
4631   my $trim_len = $opt{'trim_len'} || 60;
4632
4633   my @b;
4634   #credits
4635   foreach ( $self->cust_credited ) {
4636
4637     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4638
4639     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4640     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4641     $reason = " ($reason) " if $reason;
4642
4643     push @b, {
4644       #'description' => 'Credit ref\#'. $_->crednum.
4645       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
4646       #                 $reason,
4647       'description' => 'Credit applied '.
4648                        time2str($date_format,$_->cust_credit->_date). $reason,
4649       'amount'      => sprintf("%.2f",$_->amount),
4650     };
4651   }
4652
4653   @b;
4654
4655 }
4656
4657 sub _items_payments {
4658   my $self = shift;
4659
4660   my @b;
4661   #get & print payments
4662   foreach ( $self->cust_bill_pay ) {
4663
4664     #something more elaborate if $_->amount ne ->cust_pay->paid ?
4665
4666     push @b, {
4667       'description' => "Payment received ".
4668                        time2str($date_format,$_->cust_pay->_date ),
4669       'amount'      => sprintf("%.2f", $_->amount )
4670     };
4671   }
4672
4673   @b;
4674
4675 }
4676
4677 =item call_details [ OPTION => VALUE ... ]
4678
4679 Returns an array of CSV strings representing the call details for this invoice
4680 The only option available is the boolean prepend_billed_number
4681
4682 =cut
4683
4684 sub call_details {
4685   my ($self, %opt) = @_;
4686
4687   my $format_function = sub { shift };
4688
4689   if ($opt{prepend_billed_number}) {
4690     $format_function = sub {
4691       my $detail = shift;
4692       my $row = shift;
4693
4694       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4695       
4696     };
4697   }
4698
4699   my @details = map { $_->details( 'format_function' => $format_function,
4700                                    'escape_function' => sub{ return() },
4701                                  )
4702                     }
4703                   grep { $_->pkgnum }
4704                   $self->cust_bill_pkg;
4705   my $header = $details[0];
4706   ( $header, grep { $_ ne $header } @details );
4707 }
4708
4709
4710 =back
4711
4712 =head1 SUBROUTINES
4713
4714 =over 4
4715
4716 =item process_reprint
4717
4718 =cut
4719
4720 sub process_reprint {
4721   process_re_X('print', @_);
4722 }
4723
4724 =item process_reemail
4725
4726 =cut
4727
4728 sub process_reemail {
4729   process_re_X('email', @_);
4730 }
4731
4732 =item process_refax
4733
4734 =cut
4735
4736 sub process_refax {
4737   process_re_X('fax', @_);
4738 }
4739
4740 =item process_reftp
4741
4742 =cut
4743
4744 sub process_reftp {
4745   process_re_X('ftp', @_);
4746 }
4747
4748 =item respool
4749
4750 =cut
4751
4752 sub process_respool {
4753   process_re_X('spool', @_);
4754 }
4755
4756 use Storable qw(thaw);
4757 use Data::Dumper;
4758 use MIME::Base64;
4759 sub process_re_X {
4760   my( $method, $job ) = ( shift, shift );
4761   warn "$me process_re_X $method for job $job\n" if $DEBUG;
4762
4763   my $param = thaw(decode_base64(shift));
4764   warn Dumper($param) if $DEBUG;
4765
4766   re_X(
4767     $method,
4768     $job,
4769     %$param,
4770   );
4771
4772 }
4773
4774 sub re_X {
4775   my($method, $job, %param ) = @_;
4776   if ( $DEBUG ) {
4777     warn "re_X $method for job $job with param:\n".
4778          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
4779   }
4780
4781   #some false laziness w/search/cust_bill.html
4782   my $distinct = '';
4783   my $orderby = 'ORDER BY cust_bill._date';
4784
4785   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4786
4787   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4788      
4789   my @cust_bill = qsearch( {
4790     #'select'    => "cust_bill.*",
4791     'table'     => 'cust_bill',
4792     'addl_from' => $addl_from,
4793     'hashref'   => {},
4794     'extra_sql' => $extra_sql,
4795     'order_by'  => $orderby,
4796     'debug' => 1,
4797   } );
4798
4799   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4800
4801   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4802     if $DEBUG;
4803
4804   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4805   foreach my $cust_bill ( @cust_bill ) {
4806     $cust_bill->$method();
4807
4808     if ( $job ) { #progressbar foo
4809       $num++;
4810       if ( time - $min_sec > $last ) {
4811         my $error = $job->update_statustext(
4812           int( 100 * $num / scalar(@cust_bill) )
4813         );
4814         die $error if $error;
4815         $last = time;
4816       }
4817     }
4818
4819   }
4820
4821 }
4822
4823 =back
4824
4825 =head1 CLASS METHODS
4826
4827 =over 4
4828
4829 =item owed_sql
4830
4831 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4832
4833 =cut
4834
4835 sub owed_sql {
4836   my ($class, $start, $end) = @_;
4837   'charged - '. 
4838     $class->paid_sql($start, $end). ' - '. 
4839     $class->credited_sql($start, $end);
4840 }
4841
4842 =item net_sql
4843
4844 Returns an SQL fragment to retreive the net amount (charged minus credited).
4845
4846 =cut
4847
4848 sub net_sql {
4849   my ($class, $start, $end) = @_;
4850   'charged - '. $class->credited_sql($start, $end);
4851 }
4852
4853 =item paid_sql
4854
4855 Returns an SQL fragment to retreive the amount paid against this invoice.
4856
4857 =cut
4858
4859 sub paid_sql {
4860   my ($class, $start, $end) = @_;
4861   $start &&= "AND cust_bill_pay._date <= $start";
4862   $end   &&= "AND cust_bill_pay._date > $end";
4863   $start = '' unless defined($start);
4864   $end   = '' unless defined($end);
4865   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4866        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
4867 }
4868
4869 =item credited_sql
4870
4871 Returns an SQL fragment to retreive the amount credited against this invoice.
4872
4873 =cut
4874
4875 sub credited_sql {
4876   my ($class, $start, $end) = @_;
4877   $start &&= "AND cust_credit_bill._date <= $start";
4878   $end   &&= "AND cust_credit_bill._date >  $end";
4879   $start = '' unless defined($start);
4880   $end   = '' unless defined($end);
4881   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4882        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
4883 }
4884
4885 =item due_date_sql
4886
4887 Returns an SQL fragment to retrieve the due date of an invoice.
4888 Currently only supported on PostgreSQL.
4889
4890 =cut
4891
4892 sub due_date_sql {
4893 'COALESCE(
4894   SUBSTRING(
4895     COALESCE(
4896       cust_bill.invoice_terms,
4897       cust_main.invoice_terms,
4898       \''.($conf->config('invoice_default_terms') || '').'\'
4899     ), E\'Net (\\\\d+)\'
4900   )::INTEGER, 0
4901 ) * 86400 + cust_bill._date'
4902 }
4903
4904 =item search_sql_where HASHREF
4905
4906 Class method which returns an SQL WHERE fragment to search for parameters
4907 specified in HASHREF.  Valid parameters are
4908
4909 =over 4
4910
4911 =item _date
4912
4913 List reference of start date, end date, as UNIX timestamps.
4914
4915 =item invnum_min
4916
4917 =item invnum_max
4918
4919 =item agentnum
4920
4921 =item charged
4922
4923 List reference of charged limits (exclusive).
4924
4925 =item owed
4926
4927 List reference of charged limits (exclusive).
4928
4929 =item open
4930
4931 flag, return open invoices only
4932
4933 =item net
4934
4935 flag, return net invoices only
4936
4937 =item days
4938
4939 =item newest_percust
4940
4941 =back
4942
4943 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4944
4945 =cut
4946
4947 sub search_sql_where {
4948   my($class, $param) = @_;
4949   if ( $DEBUG ) {
4950     warn "$me search_sql_where called with params: \n".
4951          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
4952   }
4953
4954   my @search = ();
4955
4956   #agentnum
4957   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4958     push @search, "cust_main.agentnum = $1";
4959   }
4960
4961   #_date
4962   if ( $param->{_date} ) {
4963     my($beginning, $ending) = @{$param->{_date}};
4964
4965     push @search, "cust_bill._date >= $beginning",
4966                   "cust_bill._date <  $ending";
4967   }
4968
4969   #invnum
4970   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4971     push @search, "cust_bill.invnum >= $1";
4972   }
4973   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4974     push @search, "cust_bill.invnum <= $1";
4975   }
4976
4977   #charged
4978   if ( $param->{charged} ) {
4979     my @charged = ref($param->{charged})
4980                     ? @{ $param->{charged} }
4981                     : ($param->{charged});
4982
4983     push @search, map { s/^charged/cust_bill.charged/; $_; }
4984                       @charged;
4985   }
4986
4987   my $owed_sql = FS::cust_bill->owed_sql;
4988
4989   #owed
4990   if ( $param->{owed} ) {
4991     my @owed = ref($param->{owed})
4992                  ? @{ $param->{owed} }
4993                  : ($param->{owed});
4994     push @search, map { s/^owed/$owed_sql/; $_; }
4995                       @owed;
4996   }
4997
4998   #open/net flags
4999   push @search, "0 != $owed_sql"
5000     if $param->{'open'};
5001   push @search, '0 != '. FS::cust_bill->net_sql
5002     if $param->{'net'};
5003
5004   #days
5005   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5006     if $param->{'days'};
5007
5008   #newest_percust
5009   if ( $param->{'newest_percust'} ) {
5010
5011     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5012     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5013
5014     my @newest_where = map { my $x = $_;
5015                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5016                              $x;
5017                            }
5018                            grep ! /^cust_main./, @search;
5019     my $newest_where = scalar(@newest_where)
5020                          ? ' AND '. join(' AND ', @newest_where)
5021                          : '';
5022
5023
5024     push @search, "cust_bill._date = (
5025       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5026         WHERE newest_cust_bill.custnum = cust_bill.custnum
5027           $newest_where
5028     )";
5029
5030   }
5031
5032   #agent virtualization
5033   my $curuser = $FS::CurrentUser::CurrentUser;
5034   if ( $curuser->username eq 'fs_queue'
5035        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5036     my $username = $1;
5037     my $newuser = qsearchs('access_user', {
5038       'username' => $username,
5039       'disabled' => '',
5040     } );
5041     if ( $newuser ) {
5042       $curuser = $newuser;
5043     } else {
5044       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5045     }
5046   }
5047   push @search, $curuser->agentnums_sql;
5048
5049   join(' AND ', @search );
5050
5051 }
5052
5053 =back
5054
5055 =head1 BUGS
5056
5057 The delete method.
5058
5059 =head1 SEE ALSO
5060
5061 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5062 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5063 documentation.
5064
5065 =cut
5066
5067 1;
5068