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