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