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