support for new invoice template, #13655
[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 # I like how _date_pretty was documented but this one wasn't.
3701
3702 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3703
3704 Generate section information for all items appearing on this invoice.
3705 This will only be called for multi-section invoices.
3706
3707 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
3708 related display records (L<FS::cust_bill_pkg_display>) and organize 
3709 them into two groups ("early" and "late" according to whether they come 
3710 before or after the total), then into sections.  A subtotal is calculated 
3711 for each section.
3712
3713 Section descriptions are returned in sort weight order.  Each consists 
3714 of a hash containing:
3715
3716 description: the package category name, escaped
3717 subtotal: the total charges in that section
3718 tax_section: a flag indicating that the section contains only tax charges
3719 summarized: same as tax_section, for some reason
3720 sort_weight: the package category's sort weight
3721
3722 If 'condense' is set on the display record, it also contains everything 
3723 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3724 coderefs to generate parts of the invoice.  This is not advised.
3725
3726 Takes way too many arguments, all mandatory:
3727
3728 LATE: an arrayref to push the "late" section hashes onto.  The "early"
3729 group is simply returned from the method.  Yes, I know.  Don't ask.
3730
3731 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3732 Turning this on has the following effects:
3733 - Ignores display items with the 'summary' flag.
3734 - Combines all items into the "early" group.
3735 - Creates sections for all non-disabled package categories, even if they 
3736 have no charges on this invoice, as well as a section with no name.
3737
3738 ESCAPE: an escape function to use for section titles.  Why not just 
3739 let the calling environment escape things itself?  Beats the heck out 
3740 of me.
3741
3742 EXTRA_SECTIONS: an arrayref of additional sections to return after the 
3743 sorted list.  If there are any of these, section subtotals exclude 
3744 usage charges.
3745
3746 FORMAT: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
3747 passed through to C<_condense_section()>.
3748
3749 =cut
3750
3751 use vars qw(%pkg_category_cache);
3752 sub _items_sections {
3753   my $self = shift;
3754   my $late = shift;
3755   my $summarypage = shift;
3756   my $escape = shift;
3757   my $extra_sections = shift;
3758   my $format = shift;
3759
3760   my %subtotal = ();
3761   my %late_subtotal = ();
3762   my %not_tax = ();
3763
3764   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3765   {
3766
3767       my $usage = $cust_bill_pkg->usage;
3768
3769       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3770         next if ( $display->summary && $summarypage );
3771
3772         my $section = $display->section;
3773         my $type    = $display->type;
3774
3775         $not_tax{$section} = 1
3776           unless $cust_bill_pkg->pkgnum == 0;
3777
3778         if ( $display->post_total && !$summarypage ) {
3779           if (! $type || $type eq 'S') {
3780             $late_subtotal{$section} += $cust_bill_pkg->setup
3781               if $cust_bill_pkg->setup != 0;
3782           }
3783
3784           if (! $type) {
3785             $late_subtotal{$section} += $cust_bill_pkg->recur
3786               if $cust_bill_pkg->recur != 0;
3787           }
3788
3789           if ($type && $type eq 'R') {
3790             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3791               if $cust_bill_pkg->recur != 0;
3792           }
3793           
3794           if ($type && $type eq 'U') {
3795             $late_subtotal{$section} += $usage
3796               unless scalar(@$extra_sections);
3797           }
3798
3799         } else {
3800
3801           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3802
3803           if (! $type || $type eq 'S') {
3804             $subtotal{$section} += $cust_bill_pkg->setup
3805               if $cust_bill_pkg->setup != 0;
3806           }
3807
3808           if (! $type) {
3809             $subtotal{$section} += $cust_bill_pkg->recur
3810               if $cust_bill_pkg->recur != 0;
3811           }
3812
3813           if ($type && $type eq 'R') {
3814             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3815               if $cust_bill_pkg->recur != 0;
3816           }
3817           
3818           if ($type && $type eq 'U') {
3819             $subtotal{$section} += $usage
3820               unless scalar(@$extra_sections);
3821           }
3822
3823         }
3824
3825       }
3826
3827   }
3828
3829   %pkg_category_cache = ();
3830
3831   push @$late, map { { 'description' => &{$escape}($_),
3832                        'subtotal'    => $late_subtotal{$_},
3833                        'post_total'  => 1,
3834                        'sort_weight' => ( _pkg_category($_)
3835                                             ? _pkg_category($_)->weight
3836                                             : 0
3837                                        ),
3838                        ((_pkg_category($_) && _pkg_category($_)->condense)
3839                                            ? $self->_condense_section($format)
3840                                            : ()
3841                        ),
3842                    } }
3843                  sort _sectionsort keys %late_subtotal;
3844
3845   my @sections;
3846   if ( $summarypage ) {
3847     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3848                 map { $_->categoryname } qsearch('pkg_category', {});
3849     push @sections, '' if exists($subtotal{''});
3850   } else {
3851     @sections = keys %subtotal;
3852   }
3853
3854   my @early = map { { 'description' => &{$escape}($_),
3855                       'subtotal'    => $subtotal{$_},
3856                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3857                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3858                       'sort_weight' => ( _pkg_category($_)
3859                                            ? _pkg_category($_)->weight
3860                                            : 0
3861                                        ),
3862                        ((_pkg_category($_) && _pkg_category($_)->condense)
3863                                            ? $self->_condense_section($format)
3864                                            : ()
3865                        ),
3866                     }
3867                   } @sections;
3868   push @early, @$extra_sections if $extra_sections;
3869
3870   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3871
3872 }
3873
3874 #helper subs for above
3875
3876 sub _sectionsort {
3877   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3878 }
3879
3880 sub _pkg_category {
3881   my $categoryname = shift;
3882   $pkg_category_cache{$categoryname} ||=
3883     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3884 }
3885
3886 my %condensed_format = (
3887   'label' => [ qw( Description Qty Amount ) ],
3888   'fields' => [
3889                 sub { shift->{description} },
3890                 sub { shift->{quantity} },
3891                 sub { my($href, %opt) = @_;
3892                       ($opt{dollar} || ''). $href->{amount};
3893                     },
3894               ],
3895   'align'  => [ qw( l r r ) ],
3896   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3897   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3898 );
3899
3900 sub _condense_section {
3901   my ( $self, $format ) = ( shift, shift );
3902   ( 'condensed' => 1,
3903     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3904       qw( description_generator
3905           header_generator
3906           total_generator
3907           total_line_generator
3908         )
3909   );
3910 }
3911
3912 sub _condensed_generator_defaults {
3913   my ( $self, $format ) = ( shift, shift );
3914   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3915 }
3916
3917 my %html_align = (
3918   'c' => 'center',
3919   'l' => 'left',
3920   'r' => 'right',
3921 );
3922
3923 sub _condensed_header_generator {
3924   my ( $self, $format ) = ( shift, shift );
3925
3926   my ( $f, $prefix, $suffix, $separator, $column ) =
3927     _condensed_generator_defaults($format);
3928
3929   if ($format eq 'latex') {
3930     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3931     $suffix = "\\\\\n\\hline";
3932     $separator = "&\n";
3933     $column =
3934       sub { my ($d,$a,$s,$w) = @_;
3935             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3936           };
3937   } elsif ( $format eq 'html' ) {
3938     $prefix = '<th></th>';
3939     $suffix = '';
3940     $separator = '';
3941     $column =
3942       sub { my ($d,$a,$s,$w) = @_;
3943             return qq!<th align="$html_align{$a}">$d</th>!;
3944       };
3945   }
3946
3947   sub {
3948     my @args = @_;
3949     my @result = ();
3950
3951     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3952       push @result,
3953         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3954     }
3955
3956     $prefix. join($separator, @result). $suffix;
3957   };
3958
3959 }
3960
3961 sub _condensed_description_generator {
3962   my ( $self, $format ) = ( shift, shift );
3963
3964   my ( $f, $prefix, $suffix, $separator, $column ) =
3965     _condensed_generator_defaults($format);
3966
3967   my $money_char = '$';
3968   if ($format eq 'latex') {
3969     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3970     $suffix = '\\\\';
3971     $separator = " & \n";
3972     $column =
3973       sub { my ($d,$a,$s,$w) = @_;
3974             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3975           };
3976     $money_char = '\\dollar';
3977   }elsif ( $format eq 'html' ) {
3978     $prefix = '"><td align="center"></td>';
3979     $suffix = '';
3980     $separator = '';
3981     $column =
3982       sub { my ($d,$a,$s,$w) = @_;
3983             return qq!<td align="$html_align{$a}">$d</td>!;
3984       };
3985     #$money_char = $conf->config('money_char') || '$';
3986     $money_char = '';  # this is madness
3987   }
3988
3989   sub {
3990     #my @args = @_;
3991     my $href = shift;
3992     my @result = ();
3993
3994     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3995       my $dollar = '';
3996       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3997       push @result,
3998         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3999                     map { $f->{$_}->[$i] } qw(align span width)
4000                   );
4001     }
4002
4003     $prefix. join( $separator, @result ). $suffix;
4004   };
4005
4006 }
4007
4008 sub _condensed_total_generator {
4009   my ( $self, $format ) = ( shift, shift );
4010
4011   my ( $f, $prefix, $suffix, $separator, $column ) =
4012     _condensed_generator_defaults($format);
4013   my $style = '';
4014
4015   if ($format eq 'latex') {
4016     $prefix = "& ";
4017     $suffix = "\\\\\n";
4018     $separator = " & \n";
4019     $column =
4020       sub { my ($d,$a,$s,$w) = @_;
4021             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4022           };
4023   }elsif ( $format eq 'html' ) {
4024     $prefix = '';
4025     $suffix = '';
4026     $separator = '';
4027     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4028     $column =
4029       sub { my ($d,$a,$s,$w) = @_;
4030             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4031       };
4032   }
4033
4034
4035   sub {
4036     my @args = @_;
4037     my @result = ();
4038
4039     #  my $r = &{$f->{fields}->[$i]}(@args);
4040     #  $r .= ' Total' unless $i;
4041
4042     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4043       push @result,
4044         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4045                     map { $f->{$_}->[$i] } qw(align span width)
4046                   );
4047     }
4048
4049     $prefix. join( $separator, @result ). $suffix;
4050   };
4051
4052 }
4053
4054 =item total_line_generator FORMAT
4055
4056 Returns a coderef used for generation of invoice total line items for this
4057 usage_class.  FORMAT is either html or latex
4058
4059 =cut
4060
4061 # should not be used: will have issues with hash element names (description vs
4062 # total_item and amount vs total_amount -- another array of functions?
4063
4064 sub _condensed_total_line_generator {
4065   my ( $self, $format ) = ( shift, shift );
4066
4067   my ( $f, $prefix, $suffix, $separator, $column ) =
4068     _condensed_generator_defaults($format);
4069   my $style = '';
4070
4071   if ($format eq 'latex') {
4072     $prefix = "& ";
4073     $suffix = "\\\\\n";
4074     $separator = " & \n";
4075     $column =
4076       sub { my ($d,$a,$s,$w) = @_;
4077             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4078           };
4079   }elsif ( $format eq 'html' ) {
4080     $prefix = '';
4081     $suffix = '';
4082     $separator = '';
4083     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4084     $column =
4085       sub { my ($d,$a,$s,$w) = @_;
4086             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4087       };
4088   }
4089
4090
4091   sub {
4092     my @args = @_;
4093     my @result = ();
4094
4095     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4096       push @result,
4097         &{$column}( &{$f->{fields}->[$i]}(@args),
4098                     map { $f->{$_}->[$i] } qw(align span width)
4099                   );
4100     }
4101
4102     $prefix. join( $separator, @result ). $suffix;
4103   };
4104
4105 }
4106
4107 #sub _items_extra_usage_sections {
4108 #  my $self = shift;
4109 #  my $escape = shift;
4110 #
4111 #  my %sections = ();
4112 #
4113 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
4114 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4115 #  {
4116 #    next unless $cust_bill_pkg->pkgnum > 0;
4117 #
4118 #    foreach my $section ( keys %usage_class ) {
4119 #
4120 #      my $usage = $cust_bill_pkg->usage($section);
4121 #
4122 #      next unless $usage && $usage > 0;
4123 #
4124 #      $sections{$section} ||= 0;
4125 #      $sections{$section} += $usage;
4126 #
4127 #    }
4128 #
4129 #  }
4130 #
4131 #  map { { 'description' => &{$escape}($_),
4132 #          'subtotal'    => $sections{$_},
4133 #          'summarized'  => '',
4134 #          'tax_section' => '',
4135 #        }
4136 #      }
4137 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4138 #
4139 #}
4140
4141 sub _items_extra_usage_sections {
4142   my $self = shift;
4143   my $conf = $self->conf;
4144   my $escape = shift;
4145   my $format = shift;
4146
4147   my %sections = ();
4148   my %classnums = ();
4149   my %lines = ();
4150
4151   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4152
4153   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4154   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4155     next unless $cust_bill_pkg->pkgnum > 0;
4156
4157     foreach my $classnum ( keys %usage_class ) {
4158       my $section = $usage_class{$classnum}->classname;
4159       $classnums{$section} = $classnum;
4160
4161       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4162         my $amount = $detail->amount;
4163         next unless $amount && $amount > 0;
4164  
4165         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4166         $sections{$section}{amount} += $amount;  #subtotal
4167         $sections{$section}{calls}++;
4168         $sections{$section}{duration} += $detail->duration;
4169
4170         my $desc = $detail->regionname; 
4171         my $description = $desc;
4172         $description = substr($desc, 0, $maxlength). '...'
4173           if $format eq 'latex' && length($desc) > $maxlength;
4174
4175         $lines{$section}{$desc} ||= {
4176           description     => &{$escape}($description),
4177           #pkgpart         => $part_pkg->pkgpart,
4178           pkgnum          => $cust_bill_pkg->pkgnum,
4179           ref             => '',
4180           amount          => 0,
4181           calls           => 0,
4182           duration        => 0,
4183           #unit_amount     => $cust_bill_pkg->unitrecur,
4184           quantity        => $cust_bill_pkg->quantity,
4185           product_code    => 'N/A',
4186           ext_description => [],
4187         };
4188
4189         $lines{$section}{$desc}{amount} += $amount;
4190         $lines{$section}{$desc}{calls}++;
4191         $lines{$section}{$desc}{duration} += $detail->duration;
4192
4193       }
4194     }
4195   }
4196
4197   my %sectionmap = ();
4198   foreach (keys %sections) {
4199     my $usage_class = $usage_class{$classnums{$_}};
4200     $sectionmap{$_} = { 'description' => &{$escape}($_),
4201                         'amount'    => $sections{$_}{amount},    #subtotal
4202                         'calls'       => $sections{$_}{calls},
4203                         'duration'    => $sections{$_}{duration},
4204                         'summarized'  => '',
4205                         'tax_section' => '',
4206                         'sort_weight' => $usage_class->weight,
4207                         ( $usage_class->format
4208                           ? ( map { $_ => $usage_class->$_($format) }
4209                               qw( description_generator header_generator total_generator total_line_generator )
4210                             )
4211                           : ()
4212                         ), 
4213                       };
4214   }
4215
4216   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4217                  values %sectionmap;
4218
4219   my @lines = ();
4220   foreach my $section ( keys %lines ) {
4221     foreach my $line ( keys %{$lines{$section}} ) {
4222       my $l = $lines{$section}{$line};
4223       $l->{section}     = $sectionmap{$section};
4224       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4225       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4226       push @lines, $l;
4227     }
4228   }
4229
4230   return(\@sections, \@lines);
4231
4232 }
4233
4234 sub _did_summary {
4235     my $self = shift;
4236     my $end = $self->_date;
4237
4238     # start at date of previous invoice + 1 second or 0 if no previous invoice
4239     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4240     $start = 0 if !$start;
4241     $start++;
4242
4243     my $cust_main = $self->cust_main;
4244     my @pkgs = $cust_main->all_pkgs;
4245     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4246         = (0,0,0,0,0);
4247     my @seen = ();
4248     foreach my $pkg ( @pkgs ) {
4249         my @h_cust_svc = $pkg->h_cust_svc($end);
4250         foreach my $h_cust_svc ( @h_cust_svc ) {
4251             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4252             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4253
4254             my $inserted = $h_cust_svc->date_inserted;
4255             my $deleted = $h_cust_svc->date_deleted;
4256             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4257             my $phone_deleted;
4258             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
4259             
4260 # DID either activated or ported in; cannot be both for same DID simultaneously
4261             if ($inserted >= $start && $inserted <= $end && $phone_inserted
4262                 && (!$phone_inserted->lnp_status 
4263                     || $phone_inserted->lnp_status eq ''
4264                     || $phone_inserted->lnp_status eq 'native')) {
4265                 $num_activated++;
4266             }
4267             else { # this one not so clean, should probably move to (h_)svc_phone
4268                  my $phone_portedin = qsearchs( 'h_svc_phone',
4269                       { 'svcnum' => $h_cust_svc->svcnum, 
4270                         'lnp_status' => 'portedin' },  
4271                       FS::h_svc_phone->sql_h_searchs($end),  
4272                     );
4273                  $num_portedin++ if $phone_portedin;
4274             }
4275
4276 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4277             if($deleted >= $start && $deleted <= $end && $phone_deleted
4278                 && (!$phone_deleted->lnp_status 
4279                     || $phone_deleted->lnp_status ne 'portingout')) {
4280                 $num_deactivated++;
4281             } 
4282             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
4283                 && $phone_deleted->lnp_status 
4284                 && $phone_deleted->lnp_status eq 'portingout') {
4285                 $num_portedout++;
4286             }
4287
4288             # increment usage minutes
4289         if ( $phone_inserted ) {
4290             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4291             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4292         }
4293         else {
4294             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4295         }
4296
4297             # don't look at this service again
4298             push @seen, $h_cust_svc->svcnum;
4299         }
4300     }
4301
4302     $minutes = sprintf("%d", $minutes);
4303     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
4304         . "$num_deactivated  Ported-Out: $num_portedout ",
4305             "Total Minutes: $minutes");
4306 }
4307
4308 sub _items_accountcode_cdr {
4309     my $self = shift;
4310     my $escape = shift;
4311     my $format = shift;
4312
4313     my $section = { 'amount'        => 0,
4314                     'calls'         => 0,
4315                     'duration'      => 0,
4316                     'sort_weight'   => '',
4317                     'phonenum'      => '',
4318                     'description'   => 'Usage by Account Code',
4319                     'post_total'    => '',
4320                     'summarized'    => '',
4321                     'header'        => '',
4322                   };
4323     my @lines;
4324     my %accountcodes = ();
4325
4326     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4327         next unless $cust_bill_pkg->pkgnum > 0;
4328
4329         my @header = $cust_bill_pkg->details_header;
4330         next unless scalar(@header);
4331         $section->{'header'} = join(',',@header);
4332
4333         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4334
4335             $section->{'header'} = $detail->formatted('format' => $format)
4336                 if($detail->detail eq $section->{'header'}); 
4337       
4338             my $accountcode = $detail->accountcode;
4339             next unless $accountcode;
4340
4341             my $amount = $detail->amount;
4342             next unless $amount && $amount > 0;
4343
4344             $accountcodes{$accountcode} ||= {
4345                     description => $accountcode,
4346                     pkgnum      => '',
4347                     ref         => '',
4348                     amount      => 0,
4349                     calls       => 0,
4350                     duration    => 0,
4351                     quantity    => '',
4352                     product_code => 'N/A',
4353                     section     => $section,
4354                     ext_description => [ $section->{'header'} ],
4355                     detail_temp => [],
4356             };
4357
4358             $section->{'amount'} += $amount;
4359             $accountcodes{$accountcode}{'amount'} += $amount;
4360             $accountcodes{$accountcode}{calls}++;
4361             $accountcodes{$accountcode}{duration} += $detail->duration;
4362             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4363         }
4364     }
4365
4366     foreach my $l ( values %accountcodes ) {
4367         $l->{amount} = sprintf( "%.2f", $l->{amount} );
4368         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4369         foreach my $sorted_detail ( @sorted_detail ) {
4370             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4371         }
4372         delete $l->{detail_temp};
4373         push @lines, $l;
4374     }
4375
4376     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4377
4378     return ($section,\@sorted_lines);
4379 }
4380
4381 sub _items_svc_phone_sections {
4382   my $self = shift;
4383   my $conf = $self->conf;
4384   my $escape = shift;
4385   my $format = shift;
4386
4387   my %sections = ();
4388   my %classnums = ();
4389   my %lines = ();
4390
4391   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4392
4393   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4394   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4395
4396   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4397     next unless $cust_bill_pkg->pkgnum > 0;
4398
4399     my @header = $cust_bill_pkg->details_header;
4400     next unless scalar(@header);
4401
4402     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4403
4404       my $phonenum = $detail->phonenum;
4405       next unless $phonenum;
4406
4407       my $amount = $detail->amount;
4408       next unless $amount && $amount > 0;
4409
4410       $sections{$phonenum} ||= { 'amount'      => 0,
4411                                  'calls'       => 0,
4412                                  'duration'    => 0,
4413                                  'sort_weight' => -1,
4414                                  'phonenum'    => $phonenum,
4415                                 };
4416       $sections{$phonenum}{amount} += $amount;  #subtotal
4417       $sections{$phonenum}{calls}++;
4418       $sections{$phonenum}{duration} += $detail->duration;
4419
4420       my $desc = $detail->regionname; 
4421       my $description = $desc;
4422       $description = substr($desc, 0, $maxlength). '...'
4423         if $format eq 'latex' && length($desc) > $maxlength;
4424
4425       $lines{$phonenum}{$desc} ||= {
4426         description     => &{$escape}($description),
4427         #pkgpart         => $part_pkg->pkgpart,
4428         pkgnum          => '',
4429         ref             => '',
4430         amount          => 0,
4431         calls           => 0,
4432         duration        => 0,
4433         #unit_amount     => '',
4434         quantity        => '',
4435         product_code    => 'N/A',
4436         ext_description => [],
4437       };
4438
4439       $lines{$phonenum}{$desc}{amount} += $amount;
4440       $lines{$phonenum}{$desc}{calls}++;
4441       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4442
4443       my $line = $usage_class{$detail->classnum}->classname;
4444       $sections{"$phonenum $line"} ||=
4445         { 'amount' => 0,
4446           'calls' => 0,
4447           'duration' => 0,
4448           'sort_weight' => $usage_class{$detail->classnum}->weight,
4449           'phonenum' => $phonenum,
4450           'header'  => [ @header ],
4451         };
4452       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4453       $sections{"$phonenum $line"}{calls}++;
4454       $sections{"$phonenum $line"}{duration} += $detail->duration;
4455
4456       $lines{"$phonenum $line"}{$desc} ||= {
4457         description     => &{$escape}($description),
4458         #pkgpart         => $part_pkg->pkgpart,
4459         pkgnum          => '',
4460         ref             => '',
4461         amount          => 0,
4462         calls           => 0,
4463         duration        => 0,
4464         #unit_amount     => '',
4465         quantity        => '',
4466         product_code    => 'N/A',
4467         ext_description => [],
4468       };
4469
4470       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4471       $lines{"$phonenum $line"}{$desc}{calls}++;
4472       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4473       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4474            $detail->formatted('format' => $format);
4475
4476     }
4477   }
4478
4479   my %sectionmap = ();
4480   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4481   foreach ( keys %sections ) {
4482     my @header = @{ $sections{$_}{header} || [] };
4483     my $usage_simple =
4484       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4485     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4486     my $usage_class = $summary ? $simple : $usage_simple;
4487     my $ending = $summary ? ' usage charges' : '';
4488     my %gen_opt = ();
4489     unless ($summary) {
4490       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4491     }
4492     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4493                         'amount'    => $sections{$_}{amount},    #subtotal
4494                         'calls'       => $sections{$_}{calls},
4495                         'duration'    => $sections{$_}{duration},
4496                         'summarized'  => '',
4497                         'tax_section' => '',
4498                         'phonenum'    => $sections{$_}{phonenum},
4499                         'sort_weight' => $sections{$_}{sort_weight},
4500                         'post_total'  => $summary, #inspire pagebreak
4501                         (
4502                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4503                             qw( description_generator
4504                                 header_generator
4505                                 total_generator
4506                                 total_line_generator
4507                               )
4508                           )
4509                         ), 
4510                       };
4511   }
4512
4513   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4514                         $a->{sort_weight} <=> $b->{sort_weight}
4515                       }
4516                  values %sectionmap;
4517
4518   my @lines = ();
4519   foreach my $section ( keys %lines ) {
4520     foreach my $line ( keys %{$lines{$section}} ) {
4521       my $l = $lines{$section}{$line};
4522       $l->{section}     = $sectionmap{$section};
4523       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4524       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4525       push @lines, $l;
4526     }
4527   }
4528   
4529   if($conf->exists('phone_usage_class_summary')) { 
4530       # this only works with Latex
4531       my @newlines;
4532       my @newsections;
4533
4534       # after this, we'll have only two sections per DID:
4535       # Calls Summary and Calls Detail
4536       foreach my $section ( @sections ) {
4537         if($section->{'post_total'}) {
4538             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4539             $section->{'total_line_generator'} = sub { '' };
4540             $section->{'total_generator'} = sub { '' };
4541             $section->{'header_generator'} = sub { '' };
4542             $section->{'description_generator'} = '';
4543             push @newsections, $section;
4544             my %calls_detail = %$section;
4545             $calls_detail{'post_total'} = '';
4546             $calls_detail{'sort_weight'} = '';
4547             $calls_detail{'description_generator'} = sub { '' };
4548             $calls_detail{'header_generator'} = sub {
4549                 return ' & Date/Time & Called Number & Duration & Price'
4550                     if $format eq 'latex';
4551                 '';
4552             };
4553             $calls_detail{'description'} = 'Calls Detail: '
4554                                                     . $section->{'phonenum'};
4555             push @newsections, \%calls_detail;  
4556         }
4557       }
4558
4559       # after this, each usage class is collapsed/summarized into a single
4560       # line under the Calls Summary section
4561       foreach my $newsection ( @newsections ) {
4562         if($newsection->{'post_total'}) { # this means Calls Summary
4563             foreach my $section ( @sections ) {
4564                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
4565                                 && !$section->{'post_total'});
4566                 my $newdesc = $section->{'description'};
4567                 my $tn = $section->{'phonenum'};
4568                 $newdesc =~ s/$tn//g;
4569                 my $line = {  ext_description => [],
4570                               pkgnum => '',
4571                               ref => '',
4572                               quantity => '',
4573                               calls => $section->{'calls'},
4574                               section => $newsection,
4575                               duration => $section->{'duration'},
4576                               description => $newdesc,
4577                               amount => sprintf("%.2f",$section->{'amount'}),
4578                               product_code => 'N/A',
4579                             };
4580                 push @newlines, $line;
4581             }
4582         }
4583       }
4584
4585       # after this, Calls Details is populated with all CDRs
4586       foreach my $newsection ( @newsections ) {
4587         if(!$newsection->{'post_total'}) { # this means Calls Details
4588             foreach my $line ( @lines ) {
4589                 next unless (scalar(@{$line->{'ext_description'}}) &&
4590                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4591                             );
4592                 my @extdesc = @{$line->{'ext_description'}};
4593                 my @newextdesc;
4594                 foreach my $extdesc ( @extdesc ) {
4595                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4596                     push @newextdesc, $extdesc;
4597                 }
4598                 $line->{'ext_description'} = \@newextdesc;
4599                 $line->{'section'} = $newsection;
4600                 push @newlines, $line;
4601             }
4602         }
4603       }
4604
4605       return(\@newsections, \@newlines);
4606   }
4607
4608   return(\@sections, \@lines);
4609
4610 }
4611
4612 sub _items { # seems to be unused
4613   my $self = shift;
4614
4615   #my @display = scalar(@_)
4616   #              ? @_
4617   #              : qw( _items_previous _items_pkg );
4618   #              #: qw( _items_pkg );
4619   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4620   my @display = qw( _items_previous _items_pkg );
4621
4622   my @b = ();
4623   foreach my $display ( @display ) {
4624     push @b, $self->$display(@_);
4625   }
4626   @b;
4627 }
4628
4629 sub _items_previous {
4630   my $self = shift;
4631   my $conf = $self->conf;
4632   my $cust_main = $self->cust_main;
4633   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4634   my @b = ();
4635   foreach ( @pr_cust_bill ) {
4636     my $date = $conf->exists('invoice_show_prior_due_date')
4637                ? 'due '. $_->due_date2str($date_format)
4638                : time2str($date_format, $_->_date);
4639     push @b, {
4640       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4641       #'pkgpart'     => 'N/A',
4642       'pkgnum'      => 'N/A',
4643       'amount'      => sprintf("%.2f", $_->owed),
4644     };
4645   }
4646   @b;
4647
4648   #{
4649   #    'description'     => 'Previous Balance',
4650   #    #'pkgpart'         => 'N/A',
4651   #    'pkgnum'          => 'N/A',
4652   #    'amount'          => sprintf("%10.2f", $pr_total ),
4653   #    'ext_description' => [ map {
4654   #                                 "Invoice ". $_->invnum.
4655   #                                 " (". time2str("%x",$_->_date). ") ".
4656   #                                 sprintf("%10.2f", $_->owed)
4657   #                         } @pr_cust_bill ],
4658
4659   #};
4660 }
4661
4662 =item _items_pkg [ OPTIONS ]
4663
4664 Return line item hashes for each package item on this invoice. Nearly 
4665 equivalent to 
4666
4667 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4668
4669 The only OPTIONS accepted is 'section', which may point to a hashref 
4670 with a key named 'condensed', which may have a true value.  If it 
4671 does, this method tries to merge identical items into items with 
4672 'quantity' equal to the number of items (not the sum of their 
4673 separate quantities, for some reason).
4674
4675 =cut
4676
4677 sub _items_pkg {
4678   my $self = shift;
4679   my %options = @_;
4680
4681   warn "$me _items_pkg searching for all package line items\n"
4682     if $DEBUG > 1;
4683
4684   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4685
4686   warn "$me _items_pkg filtering line items\n"
4687     if $DEBUG > 1;
4688   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4689
4690   if ($options{section} && $options{section}->{condensed}) {
4691
4692     warn "$me _items_pkg condensing section\n"
4693       if $DEBUG > 1;
4694
4695     my %itemshash = ();
4696     local $Storable::canonical = 1;
4697     foreach ( @items ) {
4698       my $item = { %$_ };
4699       delete $item->{ref};
4700       delete $item->{ext_description};
4701       my $key = freeze($item);
4702       $itemshash{$key} ||= 0;
4703       $itemshash{$key} ++; # += $item->{quantity};
4704     }
4705     @items = sort { $a->{description} cmp $b->{description} }
4706              map { my $i = thaw($_);
4707                    $i->{quantity} = $itemshash{$_};
4708                    $i->{amount} =
4709                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4710                    $i;
4711                  }
4712              keys %itemshash;
4713   }
4714
4715   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4716     if $DEBUG > 1;
4717
4718   @items;
4719 }
4720
4721 sub _taxsort {
4722   return 0 unless $a->itemdesc cmp $b->itemdesc;
4723   return -1 if $b->itemdesc eq 'Tax';
4724   return 1 if $a->itemdesc eq 'Tax';
4725   return -1 if $b->itemdesc eq 'Other surcharges';
4726   return 1 if $a->itemdesc eq 'Other surcharges';
4727   $a->itemdesc cmp $b->itemdesc;
4728 }
4729
4730 sub _items_tax {
4731   my $self = shift;
4732   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4733   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4734 }
4735
4736 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4737
4738 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4739 list of hashrefs describing the line items they generate on the invoice.
4740
4741 OPTIONS may include:
4742
4743 format: the invoice format.
4744
4745 escape_function: the function used to escape strings.
4746
4747 format_function: the function used to format CDRs.
4748
4749 section: a hashref containing 'description'; if this is present, 
4750 cust_bill_pkg_display records not belonging to this section are 
4751 ignored.
4752
4753 multisection: a flag indicating that this is a multisection invoice,
4754 which does something complicated.
4755
4756 multilocation: a flag to display the location label for the package.
4757
4758 Returns a list of hashrefs, each of which may contain:
4759
4760 pkgnum, description, amount, unit_amount, quantity, _is_setup, and 
4761 ext_description, which is an arrayref of detail lines to show below 
4762 the package line.
4763
4764 =cut
4765
4766 sub _items_cust_bill_pkg {
4767   my $self = shift;
4768   my $conf = $self->conf;
4769   my $cust_bill_pkgs = shift;
4770   my %opt = @_;
4771
4772   my $format = $opt{format} || '';
4773   my $escape_function = $opt{escape_function} || sub { shift };
4774   my $format_function = $opt{format_function} || '';
4775   my $unsquelched = $opt{unsquelched} || ''; #unused
4776   my $section = $opt{section}->{description} if $opt{section};
4777   my $summary_page = $opt{summary_page} || ''; #unused
4778   my $multilocation = $opt{multilocation} || '';
4779   my $multisection = $opt{multisection} || '';
4780   my $discount_show_always = 0;
4781
4782   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4783
4784   my @b = ();
4785   my ($s, $r, $u) = ( undef, undef, undef );
4786   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4787   {
4788
4789     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4790       if ( $_ && !$cust_bill_pkg->hidden ) {
4791         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4792         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4793         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4794         push @b, { %$_ }
4795           if $_->{amount} != 0
4796           || $discount_show_always
4797           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4798           || (   $_->{_is_setup} && $_->{setup_show_zero} )
4799         ;
4800         $_ = undef;
4801       }
4802     }
4803
4804     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4805          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4806       if $DEBUG > 1;
4807
4808     foreach my $display ( grep { defined($section)
4809                                  ? $_->section eq $section
4810                                  : 1
4811                                }
4812                           #grep { !$_->summary || !$summary_page } # bunk!
4813                           grep { !$_->summary || $multisection }
4814                           $cust_bill_pkg->cust_bill_pkg_display
4815                         )
4816     {
4817
4818       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4819            $display->billpkgdisplaynum. "\n"
4820         if $DEBUG > 1;
4821
4822       my $type = $display->type;
4823
4824       my $desc = $cust_bill_pkg->desc;
4825       $desc = substr($desc, 0, $maxlength). '...'
4826         if $format eq 'latex' && length($desc) > $maxlength;
4827
4828       my %details_opt = ( 'format'          => $format,
4829                           'escape_function' => $escape_function,
4830                           'format_function' => $format_function,
4831                         );
4832
4833       if ( $cust_bill_pkg->pkgnum > 0 ) {
4834
4835         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4836           if $DEBUG > 1;
4837  
4838         my $cust_pkg = $cust_bill_pkg->cust_pkg;
4839
4840         # start/end dates for invoice formats that do nonstandard 
4841         # things with them
4842         my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4843
4844         if (    (!$type || $type eq 'S')
4845              && (    $cust_bill_pkg->setup != 0
4846                   || $cust_bill_pkg->setup_show_zero
4847                 )
4848            )
4849          {
4850
4851           warn "$me _items_cust_bill_pkg adding setup\n"
4852             if $DEBUG > 1;
4853
4854           my $description = $desc;
4855           $description .= ' Setup'
4856             if $cust_bill_pkg->recur != 0
4857             || $discount_show_always
4858             || $cust_bill_pkg->recur_show_zero;
4859
4860           my @d = ();
4861           unless ( $cust_pkg->part_pkg->hide_svc_detail
4862                 || $cust_bill_pkg->hidden )
4863           {
4864
4865             push @d, map &{$escape_function}($_),
4866                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
4867               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4868
4869             if ( $multilocation ) {
4870               my $loc = $cust_pkg->location_label;
4871               $loc = substr($loc, 0, $maxlength). '...'
4872                 if $format eq 'latex' && length($loc) > $maxlength;
4873               push @d, &{$escape_function}($loc);
4874             }
4875
4876           }
4877
4878           push @d, $cust_bill_pkg->details(%details_opt)
4879             if $cust_bill_pkg->recur == 0;
4880
4881           if ( $cust_bill_pkg->hidden ) {
4882             $s->{amount}      += $cust_bill_pkg->setup;
4883             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4884             push @{ $s->{ext_description} }, @d;
4885           } else {
4886             $s = {
4887               _is_setup       => 1,
4888               description     => $description,
4889               #pkgpart         => $part_pkg->pkgpart,
4890               pkgnum          => $cust_bill_pkg->pkgnum,
4891               amount          => $cust_bill_pkg->setup,
4892               setup_show_zero => $cust_bill_pkg->setup_show_zero,
4893               unit_amount     => $cust_bill_pkg->unitsetup,
4894               quantity        => $cust_bill_pkg->quantity,
4895               ext_description => \@d,
4896             };
4897           };
4898
4899         }
4900
4901         if (    ( !$type || $type eq 'R' || $type eq 'U' )
4902              && (
4903                      $cust_bill_pkg->recur != 0
4904                   || $cust_bill_pkg->setup == 0
4905                   || $discount_show_always
4906                   || $cust_bill_pkg->recur_show_zero
4907                 )
4908            )
4909         {
4910
4911           warn "$me _items_cust_bill_pkg adding recur/usage\n"
4912             if $DEBUG > 1;
4913
4914           my $is_summary = $display->summary;
4915           my $description = ($is_summary && $type && $type eq 'U')
4916                             ? "Usage charges" : $desc;
4917
4918           $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4919                           " - ". time2str($date_format, $cust_bill_pkg->edate).
4920                           ")"
4921             unless $conf->exists('disable_line_item_date_ranges')
4922                 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4923
4924           my @d = ();
4925
4926           #at least until cust_bill_pkg has "past" ranges in addition to
4927           #the "future" sdate/edate ones... see #3032
4928           my @dates = ( $self->_date );
4929           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4930           push @dates, $prev->sdate if $prev;
4931           push @dates, undef if !$prev;
4932
4933           unless ( $cust_pkg->part_pkg->hide_svc_detail
4934                 || $cust_bill_pkg->itemdesc
4935                 || $cust_bill_pkg->hidden
4936                 || $is_summary && $type && $type eq 'U' )
4937           {
4938
4939             warn "$me _items_cust_bill_pkg adding service details\n"
4940               if $DEBUG > 1;
4941
4942             push @d, map &{$escape_function}($_),
4943                          $cust_pkg->h_labels_short(@dates, 'I')
4944                                                    #$cust_bill_pkg->edate,
4945                                                    #$cust_bill_pkg->sdate)
4946               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4947
4948             warn "$me _items_cust_bill_pkg done adding service details\n"
4949               if $DEBUG > 1;
4950
4951             if ( $multilocation ) {
4952               my $loc = $cust_pkg->location_label;
4953               $loc = substr($loc, 0, $maxlength). '...'
4954                 if $format eq 'latex' && length($loc) > $maxlength;
4955               push @d, &{$escape_function}($loc);
4956             }
4957
4958           }
4959
4960           unless ( $is_summary ) {
4961             warn "$me _items_cust_bill_pkg adding details\n"
4962               if $DEBUG > 1;
4963
4964             #instead of omitting details entirely in this case (unwanted side
4965             # effects), just omit CDRs
4966             $details_opt{'format_function'} = sub { () }
4967               if $type && $type eq 'R';
4968
4969             push @d, $cust_bill_pkg->details(%details_opt);
4970           }
4971
4972           warn "$me _items_cust_bill_pkg calculating amount\n"
4973             if $DEBUG > 1;
4974   
4975           my $amount = 0;
4976           if (!$type) {
4977             $amount = $cust_bill_pkg->recur;
4978           } elsif ($type eq 'R') {
4979             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4980           } elsif ($type eq 'U') {
4981             $amount = $cust_bill_pkg->usage;
4982           }
4983   
4984           if ( !$type || $type eq 'R' ) {
4985
4986             warn "$me _items_cust_bill_pkg adding recur\n"
4987               if $DEBUG > 1;
4988
4989             if ( $cust_bill_pkg->hidden ) {
4990               $r->{amount}      += $amount;
4991               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4992               push @{ $r->{ext_description} }, @d;
4993             } else {
4994               $r = {
4995                 description     => $description,
4996                 #pkgpart         => $part_pkg->pkgpart,
4997                 pkgnum          => $cust_bill_pkg->pkgnum,
4998                 amount          => $amount,
4999                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5000                 unit_amount     => $cust_bill_pkg->unitrecur,
5001                 quantity        => $cust_bill_pkg->quantity,
5002                 %item_dates,
5003                 ext_description => \@d,
5004               };
5005             }
5006
5007           } else {  # $type eq 'U'
5008
5009             warn "$me _items_cust_bill_pkg adding usage\n"
5010               if $DEBUG > 1;
5011
5012             if ( $cust_bill_pkg->hidden ) {
5013               $u->{amount}      += $amount;
5014               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5015               push @{ $u->{ext_description} }, @d;
5016             } else {
5017               $u = {
5018                 description     => $description,
5019                 #pkgpart         => $part_pkg->pkgpart,
5020                 pkgnum          => $cust_bill_pkg->pkgnum,
5021                 amount          => $amount,
5022                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5023                 unit_amount     => $cust_bill_pkg->unitrecur,
5024                 quantity        => $cust_bill_pkg->quantity,
5025                 %item_dates,
5026                 ext_description => \@d,
5027               };
5028             }
5029           }
5030
5031         } # recurring or usage with recurring charge
5032
5033       } else { #pkgnum tax or one-shot line item (??)
5034
5035         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5036           if $DEBUG > 1;
5037
5038         if ( $cust_bill_pkg->setup != 0 ) {
5039           push @b, {
5040             'description' => $desc,
5041             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
5042           };
5043         }
5044         if ( $cust_bill_pkg->recur != 0 ) {
5045           push @b, {
5046             'description' => "$desc (".
5047                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5048                              time2str($date_format, $cust_bill_pkg->edate). ')',
5049             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
5050           };
5051         }
5052
5053       }
5054
5055     }
5056
5057     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5058                                 && $conf->exists('discount-show-always'));
5059
5060   }
5061
5062   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5063     if ( $_  ) {
5064       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
5065       $_->{amount}      =~ s/^\-0\.00$/0.00/;
5066       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5067       push @b, { %$_ }
5068         if $_->{amount} != 0
5069         || $discount_show_always
5070         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5071         || (   $_->{_is_setup} && $_->{setup_show_zero} )
5072     }
5073   }
5074
5075   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5076     if $DEBUG > 1;
5077
5078   @b;
5079
5080 }
5081
5082 sub _items_credits {
5083   my( $self, %opt ) = @_;
5084   my $trim_len = $opt{'trim_len'} || 60;
5085
5086   my @b;
5087   #credits
5088   foreach ( $self->cust_credited ) {
5089
5090     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5091
5092     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5093     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5094     $reason = " ($reason) " if $reason;
5095
5096     push @b, {
5097       #'description' => 'Credit ref\#'. $_->crednum.
5098       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
5099       #                 $reason,
5100       'description' => $self->mt('Credit applied').' '.
5101                        time2str($date_format,$_->cust_credit->_date). $reason,
5102       'amount'      => sprintf("%.2f",$_->amount),
5103     };
5104   }
5105
5106   @b;
5107
5108 }
5109
5110 sub _items_payments {
5111   my $self = shift;
5112
5113   my @b;
5114   #get & print payments
5115   foreach ( $self->cust_bill_pay ) {
5116
5117     #something more elaborate if $_->amount ne ->cust_pay->paid ?
5118
5119     push @b, {
5120       'description' => $self->mt('Payment received').' '.
5121                        time2str($date_format,$_->cust_pay->_date ),
5122       'amount'      => sprintf("%.2f", $_->amount )
5123     };
5124   }
5125
5126   @b;
5127
5128 }
5129
5130 =item call_details [ OPTION => VALUE ... ]
5131
5132 Returns an array of CSV strings representing the call details for this invoice
5133 The only option available is the boolean prepend_billed_number
5134
5135 =cut
5136
5137 sub call_details {
5138   my ($self, %opt) = @_;
5139
5140   my $format_function = sub { shift };
5141
5142   if ($opt{prepend_billed_number}) {
5143     $format_function = sub {
5144       my $detail = shift;
5145       my $row = shift;
5146
5147       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5148       
5149     };
5150   }
5151
5152   my @details = map { $_->details( 'format_function' => $format_function,
5153                                    'escape_function' => sub{ return() },
5154                                  )
5155                     }
5156                   grep { $_->pkgnum }
5157                   $self->cust_bill_pkg;
5158   my $header = $details[0];
5159   ( $header, grep { $_ ne $header } @details );
5160 }
5161
5162
5163 =back
5164
5165 =head1 SUBROUTINES
5166
5167 =over 4
5168
5169 =item process_reprint
5170
5171 =cut
5172
5173 sub process_reprint {
5174   process_re_X('print', @_);
5175 }
5176
5177 =item process_reemail
5178
5179 =cut
5180
5181 sub process_reemail {
5182   process_re_X('email', @_);
5183 }
5184
5185 =item process_refax
5186
5187 =cut
5188
5189 sub process_refax {
5190   process_re_X('fax', @_);
5191 }
5192
5193 =item process_reftp
5194
5195 =cut
5196
5197 sub process_reftp {
5198   process_re_X('ftp', @_);
5199 }
5200
5201 =item respool
5202
5203 =cut
5204
5205 sub process_respool {
5206   process_re_X('spool', @_);
5207 }
5208
5209 use Storable qw(thaw);
5210 use Data::Dumper;
5211 use MIME::Base64;
5212 sub process_re_X {
5213   my( $method, $job ) = ( shift, shift );
5214   warn "$me process_re_X $method for job $job\n" if $DEBUG;
5215
5216   my $param = thaw(decode_base64(shift));
5217   warn Dumper($param) if $DEBUG;
5218
5219   re_X(
5220     $method,
5221     $job,
5222     %$param,
5223   );
5224
5225 }
5226
5227 sub re_X {
5228   my($method, $job, %param ) = @_;
5229   if ( $DEBUG ) {
5230     warn "re_X $method for job $job with param:\n".
5231          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
5232   }
5233
5234   #some false laziness w/search/cust_bill.html
5235   my $distinct = '';
5236   my $orderby = 'ORDER BY cust_bill._date';
5237
5238   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5239
5240   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5241      
5242   my @cust_bill = qsearch( {
5243     #'select'    => "cust_bill.*",
5244     'table'     => 'cust_bill',
5245     'addl_from' => $addl_from,
5246     'hashref'   => {},
5247     'extra_sql' => $extra_sql,
5248     'order_by'  => $orderby,
5249     'debug' => 1,
5250   } );
5251
5252   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5253
5254   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5255     if $DEBUG;
5256
5257   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5258   foreach my $cust_bill ( @cust_bill ) {
5259     $cust_bill->$method();
5260
5261     if ( $job ) { #progressbar foo
5262       $num++;
5263       if ( time - $min_sec > $last ) {
5264         my $error = $job->update_statustext(
5265           int( 100 * $num / scalar(@cust_bill) )
5266         );
5267         die $error if $error;
5268         $last = time;
5269       }
5270     }
5271
5272   }
5273
5274 }
5275
5276 =back
5277
5278 =head1 CLASS METHODS
5279
5280 =over 4
5281
5282 =item owed_sql
5283
5284 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5285
5286 =cut
5287
5288 sub owed_sql {
5289   my ($class, $start, $end) = @_;
5290   'charged - '. 
5291     $class->paid_sql($start, $end). ' - '. 
5292     $class->credited_sql($start, $end);
5293 }
5294
5295 =item net_sql
5296
5297 Returns an SQL fragment to retreive the net amount (charged minus credited).
5298
5299 =cut
5300
5301 sub net_sql {
5302   my ($class, $start, $end) = @_;
5303   'charged - '. $class->credited_sql($start, $end);
5304 }
5305
5306 =item paid_sql
5307
5308 Returns an SQL fragment to retreive the amount paid against this invoice.
5309
5310 =cut
5311
5312 sub paid_sql {
5313   my ($class, $start, $end) = @_;
5314   $start &&= "AND cust_bill_pay._date <= $start";
5315   $end   &&= "AND cust_bill_pay._date > $end";
5316   $start = '' unless defined($start);
5317   $end   = '' unless defined($end);
5318   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5319        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
5320 }
5321
5322 =item credited_sql
5323
5324 Returns an SQL fragment to retreive the amount credited against this invoice.
5325
5326 =cut
5327
5328 sub credited_sql {
5329   my ($class, $start, $end) = @_;
5330   $start &&= "AND cust_credit_bill._date <= $start";
5331   $end   &&= "AND cust_credit_bill._date >  $end";
5332   $start = '' unless defined($start);
5333   $end   = '' unless defined($end);
5334   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5335        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
5336 }
5337
5338 =item due_date_sql
5339
5340 Returns an SQL fragment to retrieve the due date of an invoice.
5341 Currently only supported on PostgreSQL.
5342
5343 =cut
5344
5345 sub due_date_sql {
5346   my $conf = new FS::Conf;
5347 'COALESCE(
5348   SUBSTRING(
5349     COALESCE(
5350       cust_bill.invoice_terms,
5351       cust_main.invoice_terms,
5352       \''.($conf->config('invoice_default_terms') || '').'\'
5353     ), E\'Net (\\\\d+)\'
5354   )::INTEGER, 0
5355 ) * 86400 + cust_bill._date'
5356 }
5357
5358 =item search_sql_where HASHREF
5359
5360 Class method which returns an SQL WHERE fragment to search for parameters
5361 specified in HASHREF.  Valid parameters are
5362
5363 =over 4
5364
5365 =item _date
5366
5367 List reference of start date, end date, as UNIX timestamps.
5368
5369 =item invnum_min
5370
5371 =item invnum_max
5372
5373 =item agentnum
5374
5375 =item charged
5376
5377 List reference of charged limits (exclusive).
5378
5379 =item owed
5380
5381 List reference of charged limits (exclusive).
5382
5383 =item open
5384
5385 flag, return open invoices only
5386
5387 =item net
5388
5389 flag, return net invoices only
5390
5391 =item days
5392
5393 =item newest_percust
5394
5395 =back
5396
5397 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5398
5399 =cut
5400
5401 sub search_sql_where {
5402   my($class, $param) = @_;
5403   if ( $DEBUG ) {
5404     warn "$me search_sql_where called with params: \n".
5405          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
5406   }
5407
5408   my @search = ();
5409
5410   #agentnum
5411   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5412     push @search, "cust_main.agentnum = $1";
5413   }
5414
5415   #agentnum
5416   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5417     push @search, "cust_bill.custnum = $1";
5418   }
5419
5420   #_date
5421   if ( $param->{_date} ) {
5422     my($beginning, $ending) = @{$param->{_date}};
5423
5424     push @search, "cust_bill._date >= $beginning",
5425                   "cust_bill._date <  $ending";
5426   }
5427
5428   #invnum
5429   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5430     push @search, "cust_bill.invnum >= $1";
5431   }
5432   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5433     push @search, "cust_bill.invnum <= $1";
5434   }
5435
5436   #charged
5437   if ( $param->{charged} ) {
5438     my @charged = ref($param->{charged})
5439                     ? @{ $param->{charged} }
5440                     : ($param->{charged});
5441
5442     push @search, map { s/^charged/cust_bill.charged/; $_; }
5443                       @charged;
5444   }
5445
5446   my $owed_sql = FS::cust_bill->owed_sql;
5447
5448   #owed
5449   if ( $param->{owed} ) {
5450     my @owed = ref($param->{owed})
5451                  ? @{ $param->{owed} }
5452                  : ($param->{owed});
5453     push @search, map { s/^owed/$owed_sql/; $_; }
5454                       @owed;
5455   }
5456
5457   #open/net flags
5458   push @search, "0 != $owed_sql"
5459     if $param->{'open'};
5460   push @search, '0 != '. FS::cust_bill->net_sql
5461     if $param->{'net'};
5462
5463   #days
5464   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5465     if $param->{'days'};
5466
5467   #newest_percust
5468   if ( $param->{'newest_percust'} ) {
5469
5470     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5471     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5472
5473     my @newest_where = map { my $x = $_;
5474                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5475                              $x;
5476                            }
5477                            grep ! /^cust_main./, @search;
5478     my $newest_where = scalar(@newest_where)
5479                          ? ' AND '. join(' AND ', @newest_where)
5480                          : '';
5481
5482
5483     push @search, "cust_bill._date = (
5484       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5485         WHERE newest_cust_bill.custnum = cust_bill.custnum
5486           $newest_where
5487     )";
5488
5489   }
5490
5491   #agent virtualization
5492   my $curuser = $FS::CurrentUser::CurrentUser;
5493   if ( $curuser->username eq 'fs_queue'
5494        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5495     my $username = $1;
5496     my $newuser = qsearchs('access_user', {
5497       'username' => $username,
5498       'disabled' => '',
5499     } );
5500     if ( $newuser ) {
5501       $curuser = $newuser;
5502     } else {
5503       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5504     }
5505   }
5506   push @search, $curuser->agentnums_sql;
5507
5508   join(' AND ', @search );
5509
5510 }
5511
5512 =back
5513
5514 =head1 BUGS
5515
5516 The delete method.
5517
5518 =head1 SEE ALSO
5519
5520 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5521 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5522 documentation.
5523
5524 =cut
5525
5526 1;
5527