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