radius usage totals for invoice, #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       $detail->{'seconds'} = $line_item->{'seconds'};
3071   
3072       push @detail_items, $detail;
3073       push @buf, ( [ $detail->{'description'},
3074                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3075                    ],
3076                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3077                  );
3078     }
3079
3080     if ( $section->{'description'} ) {
3081       push @buf, ( ['','-----------'],
3082                    [ $section->{'description'}. ' sub-total',
3083                       $section->{'subtotal'} # already formatted this 
3084                    ],
3085                    [ '', '' ],
3086                    [ '', '' ],
3087                  );
3088     }
3089   
3090   }
3091   
3092   $invoice_data{current_less_finance} =
3093     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3094
3095   if ( $multisection && !$conf->exists('disable_previous_balance')
3096     || $conf->exists('previous_balance-summary_only') )
3097   {
3098     unshift @sections, $previous_section if $pr_total;
3099   }
3100
3101   warn "$me adding taxes\n"
3102     if $DEBUG > 1;
3103
3104   foreach my $tax ( $self->_items_tax ) {
3105
3106     $taxtotal += $tax->{'amount'};
3107
3108     my $description = &$escape_function( $tax->{'description'} );
3109     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
3110
3111     if ( $multisection ) {
3112
3113       my $money = $old_latex ? '' : $money_char;
3114       push @detail_items, {
3115         ext_description => [],
3116         ref          => '',
3117         quantity     => '',
3118         description  => $description,
3119         amount       => $money. $amount,
3120         product_code => '',
3121         section      => $tax_section,
3122       };
3123
3124     } else {
3125
3126       push @total_items, {
3127         'total_item'   => $description,
3128         'total_amount' => $other_money_char. $amount,
3129       };
3130
3131     }
3132
3133     push @buf,[ $description,
3134                 $money_char. $amount,
3135               ];
3136
3137   }
3138   
3139   if ( $taxtotal ) {
3140     my $total = {};
3141     $total->{'total_item'} = $self->mt('Sub-total');
3142     $total->{'total_amount'} =
3143       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3144
3145     if ( $multisection ) {
3146       $tax_section->{'subtotal'} = $other_money_char.
3147                                    sprintf('%.2f', $taxtotal);
3148       $tax_section->{'pretotal'} = 'New charges sub-total '.
3149                                    $total->{'total_amount'};
3150       push @sections, $tax_section if $taxtotal;
3151     }else{
3152       unshift @total_items, $total;
3153     }
3154   }
3155   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3156
3157   push @buf,['','-----------'];
3158   push @buf,[$self->mt( 
3159               $conf->exists('disable_previous_balance') 
3160                ? 'Total Charges'
3161                : 'Total New Charges'
3162              ),
3163              $money_char. sprintf("%10.2f",$self->charged) ];
3164   push @buf,['',''];
3165
3166   {
3167     my $total = {};
3168     my $item = 'Total';
3169     $item = $conf->config('previous_balance-exclude_from_total')
3170          || 'Total New Charges'
3171       if $conf->exists('previous_balance-exclude_from_total');
3172     my $amount = $self->charged +
3173                    ( $conf->exists('disable_previous_balance') ||
3174                      $conf->exists('previous_balance-exclude_from_total')
3175                      ? 0
3176                      : $pr_total
3177                    );
3178     $total->{'total_item'} = &$embolden_function($self->mt($item));
3179     $total->{'total_amount'} =
3180       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
3181     if ( $multisection ) {
3182       if ( $adjust_section->{'sort_weight'} ) {
3183         $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3184           $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
3185       } else {
3186         $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3187           $other_money_char.  sprintf('%.2f', $self->charged );
3188       } 
3189     }else{
3190       push @total_items, $total;
3191     }
3192     push @buf,['','-----------'];
3193     push @buf,[$item,
3194                $money_char.
3195                sprintf( '%10.2f', $amount )
3196               ];
3197     push @buf,['',''];
3198   }
3199   
3200   unless ( $conf->exists('disable_previous_balance') ) {
3201     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3202   
3203     # credits
3204     my $credittotal = 0;
3205     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3206
3207       my $total;
3208       $total->{'total_item'} = &$escape_function($credit->{'description'});
3209       $credittotal += $credit->{'amount'};
3210       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3211       $adjusttotal += $credit->{'amount'};
3212       if ( $multisection ) {
3213         my $money = $old_latex ? '' : $money_char;
3214         push @detail_items, {
3215           ext_description => [],
3216           ref          => '',
3217           quantity     => '',
3218           description  => &$escape_function($credit->{'description'}),
3219           amount       => $money. $credit->{'amount'},
3220           product_code => '',
3221           section      => $adjust_section,
3222         };
3223       } else {
3224         push @total_items, $total;
3225       }
3226
3227     }
3228     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3229
3230     #credits (again)
3231     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3232       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3233     }
3234
3235     # payments
3236     my $paymenttotal = 0;
3237     foreach my $payment ( $self->_items_payments ) {
3238       my $total = {};
3239       $total->{'total_item'} = &$escape_function($payment->{'description'});
3240       $paymenttotal += $payment->{'amount'};
3241       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3242       $adjusttotal += $payment->{'amount'};
3243       if ( $multisection ) {
3244         my $money = $old_latex ? '' : $money_char;
3245         push @detail_items, {
3246           ext_description => [],
3247           ref          => '',
3248           quantity     => '',
3249           description  => &$escape_function($payment->{'description'}),
3250           amount       => $money. $payment->{'amount'},
3251           product_code => '',
3252           section      => $adjust_section,
3253         };
3254       }else{
3255         push @total_items, $total;
3256       }
3257       push @buf, [ $payment->{'description'},
3258                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
3259                  ];
3260     }
3261     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3262   
3263     if ( $multisection ) {
3264       $adjust_section->{'subtotal'} = $other_money_char.
3265                                       sprintf('%.2f', $adjusttotal);
3266       push @sections, $adjust_section
3267         unless $adjust_section->{sort_weight};
3268     }
3269
3270     { 
3271       my $total;
3272       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3273       $total->{'total_amount'} =
3274         &$embolden_function(
3275           $other_money_char. sprintf('%.2f', $summarypage 
3276                                                ? $self->charged +
3277                                                  $self->billing_balance
3278                                                : $self->owed + $pr_total
3279                                     )
3280         );
3281       if ( $multisection && !$adjust_section->{sort_weight} ) {
3282         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3283                                          $total->{'total_amount'};
3284       }else{
3285         push @total_items, $total;
3286       }
3287       push @buf,['','-----------'];
3288       push @buf,[$self->balance_due_msg, $money_char. 
3289         sprintf("%10.2f", $balance_due ) ];
3290     }
3291
3292     if ( $conf->exists('previous_balance-show_credit')
3293         and $cust_main->balance < 0 ) {
3294       my $credit_total = {
3295         'total_item'    => &$embolden_function($self->credit_balance_msg),
3296         'total_amount'  => &$embolden_function(
3297           $other_money_char. sprintf('%.2f', -$cust_main->balance)
3298         ),
3299       };
3300       if ( $multisection ) {
3301         $adjust_section->{'posttotal'} .= $newline_token .
3302           $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3303       }
3304       else {
3305         push @total_items, $credit_total;
3306       }
3307       push @buf,['','-----------'];
3308       push @buf,[$self->credit_balance_msg, $money_char. 
3309         sprintf("%10.2f", -$cust_main->balance ) ];
3310     }
3311   }
3312
3313   if ( $multisection ) {
3314     if ($conf->exists('svc_phone_sections')) {
3315       my $total;
3316       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3317       $total->{'total_amount'} =
3318         &$embolden_function(
3319           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3320         );
3321       my $last_section = pop @sections;
3322       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3323                                      $total->{'total_amount'};
3324       push @sections, $last_section;
3325     }
3326     push @sections, @$late_sections
3327       if $unsquelched;
3328   }
3329
3330   my @includelist = ();
3331   push @includelist, 'summary' if $summarypage;
3332   foreach my $include ( @includelist ) {
3333
3334     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3335     my @inc_src;
3336
3337     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3338
3339       @inc_src = $conf->config($inc_file, $agentnum);
3340
3341     } else {
3342
3343       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3344
3345       my $convert_map = $convert_maps{$format}{$include};
3346
3347       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3348                        s/--\@\]/$delimiters{$format}[1]/g;
3349                        $_;
3350                      } 
3351                  &$convert_map( $conf->config($inc_file, $agentnum) );
3352
3353     }
3354
3355     my $inc_tt = new Text::Template (
3356       TYPE       => 'ARRAY',
3357       SOURCE     => [ map "$_\n", @inc_src ],
3358       DELIMITERS => $delimiters{$format},
3359     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3360
3361     unless ( $inc_tt->compile() ) {
3362       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3363       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3364       die $error;
3365     }
3366
3367     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3368
3369     $invoice_data{$include} =~ s/\n+$//
3370       if ($format eq 'latex');
3371   }
3372
3373   $invoice_lines = 0;
3374   my $wasfunc = 0;
3375   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3376     /invoice_lines\((\d*)\)/;
3377     $invoice_lines += $1 || scalar(@buf);
3378     $wasfunc=1;
3379   }
3380   die "no invoice_lines() functions in template?"
3381     if ( $format eq 'template' && !$wasfunc );
3382
3383   if ($format eq 'template') {
3384
3385     if ( $invoice_lines ) {
3386       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3387       $invoice_data{'total_pages'}++
3388         if scalar(@buf) % $invoice_lines;
3389     }
3390
3391     #setup subroutine for the template
3392     #sub FS::cust_bill::_template::invoice_lines { # good god, no
3393     $invoice_data{invoice_lines} = sub { # much better
3394       my $lines = shift || scalar(@buf);
3395       map { 
3396         scalar(@buf)
3397           ? shift @buf
3398           : [ '', '' ];
3399       }
3400       ( 1 .. $lines );
3401     };
3402
3403     my $lines;
3404     my @collect;
3405     while (@buf) {
3406       push @collect, split("\n",
3407         $text_template->fill_in( HASH => \%invoice_data )
3408       );
3409       $invoice_data{'page'}++;
3410     }
3411     map "$_\n", @collect;
3412   }else{
3413     # this is where we actually create the invoice
3414     warn "filling in template for invoice ". $self->invnum. "\n"
3415       if $DEBUG;
3416     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3417       if $DEBUG > 1;
3418
3419     $text_template->fill_in(HASH => \%invoice_data);
3420   }
3421 }
3422
3423 # helper routine for generating date ranges
3424 sub _prior_month30s {
3425   my $self = shift;
3426   my @ranges = (
3427    [ 1,       2592000 ], # 0-30 days ago
3428    [ 2592000, 5184000 ], # 30-60 days ago
3429    [ 5184000, 7776000 ], # 60-90 days ago
3430    [ 7776000, 0       ], # 90+   days ago
3431   );
3432
3433   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3434           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3435       ] }
3436   @ranges;
3437 }
3438
3439 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3440
3441 Returns an postscript invoice, as a scalar.
3442
3443 Options can be passed as a hashref (recommended) or as a list of time, template
3444 and then any key/value pairs for any other options.
3445
3446 I<time> an optional value used to control the printing of overdue messages.  The
3447 default is now.  It isn't the date of the invoice; that's the `_date' field.
3448 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3449 L<Time::Local> and L<Date::Parse> for conversion functions.
3450
3451 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3452
3453 =cut
3454
3455 sub print_ps {
3456   my $self = shift;
3457
3458   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3459   my $ps = generate_ps($file);
3460   unlink($logofile);
3461   unlink($barcodefile) if $barcodefile;
3462
3463   $ps;
3464 }
3465
3466 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3467
3468 Returns an PDF invoice, as a scalar.
3469
3470 Options can be passed as a hashref (recommended) or as a list of time, template
3471 and then any key/value pairs for any other options.
3472
3473 I<time> an optional value used to control the printing of overdue messages.  The
3474 default is now.  It isn't the date of the invoice; that's the `_date' field.
3475 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3476 L<Time::Local> and L<Date::Parse> for conversion functions.
3477
3478 I<template>, if specified, is the name of a suffix for alternate invoices.
3479
3480 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3481
3482 =cut
3483
3484 sub print_pdf {
3485   my $self = shift;
3486
3487   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3488   my $pdf = generate_pdf($file);
3489   unlink($logofile);
3490   unlink($barcodefile) if $barcodefile;
3491
3492   $pdf;
3493 }
3494
3495 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3496
3497 Returns an HTML invoice, as a scalar.
3498
3499 I<time> an optional value used to control the printing of overdue messages.  The
3500 default is now.  It isn't the date of the invoice; that's the `_date' field.
3501 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3502 L<Time::Local> and L<Date::Parse> for conversion functions.
3503
3504 I<template>, if specified, is the name of a suffix for alternate invoices.
3505
3506 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3507
3508 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3509 when emailing the invoice as part of a multipart/related MIME email.
3510
3511 =cut
3512
3513 sub print_html {
3514   my $self = shift;
3515   my %params;
3516   if ( ref($_[0]) ) {
3517     %params = %{ shift() }; 
3518   }else{
3519     $params{'time'} = shift;
3520     $params{'template'} = shift;
3521     $params{'cid'} = shift;
3522   }
3523
3524   $params{'format'} = 'html';
3525   
3526   $self->print_generic( %params );
3527 }
3528
3529 # quick subroutine for print_latex
3530 #
3531 # There are ten characters that LaTeX treats as special characters, which
3532 # means that they do not simply typeset themselves: 
3533 #      # $ % & ~ _ ^ \ { }
3534 #
3535 # TeX ignores blanks following an escaped character; if you want a blank (as
3536 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3537
3538 sub _latex_escape {
3539   my $value = shift;
3540   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3541   $value =~ s/([<>])/\$$1\$/g;
3542   $value;
3543 }
3544
3545 sub _html_escape {
3546   my $value = shift;
3547   encode_entities($value);
3548   $value;
3549 }
3550
3551 sub _html_escape_nbsp {
3552   my $value = _html_escape(shift);
3553   $value =~ s/ +/&nbsp;/g;
3554   $value;
3555 }
3556
3557 #utility methods for print_*
3558
3559 sub _translate_old_latex_format {
3560   warn "_translate_old_latex_format called\n"
3561     if $DEBUG; 
3562
3563   my @template = ();
3564   while ( @_ ) {
3565     my $line = shift;
3566   
3567     if ( $line =~ /^%%Detail\s*$/ ) {
3568   
3569       push @template, q![@--!,
3570                       q!  foreach my $_tr_line (@detail_items) {!,
3571                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3572                       q!      $_tr_line->{'description'} .= !, 
3573                       q!        "\\tabularnewline\n~~".!,
3574                       q!        join( "\\tabularnewline\n~~",!,
3575                       q!          @{$_tr_line->{'ext_description'}}!,
3576                       q!        );!,
3577                       q!    }!;
3578
3579       while ( ( my $line_item_line = shift )
3580               !~ /^%%EndDetail\s*$/                            ) {
3581         $line_item_line =~ s/'/\\'/g;    # nice LTS
3582         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3583         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3584         push @template, "    \$OUT .= '$line_item_line';";
3585       }
3586
3587       push @template, '}',
3588                       '--@]';
3589       #' doh, gvim
3590     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3591
3592       push @template, '[@--',
3593                       '  foreach my $_tr_line (@total_items) {';
3594
3595       while ( ( my $total_item_line = shift )
3596               !~ /^%%EndTotalDetails\s*$/                      ) {
3597         $total_item_line =~ s/'/\\'/g;    # nice LTS
3598         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3599         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3600         push @template, "    \$OUT .= '$total_item_line';";
3601       }
3602
3603       push @template, '}',
3604                       '--@]';
3605
3606     } else {
3607       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3608       push @template, $line;  
3609     }
3610   
3611   }
3612
3613   if ($DEBUG) {
3614     warn "$_\n" foreach @template;
3615   }
3616
3617   (@template);
3618 }
3619
3620 sub terms {
3621   my $self = shift;
3622   my $conf = $self->conf;
3623
3624   #check for an invoice-specific override
3625   return $self->invoice_terms if $self->invoice_terms;
3626   
3627   #check for a customer- specific override
3628   my $cust_main = $self->cust_main;
3629   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3630
3631   #use configured default
3632   $conf->config('invoice_default_terms') || '';
3633 }
3634
3635 sub due_date {
3636   my $self = shift;
3637   my $duedate = '';
3638   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3639     $duedate = $self->_date() + ( $1 * 86400 );
3640   }
3641   $duedate;
3642 }
3643
3644 sub due_date2str {
3645   my $self = shift;
3646   $self->due_date ? time2str(shift, $self->due_date) : '';
3647 }
3648
3649 sub balance_due_msg {
3650   my $self = shift;
3651   my $msg = $self->mt('Balance Due');
3652   return $msg unless $self->terms;
3653   if ( $self->due_date ) {
3654     $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3655       $self->due_date2str($date_format);
3656   } elsif ( $self->terms ) {
3657     $msg .= ' - '. $self->terms;
3658   }
3659   $msg;
3660 }
3661
3662 sub balance_due_date {
3663   my $self = shift;
3664   my $conf = $self->conf;
3665   my $duedate = '';
3666   if (    $conf->exists('invoice_default_terms') 
3667        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3668     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3669   }
3670   $duedate;
3671 }
3672
3673 sub credit_balance_msg { 
3674   my $self = shift;
3675   $self->mt('Credit Balance Remaining')
3676 }
3677
3678 =item invnum_date_pretty
3679
3680 Returns a string with the invoice number and date, for example:
3681 "Invoice #54 (3/20/2008)"
3682
3683 =cut
3684
3685 sub invnum_date_pretty {
3686   my $self = shift;
3687   $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3688 }
3689
3690 =item _date_pretty
3691
3692 Returns a string with the date, for example: "3/20/2008"
3693
3694 =cut
3695
3696 sub _date_pretty {
3697   my $self = shift;
3698   time2str($date_format, $self->_date);
3699 }
3700
3701 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3702
3703 Generate section information for all items appearing on this invoice.
3704 This will only be called for multi-section invoices.
3705
3706 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
3707 related display records (L<FS::cust_bill_pkg_display>) and organize 
3708 them into two groups ("early" and "late" according to whether they come 
3709 before or after the total), then into sections.  A subtotal is calculated 
3710 for each section.
3711
3712 Section descriptions are returned in sort weight order.  Each consists 
3713 of a hash containing:
3714
3715 description: the package category name, escaped
3716 subtotal: the total charges in that section
3717 tax_section: a flag indicating that the section contains only tax charges
3718 summarized: same as tax_section, for some reason
3719 sort_weight: the package category's sort weight
3720
3721 If 'condense' is set on the display record, it also contains everything 
3722 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3723 coderefs to generate parts of the invoice.  This is not advised.
3724
3725 Arguments:
3726
3727 LATE: an arrayref to push the "late" section hashes onto.  The "early"
3728 group is simply returned from the method.
3729
3730 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3731 Turning this on has the following effects:
3732 - Ignores display items with the 'summary' flag.
3733 - Combines all items into the "early" group.
3734 - Creates sections for all non-disabled package categories, even if they 
3735 have no charges on this invoice, as well as a section with no name.
3736
3737 ESCAPE: an escape function to use for section titles.
3738
3739 EXTRA_SECTIONS: an arrayref of additional sections to return after the 
3740 sorted list.  If there are any of these, section subtotals exclude 
3741 usage charges.
3742
3743 FORMAT: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
3744 passed through to C<_condense_section()>.
3745
3746 =cut
3747
3748 use vars qw(%pkg_category_cache);
3749 sub _items_sections {
3750   my $self = shift;
3751   my $late = shift;
3752   my $summarypage = shift;
3753   my $escape = shift;
3754   my $extra_sections = shift;
3755   my $format = shift;
3756
3757   my %subtotal = ();
3758   my %late_subtotal = ();
3759   my %not_tax = ();
3760
3761   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3762   {
3763
3764       my $usage = $cust_bill_pkg->usage;
3765
3766       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3767         next if ( $display->summary && $summarypage );
3768
3769         my $section = $display->section;
3770         my $type    = $display->type;
3771
3772         $not_tax{$section} = 1
3773           unless $cust_bill_pkg->pkgnum == 0;
3774
3775         if ( $display->post_total && !$summarypage ) {
3776           if (! $type || $type eq 'S') {
3777             $late_subtotal{$section} += $cust_bill_pkg->setup
3778               if $cust_bill_pkg->setup != 0;
3779           }
3780
3781           if (! $type) {
3782             $late_subtotal{$section} += $cust_bill_pkg->recur
3783               if $cust_bill_pkg->recur != 0;
3784           }
3785
3786           if ($type && $type eq 'R') {
3787             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3788               if $cust_bill_pkg->recur != 0;
3789           }
3790           
3791           if ($type && $type eq 'U') {
3792             $late_subtotal{$section} += $usage
3793               unless scalar(@$extra_sections);
3794           }
3795
3796         } else {
3797
3798           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3799
3800           if (! $type || $type eq 'S') {
3801             $subtotal{$section} += $cust_bill_pkg->setup
3802               if $cust_bill_pkg->setup != 0;
3803           }
3804
3805           if (! $type) {
3806             $subtotal{$section} += $cust_bill_pkg->recur
3807               if $cust_bill_pkg->recur != 0;
3808           }
3809
3810           if ($type && $type eq 'R') {
3811             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3812               if $cust_bill_pkg->recur != 0;
3813           }
3814           
3815           if ($type && $type eq 'U') {
3816             $subtotal{$section} += $usage
3817               unless scalar(@$extra_sections);
3818           }
3819
3820         }
3821
3822       }
3823
3824   }
3825
3826   %pkg_category_cache = ();
3827
3828   push @$late, map { { 'description' => &{$escape}($_),
3829                        'subtotal'    => $late_subtotal{$_},
3830                        'post_total'  => 1,
3831                        'sort_weight' => ( _pkg_category($_)
3832                                             ? _pkg_category($_)->weight
3833                                             : 0
3834                                        ),
3835                        ((_pkg_category($_) && _pkg_category($_)->condense)
3836                                            ? $self->_condense_section($format)
3837                                            : ()
3838                        ),
3839                    } }
3840                  sort _sectionsort keys %late_subtotal;
3841
3842   my @sections;
3843   if ( $summarypage ) {
3844     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3845                 map { $_->categoryname } qsearch('pkg_category', {});
3846     push @sections, '' if exists($subtotal{''});
3847   } else {
3848     @sections = keys %subtotal;
3849   }
3850
3851   my @early = map { { 'description' => &{$escape}($_),
3852                       'subtotal'    => $subtotal{$_},
3853                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3854                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3855                       'sort_weight' => ( _pkg_category($_)
3856                                            ? _pkg_category($_)->weight
3857                                            : 0
3858                                        ),
3859                        ((_pkg_category($_) && _pkg_category($_)->condense)
3860                                            ? $self->_condense_section($format)
3861                                            : ()
3862                        ),
3863                     }
3864                   } @sections;
3865   push @early, @$extra_sections if $extra_sections;
3866
3867   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3868
3869 }
3870
3871 #helper subs for above
3872
3873 sub _sectionsort {
3874   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3875 }
3876
3877 sub _pkg_category {
3878   my $categoryname = shift;
3879   $pkg_category_cache{$categoryname} ||=
3880     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3881 }
3882
3883 my %condensed_format = (
3884   'label' => [ qw( Description Qty Amount ) ],
3885   'fields' => [
3886                 sub { shift->{description} },
3887                 sub { shift->{quantity} },
3888                 sub { my($href, %opt) = @_;
3889                       ($opt{dollar} || ''). $href->{amount};
3890                     },
3891               ],
3892   'align'  => [ qw( l r r ) ],
3893   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3894   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3895 );
3896
3897 sub _condense_section {
3898   my ( $self, $format ) = ( shift, shift );
3899   ( 'condensed' => 1,
3900     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3901       qw( description_generator
3902           header_generator
3903           total_generator
3904           total_line_generator
3905         )
3906   );
3907 }
3908
3909 sub _condensed_generator_defaults {
3910   my ( $self, $format ) = ( shift, shift );
3911   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3912 }
3913
3914 my %html_align = (
3915   'c' => 'center',
3916   'l' => 'left',
3917   'r' => 'right',
3918 );
3919
3920 sub _condensed_header_generator {
3921   my ( $self, $format ) = ( shift, shift );
3922
3923   my ( $f, $prefix, $suffix, $separator, $column ) =
3924     _condensed_generator_defaults($format);
3925
3926   if ($format eq 'latex') {
3927     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3928     $suffix = "\\\\\n\\hline";
3929     $separator = "&\n";
3930     $column =
3931       sub { my ($d,$a,$s,$w) = @_;
3932             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3933           };
3934   } elsif ( $format eq 'html' ) {
3935     $prefix = '<th></th>';
3936     $suffix = '';
3937     $separator = '';
3938     $column =
3939       sub { my ($d,$a,$s,$w) = @_;
3940             return qq!<th align="$html_align{$a}">$d</th>!;
3941       };
3942   }
3943
3944   sub {
3945     my @args = @_;
3946     my @result = ();
3947
3948     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3949       push @result,
3950         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3951     }
3952
3953     $prefix. join($separator, @result). $suffix;
3954   };
3955
3956 }
3957
3958 sub _condensed_description_generator {
3959   my ( $self, $format ) = ( shift, shift );
3960
3961   my ( $f, $prefix, $suffix, $separator, $column ) =
3962     _condensed_generator_defaults($format);
3963
3964   my $money_char = '$';
3965   if ($format eq 'latex') {
3966     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3967     $suffix = '\\\\';
3968     $separator = " & \n";
3969     $column =
3970       sub { my ($d,$a,$s,$w) = @_;
3971             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3972           };
3973     $money_char = '\\dollar';
3974   }elsif ( $format eq 'html' ) {
3975     $prefix = '"><td align="center"></td>';
3976     $suffix = '';
3977     $separator = '';
3978     $column =
3979       sub { my ($d,$a,$s,$w) = @_;
3980             return qq!<td align="$html_align{$a}">$d</td>!;
3981       };
3982     #$money_char = $conf->config('money_char') || '$';
3983     $money_char = '';  # this is madness
3984   }
3985
3986   sub {
3987     #my @args = @_;
3988     my $href = shift;
3989     my @result = ();
3990
3991     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3992       my $dollar = '';
3993       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3994       push @result,
3995         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3996                     map { $f->{$_}->[$i] } qw(align span width)
3997                   );
3998     }
3999
4000     $prefix. join( $separator, @result ). $suffix;
4001   };
4002
4003 }
4004
4005 sub _condensed_total_generator {
4006   my ( $self, $format ) = ( shift, shift );
4007
4008   my ( $f, $prefix, $suffix, $separator, $column ) =
4009     _condensed_generator_defaults($format);
4010   my $style = '';
4011
4012   if ($format eq 'latex') {
4013     $prefix = "& ";
4014     $suffix = "\\\\\n";
4015     $separator = " & \n";
4016     $column =
4017       sub { my ($d,$a,$s,$w) = @_;
4018             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4019           };
4020   }elsif ( $format eq 'html' ) {
4021     $prefix = '';
4022     $suffix = '';
4023     $separator = '';
4024     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4025     $column =
4026       sub { my ($d,$a,$s,$w) = @_;
4027             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4028       };
4029   }
4030
4031
4032   sub {
4033     my @args = @_;
4034     my @result = ();
4035
4036     #  my $r = &{$f->{fields}->[$i]}(@args);
4037     #  $r .= ' Total' unless $i;
4038
4039     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4040       push @result,
4041         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4042                     map { $f->{$_}->[$i] } qw(align span width)
4043                   );
4044     }
4045
4046     $prefix. join( $separator, @result ). $suffix;
4047   };
4048
4049 }
4050
4051 =item total_line_generator FORMAT
4052
4053 Returns a coderef used for generation of invoice total line items for this
4054 usage_class.  FORMAT is either html or latex
4055
4056 =cut
4057
4058 # should not be used: will have issues with hash element names (description vs
4059 # total_item and amount vs total_amount -- another array of functions?
4060
4061 sub _condensed_total_line_generator {
4062   my ( $self, $format ) = ( shift, shift );
4063
4064   my ( $f, $prefix, $suffix, $separator, $column ) =
4065     _condensed_generator_defaults($format);
4066   my $style = '';
4067
4068   if ($format eq 'latex') {
4069     $prefix = "& ";
4070     $suffix = "\\\\\n";
4071     $separator = " & \n";
4072     $column =
4073       sub { my ($d,$a,$s,$w) = @_;
4074             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4075           };
4076   }elsif ( $format eq 'html' ) {
4077     $prefix = '';
4078     $suffix = '';
4079     $separator = '';
4080     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4081     $column =
4082       sub { my ($d,$a,$s,$w) = @_;
4083             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4084       };
4085   }
4086
4087
4088   sub {
4089     my @args = @_;
4090     my @result = ();
4091
4092     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4093       push @result,
4094         &{$column}( &{$f->{fields}->[$i]}(@args),
4095                     map { $f->{$_}->[$i] } qw(align span width)
4096                   );
4097     }
4098
4099     $prefix. join( $separator, @result ). $suffix;
4100   };
4101
4102 }
4103
4104 #sub _items_extra_usage_sections {
4105 #  my $self = shift;
4106 #  my $escape = shift;
4107 #
4108 #  my %sections = ();
4109 #
4110 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
4111 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4112 #  {
4113 #    next unless $cust_bill_pkg->pkgnum > 0;
4114 #
4115 #    foreach my $section ( keys %usage_class ) {
4116 #
4117 #      my $usage = $cust_bill_pkg->usage($section);
4118 #
4119 #      next unless $usage && $usage > 0;
4120 #
4121 #      $sections{$section} ||= 0;
4122 #      $sections{$section} += $usage;
4123 #
4124 #    }
4125 #
4126 #  }
4127 #
4128 #  map { { 'description' => &{$escape}($_),
4129 #          'subtotal'    => $sections{$_},
4130 #          'summarized'  => '',
4131 #          'tax_section' => '',
4132 #        }
4133 #      }
4134 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4135 #
4136 #}
4137
4138 sub _items_extra_usage_sections {
4139   my $self = shift;
4140   my $conf = $self->conf;
4141   my $escape = shift;
4142   my $format = shift;
4143
4144   my %sections = ();
4145   my %classnums = ();
4146   my %lines = ();
4147
4148   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4149
4150   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4151   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4152     next unless $cust_bill_pkg->pkgnum > 0;
4153
4154     foreach my $classnum ( keys %usage_class ) {
4155       my $section = $usage_class{$classnum}->classname;
4156       $classnums{$section} = $classnum;
4157
4158       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4159         my $amount = $detail->amount;
4160         next unless $amount && $amount > 0;
4161  
4162         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4163         $sections{$section}{amount} += $amount;  #subtotal
4164         $sections{$section}{calls}++;
4165         $sections{$section}{duration} += $detail->duration;
4166
4167         my $desc = $detail->regionname; 
4168         my $description = $desc;
4169         $description = substr($desc, 0, $maxlength). '...'
4170           if $format eq 'latex' && length($desc) > $maxlength;
4171
4172         $lines{$section}{$desc} ||= {
4173           description     => &{$escape}($description),
4174           #pkgpart         => $part_pkg->pkgpart,
4175           pkgnum          => $cust_bill_pkg->pkgnum,
4176           ref             => '',
4177           amount          => 0,
4178           calls           => 0,
4179           duration        => 0,
4180           #unit_amount     => $cust_bill_pkg->unitrecur,
4181           quantity        => $cust_bill_pkg->quantity,
4182           product_code    => 'N/A',
4183           ext_description => [],
4184         };
4185
4186         $lines{$section}{$desc}{amount} += $amount;
4187         $lines{$section}{$desc}{calls}++;
4188         $lines{$section}{$desc}{duration} += $detail->duration;
4189
4190       }
4191     }
4192   }
4193
4194   my %sectionmap = ();
4195   foreach (keys %sections) {
4196     my $usage_class = $usage_class{$classnums{$_}};
4197     $sectionmap{$_} = { 'description' => &{$escape}($_),
4198                         'amount'    => $sections{$_}{amount},    #subtotal
4199                         'calls'       => $sections{$_}{calls},
4200                         'duration'    => $sections{$_}{duration},
4201                         'summarized'  => '',
4202                         'tax_section' => '',
4203                         'sort_weight' => $usage_class->weight,
4204                         ( $usage_class->format
4205                           ? ( map { $_ => $usage_class->$_($format) }
4206                               qw( description_generator header_generator total_generator total_line_generator )
4207                             )
4208                           : ()
4209                         ), 
4210                       };
4211   }
4212
4213   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4214                  values %sectionmap;
4215
4216   my @lines = ();
4217   foreach my $section ( keys %lines ) {
4218     foreach my $line ( keys %{$lines{$section}} ) {
4219       my $l = $lines{$section}{$line};
4220       $l->{section}     = $sectionmap{$section};
4221       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4222       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4223       push @lines, $l;
4224     }
4225   }
4226
4227   return(\@sections, \@lines);
4228
4229 }
4230
4231 sub _did_summary {
4232     my $self = shift;
4233     my $end = $self->_date;
4234
4235     # start at date of previous invoice + 1 second or 0 if no previous invoice
4236     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4237     $start = 0 if !$start;
4238     $start++;
4239
4240     my $cust_main = $self->cust_main;
4241     my @pkgs = $cust_main->all_pkgs;
4242     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4243         = (0,0,0,0,0);
4244     my @seen = ();
4245     foreach my $pkg ( @pkgs ) {
4246         my @h_cust_svc = $pkg->h_cust_svc($end);
4247         foreach my $h_cust_svc ( @h_cust_svc ) {
4248             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4249             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4250
4251             my $inserted = $h_cust_svc->date_inserted;
4252             my $deleted = $h_cust_svc->date_deleted;
4253             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4254             my $phone_deleted;
4255             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
4256             
4257 # DID either activated or ported in; cannot be both for same DID simultaneously
4258             if ($inserted >= $start && $inserted <= $end && $phone_inserted
4259                 && (!$phone_inserted->lnp_status 
4260                     || $phone_inserted->lnp_status eq ''
4261                     || $phone_inserted->lnp_status eq 'native')) {
4262                 $num_activated++;
4263             }
4264             else { # this one not so clean, should probably move to (h_)svc_phone
4265                  my $phone_portedin = qsearchs( 'h_svc_phone',
4266                       { 'svcnum' => $h_cust_svc->svcnum, 
4267                         'lnp_status' => 'portedin' },  
4268                       FS::h_svc_phone->sql_h_searchs($end),  
4269                     );
4270                  $num_portedin++ if $phone_portedin;
4271             }
4272
4273 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4274             if($deleted >= $start && $deleted <= $end && $phone_deleted
4275                 && (!$phone_deleted->lnp_status 
4276                     || $phone_deleted->lnp_status ne 'portingout')) {
4277                 $num_deactivated++;
4278             } 
4279             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
4280                 && $phone_deleted->lnp_status 
4281                 && $phone_deleted->lnp_status eq 'portingout') {
4282                 $num_portedout++;
4283             }
4284
4285             # increment usage minutes
4286         if ( $phone_inserted ) {
4287             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4288             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4289         }
4290         else {
4291             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4292         }
4293
4294             # don't look at this service again
4295             push @seen, $h_cust_svc->svcnum;
4296         }
4297     }
4298
4299     $minutes = sprintf("%d", $minutes);
4300     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
4301         . "$num_deactivated  Ported-Out: $num_portedout ",
4302             "Total Minutes: $minutes");
4303 }
4304
4305 sub _items_accountcode_cdr {
4306     my $self = shift;
4307     my $escape = shift;
4308     my $format = shift;
4309
4310     my $section = { 'amount'        => 0,
4311                     'calls'         => 0,
4312                     'duration'      => 0,
4313                     'sort_weight'   => '',
4314                     'phonenum'      => '',
4315                     'description'   => 'Usage by Account Code',
4316                     'post_total'    => '',
4317                     'summarized'    => '',
4318                     'header'        => '',
4319                   };
4320     my @lines;
4321     my %accountcodes = ();
4322
4323     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4324         next unless $cust_bill_pkg->pkgnum > 0;
4325
4326         my @header = $cust_bill_pkg->details_header;
4327         next unless scalar(@header);
4328         $section->{'header'} = join(',',@header);
4329
4330         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4331
4332             $section->{'header'} = $detail->formatted('format' => $format)
4333                 if($detail->detail eq $section->{'header'}); 
4334       
4335             my $accountcode = $detail->accountcode;
4336             next unless $accountcode;
4337
4338             my $amount = $detail->amount;
4339             next unless $amount && $amount > 0;
4340
4341             $accountcodes{$accountcode} ||= {
4342                     description => $accountcode,
4343                     pkgnum      => '',
4344                     ref         => '',
4345                     amount      => 0,
4346                     calls       => 0,
4347                     duration    => 0,
4348                     quantity    => '',
4349                     product_code => 'N/A',
4350                     section     => $section,
4351                     ext_description => [ $section->{'header'} ],
4352                     detail_temp => [],
4353             };
4354
4355             $section->{'amount'} += $amount;
4356             $accountcodes{$accountcode}{'amount'} += $amount;
4357             $accountcodes{$accountcode}{calls}++;
4358             $accountcodes{$accountcode}{duration} += $detail->duration;
4359             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4360         }
4361     }
4362
4363     foreach my $l ( values %accountcodes ) {
4364         $l->{amount} = sprintf( "%.2f", $l->{amount} );
4365         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4366         foreach my $sorted_detail ( @sorted_detail ) {
4367             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4368         }
4369         delete $l->{detail_temp};
4370         push @lines, $l;
4371     }
4372
4373     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4374
4375     return ($section,\@sorted_lines);
4376 }
4377
4378 sub _items_svc_phone_sections {
4379   my $self = shift;
4380   my $conf = $self->conf;
4381   my $escape = shift;
4382   my $format = shift;
4383
4384   my %sections = ();
4385   my %classnums = ();
4386   my %lines = ();
4387
4388   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4389
4390   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4391   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4392
4393   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4394     next unless $cust_bill_pkg->pkgnum > 0;
4395
4396     my @header = $cust_bill_pkg->details_header;
4397     next unless scalar(@header);
4398
4399     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4400
4401       my $phonenum = $detail->phonenum;
4402       next unless $phonenum;
4403
4404       my $amount = $detail->amount;
4405       next unless $amount && $amount > 0;
4406
4407       $sections{$phonenum} ||= { 'amount'      => 0,
4408                                  'calls'       => 0,
4409                                  'duration'    => 0,
4410                                  'sort_weight' => -1,
4411                                  'phonenum'    => $phonenum,
4412                                 };
4413       $sections{$phonenum}{amount} += $amount;  #subtotal
4414       $sections{$phonenum}{calls}++;
4415       $sections{$phonenum}{duration} += $detail->duration;
4416
4417       my $desc = $detail->regionname; 
4418       my $description = $desc;
4419       $description = substr($desc, 0, $maxlength). '...'
4420         if $format eq 'latex' && length($desc) > $maxlength;
4421
4422       $lines{$phonenum}{$desc} ||= {
4423         description     => &{$escape}($description),
4424         #pkgpart         => $part_pkg->pkgpart,
4425         pkgnum          => '',
4426         ref             => '',
4427         amount          => 0,
4428         calls           => 0,
4429         duration        => 0,
4430         #unit_amount     => '',
4431         quantity        => '',
4432         product_code    => 'N/A',
4433         ext_description => [],
4434       };
4435
4436       $lines{$phonenum}{$desc}{amount} += $amount;
4437       $lines{$phonenum}{$desc}{calls}++;
4438       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4439
4440       my $line = $usage_class{$detail->classnum}->classname;
4441       $sections{"$phonenum $line"} ||=
4442         { 'amount' => 0,
4443           'calls' => 0,
4444           'duration' => 0,
4445           'sort_weight' => $usage_class{$detail->classnum}->weight,
4446           'phonenum' => $phonenum,
4447           'header'  => [ @header ],
4448         };
4449       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4450       $sections{"$phonenum $line"}{calls}++;
4451       $sections{"$phonenum $line"}{duration} += $detail->duration;
4452
4453       $lines{"$phonenum $line"}{$desc} ||= {
4454         description     => &{$escape}($description),
4455         #pkgpart         => $part_pkg->pkgpart,
4456         pkgnum          => '',
4457         ref             => '',
4458         amount          => 0,
4459         calls           => 0,
4460         duration        => 0,
4461         #unit_amount     => '',
4462         quantity        => '',
4463         product_code    => 'N/A',
4464         ext_description => [],
4465       };
4466
4467       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4468       $lines{"$phonenum $line"}{$desc}{calls}++;
4469       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4470       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4471            $detail->formatted('format' => $format);
4472
4473     }
4474   }
4475
4476   my %sectionmap = ();
4477   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4478   foreach ( keys %sections ) {
4479     my @header = @{ $sections{$_}{header} || [] };
4480     my $usage_simple =
4481       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4482     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4483     my $usage_class = $summary ? $simple : $usage_simple;
4484     my $ending = $summary ? ' usage charges' : '';
4485     my %gen_opt = ();
4486     unless ($summary) {
4487       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4488     }
4489     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4490                         'amount'    => $sections{$_}{amount},    #subtotal
4491                         'calls'       => $sections{$_}{calls},
4492                         'duration'    => $sections{$_}{duration},
4493                         'summarized'  => '',
4494                         'tax_section' => '',
4495                         'phonenum'    => $sections{$_}{phonenum},
4496                         'sort_weight' => $sections{$_}{sort_weight},
4497                         'post_total'  => $summary, #inspire pagebreak
4498                         (
4499                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4500                             qw( description_generator
4501                                 header_generator
4502                                 total_generator
4503                                 total_line_generator
4504                               )
4505                           )
4506                         ), 
4507                       };
4508   }
4509
4510   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4511                         $a->{sort_weight} <=> $b->{sort_weight}
4512                       }
4513                  values %sectionmap;
4514
4515   my @lines = ();
4516   foreach my $section ( keys %lines ) {
4517     foreach my $line ( keys %{$lines{$section}} ) {
4518       my $l = $lines{$section}{$line};
4519       $l->{section}     = $sectionmap{$section};
4520       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4521       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4522       push @lines, $l;
4523     }
4524   }
4525   
4526   if($conf->exists('phone_usage_class_summary')) { 
4527       # this only works with Latex
4528       my @newlines;
4529       my @newsections;
4530
4531       # after this, we'll have only two sections per DID:
4532       # Calls Summary and Calls Detail
4533       foreach my $section ( @sections ) {
4534         if($section->{'post_total'}) {
4535             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4536             $section->{'total_line_generator'} = sub { '' };
4537             $section->{'total_generator'} = sub { '' };
4538             $section->{'header_generator'} = sub { '' };
4539             $section->{'description_generator'} = '';
4540             push @newsections, $section;
4541             my %calls_detail = %$section;
4542             $calls_detail{'post_total'} = '';
4543             $calls_detail{'sort_weight'} = '';
4544             $calls_detail{'description_generator'} = sub { '' };
4545             $calls_detail{'header_generator'} = sub {
4546                 return ' & Date/Time & Called Number & Duration & Price'
4547                     if $format eq 'latex';
4548                 '';
4549             };
4550             $calls_detail{'description'} = 'Calls Detail: '
4551                                                     . $section->{'phonenum'};
4552             push @newsections, \%calls_detail;  
4553         }
4554       }
4555
4556       # after this, each usage class is collapsed/summarized into a single
4557       # line under the Calls Summary section
4558       foreach my $newsection ( @newsections ) {
4559         if($newsection->{'post_total'}) { # this means Calls Summary
4560             foreach my $section ( @sections ) {
4561                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
4562                                 && !$section->{'post_total'});
4563                 my $newdesc = $section->{'description'};
4564                 my $tn = $section->{'phonenum'};
4565                 $newdesc =~ s/$tn//g;
4566                 my $line = {  ext_description => [],
4567                               pkgnum => '',
4568                               ref => '',
4569                               quantity => '',
4570                               calls => $section->{'calls'},
4571                               section => $newsection,
4572                               duration => $section->{'duration'},
4573                               description => $newdesc,
4574                               amount => sprintf("%.2f",$section->{'amount'}),
4575                               product_code => 'N/A',
4576                             };
4577                 push @newlines, $line;
4578             }
4579         }
4580       }
4581
4582       # after this, Calls Details is populated with all CDRs
4583       foreach my $newsection ( @newsections ) {
4584         if(!$newsection->{'post_total'}) { # this means Calls Details
4585             foreach my $line ( @lines ) {
4586                 next unless (scalar(@{$line->{'ext_description'}}) &&
4587                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4588                             );
4589                 my @extdesc = @{$line->{'ext_description'}};
4590                 my @newextdesc;
4591                 foreach my $extdesc ( @extdesc ) {
4592                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4593                     push @newextdesc, $extdesc;
4594                 }
4595                 $line->{'ext_description'} = \@newextdesc;
4596                 $line->{'section'} = $newsection;
4597                 push @newlines, $line;
4598             }
4599         }
4600       }
4601
4602       return(\@newsections, \@newlines);
4603   }
4604
4605   return(\@sections, \@lines);
4606
4607 }
4608
4609 sub _items { # seems to be unused
4610   my $self = shift;
4611
4612   #my @display = scalar(@_)
4613   #              ? @_
4614   #              : qw( _items_previous _items_pkg );
4615   #              #: qw( _items_pkg );
4616   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4617   my @display = qw( _items_previous _items_pkg );
4618
4619   my @b = ();
4620   foreach my $display ( @display ) {
4621     push @b, $self->$display(@_);
4622   }
4623   @b;
4624 }
4625
4626 sub _items_previous {
4627   my $self = shift;
4628   my $conf = $self->conf;
4629   my $cust_main = $self->cust_main;
4630   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4631   my @b = ();
4632   foreach ( @pr_cust_bill ) {
4633     my $date = $conf->exists('invoice_show_prior_due_date')
4634                ? 'due '. $_->due_date2str($date_format)
4635                : time2str($date_format, $_->_date);
4636     push @b, {
4637       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4638       #'pkgpart'     => 'N/A',
4639       'pkgnum'      => 'N/A',
4640       'amount'      => sprintf("%.2f", $_->owed),
4641     };
4642   }
4643   @b;
4644
4645   #{
4646   #    'description'     => 'Previous Balance',
4647   #    #'pkgpart'         => 'N/A',
4648   #    'pkgnum'          => 'N/A',
4649   #    'amount'          => sprintf("%10.2f", $pr_total ),
4650   #    'ext_description' => [ map {
4651   #                                 "Invoice ". $_->invnum.
4652   #                                 " (". time2str("%x",$_->_date). ") ".
4653   #                                 sprintf("%10.2f", $_->owed)
4654   #                         } @pr_cust_bill ],
4655
4656   #};
4657 }
4658
4659 =item _items_pkg [ OPTIONS ]
4660
4661 Return line item hashes for each package item on this invoice. Nearly 
4662 equivalent to 
4663
4664 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4665
4666 The only OPTIONS accepted is 'section', which may point to a hashref 
4667 with a key named 'condensed', which may have a true value.  If it 
4668 does, this method tries to merge identical items into items with 
4669 'quantity' equal to the number of items (not the sum of their 
4670 separate quantities, for some reason).
4671
4672 =cut
4673
4674 sub _items_pkg {
4675   my $self = shift;
4676   my %options = @_;
4677
4678   warn "$me _items_pkg searching for all package line items\n"
4679     if $DEBUG > 1;
4680
4681   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4682
4683   warn "$me _items_pkg filtering line items\n"
4684     if $DEBUG > 1;
4685   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4686
4687   if ($options{section} && $options{section}->{condensed}) {
4688
4689     warn "$me _items_pkg condensing section\n"
4690       if $DEBUG > 1;
4691
4692     my %itemshash = ();
4693     local $Storable::canonical = 1;
4694     foreach ( @items ) {
4695       my $item = { %$_ };
4696       delete $item->{ref};
4697       delete $item->{ext_description};
4698       my $key = freeze($item);
4699       $itemshash{$key} ||= 0;
4700       $itemshash{$key} ++; # += $item->{quantity};
4701     }
4702     @items = sort { $a->{description} cmp $b->{description} }
4703              map { my $i = thaw($_);
4704                    $i->{quantity} = $itemshash{$_};
4705                    $i->{amount} =
4706                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4707                    $i;
4708                  }
4709              keys %itemshash;
4710   }
4711
4712   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4713     if $DEBUG > 1;
4714
4715   @items;
4716 }
4717
4718 sub _taxsort {
4719   return 0 unless $a->itemdesc cmp $b->itemdesc;
4720   return -1 if $b->itemdesc eq 'Tax';
4721   return 1 if $a->itemdesc eq 'Tax';
4722   return -1 if $b->itemdesc eq 'Other surcharges';
4723   return 1 if $a->itemdesc eq 'Other surcharges';
4724   $a->itemdesc cmp $b->itemdesc;
4725 }
4726
4727 sub _items_tax {
4728   my $self = shift;
4729   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4730   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4731 }
4732
4733 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4734
4735 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4736 list of hashrefs describing the line items they generate on the invoice.
4737
4738 OPTIONS may include:
4739
4740 format: the invoice format.
4741
4742 escape_function: the function used to escape strings.
4743
4744 format_function: the function used to format CDRs.
4745
4746 section: a hashref containing 'description'; if this is present, 
4747 cust_bill_pkg_display records not belonging to this section are 
4748 ignored.
4749
4750 multisection: a flag indicating that this is a multisection invoice,
4751 which does something complicated.
4752
4753 multilocation: a flag to display the location label for the package.
4754
4755 Returns a list of hashrefs, each of which may contain:
4756
4757 pkgnum, description, amount, unit_amount, quantity, _is_setup, and 
4758 ext_description, which is an arrayref of detail lines to show below 
4759 the package line.
4760
4761 =cut
4762
4763 sub _items_cust_bill_pkg {
4764   my $self = shift;
4765   my $conf = $self->conf;
4766   my $cust_bill_pkgs = shift;
4767   my %opt = @_;
4768
4769   my $format = $opt{format} || '';
4770   my $escape_function = $opt{escape_function} || sub { shift };
4771   my $format_function = $opt{format_function} || '';
4772   my $unsquelched = $opt{unsquelched} || ''; #unused
4773   my $section = $opt{section}->{description} if $opt{section};
4774   my $summary_page = $opt{summary_page} || ''; #unused
4775   my $multilocation = $opt{multilocation} || '';
4776   my $multisection = $opt{multisection} || '';
4777   my $discount_show_always = 0;
4778
4779   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4780
4781   my @b = ();
4782   my ($s, $r, $u) = ( undef, undef, undef );
4783   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4784   {
4785
4786     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4787       if ( $_ && !$cust_bill_pkg->hidden ) {
4788         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4789         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4790         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4791         push @b, { %$_ }
4792           if $_->{amount} != 0
4793           || $discount_show_always
4794           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4795           || (   $_->{_is_setup} && $_->{setup_show_zero} )
4796         ;
4797         $_ = undef;
4798       }
4799     }
4800
4801     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4802          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4803       if $DEBUG > 1;
4804
4805     foreach my $display ( grep { defined($section)
4806                                  ? $_->section eq $section
4807                                  : 1
4808                                }
4809                           #grep { !$_->summary || !$summary_page } # bunk!
4810                           grep { !$_->summary || $multisection }
4811                           $cust_bill_pkg->cust_bill_pkg_display
4812                         )
4813     {
4814
4815       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4816            $display->billpkgdisplaynum. "\n"
4817         if $DEBUG > 1;
4818
4819       my $type = $display->type;
4820
4821       my $desc = $cust_bill_pkg->desc;
4822       $desc = substr($desc, 0, $maxlength). '...'
4823         if $format eq 'latex' && length($desc) > $maxlength;
4824
4825       my %details_opt = ( 'format'          => $format,
4826                           'escape_function' => $escape_function,
4827                           'format_function' => $format_function,
4828                         );
4829
4830       if ( $cust_bill_pkg->pkgnum > 0 ) {
4831
4832         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4833           if $DEBUG > 1;
4834  
4835         my $cust_pkg = $cust_bill_pkg->cust_pkg;
4836
4837         # start/end dates for invoice formats that do nonstandard 
4838         # things with them
4839         my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4840
4841         if (    (!$type || $type eq 'S')
4842              && (    $cust_bill_pkg->setup != 0
4843                   || $cust_bill_pkg->setup_show_zero
4844                 )
4845            )
4846          {
4847
4848           warn "$me _items_cust_bill_pkg adding setup\n"
4849             if $DEBUG > 1;
4850
4851           my $description = $desc;
4852           $description .= ' Setup'
4853             if $cust_bill_pkg->recur != 0
4854             || $discount_show_always
4855             || $cust_bill_pkg->recur_show_zero;
4856
4857           my @d = ();
4858           unless ( $cust_pkg->part_pkg->hide_svc_detail
4859                 || $cust_bill_pkg->hidden )
4860           {
4861
4862             push @d, map &{$escape_function}($_),
4863                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
4864               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4865
4866             if ( $multilocation ) {
4867               my $loc = $cust_pkg->location_label;
4868               $loc = substr($loc, 0, $maxlength). '...'
4869                 if $format eq 'latex' && length($loc) > $maxlength;
4870               push @d, &{$escape_function}($loc);
4871             }
4872
4873           } #unless hiding service details
4874
4875           push @d, $cust_bill_pkg->details(%details_opt)
4876             if $cust_bill_pkg->recur == 0;
4877
4878           if ( $cust_bill_pkg->hidden ) {
4879             $s->{amount}      += $cust_bill_pkg->setup;
4880             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4881             push @{ $s->{ext_description} }, @d;
4882           } else {
4883             $s = {
4884               _is_setup       => 1,
4885               description     => $description,
4886               #pkgpart         => $part_pkg->pkgpart,
4887               pkgnum          => $cust_bill_pkg->pkgnum,
4888               amount          => $cust_bill_pkg->setup,
4889               setup_show_zero => $cust_bill_pkg->setup_show_zero,
4890               unit_amount     => $cust_bill_pkg->unitsetup,
4891               quantity        => $cust_bill_pkg->quantity,
4892               ext_description => \@d,
4893             };
4894           };
4895
4896         }
4897
4898         if (    ( !$type || $type eq 'R' || $type eq 'U' )
4899              && (
4900                      $cust_bill_pkg->recur != 0
4901                   || $cust_bill_pkg->setup == 0
4902                   || $discount_show_always
4903                   || $cust_bill_pkg->recur_show_zero
4904                 )
4905            )
4906         {
4907
4908           warn "$me _items_cust_bill_pkg adding recur/usage\n"
4909             if $DEBUG > 1;
4910
4911           my $is_summary = $display->summary;
4912           my $description = ($is_summary && $type && $type eq 'U')
4913                             ? "Usage charges" : $desc;
4914
4915           $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4916                           " - ". time2str($date_format, $cust_bill_pkg->edate).
4917                           ")"
4918             unless $conf->exists('disable_line_item_date_ranges')
4919                 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4920
4921           my @d = ();
4922           my @seconds = (); # for display of usage info
4923
4924           #at least until cust_bill_pkg has "past" ranges in addition to
4925           #the "future" sdate/edate ones... see #3032
4926           my @dates = ( $self->_date );
4927           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4928           push @dates, $prev->sdate if $prev;
4929           push @dates, undef if !$prev;
4930
4931           unless ( $cust_pkg->part_pkg->hide_svc_detail
4932                 || $cust_bill_pkg->itemdesc
4933                 || $cust_bill_pkg->hidden
4934                 || $is_summary && $type && $type eq 'U' )
4935           {
4936
4937             warn "$me _items_cust_bill_pkg adding service details\n"
4938               if $DEBUG > 1;
4939
4940             push @d, map &{$escape_function}($_),
4941                          $cust_pkg->h_labels_short(@dates, 'I')
4942                                                    #$cust_bill_pkg->edate,
4943                                                    #$cust_bill_pkg->sdate)
4944               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4945
4946             warn "$me _items_cust_bill_pkg done adding service details\n"
4947               if $DEBUG > 1;
4948
4949             if ( $multilocation ) {
4950               my $loc = $cust_pkg->location_label;
4951               $loc = substr($loc, 0, $maxlength). '...'
4952                 if $format eq 'latex' && length($loc) > $maxlength;
4953               push @d, &{$escape_function}($loc);
4954             }
4955
4956             # Display of seconds_since_sqlradacct:
4957             # On the invoice, when processing @detail_items, look for a field
4958             # named 'seconds'.  This will contain total seconds for each 
4959             # service, in the same order as @ext_description.  For services 
4960             # that don't support this it will show undef.
4961             if ( $conf->exists('svc_acct-usage_seconds') 
4962                  and ! $cust_bill_pkg->pkgpart_override ) {
4963               foreach my $cust_svc ( 
4964                   $cust_pkg->h_cust_svc(@dates, 'I') 
4965                 ) {
4966
4967                 # eval because not having any part_export_usage exports 
4968                 # is a fatal error, last_bill/_date because that's how 
4969                 # sqlradius_hour billing does it
4970                 my $sec = eval {
4971                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
4972                 };
4973                 push @seconds, $sec;
4974               }
4975             } #if svc_acct-usage_seconds
4976
4977           }
4978
4979           unless ( $is_summary ) {
4980             warn "$me _items_cust_bill_pkg adding details\n"
4981               if $DEBUG > 1;
4982
4983             #instead of omitting details entirely in this case (unwanted side
4984             # effects), just omit CDRs
4985             $details_opt{'format_function'} = sub { () }
4986               if $type && $type eq 'R';
4987
4988             push @d, $cust_bill_pkg->details(%details_opt);
4989           }
4990
4991           warn "$me _items_cust_bill_pkg calculating amount\n"
4992             if $DEBUG > 1;
4993   
4994           my $amount = 0;
4995           if (!$type) {
4996             $amount = $cust_bill_pkg->recur;
4997           } elsif ($type eq 'R') {
4998             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4999           } elsif ($type eq 'U') {
5000             $amount = $cust_bill_pkg->usage;
5001           }
5002   
5003           if ( !$type || $type eq 'R' ) {
5004
5005             warn "$me _items_cust_bill_pkg adding recur\n"
5006               if $DEBUG > 1;
5007
5008             if ( $cust_bill_pkg->hidden ) {
5009               $r->{amount}      += $amount;
5010               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5011               push @{ $r->{ext_description} }, @d;
5012             } else {
5013               $r = {
5014                 description     => $description,
5015                 #pkgpart         => $part_pkg->pkgpart,
5016                 pkgnum          => $cust_bill_pkg->pkgnum,
5017                 amount          => $amount,
5018                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5019                 unit_amount     => $cust_bill_pkg->unitrecur,
5020                 quantity        => $cust_bill_pkg->quantity,
5021                 %item_dates,
5022                 ext_description => \@d,
5023               };
5024               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5025             }
5026
5027           } else {  # $type eq 'U'
5028
5029             warn "$me _items_cust_bill_pkg adding usage\n"
5030               if $DEBUG > 1;
5031
5032             if ( $cust_bill_pkg->hidden ) {
5033               $u->{amount}      += $amount;
5034               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5035               push @{ $u->{ext_description} }, @d;
5036             } else {
5037               $u = {
5038                 description     => $description,
5039                 #pkgpart         => $part_pkg->pkgpart,
5040                 pkgnum          => $cust_bill_pkg->pkgnum,
5041                 amount          => $amount,
5042                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5043                 unit_amount     => $cust_bill_pkg->unitrecur,
5044                 quantity        => $cust_bill_pkg->quantity,
5045                 %item_dates,
5046                 ext_description => \@d,
5047               };
5048             }
5049           }
5050
5051         } # recurring or usage with recurring charge
5052
5053       } else { #pkgnum tax or one-shot line item (??)
5054
5055         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5056           if $DEBUG > 1;
5057
5058         if ( $cust_bill_pkg->setup != 0 ) {
5059           push @b, {
5060             'description' => $desc,
5061             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
5062           };
5063         }
5064         if ( $cust_bill_pkg->recur != 0 ) {
5065           push @b, {
5066             'description' => "$desc (".
5067                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5068                              time2str($date_format, $cust_bill_pkg->edate). ')',
5069             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
5070           };
5071         }
5072
5073       }
5074
5075     }
5076
5077     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5078                                 && $conf->exists('discount-show-always'));
5079
5080   }
5081
5082   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5083     if ( $_  ) {
5084       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
5085       $_->{amount}      =~ s/^\-0\.00$/0.00/;
5086       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5087       push @b, { %$_ }
5088         if $_->{amount} != 0
5089         || $discount_show_always
5090         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5091         || (   $_->{_is_setup} && $_->{setup_show_zero} )
5092     }
5093   }
5094
5095   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5096     if $DEBUG > 1;
5097
5098   @b;
5099
5100 }
5101
5102 sub _items_credits {
5103   my( $self, %opt ) = @_;
5104   my $trim_len = $opt{'trim_len'} || 60;
5105
5106   my @b;
5107   #credits
5108   foreach ( $self->cust_credited ) {
5109
5110     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5111
5112     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5113     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5114     $reason = " ($reason) " if $reason;
5115
5116     push @b, {
5117       #'description' => 'Credit ref\#'. $_->crednum.
5118       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
5119       #                 $reason,
5120       'description' => $self->mt('Credit applied').' '.
5121                        time2str($date_format,$_->cust_credit->_date). $reason,
5122       'amount'      => sprintf("%.2f",$_->amount),
5123     };
5124   }
5125
5126   @b;
5127
5128 }
5129
5130 sub _items_payments {
5131   my $self = shift;
5132
5133   my @b;
5134   #get & print payments
5135   foreach ( $self->cust_bill_pay ) {
5136
5137     #something more elaborate if $_->amount ne ->cust_pay->paid ?
5138
5139     push @b, {
5140       'description' => $self->mt('Payment received').' '.
5141                        time2str($date_format,$_->cust_pay->_date ),
5142       'amount'      => sprintf("%.2f", $_->amount )
5143     };
5144   }
5145
5146   @b;
5147
5148 }
5149
5150 =item call_details [ OPTION => VALUE ... ]
5151
5152 Returns an array of CSV strings representing the call details for this invoice
5153 The only option available is the boolean prepend_billed_number
5154
5155 =cut
5156
5157 sub call_details {
5158   my ($self, %opt) = @_;
5159
5160   my $format_function = sub { shift };
5161
5162   if ($opt{prepend_billed_number}) {
5163     $format_function = sub {
5164       my $detail = shift;
5165       my $row = shift;
5166
5167       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5168       
5169     };
5170   }
5171
5172   my @details = map { $_->details( 'format_function' => $format_function,
5173                                    'escape_function' => sub{ return() },
5174                                  )
5175                     }
5176                   grep { $_->pkgnum }
5177                   $self->cust_bill_pkg;
5178   my $header = $details[0];
5179   ( $header, grep { $_ ne $header } @details );
5180 }
5181
5182
5183 =back
5184
5185 =head1 SUBROUTINES
5186
5187 =over 4
5188
5189 =item process_reprint
5190
5191 =cut
5192
5193 sub process_reprint {
5194   process_re_X('print', @_);
5195 }
5196
5197 =item process_reemail
5198
5199 =cut
5200
5201 sub process_reemail {
5202   process_re_X('email', @_);
5203 }
5204
5205 =item process_refax
5206
5207 =cut
5208
5209 sub process_refax {
5210   process_re_X('fax', @_);
5211 }
5212
5213 =item process_reftp
5214
5215 =cut
5216
5217 sub process_reftp {
5218   process_re_X('ftp', @_);
5219 }
5220
5221 =item respool
5222
5223 =cut
5224
5225 sub process_respool {
5226   process_re_X('spool', @_);
5227 }
5228
5229 use Storable qw(thaw);
5230 use Data::Dumper;
5231 use MIME::Base64;
5232 sub process_re_X {
5233   my( $method, $job ) = ( shift, shift );
5234   warn "$me process_re_X $method for job $job\n" if $DEBUG;
5235
5236   my $param = thaw(decode_base64(shift));
5237   warn Dumper($param) if $DEBUG;
5238
5239   re_X(
5240     $method,
5241     $job,
5242     %$param,
5243   );
5244
5245 }
5246
5247 sub re_X {
5248   my($method, $job, %param ) = @_;
5249   if ( $DEBUG ) {
5250     warn "re_X $method for job $job with param:\n".
5251          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
5252   }
5253
5254   #some false laziness w/search/cust_bill.html
5255   my $distinct = '';
5256   my $orderby = 'ORDER BY cust_bill._date';
5257
5258   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5259
5260   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5261      
5262   my @cust_bill = qsearch( {
5263     #'select'    => "cust_bill.*",
5264     'table'     => 'cust_bill',
5265     'addl_from' => $addl_from,
5266     'hashref'   => {},
5267     'extra_sql' => $extra_sql,
5268     'order_by'  => $orderby,
5269     'debug' => 1,
5270   } );
5271
5272   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5273
5274   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5275     if $DEBUG;
5276
5277   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5278   foreach my $cust_bill ( @cust_bill ) {
5279     $cust_bill->$method();
5280
5281     if ( $job ) { #progressbar foo
5282       $num++;
5283       if ( time - $min_sec > $last ) {
5284         my $error = $job->update_statustext(
5285           int( 100 * $num / scalar(@cust_bill) )
5286         );
5287         die $error if $error;
5288         $last = time;
5289       }
5290     }
5291
5292   }
5293
5294 }
5295
5296 =back
5297
5298 =head1 CLASS METHODS
5299
5300 =over 4
5301
5302 =item owed_sql
5303
5304 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5305
5306 =cut
5307
5308 sub owed_sql {
5309   my ($class, $start, $end) = @_;
5310   'charged - '. 
5311     $class->paid_sql($start, $end). ' - '. 
5312     $class->credited_sql($start, $end);
5313 }
5314
5315 =item net_sql
5316
5317 Returns an SQL fragment to retreive the net amount (charged minus credited).
5318
5319 =cut
5320
5321 sub net_sql {
5322   my ($class, $start, $end) = @_;
5323   'charged - '. $class->credited_sql($start, $end);
5324 }
5325
5326 =item paid_sql
5327
5328 Returns an SQL fragment to retreive the amount paid against this invoice.
5329
5330 =cut
5331
5332 sub paid_sql {
5333   my ($class, $start, $end) = @_;
5334   $start &&= "AND cust_bill_pay._date <= $start";
5335   $end   &&= "AND cust_bill_pay._date > $end";
5336   $start = '' unless defined($start);
5337   $end   = '' unless defined($end);
5338   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5339        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
5340 }
5341
5342 =item credited_sql
5343
5344 Returns an SQL fragment to retreive the amount credited against this invoice.
5345
5346 =cut
5347
5348 sub credited_sql {
5349   my ($class, $start, $end) = @_;
5350   $start &&= "AND cust_credit_bill._date <= $start";
5351   $end   &&= "AND cust_credit_bill._date >  $end";
5352   $start = '' unless defined($start);
5353   $end   = '' unless defined($end);
5354   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5355        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
5356 }
5357
5358 =item due_date_sql
5359
5360 Returns an SQL fragment to retrieve the due date of an invoice.
5361 Currently only supported on PostgreSQL.
5362
5363 =cut
5364
5365 sub due_date_sql {
5366   my $conf = new FS::Conf;
5367 'COALESCE(
5368   SUBSTRING(
5369     COALESCE(
5370       cust_bill.invoice_terms,
5371       cust_main.invoice_terms,
5372       \''.($conf->config('invoice_default_terms') || '').'\'
5373     ), E\'Net (\\\\d+)\'
5374   )::INTEGER, 0
5375 ) * 86400 + cust_bill._date'
5376 }
5377
5378 =item search_sql_where HASHREF
5379
5380 Class method which returns an SQL WHERE fragment to search for parameters
5381 specified in HASHREF.  Valid parameters are
5382
5383 =over 4
5384
5385 =item _date
5386
5387 List reference of start date, end date, as UNIX timestamps.
5388
5389 =item invnum_min
5390
5391 =item invnum_max
5392
5393 =item agentnum
5394
5395 =item charged
5396
5397 List reference of charged limits (exclusive).
5398
5399 =item owed
5400
5401 List reference of charged limits (exclusive).
5402
5403 =item open
5404
5405 flag, return open invoices only
5406
5407 =item net
5408
5409 flag, return net invoices only
5410
5411 =item days
5412
5413 =item newest_percust
5414
5415 =back
5416
5417 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5418
5419 =cut
5420
5421 sub search_sql_where {
5422   my($class, $param) = @_;
5423   if ( $DEBUG ) {
5424     warn "$me search_sql_where called with params: \n".
5425          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
5426   }
5427
5428   my @search = ();
5429
5430   #agentnum
5431   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5432     push @search, "cust_main.agentnum = $1";
5433   }
5434
5435   #agentnum
5436   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5437     push @search, "cust_bill.custnum = $1";
5438   }
5439
5440   #_date
5441   if ( $param->{_date} ) {
5442     my($beginning, $ending) = @{$param->{_date}};
5443
5444     push @search, "cust_bill._date >= $beginning",
5445                   "cust_bill._date <  $ending";
5446   }
5447
5448   #invnum
5449   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5450     push @search, "cust_bill.invnum >= $1";
5451   }
5452   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5453     push @search, "cust_bill.invnum <= $1";
5454   }
5455
5456   #charged
5457   if ( $param->{charged} ) {
5458     my @charged = ref($param->{charged})
5459                     ? @{ $param->{charged} }
5460                     : ($param->{charged});
5461
5462     push @search, map { s/^charged/cust_bill.charged/; $_; }
5463                       @charged;
5464   }
5465
5466   my $owed_sql = FS::cust_bill->owed_sql;
5467
5468   #owed
5469   if ( $param->{owed} ) {
5470     my @owed = ref($param->{owed})
5471                  ? @{ $param->{owed} }
5472                  : ($param->{owed});
5473     push @search, map { s/^owed/$owed_sql/; $_; }
5474                       @owed;
5475   }
5476
5477   #open/net flags
5478   push @search, "0 != $owed_sql"
5479     if $param->{'open'};
5480   push @search, '0 != '. FS::cust_bill->net_sql
5481     if $param->{'net'};
5482
5483   #days
5484   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5485     if $param->{'days'};
5486
5487   #newest_percust
5488   if ( $param->{'newest_percust'} ) {
5489
5490     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5491     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5492
5493     my @newest_where = map { my $x = $_;
5494                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5495                              $x;
5496                            }
5497                            grep ! /^cust_main./, @search;
5498     my $newest_where = scalar(@newest_where)
5499                          ? ' AND '. join(' AND ', @newest_where)
5500                          : '';
5501
5502
5503     push @search, "cust_bill._date = (
5504       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5505         WHERE newest_cust_bill.custnum = cust_bill.custnum
5506           $newest_where
5507     )";
5508
5509   }
5510
5511   #agent virtualization
5512   my $curuser = $FS::CurrentUser::CurrentUser;
5513   if ( $curuser->username eq 'fs_queue'
5514        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5515     my $username = $1;
5516     my $newuser = qsearchs('access_user', {
5517       'username' => $username,
5518       'disabled' => '',
5519     } );
5520     if ( $newuser ) {
5521       $curuser = $newuser;
5522     } else {
5523       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5524     }
5525   }
5526   push @search, $curuser->agentnums_sql;
5527
5528   join(' AND ', @search );
5529
5530 }
5531
5532 =back
5533
5534 =head1 BUGS
5535
5536 The delete method.
5537
5538 =head1 SEE ALSO
5539
5540 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5541 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5542 documentation.
5543
5544 =cut
5545
5546 1;
5547