taqua accountcode billing, part 2 of 2, 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       push @{$late_sections}, $accountcode_section;
2836       push @detail_items, @$accountcode_lines;
2837     }
2838   }else{
2839     push @sections, { 'description' => '', 'subtotal' => '' };
2840   }
2841
2842   unless (    $conf->exists('disable_previous_balance')
2843            || $conf->exists('previous_balance-summary_only')
2844          )
2845   {
2846
2847     warn "$me adding previous balances\n"
2848       if $DEBUG > 1;
2849
2850     foreach my $line_item ( $self->_items_previous ) {
2851
2852       my $detail = {
2853         ext_description => [],
2854       };
2855       $detail->{'ref'} = $line_item->{'pkgnum'};
2856       $detail->{'quantity'} = 1;
2857       $detail->{'section'} = $previous_section;
2858       $detail->{'description'} = &$escape_function($line_item->{'description'});
2859       if ( exists $line_item->{'ext_description'} ) {
2860         @{$detail->{'ext_description'}} = map {
2861           &$escape_function($_);
2862         } @{$line_item->{'ext_description'}};
2863       }
2864       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2865                             $line_item->{'amount'};
2866       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2867
2868       push @detail_items, $detail;
2869       push @buf, [ $detail->{'description'},
2870                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2871                  ];
2872     }
2873
2874   }
2875   
2876   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2877     push @buf, ['','-----------'];
2878     push @buf, [ 'Total Previous Balance',
2879                  $money_char. sprintf("%10.2f", $pr_total) ];
2880     push @buf, ['',''];
2881   }
2882  
2883   if ( $conf->exists('svc_phone-did-summary') ) {
2884       warn "$me adding DID summary\n"
2885         if $DEBUG > 1;
2886
2887       my ($didsummary,$minutes) = $self->_did_summary;
2888       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2889       push @detail_items, 
2890         { 'description' => $didsummary_desc,
2891             'ext_description' => [ $didsummary, $minutes ],
2892         }
2893         if !$multisection;
2894   }
2895
2896   foreach my $section (@sections, @$late_sections) {
2897
2898     warn "$me adding section \n". Dumper($section)
2899       if $DEBUG > 1;
2900
2901     # begin some normalization
2902     $section->{'subtotal'} = $section->{'amount'}
2903       if $multisection
2904          && !exists($section->{subtotal})
2905          && exists($section->{amount});
2906
2907     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2908       if ( $invoice_data{finance_section} &&
2909            $section->{'description'} eq $invoice_data{finance_section} );
2910
2911     $section->{'subtotal'} = $other_money_char.
2912                              sprintf('%.2f', $section->{'subtotal'})
2913       if $multisection;
2914
2915     # continue some normalization
2916     $section->{'amount'}   = $section->{'subtotal'}
2917       if $multisection;
2918
2919
2920     if ( $section->{'description'} ) {
2921       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2922                    [ '', '' ],
2923                  );
2924     }
2925
2926     warn "$me   setting options\n"
2927       if $DEBUG > 1;
2928
2929     my $multilocation = scalar($cust_main->cust_location); #too expensive?
2930     my %options = ();
2931     $options{'section'} = $section if $multisection;
2932     $options{'format'} = $format;
2933     $options{'escape_function'} = $escape_function;
2934     $options{'format_function'} = sub { () } unless $unsquelched;
2935     $options{'unsquelched'} = $unsquelched;
2936     $options{'summary_page'} = $summarypage;
2937     $options{'skip_usage'} =
2938       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2939     $options{'multilocation'} = $multilocation;
2940     $options{'multisection'} = $multisection;
2941
2942     warn "$me   searching for line items\n"
2943       if $DEBUG > 1;
2944
2945     foreach my $line_item ( $self->_items_pkg(%options) ) {
2946
2947       warn "$me     adding line item $line_item\n"
2948         if $DEBUG > 1;
2949
2950       my $detail = {
2951         ext_description => [],
2952       };
2953       $detail->{'ref'} = $line_item->{'pkgnum'};
2954       $detail->{'quantity'} = $line_item->{'quantity'};
2955       $detail->{'section'} = $section;
2956       $detail->{'description'} = &$escape_function($line_item->{'description'});
2957       if ( exists $line_item->{'ext_description'} ) {
2958         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2959       }
2960       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2961                               $line_item->{'amount'};
2962       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2963                                  $line_item->{'unit_amount'};
2964       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2965   
2966       push @detail_items, $detail;
2967       push @buf, ( [ $detail->{'description'},
2968                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2969                    ],
2970                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2971                  );
2972     }
2973
2974     if ( $section->{'description'} ) {
2975       push @buf, ( ['','-----------'],
2976                    [ $section->{'description'}. ' sub-total',
2977                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2978                    ],
2979                    [ '', '' ],
2980                    [ '', '' ],
2981                  );
2982     }
2983   
2984   }
2985   
2986   $invoice_data{current_less_finance} =
2987     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2988
2989   if ( $multisection && !$conf->exists('disable_previous_balance')
2990     || $conf->exists('previous_balance-summary_only') )
2991   {
2992     unshift @sections, $previous_section if $pr_total;
2993   }
2994
2995   warn "$me adding taxes\n"
2996     if $DEBUG > 1;
2997
2998   foreach my $tax ( $self->_items_tax ) {
2999
3000     $taxtotal += $tax->{'amount'};
3001
3002     my $description = &$escape_function( $tax->{'description'} );
3003     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
3004
3005     if ( $multisection ) {
3006
3007       my $money = $old_latex ? '' : $money_char;
3008       push @detail_items, {
3009         ext_description => [],
3010         ref          => '',
3011         quantity     => '',
3012         description  => $description,
3013         amount       => $money. $amount,
3014         product_code => '',
3015         section      => $tax_section,
3016       };
3017
3018     } else {
3019
3020       push @total_items, {
3021         'total_item'   => $description,
3022         'total_amount' => $other_money_char. $amount,
3023       };
3024
3025     }
3026
3027     push @buf,[ $description,
3028                 $money_char. $amount,
3029               ];
3030
3031   }
3032   
3033   if ( $taxtotal ) {
3034     my $total = {};
3035     $total->{'total_item'} = 'Sub-total';
3036     $total->{'total_amount'} =
3037       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3038
3039     if ( $multisection ) {
3040       $tax_section->{'subtotal'} = $other_money_char.
3041                                    sprintf('%.2f', $taxtotal);
3042       $tax_section->{'pretotal'} = 'New charges sub-total '.
3043                                    $total->{'total_amount'};
3044       push @sections, $tax_section if $taxtotal;
3045     }else{
3046       unshift @total_items, $total;
3047     }
3048   }
3049   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3050
3051   push @buf,['','-----------'];
3052   push @buf,[( $conf->exists('disable_previous_balance') 
3053                ? 'Total Charges'
3054                : 'Total New Charges'
3055              ),
3056              $money_char. sprintf("%10.2f",$self->charged) ];
3057   push @buf,['',''];
3058
3059   {
3060     my $total = {};
3061     my $item = 'Total';
3062     $item = $conf->config('previous_balance-exclude_from_total')
3063          || 'Total New Charges'
3064       if $conf->exists('previous_balance-exclude_from_total');
3065     my $amount = $self->charged +
3066                    ( $conf->exists('disable_previous_balance') ||
3067                      $conf->exists('previous_balance-exclude_from_total')
3068                      ? 0
3069                      : $pr_total
3070                    );
3071     $total->{'total_item'} = &$embolden_function($item);
3072     $total->{'total_amount'} =
3073       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
3074     if ( $multisection ) {
3075       if ( $adjust_section->{'sort_weight'} ) {
3076         $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3077           sprintf("%.2f", ($self->billing_balance || 0) );
3078       } else {
3079         $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3080                                         sprintf('%.2f', $self->charged );
3081       } 
3082     }else{
3083       push @total_items, $total;
3084     }
3085     push @buf,['','-----------'];
3086     push @buf,[$item,
3087                $money_char.
3088                sprintf( '%10.2f', $amount )
3089               ];
3090     push @buf,['',''];
3091   }
3092   
3093   unless ( $conf->exists('disable_previous_balance') ) {
3094     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3095   
3096     # credits
3097     my $credittotal = 0;
3098     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3099
3100       my $total;
3101       $total->{'total_item'} = &$escape_function($credit->{'description'});
3102       $credittotal += $credit->{'amount'};
3103       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3104       $adjusttotal += $credit->{'amount'};
3105       if ( $multisection ) {
3106         my $money = $old_latex ? '' : $money_char;
3107         push @detail_items, {
3108           ext_description => [],
3109           ref          => '',
3110           quantity     => '',
3111           description  => &$escape_function($credit->{'description'}),
3112           amount       => $money. $credit->{'amount'},
3113           product_code => '',
3114           section      => $adjust_section,
3115         };
3116       } else {
3117         push @total_items, $total;
3118       }
3119
3120     }
3121     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3122
3123     #credits (again)
3124     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3125       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3126     }
3127
3128     # payments
3129     my $paymenttotal = 0;
3130     foreach my $payment ( $self->_items_payments ) {
3131       my $total = {};
3132       $total->{'total_item'} = &$escape_function($payment->{'description'});
3133       $paymenttotal += $payment->{'amount'};
3134       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3135       $adjusttotal += $payment->{'amount'};
3136       if ( $multisection ) {
3137         my $money = $old_latex ? '' : $money_char;
3138         push @detail_items, {
3139           ext_description => [],
3140           ref          => '',
3141           quantity     => '',
3142           description  => &$escape_function($payment->{'description'}),
3143           amount       => $money. $payment->{'amount'},
3144           product_code => '',
3145           section      => $adjust_section,
3146         };
3147       }else{
3148         push @total_items, $total;
3149       }
3150       push @buf, [ $payment->{'description'},
3151                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
3152                  ];
3153     }
3154     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3155   
3156     if ( $multisection ) {
3157       $adjust_section->{'subtotal'} = $other_money_char.
3158                                       sprintf('%.2f', $adjusttotal);
3159       push @sections, $adjust_section
3160         unless $adjust_section->{sort_weight};
3161     }
3162
3163     { 
3164       my $total;
3165       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3166       $total->{'total_amount'} =
3167         &$embolden_function(
3168           $other_money_char. sprintf('%.2f', $summarypage 
3169                                                ? $self->charged +
3170                                                  $self->billing_balance
3171                                                : $self->owed + $pr_total
3172                                     )
3173         );
3174       if ( $multisection && !$adjust_section->{sort_weight} ) {
3175         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3176                                          $total->{'total_amount'};
3177       }else{
3178         push @total_items, $total;
3179       }
3180       push @buf,['','-----------'];
3181       push @buf,[$self->balance_due_msg, $money_char. 
3182         sprintf("%10.2f", $balance_due ) ];
3183     }
3184
3185     if ( $conf->exists('previous_balance-show_credit')
3186         and $cust_main->balance < 0 ) {
3187       my $credit_total = {
3188         'total_item'    => &$embolden_function($self->credit_balance_msg),
3189         'total_amount'  => &$embolden_function(
3190           $other_money_char. sprintf('%.2f', -$cust_main->balance)
3191         ),
3192       };
3193       if ( $multisection ) {
3194         $adjust_section->{'posttotal'} .= $newline_token .
3195           $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3196       }
3197       else {
3198         push @total_items, $credit_total;
3199       }
3200       push @buf,['','-----------'];
3201       push @buf,[$self->credit_balance_msg, $money_char. 
3202         sprintf("%10.2f", -$cust_main->balance ) ];
3203     }
3204   }
3205
3206   if ( $multisection ) {
3207     if ($conf->exists('svc_phone_sections')) {
3208       my $total;
3209       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3210       $total->{'total_amount'} =
3211         &$embolden_function(
3212           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3213         );
3214       my $last_section = pop @sections;
3215       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3216                                      $total->{'total_amount'};
3217       push @sections, $last_section;
3218     }
3219     push @sections, @$late_sections
3220       if $unsquelched;
3221   }
3222
3223   my @includelist = ();
3224   push @includelist, 'summary' if $summarypage;
3225   foreach my $include ( @includelist ) {
3226
3227     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3228     my @inc_src;
3229
3230     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3231
3232       @inc_src = $conf->config($inc_file, $agentnum);
3233
3234     } else {
3235
3236       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3237
3238       my $convert_map = $convert_maps{$format}{$include};
3239
3240       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3241                        s/--\@\]/$delimiters{$format}[1]/g;
3242                        $_;
3243                      } 
3244                  &$convert_map( $conf->config($inc_file, $agentnum) );
3245
3246     }
3247
3248     my $inc_tt = new Text::Template (
3249       TYPE       => 'ARRAY',
3250       SOURCE     => [ map "$_\n", @inc_src ],
3251       DELIMITERS => $delimiters{$format},
3252     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3253
3254     unless ( $inc_tt->compile() ) {
3255       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3256       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3257       die $error;
3258     }
3259
3260     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3261
3262     $invoice_data{$include} =~ s/\n+$//
3263       if ($format eq 'latex');
3264   }
3265
3266   $invoice_lines = 0;
3267   my $wasfunc = 0;
3268   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3269     /invoice_lines\((\d*)\)/;
3270     $invoice_lines += $1 || scalar(@buf);
3271     $wasfunc=1;
3272   }
3273   die "no invoice_lines() functions in template?"
3274     if ( $format eq 'template' && !$wasfunc );
3275
3276   if ($format eq 'template') {
3277
3278     if ( $invoice_lines ) {
3279       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3280       $invoice_data{'total_pages'}++
3281         if scalar(@buf) % $invoice_lines;
3282     }
3283
3284     #setup subroutine for the template
3285     sub FS::cust_bill::_template::invoice_lines {
3286       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3287       map { 
3288         scalar(@FS::cust_bill::_template::buf)
3289           ? shift @FS::cust_bill::_template::buf
3290           : [ '', '' ];
3291       }
3292       ( 1 .. $lines );
3293     }
3294
3295     my $lines;
3296     my @collect;
3297     while (@buf) {
3298       push @collect, split("\n",
3299         $text_template->fill_in( HASH => \%invoice_data,
3300                                  PACKAGE => 'FS::cust_bill::_template'
3301                                )
3302       );
3303       $FS::cust_bill::_template::page++;
3304     }
3305     map "$_\n", @collect;
3306   }else{
3307     warn "filling in template for invoice ". $self->invnum. "\n"
3308       if $DEBUG;
3309     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3310       if $DEBUG > 1;
3311
3312     $text_template->fill_in(HASH => \%invoice_data);
3313   }
3314 }
3315
3316 # helper routine for generating date ranges
3317 sub _prior_month30s {
3318   my $self = shift;
3319   my @ranges = (
3320    [ 1,       2592000 ], # 0-30 days ago
3321    [ 2592000, 5184000 ], # 30-60 days ago
3322    [ 5184000, 7776000 ], # 60-90 days ago
3323    [ 7776000, 0       ], # 90+   days ago
3324   );
3325
3326   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3327           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3328       ] }
3329   @ranges;
3330 }
3331
3332 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3333
3334 Returns an postscript invoice, as a scalar.
3335
3336 Options can be passed as a hashref (recommended) or as a list of time, template
3337 and then any key/value pairs for any other options.
3338
3339 I<time> an optional value used to control the printing of overdue messages.  The
3340 default is now.  It isn't the date of the invoice; that's the `_date' field.
3341 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3342 L<Time::Local> and L<Date::Parse> for conversion functions.
3343
3344 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3345
3346 =cut
3347
3348 sub print_ps {
3349   my $self = shift;
3350
3351   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3352   my $ps = generate_ps($file);
3353   unlink($logofile);
3354   unlink($barcodefile) if $barcodefile;
3355
3356   $ps;
3357 }
3358
3359 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3360
3361 Returns an PDF invoice, as a scalar.
3362
3363 Options can be passed as a hashref (recommended) or as a list of time, template
3364 and then any key/value pairs for any other options.
3365
3366 I<time> an optional value used to control the printing of overdue messages.  The
3367 default is now.  It isn't the date of the invoice; that's the `_date' field.
3368 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3369 L<Time::Local> and L<Date::Parse> for conversion functions.
3370
3371 I<template>, if specified, is the name of a suffix for alternate invoices.
3372
3373 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3374
3375 =cut
3376
3377 sub print_pdf {
3378   my $self = shift;
3379
3380   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3381   my $pdf = generate_pdf($file);
3382   unlink($logofile);
3383   unlink($barcodefile) if $barcodefile;
3384
3385   $pdf;
3386 }
3387
3388 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3389
3390 Returns an HTML invoice, as a scalar.
3391
3392 I<time> an optional value used to control the printing of overdue messages.  The
3393 default is now.  It isn't the date of the invoice; that's the `_date' field.
3394 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3395 L<Time::Local> and L<Date::Parse> for conversion functions.
3396
3397 I<template>, if specified, is the name of a suffix for alternate invoices.
3398
3399 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3400
3401 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3402 when emailing the invoice as part of a multipart/related MIME email.
3403
3404 =cut
3405
3406 sub print_html {
3407   my $self = shift;
3408   my %params;
3409   if ( ref($_[0]) ) {
3410     %params = %{ shift() }; 
3411   }else{
3412     $params{'time'} = shift;
3413     $params{'template'} = shift;
3414     $params{'cid'} = shift;
3415   }
3416
3417   $params{'format'} = 'html';
3418   
3419   $self->print_generic( %params );
3420 }
3421
3422 # quick subroutine for print_latex
3423 #
3424 # There are ten characters that LaTeX treats as special characters, which
3425 # means that they do not simply typeset themselves: 
3426 #      # $ % & ~ _ ^ \ { }
3427 #
3428 # TeX ignores blanks following an escaped character; if you want a blank (as
3429 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3430
3431 sub _latex_escape {
3432   my $value = shift;
3433   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3434   $value =~ s/([<>])/\$$1\$/g;
3435   $value;
3436 }
3437
3438 sub _html_escape {
3439   my $value = shift;
3440   encode_entities($value);
3441   $value;
3442 }
3443
3444 sub _html_escape_nbsp {
3445   my $value = _html_escape(shift);
3446   $value =~ s/ +/&nbsp;/g;
3447   $value;
3448 }
3449
3450 #utility methods for print_*
3451
3452 sub _translate_old_latex_format {
3453   warn "_translate_old_latex_format called\n"
3454     if $DEBUG; 
3455
3456   my @template = ();
3457   while ( @_ ) {
3458     my $line = shift;
3459   
3460     if ( $line =~ /^%%Detail\s*$/ ) {
3461   
3462       push @template, q![@--!,
3463                       q!  foreach my $_tr_line (@detail_items) {!,
3464                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3465                       q!      $_tr_line->{'description'} .= !, 
3466                       q!        "\\tabularnewline\n~~".!,
3467                       q!        join( "\\tabularnewline\n~~",!,
3468                       q!          @{$_tr_line->{'ext_description'}}!,
3469                       q!        );!,
3470                       q!    }!;
3471
3472       while ( ( my $line_item_line = shift )
3473               !~ /^%%EndDetail\s*$/                            ) {
3474         $line_item_line =~ s/'/\\'/g;    # nice LTS
3475         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3476         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3477         push @template, "    \$OUT .= '$line_item_line';";
3478       }
3479
3480       push @template, '}',
3481                       '--@]';
3482       #' doh, gvim
3483     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3484
3485       push @template, '[@--',
3486                       '  foreach my $_tr_line (@total_items) {';
3487
3488       while ( ( my $total_item_line = shift )
3489               !~ /^%%EndTotalDetails\s*$/                      ) {
3490         $total_item_line =~ s/'/\\'/g;    # nice LTS
3491         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3492         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3493         push @template, "    \$OUT .= '$total_item_line';";
3494       }
3495
3496       push @template, '}',
3497                       '--@]';
3498
3499     } else {
3500       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3501       push @template, $line;  
3502     }
3503   
3504   }
3505
3506   if ($DEBUG) {
3507     warn "$_\n" foreach @template;
3508   }
3509
3510   (@template);
3511 }
3512
3513 sub terms {
3514   my $self = shift;
3515
3516   #check for an invoice-specific override
3517   return $self->invoice_terms if $self->invoice_terms;
3518   
3519   #check for a customer- specific override
3520   my $cust_main = $self->cust_main;
3521   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3522
3523   #use configured default
3524   $conf->config('invoice_default_terms') || '';
3525 }
3526
3527 sub due_date {
3528   my $self = shift;
3529   my $duedate = '';
3530   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3531     $duedate = $self->_date() + ( $1 * 86400 );
3532   }
3533   $duedate;
3534 }
3535
3536 sub due_date2str {
3537   my $self = shift;
3538   $self->due_date ? time2str(shift, $self->due_date) : '';
3539 }
3540
3541 sub balance_due_msg {
3542   my $self = shift;
3543   my $msg = 'Balance Due';
3544   return $msg unless $self->terms;
3545   if ( $self->due_date ) {
3546     $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3547   } elsif ( $self->terms ) {
3548     $msg .= ' - '. $self->terms;
3549   }
3550   $msg;
3551 }
3552
3553 sub balance_due_date {
3554   my $self = shift;
3555   my $duedate = '';
3556   if (    $conf->exists('invoice_default_terms') 
3557        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3558     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3559   }
3560   $duedate;
3561 }
3562
3563 sub credit_balance_msg { 'Credit Balance Remaining' }
3564
3565 =item invnum_date_pretty
3566
3567 Returns a string with the invoice number and date, for example:
3568 "Invoice #54 (3/20/2008)"
3569
3570 =cut
3571
3572 sub invnum_date_pretty {
3573   my $self = shift;
3574   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3575 }
3576
3577 =item _date_pretty
3578
3579 Returns a string with the date, for example: "3/20/2008"
3580
3581 =cut
3582
3583 sub _date_pretty {
3584   my $self = shift;
3585   time2str($date_format, $self->_date);
3586 }
3587
3588 use vars qw(%pkg_category_cache);
3589 sub _items_sections {
3590   my $self = shift;
3591   my $late = shift;
3592   my $summarypage = shift;
3593   my $escape = shift;
3594   my $extra_sections = shift;
3595   my $format = shift;
3596
3597   my %subtotal = ();
3598   my %late_subtotal = ();
3599   my %not_tax = ();
3600
3601   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3602   {
3603
3604       my $usage = $cust_bill_pkg->usage;
3605
3606       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3607         next if ( $display->summary && $summarypage );
3608
3609         my $section = $display->section;
3610         my $type    = $display->type;
3611
3612         $not_tax{$section} = 1
3613           unless $cust_bill_pkg->pkgnum == 0;
3614
3615         if ( $display->post_total && !$summarypage ) {
3616           if (! $type || $type eq 'S') {
3617             $late_subtotal{$section} += $cust_bill_pkg->setup
3618               if $cust_bill_pkg->setup != 0;
3619           }
3620
3621           if (! $type) {
3622             $late_subtotal{$section} += $cust_bill_pkg->recur
3623               if $cust_bill_pkg->recur != 0;
3624           }
3625
3626           if ($type && $type eq 'R') {
3627             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3628               if $cust_bill_pkg->recur != 0;
3629           }
3630           
3631           if ($type && $type eq 'U') {
3632             $late_subtotal{$section} += $usage
3633               unless scalar(@$extra_sections);
3634           }
3635
3636         } else {
3637
3638           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3639
3640           if (! $type || $type eq 'S') {
3641             $subtotal{$section} += $cust_bill_pkg->setup
3642               if $cust_bill_pkg->setup != 0;
3643           }
3644
3645           if (! $type) {
3646             $subtotal{$section} += $cust_bill_pkg->recur
3647               if $cust_bill_pkg->recur != 0;
3648           }
3649
3650           if ($type && $type eq 'R') {
3651             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3652               if $cust_bill_pkg->recur != 0;
3653           }
3654           
3655           if ($type && $type eq 'U') {
3656             $subtotal{$section} += $usage
3657               unless scalar(@$extra_sections);
3658           }
3659
3660         }
3661
3662       }
3663
3664   }
3665
3666   %pkg_category_cache = ();
3667
3668   push @$late, map { { 'description' => &{$escape}($_),
3669                        'subtotal'    => $late_subtotal{$_},
3670                        'post_total'  => 1,
3671                        'sort_weight' => ( _pkg_category($_)
3672                                             ? _pkg_category($_)->weight
3673                                             : 0
3674                                        ),
3675                        ((_pkg_category($_) && _pkg_category($_)->condense)
3676                                            ? $self->_condense_section($format)
3677                                            : ()
3678                        ),
3679                    } }
3680                  sort _sectionsort keys %late_subtotal;
3681
3682   my @sections;
3683   if ( $summarypage ) {
3684     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3685                 map { $_->categoryname } qsearch('pkg_category', {});
3686     push @sections, '' if exists($subtotal{''});
3687   } else {
3688     @sections = keys %subtotal;
3689   }
3690
3691   my @early = map { { 'description' => &{$escape}($_),
3692                       'subtotal'    => $subtotal{$_},
3693                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3694                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3695                       'sort_weight' => ( _pkg_category($_)
3696                                            ? _pkg_category($_)->weight
3697                                            : 0
3698                                        ),
3699                        ((_pkg_category($_) && _pkg_category($_)->condense)
3700                                            ? $self->_condense_section($format)
3701                                            : ()
3702                        ),
3703                     }
3704                   } @sections;
3705   push @early, @$extra_sections if $extra_sections;
3706
3707   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3708
3709 }
3710
3711 #helper subs for above
3712
3713 sub _sectionsort {
3714   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3715 }
3716
3717 sub _pkg_category {
3718   my $categoryname = shift;
3719   $pkg_category_cache{$categoryname} ||=
3720     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3721 }
3722
3723 my %condensed_format = (
3724   'label' => [ qw( Description Qty Amount ) ],
3725   'fields' => [
3726                 sub { shift->{description} },
3727                 sub { shift->{quantity} },
3728                 sub { my($href, %opt) = @_;
3729                       ($opt{dollar} || ''). $href->{amount};
3730                     },
3731               ],
3732   'align'  => [ qw( l r r ) ],
3733   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3734   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3735 );
3736
3737 sub _condense_section {
3738   my ( $self, $format ) = ( shift, shift );
3739   ( 'condensed' => 1,
3740     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3741       qw( description_generator
3742           header_generator
3743           total_generator
3744           total_line_generator
3745         )
3746   );
3747 }
3748
3749 sub _condensed_generator_defaults {
3750   my ( $self, $format ) = ( shift, shift );
3751   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3752 }
3753
3754 my %html_align = (
3755   'c' => 'center',
3756   'l' => 'left',
3757   'r' => 'right',
3758 );
3759
3760 sub _condensed_header_generator {
3761   my ( $self, $format ) = ( shift, shift );
3762
3763   my ( $f, $prefix, $suffix, $separator, $column ) =
3764     _condensed_generator_defaults($format);
3765
3766   if ($format eq 'latex') {
3767     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3768     $suffix = "\\\\\n\\hline";
3769     $separator = "&\n";
3770     $column =
3771       sub { my ($d,$a,$s,$w) = @_;
3772             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3773           };
3774   } elsif ( $format eq 'html' ) {
3775     $prefix = '<th></th>';
3776     $suffix = '';
3777     $separator = '';
3778     $column =
3779       sub { my ($d,$a,$s,$w) = @_;
3780             return qq!<th align="$html_align{$a}">$d</th>!;
3781       };
3782   }
3783
3784   sub {
3785     my @args = @_;
3786     my @result = ();
3787
3788     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3789       push @result,
3790         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3791     }
3792
3793     $prefix. join($separator, @result). $suffix;
3794   };
3795
3796 }
3797
3798 sub _condensed_description_generator {
3799   my ( $self, $format ) = ( shift, shift );
3800
3801   my ( $f, $prefix, $suffix, $separator, $column ) =
3802     _condensed_generator_defaults($format);
3803
3804   my $money_char = '$';
3805   if ($format eq 'latex') {
3806     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3807     $suffix = '\\\\';
3808     $separator = " & \n";
3809     $column =
3810       sub { my ($d,$a,$s,$w) = @_;
3811             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3812           };
3813     $money_char = '\\dollar';
3814   }elsif ( $format eq 'html' ) {
3815     $prefix = '"><td align="center"></td>';
3816     $suffix = '';
3817     $separator = '';
3818     $column =
3819       sub { my ($d,$a,$s,$w) = @_;
3820             return qq!<td align="$html_align{$a}">$d</td>!;
3821       };
3822     #$money_char = $conf->config('money_char') || '$';
3823     $money_char = '';  # this is madness
3824   }
3825
3826   sub {
3827     #my @args = @_;
3828     my $href = shift;
3829     my @result = ();
3830
3831     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3832       my $dollar = '';
3833       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3834       push @result,
3835         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3836                     map { $f->{$_}->[$i] } qw(align span width)
3837                   );
3838     }
3839
3840     $prefix. join( $separator, @result ). $suffix;
3841   };
3842
3843 }
3844
3845 sub _condensed_total_generator {
3846   my ( $self, $format ) = ( shift, shift );
3847
3848   my ( $f, $prefix, $suffix, $separator, $column ) =
3849     _condensed_generator_defaults($format);
3850   my $style = '';
3851
3852   if ($format eq 'latex') {
3853     $prefix = "& ";
3854     $suffix = "\\\\\n";
3855     $separator = " & \n";
3856     $column =
3857       sub { my ($d,$a,$s,$w) = @_;
3858             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3859           };
3860   }elsif ( $format eq 'html' ) {
3861     $prefix = '';
3862     $suffix = '';
3863     $separator = '';
3864     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3865     $column =
3866       sub { my ($d,$a,$s,$w) = @_;
3867             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3868       };
3869   }
3870
3871
3872   sub {
3873     my @args = @_;
3874     my @result = ();
3875
3876     #  my $r = &{$f->{fields}->[$i]}(@args);
3877     #  $r .= ' Total' unless $i;
3878
3879     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3880       push @result,
3881         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3882                     map { $f->{$_}->[$i] } qw(align span width)
3883                   );
3884     }
3885
3886     $prefix. join( $separator, @result ). $suffix;
3887   };
3888
3889 }
3890
3891 =item total_line_generator FORMAT
3892
3893 Returns a coderef used for generation of invoice total line items for this
3894 usage_class.  FORMAT is either html or latex
3895
3896 =cut
3897
3898 # should not be used: will have issues with hash element names (description vs
3899 # total_item and amount vs total_amount -- another array of functions?
3900
3901 sub _condensed_total_line_generator {
3902   my ( $self, $format ) = ( shift, shift );
3903
3904   my ( $f, $prefix, $suffix, $separator, $column ) =
3905     _condensed_generator_defaults($format);
3906   my $style = '';
3907
3908   if ($format eq 'latex') {
3909     $prefix = "& ";
3910     $suffix = "\\\\\n";
3911     $separator = " & \n";
3912     $column =
3913       sub { my ($d,$a,$s,$w) = @_;
3914             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3915           };
3916   }elsif ( $format eq 'html' ) {
3917     $prefix = '';
3918     $suffix = '';
3919     $separator = '';
3920     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3921     $column =
3922       sub { my ($d,$a,$s,$w) = @_;
3923             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3924       };
3925   }
3926
3927
3928   sub {
3929     my @args = @_;
3930     my @result = ();
3931
3932     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3933       push @result,
3934         &{$column}( &{$f->{fields}->[$i]}(@args),
3935                     map { $f->{$_}->[$i] } qw(align span width)
3936                   );
3937     }
3938
3939     $prefix. join( $separator, @result ). $suffix;
3940   };
3941
3942 }
3943
3944 #sub _items_extra_usage_sections {
3945 #  my $self = shift;
3946 #  my $escape = shift;
3947 #
3948 #  my %sections = ();
3949 #
3950 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
3951 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3952 #  {
3953 #    next unless $cust_bill_pkg->pkgnum > 0;
3954 #
3955 #    foreach my $section ( keys %usage_class ) {
3956 #
3957 #      my $usage = $cust_bill_pkg->usage($section);
3958 #
3959 #      next unless $usage && $usage > 0;
3960 #
3961 #      $sections{$section} ||= 0;
3962 #      $sections{$section} += $usage;
3963 #
3964 #    }
3965 #
3966 #  }
3967 #
3968 #  map { { 'description' => &{$escape}($_),
3969 #          'subtotal'    => $sections{$_},
3970 #          'summarized'  => '',
3971 #          'tax_section' => '',
3972 #        }
3973 #      }
3974 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3975 #
3976 #}
3977
3978 sub _items_extra_usage_sections {
3979   my $self = shift;
3980   my $escape = shift;
3981   my $format = shift;
3982
3983   my %sections = ();
3984   my %classnums = ();
3985   my %lines = ();
3986
3987   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3988   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3989     next unless $cust_bill_pkg->pkgnum > 0;
3990
3991     foreach my $classnum ( keys %usage_class ) {
3992       my $section = $usage_class{$classnum}->classname;
3993       $classnums{$section} = $classnum;
3994
3995       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3996         my $amount = $detail->amount;
3997         next unless $amount && $amount > 0;
3998  
3999         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4000         $sections{$section}{amount} += $amount;  #subtotal
4001         $sections{$section}{calls}++;
4002         $sections{$section}{duration} += $detail->duration;
4003
4004         my $desc = $detail->regionname; 
4005         my $description = $desc;
4006         $description = substr($desc, 0, 50). '...'
4007           if $format eq 'latex' && length($desc) > 50;
4008
4009         $lines{$section}{$desc} ||= {
4010           description     => &{$escape}($description),
4011           #pkgpart         => $part_pkg->pkgpart,
4012           pkgnum          => $cust_bill_pkg->pkgnum,
4013           ref             => '',
4014           amount          => 0,
4015           calls           => 0,
4016           duration        => 0,
4017           #unit_amount     => $cust_bill_pkg->unitrecur,
4018           quantity        => $cust_bill_pkg->quantity,
4019           product_code    => 'N/A',
4020           ext_description => [],
4021         };
4022
4023         $lines{$section}{$desc}{amount} += $amount;
4024         $lines{$section}{$desc}{calls}++;
4025         $lines{$section}{$desc}{duration} += $detail->duration;
4026
4027       }
4028     }
4029   }
4030
4031   my %sectionmap = ();
4032   foreach (keys %sections) {
4033     my $usage_class = $usage_class{$classnums{$_}};
4034     $sectionmap{$_} = { 'description' => &{$escape}($_),
4035                         'amount'    => $sections{$_}{amount},    #subtotal
4036                         'calls'       => $sections{$_}{calls},
4037                         'duration'    => $sections{$_}{duration},
4038                         'summarized'  => '',
4039                         'tax_section' => '',
4040                         'sort_weight' => $usage_class->weight,
4041                         ( $usage_class->format
4042                           ? ( map { $_ => $usage_class->$_($format) }
4043                               qw( description_generator header_generator total_generator total_line_generator )
4044                             )
4045                           : ()
4046                         ), 
4047                       };
4048   }
4049
4050   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4051                  values %sectionmap;
4052
4053   my @lines = ();
4054   foreach my $section ( keys %lines ) {
4055     foreach my $line ( keys %{$lines{$section}} ) {
4056       my $l = $lines{$section}{$line};
4057       $l->{section}     = $sectionmap{$section};
4058       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4059       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4060       push @lines, $l;
4061     }
4062   }
4063
4064   return(\@sections, \@lines);
4065
4066 }
4067
4068 sub _did_summary {
4069     my $self = shift;
4070     my $end = $self->_date;
4071
4072     # start at date of previous invoice + 1 second or 0 if no previous invoice
4073     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4074     $start = 0 if !$start;
4075     $start++;
4076
4077     my $cust_main = $self->cust_main;
4078     my @pkgs = $cust_main->all_pkgs;
4079     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4080         = (0,0,0,0,0);
4081     my @seen = ();
4082     foreach my $pkg ( @pkgs ) {
4083         my @h_cust_svc = $pkg->h_cust_svc($end);
4084         foreach my $h_cust_svc ( @h_cust_svc ) {
4085             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4086             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4087
4088             my $inserted = $h_cust_svc->date_inserted;
4089             my $deleted = $h_cust_svc->date_deleted;
4090             my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4091             my $phone_deleted;
4092             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
4093             
4094 # DID either activated or ported in; cannot be both for same DID simultaneously
4095             if ($inserted >= $start && $inserted <= $end && $phone_inserted
4096                 && (!$phone_inserted->lnp_status 
4097                     || $phone_inserted->lnp_status eq ''
4098                     || $phone_inserted->lnp_status eq 'native')) {
4099                 $num_activated++;
4100             }
4101             else { # this one not so clean, should probably move to (h_)svc_phone
4102                  my $phone_portedin = qsearchs( 'h_svc_phone',
4103                       { 'svcnum' => $h_cust_svc->svcnum, 
4104                         'lnp_status' => 'portedin' },  
4105                       FS::h_svc_phone->sql_h_searchs($end),  
4106                     );
4107                  $num_portedin++ if $phone_portedin;
4108             }
4109
4110 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4111             if($deleted >= $start && $deleted <= $end && $phone_deleted
4112                 && (!$phone_deleted->lnp_status 
4113                     || $phone_deleted->lnp_status ne 'portingout')) {
4114                 $num_deactivated++;
4115             } 
4116             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
4117                 && $phone_deleted->lnp_status 
4118                 && $phone_deleted->lnp_status eq 'portingout') {
4119                 $num_portedout++;
4120             }
4121
4122             # increment usage minutes
4123             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4124             foreach my $cdr ( @cdrs ) {
4125                 $minutes += $cdr->billsec/60;
4126             }
4127
4128             # don't look at this service again
4129             push @seen, $h_cust_svc->svcnum;
4130         }
4131     }
4132
4133     $minutes = sprintf("%d", $minutes);
4134     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
4135         . "$num_deactivated  Ported-Out: $num_portedout ",
4136             "Total Minutes: $minutes");
4137 }
4138
4139 sub _items_accountcode_cdr {
4140     my $self = shift;
4141     my $escape = shift;
4142     my $format = shift;
4143
4144     my $section = { 'amount'        => 0,
4145                     'calls'         => 0,
4146                     'duration'      => 0,
4147                     'sort_weight'   => '',
4148                     'phonenum'      => '',
4149                     'description'   => 'Usage by Account Code',
4150                     'post_total'    => '',
4151                     'summarized'    => '',
4152                     'total_generator' => sub { '' },
4153                     'header'        => '',
4154                   };
4155     my @lines;
4156     my %accountcodes = ();
4157
4158     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4159         next unless $cust_bill_pkg->pkgnum > 0;
4160
4161         my @header = $cust_bill_pkg->details_header;
4162         next unless scalar(@header);
4163         $section->{'header'} = join(',',@header);
4164
4165         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4166
4167             $section->{'header'} = $detail->formatted('format' => $format)
4168                 if($detail->detail eq $section->{'header'}); 
4169       
4170             my $accountcode = $detail->accountcode;
4171             next unless $accountcode;
4172
4173             my $amount = $detail->amount;
4174             next unless $amount && $amount > 0;
4175
4176             $accountcodes{$accountcode} ||= {
4177                     description => $accountcode,
4178                     pkgnum      => '',
4179                     ref         => '',
4180                     amount      => 0,
4181                     calls       => 0,
4182                     duration    => 0,
4183                     quantity    => '',
4184                     product_code => 'N/A',
4185                     section     => $section,
4186                     ext_description => [],
4187             };
4188
4189             $accountcodes{$accountcode}{'amount'} += $amount;
4190             $accountcodes{$accountcode}{calls}++;
4191             $accountcodes{$accountcode}{duration} += $detail->duration;
4192             push @{$accountcodes{$accountcode}{ext_description}},
4193                 $detail->formatted('format' => $format);
4194         }
4195     }
4196
4197     foreach my $l ( values %accountcodes ) {
4198         $l->{amount} = sprintf( "%.2f", $l->{amount} );
4199         unshift @{$l->{ext_description}}, $section->{'header'};
4200         push @lines, $l;
4201     }
4202
4203     return ($section,\@lines);
4204 }
4205
4206 sub _items_svc_phone_sections {
4207   my $self = shift;
4208   my $escape = shift;
4209   my $format = shift;
4210
4211   my %sections = ();
4212   my %classnums = ();
4213   my %lines = ();
4214
4215   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4216   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4217
4218   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4219     next unless $cust_bill_pkg->pkgnum > 0;
4220
4221     my @header = $cust_bill_pkg->details_header;
4222     next unless scalar(@header);
4223
4224     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4225
4226       my $phonenum = $detail->phonenum;
4227       next unless $phonenum;
4228
4229       my $amount = $detail->amount;
4230       next unless $amount && $amount > 0;
4231
4232       $sections{$phonenum} ||= { 'amount'      => 0,
4233                                  'calls'       => 0,
4234                                  'duration'    => 0,
4235                                  'sort_weight' => -1,
4236                                  'phonenum'    => $phonenum,
4237                                 };
4238       $sections{$phonenum}{amount} += $amount;  #subtotal
4239       $sections{$phonenum}{calls}++;
4240       $sections{$phonenum}{duration} += $detail->duration;
4241
4242       my $desc = $detail->regionname; 
4243       my $description = $desc;
4244       $description = substr($desc, 0, 50). '...'
4245         if $format eq 'latex' && length($desc) > 50;
4246
4247       $lines{$phonenum}{$desc} ||= {
4248         description     => &{$escape}($description),
4249         #pkgpart         => $part_pkg->pkgpart,
4250         pkgnum          => '',
4251         ref             => '',
4252         amount          => 0,
4253         calls           => 0,
4254         duration        => 0,
4255         #unit_amount     => '',
4256         quantity        => '',
4257         product_code    => 'N/A',
4258         ext_description => [],
4259       };
4260
4261       $lines{$phonenum}{$desc}{amount} += $amount;
4262       $lines{$phonenum}{$desc}{calls}++;
4263       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4264
4265       my $line = $usage_class{$detail->classnum}->classname;
4266       $sections{"$phonenum $line"} ||=
4267         { 'amount' => 0,
4268           'calls' => 0,
4269           'duration' => 0,
4270           'sort_weight' => $usage_class{$detail->classnum}->weight,
4271           'phonenum' => $phonenum,
4272           'header'  => [ @header ],
4273         };
4274       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4275       $sections{"$phonenum $line"}{calls}++;
4276       $sections{"$phonenum $line"}{duration} += $detail->duration;
4277
4278       $lines{"$phonenum $line"}{$desc} ||= {
4279         description     => &{$escape}($description),
4280         #pkgpart         => $part_pkg->pkgpart,
4281         pkgnum          => '',
4282         ref             => '',
4283         amount          => 0,
4284         calls           => 0,
4285         duration        => 0,
4286         #unit_amount     => '',
4287         quantity        => '',
4288         product_code    => 'N/A',
4289         ext_description => [],
4290       };
4291
4292       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4293       $lines{"$phonenum $line"}{$desc}{calls}++;
4294       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4295       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4296            $detail->formatted('format' => $format);
4297
4298     }
4299   }
4300
4301   my %sectionmap = ();
4302   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4303   foreach ( keys %sections ) {
4304     my @header = @{ $sections{$_}{header} || [] };
4305     my $usage_simple =
4306       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4307     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4308     my $usage_class = $summary ? $simple : $usage_simple;
4309     my $ending = $summary ? ' usage charges' : '';
4310     my %gen_opt = ();
4311     unless ($summary) {
4312       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4313     }
4314     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4315                         'amount'    => $sections{$_}{amount},    #subtotal
4316                         'calls'       => $sections{$_}{calls},
4317                         'duration'    => $sections{$_}{duration},
4318                         'summarized'  => '',
4319                         'tax_section' => '',
4320                         'phonenum'    => $sections{$_}{phonenum},
4321                         'sort_weight' => $sections{$_}{sort_weight},
4322                         'post_total'  => $summary, #inspire pagebreak
4323                         (
4324                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4325                             qw( description_generator
4326                                 header_generator
4327                                 total_generator
4328                                 total_line_generator
4329                               )
4330                           )
4331                         ), 
4332                       };
4333   }
4334
4335   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4336                         $a->{sort_weight} <=> $b->{sort_weight}
4337                       }
4338                  values %sectionmap;
4339
4340   my @lines = ();
4341   foreach my $section ( keys %lines ) {
4342     foreach my $line ( keys %{$lines{$section}} ) {
4343       my $l = $lines{$section}{$line};
4344       $l->{section}     = $sectionmap{$section};
4345       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4346       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4347       push @lines, $l;
4348     }
4349   }
4350   
4351   if($conf->exists('phone_usage_class_summary')) { 
4352       # this only works with Latex
4353       my @newlines;
4354       my @newsections;
4355
4356       # after this, we'll have only two sections per DID:
4357       # Calls Summary and Calls Detail
4358       foreach my $section ( @sections ) {
4359         if($section->{'post_total'}) {
4360             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4361             $section->{'total_line_generator'} = sub { '' };
4362             $section->{'total_generator'} = sub { '' };
4363             $section->{'header_generator'} = sub { '' };
4364             $section->{'description_generator'} = '';
4365             push @newsections, $section;
4366             my %calls_detail = %$section;
4367             $calls_detail{'post_total'} = '';
4368             $calls_detail{'sort_weight'} = '';
4369             $calls_detail{'description_generator'} = sub { '' };
4370             $calls_detail{'header_generator'} = sub {
4371                 return ' & Date/Time & Called Number & Duration & Price'
4372                     if $format eq 'latex';
4373                 '';
4374             };
4375             $calls_detail{'description'} = 'Calls Detail: '
4376                                                     . $section->{'phonenum'};
4377             push @newsections, \%calls_detail;  
4378         }
4379       }
4380
4381       # after this, each usage class is collapsed/summarized into a single
4382       # line under the Calls Summary section
4383       foreach my $newsection ( @newsections ) {
4384         if($newsection->{'post_total'}) { # this means Calls Summary
4385             foreach my $section ( @sections ) {
4386                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
4387                                 && !$section->{'post_total'});
4388                 my $newdesc = $section->{'description'};
4389                 my $tn = $section->{'phonenum'};
4390                 $newdesc =~ s/$tn//g;
4391                 my $line = {  ext_description => [],
4392                               pkgnum => '',
4393                               ref => '',
4394                               quantity => '',
4395                               calls => $section->{'calls'},
4396                               section => $newsection,
4397                               duration => $section->{'duration'},
4398                               description => $newdesc,
4399                               amount => sprintf("%.2f",$section->{'amount'}),
4400                               product_code => 'N/A',
4401                             };
4402                 push @newlines, $line;
4403             }
4404         }
4405       }
4406
4407       # after this, Calls Details is populated with all CDRs
4408       foreach my $newsection ( @newsections ) {
4409         if(!$newsection->{'post_total'}) { # this means Calls Details
4410             foreach my $line ( @lines ) {
4411                 next unless (scalar(@{$line->{'ext_description'}}) &&
4412                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4413                             );
4414                 my @extdesc = @{$line->{'ext_description'}};
4415                 my @newextdesc;
4416                 foreach my $extdesc ( @extdesc ) {
4417                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4418                     push @newextdesc, $extdesc;
4419                 }
4420                 $line->{'ext_description'} = \@newextdesc;
4421                 $line->{'section'} = $newsection;
4422                 push @newlines, $line;
4423             }
4424         }
4425       }
4426
4427       return(\@newsections, \@newlines);
4428   }
4429
4430   return(\@sections, \@lines);
4431
4432 }
4433
4434 sub _items {
4435   my $self = shift;
4436
4437   #my @display = scalar(@_)
4438   #              ? @_
4439   #              : qw( _items_previous _items_pkg );
4440   #              #: qw( _items_pkg );
4441   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4442   my @display = qw( _items_previous _items_pkg );
4443
4444   my @b = ();
4445   foreach my $display ( @display ) {
4446     push @b, $self->$display(@_);
4447   }
4448   @b;
4449 }
4450
4451 sub _items_previous {
4452   my $self = shift;
4453   my $cust_main = $self->cust_main;
4454   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4455   my @b = ();
4456   foreach ( @pr_cust_bill ) {
4457     my $date = $conf->exists('invoice_show_prior_due_date')
4458                ? 'due '. $_->due_date2str($date_format)
4459                : time2str($date_format, $_->_date);
4460     push @b, {
4461       'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4462       #'pkgpart'     => 'N/A',
4463       'pkgnum'      => 'N/A',
4464       'amount'      => sprintf("%.2f", $_->owed),
4465     };
4466   }
4467   @b;
4468
4469   #{
4470   #    'description'     => 'Previous Balance',
4471   #    #'pkgpart'         => 'N/A',
4472   #    'pkgnum'          => 'N/A',
4473   #    'amount'          => sprintf("%10.2f", $pr_total ),
4474   #    'ext_description' => [ map {
4475   #                                 "Invoice ". $_->invnum.
4476   #                                 " (". time2str("%x",$_->_date). ") ".
4477   #                                 sprintf("%10.2f", $_->owed)
4478   #                         } @pr_cust_bill ],
4479
4480   #};
4481 }
4482
4483 sub _items_pkg {
4484   my $self = shift;
4485   my %options = @_;
4486
4487   warn "$me _items_pkg searching for all package line items\n"
4488     if $DEBUG > 1;
4489
4490   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4491
4492   warn "$me _items_pkg filtering line items\n"
4493     if $DEBUG > 1;
4494   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4495
4496   if ($options{section} && $options{section}->{condensed}) {
4497
4498     warn "$me _items_pkg condensing section\n"
4499       if $DEBUG > 1;
4500
4501     my %itemshash = ();
4502     local $Storable::canonical = 1;
4503     foreach ( @items ) {
4504       my $item = { %$_ };
4505       delete $item->{ref};
4506       delete $item->{ext_description};
4507       my $key = freeze($item);
4508       $itemshash{$key} ||= 0;
4509       $itemshash{$key} ++; # += $item->{quantity};
4510     }
4511     @items = sort { $a->{description} cmp $b->{description} }
4512              map { my $i = thaw($_);
4513                    $i->{quantity} = $itemshash{$_};
4514                    $i->{amount} =
4515                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4516                    $i;
4517                  }
4518              keys %itemshash;
4519   }
4520
4521   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4522     if $DEBUG > 1;
4523
4524   @items;
4525 }
4526
4527 sub _taxsort {
4528   return 0 unless $a->itemdesc cmp $b->itemdesc;
4529   return -1 if $b->itemdesc eq 'Tax';
4530   return 1 if $a->itemdesc eq 'Tax';
4531   return -1 if $b->itemdesc eq 'Other surcharges';
4532   return 1 if $a->itemdesc eq 'Other surcharges';
4533   $a->itemdesc cmp $b->itemdesc;
4534 }
4535
4536 sub _items_tax {
4537   my $self = shift;
4538   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4539   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4540 }
4541
4542 sub _items_cust_bill_pkg {
4543   my $self = shift;
4544   my $cust_bill_pkgs = shift;
4545   my %opt = @_;
4546
4547   my $format = $opt{format} || '';
4548   my $escape_function = $opt{escape_function} || sub { shift };
4549   my $format_function = $opt{format_function} || '';
4550   my $unsquelched = $opt{unsquelched} || '';
4551   my $section = $opt{section}->{description} if $opt{section};
4552   my $summary_page = $opt{summary_page} || '';
4553   my $multilocation = $opt{multilocation} || '';
4554   my $multisection = $opt{multisection} || '';
4555   my $discount_show_always = 0;
4556
4557   my @b = ();
4558   my ($s, $r, $u) = ( undef, undef, undef );
4559   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4560   {
4561
4562     warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4563       if $DEBUG > 1;
4564
4565     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4566                                 && $conf->exists('discount-show-always'));
4567
4568     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4569       if ( $_ && !$cust_bill_pkg->hidden ) {
4570         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4571         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4572         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4573         push @b, { %$_ }
4574           unless ( $_->{amount} == 0 && !$discount_show_always );
4575         $_ = undef;
4576       }
4577     }
4578
4579     foreach my $display ( grep { defined($section)
4580                                  ? $_->section eq $section
4581                                  : 1
4582                                }
4583                           #grep { !$_->summary || !$summary_page } # bunk!
4584                           grep { !$_->summary || $multisection }
4585                           $cust_bill_pkg->cust_bill_pkg_display
4586                         )
4587     {
4588
4589       warn "$me _items_cust_bill_pkg considering display item $display\n"
4590         if $DEBUG > 1;
4591
4592       my $type = $display->type;
4593
4594       my $desc = $cust_bill_pkg->desc;
4595       $desc = substr($desc, 0, 50). '...'
4596         if $format eq 'latex' && length($desc) > 50;
4597
4598       my %details_opt = ( 'format'          => $format,
4599                           'escape_function' => $escape_function,
4600                           'format_function' => $format_function,
4601                         );
4602
4603       if ( $cust_bill_pkg->pkgnum > 0 ) {
4604
4605         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4606           if $DEBUG > 1;
4607  
4608         my $cust_pkg = $cust_bill_pkg->cust_pkg;
4609
4610         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4611
4612           warn "$me _items_cust_bill_pkg adding setup\n"
4613             if $DEBUG > 1;
4614
4615           my $description = $desc;
4616           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4617
4618           my @d = ();
4619           unless ( $cust_pkg->part_pkg->hide_svc_detail
4620                 || $cust_bill_pkg->hidden )
4621           {
4622
4623             push @d, map &{$escape_function}($_),
4624                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
4625               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4626
4627             if ( $multilocation ) {
4628               my $loc = $cust_pkg->location_label;
4629               $loc = substr($loc, 0, 50). '...'
4630                 if $format eq 'latex' && length($loc) > 50;
4631               push @d, &{$escape_function}($loc);
4632             }
4633
4634           }
4635
4636           push @d, $cust_bill_pkg->details(%details_opt)
4637             if $cust_bill_pkg->recur == 0;
4638
4639           if ( $cust_bill_pkg->hidden ) {
4640             $s->{amount}      += $cust_bill_pkg->setup;
4641             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4642             push @{ $s->{ext_description} }, @d;
4643           } else {
4644             $s = {
4645               description     => $description,
4646               #pkgpart         => $part_pkg->pkgpart,
4647               pkgnum          => $cust_bill_pkg->pkgnum,
4648               amount          => $cust_bill_pkg->setup,
4649               unit_amount     => $cust_bill_pkg->unitsetup,
4650               quantity        => $cust_bill_pkg->quantity,
4651               ext_description => \@d,
4652             };
4653           };
4654
4655         }
4656
4657         if ( ( $cust_bill_pkg->recur != 0  || $cust_bill_pkg->setup == 0 || 
4658                 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4659              ( !$type || $type eq 'R' || $type eq 'U' )
4660            )
4661         {
4662
4663           warn "$me _items_cust_bill_pkg adding recur/usage\n"
4664             if $DEBUG > 1;
4665
4666           my $is_summary = $display->summary;
4667           my $description = ($is_summary && $type && $type eq 'U')
4668                             ? "Usage charges" : $desc;
4669
4670           $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4671                           " - ". time2str($date_format, $cust_bill_pkg->edate).
4672                           ")"
4673             unless $conf->exists('disable_line_item_date_ranges');
4674
4675           my @d = ();
4676
4677           #at least until cust_bill_pkg has "past" ranges in addition to
4678           #the "future" sdate/edate ones... see #3032
4679           my @dates = ( $self->_date );
4680           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4681           push @dates, $prev->sdate if $prev;
4682           push @dates, undef if !$prev;
4683
4684           unless ( $cust_pkg->part_pkg->hide_svc_detail
4685                 || $cust_bill_pkg->itemdesc
4686                 || $cust_bill_pkg->hidden
4687                 || $is_summary && $type && $type eq 'U' )
4688           {
4689
4690             warn "$me _items_cust_bill_pkg adding service details\n"
4691               if $DEBUG > 1;
4692
4693             push @d, map &{$escape_function}($_),
4694                          $cust_pkg->h_labels_short(@dates, 'I')
4695                                                    #$cust_bill_pkg->edate,
4696                                                    #$cust_bill_pkg->sdate)
4697               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4698
4699             warn "$me _items_cust_bill_pkg done adding service details\n"
4700               if $DEBUG > 1;
4701
4702             if ( $multilocation ) {
4703               my $loc = $cust_pkg->location_label;
4704               $loc = substr($loc, 0, 50). '...'
4705                 if $format eq 'latex' && length($loc) > 50;
4706               push @d, &{$escape_function}($loc);
4707             }
4708
4709           }
4710
4711           unless ( $is_summary ) {
4712             warn "$me _items_cust_bill_pkg adding details\n"
4713               if $DEBUG > 1;
4714
4715             #instead of omitting details entirely in this case (unwanted side
4716             # effects), just omit CDRs
4717             $details_opt{'format_function'} = sub { () }
4718               if $type && $type eq 'R';
4719
4720             push @d, $cust_bill_pkg->details(%details_opt);
4721           }
4722
4723           warn "$me _items_cust_bill_pkg calculating amount\n"
4724             if $DEBUG > 1;
4725   
4726           my $amount = 0;
4727           if (!$type) {
4728             $amount = $cust_bill_pkg->recur;
4729           } elsif ($type eq 'R') {
4730             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4731           } elsif ($type eq 'U') {
4732             $amount = $cust_bill_pkg->usage;
4733           }
4734   
4735           if ( !$type || $type eq 'R' ) {
4736
4737             warn "$me _items_cust_bill_pkg adding recur\n"
4738               if $DEBUG > 1;
4739
4740             if ( $cust_bill_pkg->hidden ) {
4741               $r->{amount}      += $amount;
4742               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4743               push @{ $r->{ext_description} }, @d;
4744             } else {
4745               $r = {
4746                 description     => $description,
4747                 #pkgpart         => $part_pkg->pkgpart,
4748                 pkgnum          => $cust_bill_pkg->pkgnum,
4749                 amount          => $amount,
4750                 unit_amount     => $cust_bill_pkg->unitrecur,
4751                 quantity        => $cust_bill_pkg->quantity,
4752                 ext_description => \@d,
4753               };
4754             }
4755
4756           } else {  # $type eq 'U'
4757
4758             warn "$me _items_cust_bill_pkg adding usage\n"
4759               if $DEBUG > 1;
4760
4761             if ( $cust_bill_pkg->hidden ) {
4762               $u->{amount}      += $amount;
4763               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4764               push @{ $u->{ext_description} }, @d;
4765             } else {
4766               $u = {
4767                 description     => $description,
4768                 #pkgpart         => $part_pkg->pkgpart,
4769                 pkgnum          => $cust_bill_pkg->pkgnum,
4770                 amount          => $amount,
4771                 unit_amount     => $cust_bill_pkg->unitrecur,
4772                 quantity        => $cust_bill_pkg->quantity,
4773                 ext_description => \@d,
4774               };
4775             }
4776           }
4777
4778         } # recurring or usage with recurring charge
4779
4780       } else { #pkgnum tax or one-shot line item (??)
4781
4782         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4783           if $DEBUG > 1;
4784
4785         if ( $cust_bill_pkg->setup != 0 ) {
4786           push @b, {
4787             'description' => $desc,
4788             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
4789           };
4790         }
4791         if ( $cust_bill_pkg->recur != 0 ) {
4792           push @b, {
4793             'description' => "$desc (".
4794                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4795                              time2str($date_format, $cust_bill_pkg->edate). ')',
4796             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
4797           };
4798         }
4799
4800       }
4801
4802     }
4803
4804   }
4805
4806   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4807     if $DEBUG > 1;
4808
4809   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4810     if ( $_  ) {
4811       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4812       $_->{amount}      =~ s/^\-0\.00$/0.00/;
4813       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4814       push @b, { %$_ }
4815         unless ( $_->{amount} == 0 && !$discount_show_always );
4816     }
4817   }
4818
4819   @b;
4820
4821 }
4822
4823 sub _items_credits {
4824   my( $self, %opt ) = @_;
4825   my $trim_len = $opt{'trim_len'} || 60;
4826
4827   my @b;
4828   #credits
4829   foreach ( $self->cust_credited ) {
4830
4831     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4832
4833     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4834     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4835     $reason = " ($reason) " if $reason;
4836
4837     push @b, {
4838       #'description' => 'Credit ref\#'. $_->crednum.
4839       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
4840       #                 $reason,
4841       'description' => 'Credit applied '.
4842                        time2str($date_format,$_->cust_credit->_date). $reason,
4843       'amount'      => sprintf("%.2f",$_->amount),
4844     };
4845   }
4846
4847   @b;
4848
4849 }
4850
4851 sub _items_payments {
4852   my $self = shift;
4853
4854   my @b;
4855   #get & print payments
4856   foreach ( $self->cust_bill_pay ) {
4857
4858     #something more elaborate if $_->amount ne ->cust_pay->paid ?
4859
4860     push @b, {
4861       'description' => "Payment received ".
4862                        time2str($date_format,$_->cust_pay->_date ),
4863       'amount'      => sprintf("%.2f", $_->amount )
4864     };
4865   }
4866
4867   @b;
4868
4869 }
4870
4871 =item call_details [ OPTION => VALUE ... ]
4872
4873 Returns an array of CSV strings representing the call details for this invoice
4874 The only option available is the boolean prepend_billed_number
4875
4876 =cut
4877
4878 sub call_details {
4879   my ($self, %opt) = @_;
4880
4881   my $format_function = sub { shift };
4882
4883   if ($opt{prepend_billed_number}) {
4884     $format_function = sub {
4885       my $detail = shift;
4886       my $row = shift;
4887
4888       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4889       
4890     };
4891   }
4892
4893   my @details = map { $_->details( 'format_function' => $format_function,
4894                                    'escape_function' => sub{ return() },
4895                                  )
4896                     }
4897                   grep { $_->pkgnum }
4898                   $self->cust_bill_pkg;
4899   my $header = $details[0];
4900   ( $header, grep { $_ ne $header } @details );
4901 }
4902
4903
4904 =back
4905
4906 =head1 SUBROUTINES
4907
4908 =over 4
4909
4910 =item process_reprint
4911
4912 =cut
4913
4914 sub process_reprint {
4915   process_re_X('print', @_);
4916 }
4917
4918 =item process_reemail
4919
4920 =cut
4921
4922 sub process_reemail {
4923   process_re_X('email', @_);
4924 }
4925
4926 =item process_refax
4927
4928 =cut
4929
4930 sub process_refax {
4931   process_re_X('fax', @_);
4932 }
4933
4934 =item process_reftp
4935
4936 =cut
4937
4938 sub process_reftp {
4939   process_re_X('ftp', @_);
4940 }
4941
4942 =item respool
4943
4944 =cut
4945
4946 sub process_respool {
4947   process_re_X('spool', @_);
4948 }
4949
4950 use Storable qw(thaw);
4951 use Data::Dumper;
4952 use MIME::Base64;
4953 sub process_re_X {
4954   my( $method, $job ) = ( shift, shift );
4955   warn "$me process_re_X $method for job $job\n" if $DEBUG;
4956
4957   my $param = thaw(decode_base64(shift));
4958   warn Dumper($param) if $DEBUG;
4959
4960   re_X(
4961     $method,
4962     $job,
4963     %$param,
4964   );
4965
4966 }
4967
4968 sub re_X {
4969   my($method, $job, %param ) = @_;
4970   if ( $DEBUG ) {
4971     warn "re_X $method for job $job with param:\n".
4972          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
4973   }
4974
4975   #some false laziness w/search/cust_bill.html
4976   my $distinct = '';
4977   my $orderby = 'ORDER BY cust_bill._date';
4978
4979   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4980
4981   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4982      
4983   my @cust_bill = qsearch( {
4984     #'select'    => "cust_bill.*",
4985     'table'     => 'cust_bill',
4986     'addl_from' => $addl_from,
4987     'hashref'   => {},
4988     'extra_sql' => $extra_sql,
4989     'order_by'  => $orderby,
4990     'debug' => 1,
4991   } );
4992
4993   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4994
4995   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4996     if $DEBUG;
4997
4998   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4999   foreach my $cust_bill ( @cust_bill ) {
5000     $cust_bill->$method();
5001
5002     if ( $job ) { #progressbar foo
5003       $num++;
5004       if ( time - $min_sec > $last ) {
5005         my $error = $job->update_statustext(
5006           int( 100 * $num / scalar(@cust_bill) )
5007         );
5008         die $error if $error;
5009         $last = time;
5010       }
5011     }
5012
5013   }
5014
5015 }
5016
5017 =back
5018
5019 =head1 CLASS METHODS
5020
5021 =over 4
5022
5023 =item owed_sql
5024
5025 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5026
5027 =cut
5028
5029 sub owed_sql {
5030   my ($class, $start, $end) = @_;
5031   'charged - '. 
5032     $class->paid_sql($start, $end). ' - '. 
5033     $class->credited_sql($start, $end);
5034 }
5035
5036 =item net_sql
5037
5038 Returns an SQL fragment to retreive the net amount (charged minus credited).
5039
5040 =cut
5041
5042 sub net_sql {
5043   my ($class, $start, $end) = @_;
5044   'charged - '. $class->credited_sql($start, $end);
5045 }
5046
5047 =item paid_sql
5048
5049 Returns an SQL fragment to retreive the amount paid against this invoice.
5050
5051 =cut
5052
5053 sub paid_sql {
5054   my ($class, $start, $end) = @_;
5055   $start &&= "AND cust_bill_pay._date <= $start";
5056   $end   &&= "AND cust_bill_pay._date > $end";
5057   $start = '' unless defined($start);
5058   $end   = '' unless defined($end);
5059   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5060        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
5061 }
5062
5063 =item credited_sql
5064
5065 Returns an SQL fragment to retreive the amount credited against this invoice.
5066
5067 =cut
5068
5069 sub credited_sql {
5070   my ($class, $start, $end) = @_;
5071   $start &&= "AND cust_credit_bill._date <= $start";
5072   $end   &&= "AND cust_credit_bill._date >  $end";
5073   $start = '' unless defined($start);
5074   $end   = '' unless defined($end);
5075   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5076        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
5077 }
5078
5079 =item due_date_sql
5080
5081 Returns an SQL fragment to retrieve the due date of an invoice.
5082 Currently only supported on PostgreSQL.
5083
5084 =cut
5085
5086 sub due_date_sql {
5087 'COALESCE(
5088   SUBSTRING(
5089     COALESCE(
5090       cust_bill.invoice_terms,
5091       cust_main.invoice_terms,
5092       \''.($conf->config('invoice_default_terms') || '').'\'
5093     ), E\'Net (\\\\d+)\'
5094   )::INTEGER, 0
5095 ) * 86400 + cust_bill._date'
5096 }
5097
5098 =item search_sql_where HASHREF
5099
5100 Class method which returns an SQL WHERE fragment to search for parameters
5101 specified in HASHREF.  Valid parameters are
5102
5103 =over 4
5104
5105 =item _date
5106
5107 List reference of start date, end date, as UNIX timestamps.
5108
5109 =item invnum_min
5110
5111 =item invnum_max
5112
5113 =item agentnum
5114
5115 =item charged
5116
5117 List reference of charged limits (exclusive).
5118
5119 =item owed
5120
5121 List reference of charged limits (exclusive).
5122
5123 =item open
5124
5125 flag, return open invoices only
5126
5127 =item net
5128
5129 flag, return net invoices only
5130
5131 =item days
5132
5133 =item newest_percust
5134
5135 =back
5136
5137 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5138
5139 =cut
5140
5141 sub search_sql_where {
5142   my($class, $param) = @_;
5143   if ( $DEBUG ) {
5144     warn "$me search_sql_where called with params: \n".
5145          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
5146   }
5147
5148   my @search = ();
5149
5150   #agentnum
5151   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5152     push @search, "cust_main.agentnum = $1";
5153   }
5154
5155   #_date
5156   if ( $param->{_date} ) {
5157     my($beginning, $ending) = @{$param->{_date}};
5158
5159     push @search, "cust_bill._date >= $beginning",
5160                   "cust_bill._date <  $ending";
5161   }
5162
5163   #invnum
5164   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5165     push @search, "cust_bill.invnum >= $1";
5166   }
5167   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5168     push @search, "cust_bill.invnum <= $1";
5169   }
5170
5171   #charged
5172   if ( $param->{charged} ) {
5173     my @charged = ref($param->{charged})
5174                     ? @{ $param->{charged} }
5175                     : ($param->{charged});
5176
5177     push @search, map { s/^charged/cust_bill.charged/; $_; }
5178                       @charged;
5179   }
5180
5181   my $owed_sql = FS::cust_bill->owed_sql;
5182
5183   #owed
5184   if ( $param->{owed} ) {
5185     my @owed = ref($param->{owed})
5186                  ? @{ $param->{owed} }
5187                  : ($param->{owed});
5188     push @search, map { s/^owed/$owed_sql/; $_; }
5189                       @owed;
5190   }
5191
5192   #open/net flags
5193   push @search, "0 != $owed_sql"
5194     if $param->{'open'};
5195   push @search, '0 != '. FS::cust_bill->net_sql
5196     if $param->{'net'};
5197
5198   #days
5199   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5200     if $param->{'days'};
5201
5202   #newest_percust
5203   if ( $param->{'newest_percust'} ) {
5204
5205     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5206     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5207
5208     my @newest_where = map { my $x = $_;
5209                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5210                              $x;
5211                            }
5212                            grep ! /^cust_main./, @search;
5213     my $newest_where = scalar(@newest_where)
5214                          ? ' AND '. join(' AND ', @newest_where)
5215                          : '';
5216
5217
5218     push @search, "cust_bill._date = (
5219       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5220         WHERE newest_cust_bill.custnum = cust_bill.custnum
5221           $newest_where
5222     )";
5223
5224   }
5225
5226   #agent virtualization
5227   my $curuser = $FS::CurrentUser::CurrentUser;
5228   if ( $curuser->username eq 'fs_queue'
5229        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5230     my $username = $1;
5231     my $newuser = qsearchs('access_user', {
5232       'username' => $username,
5233       'disabled' => '',
5234     } );
5235     if ( $newuser ) {
5236       $curuser = $newuser;
5237     } else {
5238       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5239     }
5240   }
5241   push @search, $curuser->agentnums_sql;
5242
5243   join(' AND ', @search );
5244
5245 }
5246
5247 =back
5248
5249 =head1 BUGS
5250
5251 The delete method.
5252
5253 =head1 SEE ALSO
5254
5255 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5256 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5257 documentation.
5258
5259 =cut
5260
5261 1;
5262