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