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