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