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