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