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