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