4ea67b69a718af5a029d1ec63a36e6b182691614
[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 $bill_batch = $self->get_open_bill_batch;
1524   my $cust_bill_batch = FS::cust_bill_batch->new({
1525       batchnum => $bill_batch->batchnum,
1526       invnum   => $self->invnum,
1527   });
1528   return $cust_bill_batch->insert($opt);
1529 }
1530
1531 =item get_open_batch
1532
1533 Returns the currently open batch as an FS::bill_batch object, creating a new
1534 one if necessary.  (A per-agent batch if invoice_print_pdf-spoolagent is
1535 enabled)
1536
1537 =cut
1538
1539 sub get_open_bill_batch {
1540   my $self = shift;
1541   my $hashref = { status => 'O' };
1542   $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1543                              ? $self->cust_main->agentnum
1544                              : '';
1545   my $batch = qsearchs('bill_batch', $hashref);
1546   return $batch if $batch;
1547   $batch = FS::bill_batch->new($hashref);
1548   my $error = $batch->insert;
1549   die $error if $error;
1550   return $batch;
1551 }
1552
1553 =item ftp_invoice [ TEMPLATENAME ] 
1554
1555 Sends this invoice data via FTP.
1556
1557 TEMPLATENAME is unused?
1558
1559 =cut
1560
1561 sub ftp_invoice {
1562   my $self = shift;
1563   my $template = scalar(@_) ? shift : '';
1564
1565   $self->send_csv(
1566     'protocol'   => 'ftp',
1567     'server'     => $conf->config('cust_bill-ftpserver'),
1568     'username'   => $conf->config('cust_bill-ftpusername'),
1569     'password'   => $conf->config('cust_bill-ftppassword'),
1570     'dir'        => $conf->config('cust_bill-ftpdir'),
1571     'format'     => $conf->config('cust_bill-ftpformat'),
1572   );
1573 }
1574
1575 =item spool_invoice [ TEMPLATENAME ] 
1576
1577 Spools this invoice data (see L<FS::spool_csv>)
1578
1579 TEMPLATENAME is unused?
1580
1581 =cut
1582
1583 sub spool_invoice {
1584   my $self = shift;
1585   my $template = scalar(@_) ? shift : '';
1586
1587   $self->spool_csv(
1588     'format'       => $conf->config('cust_bill-spoolformat'),
1589     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1590   );
1591 }
1592
1593 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1594
1595 Like B<send>, but only sends the invoice if it is the newest open invoice for
1596 this customer.
1597
1598 =cut
1599
1600 sub send_if_newest {
1601   my $self = shift;
1602
1603   return ''
1604     if scalar(
1605                grep { $_->owed > 0 } 
1606                     qsearch('cust_bill', {
1607                       'custnum' => $self->custnum,
1608                       #'_date'   => { op=>'>', value=>$self->_date },
1609                       'invnum'  => { op=>'>', value=>$self->invnum },
1610                     } )
1611              );
1612     
1613   $self->send(@_);
1614 }
1615
1616 =item send_csv OPTION => VALUE, ...
1617
1618 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1619
1620 Options are:
1621
1622 protocol - currently only "ftp"
1623 server
1624 username
1625 password
1626 dir
1627
1628 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1629 and YYMMDDHHMMSS is a timestamp.
1630
1631 See L</print_csv> for a description of the output format.
1632
1633 =cut
1634
1635 sub send_csv {
1636   my($self, %opt) = @_;
1637
1638   #create file(s)
1639
1640   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1641   mkdir $spooldir, 0700 unless -d $spooldir;
1642
1643   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1644   my $file = "$spooldir/$tracctnum.csv";
1645   
1646   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1647
1648   open(CSV, ">$file") or die "can't open $file: $!";
1649   print CSV $header;
1650
1651   print CSV $detail;
1652
1653   close CSV;
1654
1655   my $net;
1656   if ( $opt{protocol} eq 'ftp' ) {
1657     eval "use Net::FTP;";
1658     die $@ if $@;
1659     $net = Net::FTP->new($opt{server}) or die @$;
1660   } else {
1661     die "unknown protocol: $opt{protocol}";
1662   }
1663
1664   $net->login( $opt{username}, $opt{password} )
1665     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1666
1667   $net->binary or die "can't set binary mode";
1668
1669   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1670
1671   $net->put($file) or die "can't put $file: $!";
1672
1673   $net->quit;
1674
1675   unlink $file;
1676
1677 }
1678
1679 =item spool_csv
1680
1681 Spools CSV invoice data.
1682
1683 Options are:
1684
1685 =over 4
1686
1687 =item format - 'default' or 'billco'
1688
1689 =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>).
1690
1691 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1692
1693 =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.
1694
1695 =back
1696
1697 =cut
1698
1699 sub spool_csv {
1700   my($self, %opt) = @_;
1701
1702   my $cust_main = $self->cust_main;
1703
1704   if ( $opt{'dest'} ) {
1705     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1706                              $cust_main->invoicing_list;
1707     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1708                      || ! keys %invoicing_list;
1709   }
1710
1711   if ( $opt{'balanceover'} ) {
1712     return 'N/A'
1713       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1714   }
1715
1716   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1717   mkdir $spooldir, 0700 unless -d $spooldir;
1718
1719   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1720
1721   my $file =
1722     "$spooldir/".
1723     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1724     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1725     '.csv';
1726   
1727   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1728
1729   open(CSV, ">>$file") or die "can't open $file: $!";
1730   flock(CSV, LOCK_EX);
1731   seek(CSV, 0, 2);
1732
1733   print CSV $header;
1734
1735   if ( lc($opt{'format'}) eq 'billco' ) {
1736
1737     flock(CSV, LOCK_UN);
1738     close CSV;
1739
1740     $file =
1741       "$spooldir/".
1742       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1743       '-detail.csv';
1744
1745     open(CSV,">>$file") or die "can't open $file: $!";
1746     flock(CSV, LOCK_EX);
1747     seek(CSV, 0, 2);
1748   }
1749
1750   print CSV $detail;
1751
1752   flock(CSV, LOCK_UN);
1753   close CSV;
1754
1755   return '';
1756
1757 }
1758
1759 =item print_csv OPTION => VALUE, ...
1760
1761 Returns CSV data for this invoice.
1762
1763 Options are:
1764
1765 format - 'default' or 'billco'
1766
1767 Returns a list consisting of two scalars.  The first is a single line of CSV
1768 header information for this invoice.  The second is one or more lines of CSV
1769 detail information for this invoice.
1770
1771 If I<format> is not specified or "default", the fields of the CSV file are as
1772 follows:
1773
1774 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1775
1776 =over 4
1777
1778 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1779
1780 B<record_type> is C<cust_bill> for the initial header line only.  The
1781 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1782 fields are filled in.
1783
1784 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1785 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1786 are filled in.
1787
1788 =item invnum - invoice number
1789
1790 =item custnum - customer number
1791
1792 =item _date - invoice date
1793
1794 =item charged - total invoice amount
1795
1796 =item first - customer first name
1797
1798 =item last - customer first name
1799
1800 =item company - company name
1801
1802 =item address1 - address line 1
1803
1804 =item address2 - address line 1
1805
1806 =item city
1807
1808 =item state
1809
1810 =item zip
1811
1812 =item country
1813
1814 =item pkg - line item description
1815
1816 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1817
1818 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1819
1820 =item sdate - start date for recurring fee
1821
1822 =item edate - end date for recurring fee
1823
1824 =back
1825
1826 If I<format> is "billco", the fields of the header CSV file are as follows:
1827
1828   +-------------------------------------------------------------------+
1829   |                        FORMAT HEADER FILE                         |
1830   |-------------------------------------------------------------------|
1831   | Field | Description                   | Name       | Type | Width |
1832   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1833   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1834   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1835   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1836   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1837   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1838   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1839   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1840   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1841   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1842   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1843   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1844   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1845   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1846   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1847   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1848   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1849   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1850   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1851   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1852   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1853   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1854   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1855   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1856   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1857   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1858   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1859   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1860   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1861   +-------+-------------------------------+------------+------+-------+
1862
1863 If I<format> is "billco", the fields of the detail CSV file are as follows:
1864
1865                                   FORMAT FOR DETAIL FILE
1866         |                            |           |      |
1867   Field | Description                | Name      | Type | Width
1868   1     | N/A-Leave Empty            | RC        | CHAR |     2
1869   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1870   3     | Account Number             | TRACCTNUM | CHAR |    15
1871   4     | Invoice Number             | TRINVOICE | CHAR |    15
1872   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1873   6     | Transaction Detail         | DETAILS   | CHAR |   100
1874   7     | Amount                     | AMT       | NUM* |     9
1875   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1876   9     | Grouping Code              | GROUP     | CHAR |     2
1877   10    | User Defined               | ACCT CODE | CHAR |    15
1878
1879 =cut
1880
1881 sub print_csv {
1882   my($self, %opt) = @_;
1883   
1884   eval "use Text::CSV_XS";
1885   die $@ if $@;
1886
1887   my $cust_main = $self->cust_main;
1888
1889   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1890
1891   if ( lc($opt{'format'}) eq 'billco' ) {
1892
1893     my $taxtotal = 0;
1894     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1895
1896     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1897
1898     my( $previous_balance, @unused ) = $self->previous; #previous balance
1899
1900     my $pmt_cr_applied = 0;
1901     $pmt_cr_applied += $_->{'amount'}
1902       foreach ( $self->_items_payments, $self->_items_credits ) ;
1903
1904     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1905
1906     $csv->combine(
1907       '',                         #  1 | N/A-Leave Empty               CHAR   2
1908       '',                         #  2 | N/A-Leave Empty               CHAR  15
1909       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1910       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1911       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1912       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1913       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1914       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1915       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1916       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1917       '',                         # 10 | Ancillary Billing Information CHAR  30
1918       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1919       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1920
1921       # XXX ?
1922       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1923
1924       # XXX ?
1925       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1926
1927       $previous_balance,          # 15 | Previous Balance              NUM*   9
1928       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1929       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1930       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1931       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1932       '',                         # 20 | 30 Day Aging                  NUM*   9
1933       '',                         # 21 | 60 Day Aging                  NUM*   9
1934       '',                         # 22 | 90 Day Aging                  NUM*   9
1935       'N',                        # 23 | Y/N                           CHAR   1
1936       '',                         # 24 | Remittance automation         CHAR 100
1937       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1938       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1939       '0',                        # 27 | Federal Tax***                NUM*   9
1940       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1941       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1942     );
1943
1944   } else {
1945   
1946     $csv->combine(
1947       'cust_bill',
1948       $self->invnum,
1949       $self->custnum,
1950       time2str("%x", $self->_date),
1951       sprintf("%.2f", $self->charged),
1952       ( map { $cust_main->getfield($_) }
1953           qw( first last company address1 address2 city state zip country ) ),
1954       map { '' } (1..5),
1955     ) or die "can't create csv";
1956   }
1957
1958   my $header = $csv->string. "\n";
1959
1960   my $detail = '';
1961   if ( lc($opt{'format'}) eq 'billco' ) {
1962
1963     my $lineseq = 0;
1964     foreach my $item ( $self->_items_pkg ) {
1965
1966       $csv->combine(
1967         '',                     #  1 | N/A-Leave Empty            CHAR   2
1968         '',                     #  2 | N/A-Leave Empty            CHAR  15
1969         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1970         $self->invnum,          #  4 | Invoice Number             CHAR  15
1971         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1972         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1973         $item->{'amount'},      #  7 | Amount                     NUM*   9
1974         '',                     #  8 | Line Format Control**      CHAR   2
1975         '',                     #  9 | Grouping Code              CHAR   2
1976         '',                     # 10 | User Defined               CHAR  15
1977       );
1978
1979       $detail .= $csv->string. "\n";
1980
1981     }
1982
1983   } else {
1984
1985     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1986
1987       my($pkg, $setup, $recur, $sdate, $edate);
1988       if ( $cust_bill_pkg->pkgnum ) {
1989       
1990         ($pkg, $setup, $recur, $sdate, $edate) = (
1991           $cust_bill_pkg->part_pkg->pkg,
1992           ( $cust_bill_pkg->setup != 0
1993             ? sprintf("%.2f", $cust_bill_pkg->setup )
1994             : '' ),
1995           ( $cust_bill_pkg->recur != 0
1996             ? sprintf("%.2f", $cust_bill_pkg->recur )
1997             : '' ),
1998           ( $cust_bill_pkg->sdate 
1999             ? time2str("%x", $cust_bill_pkg->sdate)
2000             : '' ),
2001           ($cust_bill_pkg->edate 
2002             ?time2str("%x", $cust_bill_pkg->edate)
2003             : '' ),
2004         );
2005   
2006       } else { #pkgnum tax
2007         next unless $cust_bill_pkg->setup != 0;
2008         $pkg = $cust_bill_pkg->desc;
2009         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2010         ( $sdate, $edate ) = ( '', '' );
2011       }
2012   
2013       $csv->combine(
2014         'cust_bill_pkg',
2015         $self->invnum,
2016         ( map { '' } (1..11) ),
2017         ($pkg, $setup, $recur, $sdate, $edate)
2018       ) or die "can't create csv";
2019
2020       $detail .= $csv->string. "\n";
2021
2022     }
2023
2024   }
2025
2026   ( $header, $detail );
2027
2028 }
2029
2030 =item comp
2031
2032 Pays this invoice with a compliemntary payment.  If there is an error,
2033 returns the error, otherwise returns false.
2034
2035 =cut
2036
2037 sub comp {
2038   my $self = shift;
2039   my $cust_pay = new FS::cust_pay ( {
2040     'invnum'   => $self->invnum,
2041     'paid'     => $self->owed,
2042     '_date'    => '',
2043     'payby'    => 'COMP',
2044     'payinfo'  => $self->cust_main->payinfo,
2045     'paybatch' => '',
2046   } );
2047   $cust_pay->insert;
2048 }
2049
2050 =item realtime_card
2051
2052 Attempts to pay this invoice with a credit card payment via a
2053 Business::OnlinePayment realtime gateway.  See
2054 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2055 for supported processors.
2056
2057 =cut
2058
2059 sub realtime_card {
2060   my $self = shift;
2061   $self->realtime_bop( 'CC', @_ );
2062 }
2063
2064 =item realtime_ach
2065
2066 Attempts to pay this invoice with an electronic check (ACH) payment via a
2067 Business::OnlinePayment realtime gateway.  See
2068 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2069 for supported processors.
2070
2071 =cut
2072
2073 sub realtime_ach {
2074   my $self = shift;
2075   $self->realtime_bop( 'ECHECK', @_ );
2076 }
2077
2078 =item realtime_lec
2079
2080 Attempts to pay this invoice with phone bill (LEC) payment via a
2081 Business::OnlinePayment realtime gateway.  See
2082 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2083 for supported processors.
2084
2085 =cut
2086
2087 sub realtime_lec {
2088   my $self = shift;
2089   $self->realtime_bop( 'LEC', @_ );
2090 }
2091
2092 sub realtime_bop {
2093   my( $self, $method ) = (shift,shift);
2094   my %opt = @_;
2095
2096   my $cust_main = $self->cust_main;
2097   my $balance = $cust_main->balance;
2098   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2099   $amount = sprintf("%.2f", $amount);
2100   return "not run (balance $balance)" unless $amount > 0;
2101
2102   my $description = 'Internet Services';
2103   if ( $conf->exists('business-onlinepayment-description') ) {
2104     my $dtempl = $conf->config('business-onlinepayment-description');
2105
2106     my $agent_obj = $cust_main->agent
2107       or die "can't retreive agent for $cust_main (agentnum ".
2108              $cust_main->agentnum. ")";
2109     my $agent = $agent_obj->agent;
2110     my $pkgs = join(', ',
2111       map { $_->part_pkg->pkg }
2112         grep { $_->pkgnum } $self->cust_bill_pkg
2113     );
2114     $description = eval qq("$dtempl");
2115   }
2116
2117   $cust_main->realtime_bop($method, $amount,
2118     'description' => $description,
2119     'invnum'      => $self->invnum,
2120 #this didn't do what we want, it just calls apply_payments_and_credits
2121 #    'apply'       => 1,
2122     'apply_to_invoice' => 1,
2123     %opt,
2124  #what we want:
2125  #this changes application behavior: auto payments
2126                         #triggered against a specific invoice are now applied
2127                         #to that invoice instead of oldest open.
2128                         #seem okay to me...
2129   );
2130
2131 }
2132
2133 =item batch_card OPTION => VALUE...
2134
2135 Adds a payment for this invoice to the pending credit card batch (see
2136 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2137 runs the payment using a realtime gateway.
2138
2139 =cut
2140
2141 sub batch_card {
2142   my ($self, %options) = @_;
2143   my $cust_main = $self->cust_main;
2144
2145   $options{invnum} = $self->invnum;
2146   
2147   $cust_main->batch_card(%options);
2148 }
2149
2150 sub _agent_template {
2151   my $self = shift;
2152   $self->cust_main->agent_template;
2153 }
2154
2155 sub _agent_invoice_from {
2156   my $self = shift;
2157   $self->cust_main->agent_invoice_from;
2158 }
2159
2160 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2161
2162 Returns an text invoice, as a list of lines.
2163
2164 Options can be passed as a hashref (recommended) or as a list of time, template
2165 and then any key/value pairs for any other options.
2166
2167 I<time>, if specified, is used to control the printing of overdue messages.  The
2168 default is now.  It isn't the date of the invoice; that's the `_date' field.
2169 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2170 L<Time::Local> and L<Date::Parse> for conversion functions.
2171
2172 I<template>, if specified, is the name of a suffix for alternate invoices.
2173
2174 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2175
2176 =cut
2177
2178 sub print_text {
2179   my $self = shift;
2180   my( $today, $template, %opt );
2181   if ( ref($_[0]) ) {
2182     %opt = %{ shift() };
2183     $today = delete($opt{'time'}) || '';
2184     $template = delete($opt{template}) || '';
2185   } else {
2186     ( $today, $template, %opt ) = @_;
2187   }
2188
2189   my %params = ( 'format' => 'template' );
2190   $params{'time'} = $today if $today;
2191   $params{'template'} = $template if $template;
2192   $params{$_} = $opt{$_} 
2193     foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2194
2195   $self->print_generic( %params );
2196 }
2197
2198 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2199
2200 Internal method - returns a filename of a filled-in LaTeX template for this
2201 invoice (Note: add ".tex" to get the actual filename), and a filename of
2202 an associated logo (with the .eps extension included).
2203
2204 See print_ps and print_pdf for methods that return PostScript and PDF output.
2205
2206 Options can be passed as a hashref (recommended) or as a list of time, template
2207 and then any key/value pairs for any other options.
2208
2209 I<time>, if specified, is used to control the printing of overdue messages.  The
2210 default is now.  It isn't the date of the invoice; that's the `_date' field.
2211 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2212 L<Time::Local> and L<Date::Parse> for conversion functions.
2213
2214 I<template>, if specified, is the name of a suffix for alternate invoices.
2215
2216 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2217
2218 =cut
2219
2220 sub print_latex {
2221   my $self = shift;
2222   my( $today, $template, %opt );
2223   if ( ref($_[0]) ) {
2224     %opt = %{ shift() };
2225     $today = delete($opt{'time'}) || '';
2226     $template = delete($opt{template}) || '';
2227   } else {
2228     ( $today, $template, %opt ) = @_;
2229   }
2230
2231   my %params = ( 'format' => 'latex' );
2232   $params{'time'} = $today if $today;
2233   $params{'template'} = $template if $template;
2234   $params{$_} = $opt{$_} 
2235     foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2236
2237   $template ||= $self->_agent_template;
2238
2239   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2240   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2241                            DIR      => $dir,
2242                            SUFFIX   => '.eps',
2243                            UNLINK   => 0,
2244                          ) or die "can't open temp file: $!\n";
2245
2246   my $agentnum = $self->cust_main->agentnum;
2247
2248   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2249     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2250       or die "can't write temp file: $!\n";
2251   } else {
2252     print $lh $conf->config_binary('logo.eps', $agentnum)
2253       or die "can't write temp file: $!\n";
2254   }
2255   close $lh;
2256   $params{'logo_file'} = $lh->filename;
2257
2258   if($conf->exists('invoice-barcode')){
2259       my $png_file = $self->invoice_barcode($dir);
2260       my $eps_file = $png_file;
2261       $eps_file =~ s/\.png$/.eps/g;
2262       $png_file =~ /(barcode.*png)/;
2263       $png_file = $1;
2264       $eps_file =~ /(barcode.*eps)/;
2265       $eps_file = $1;
2266
2267       my $curr_dir = cwd();
2268       chdir($dir); 
2269       # after painfuly long experimentation, it was determined that sam2p won't
2270       # accept : and other chars in the path, no matter how hard I tried to
2271       # escape them, hence the chdir (and chdir back, just to be safe)
2272       system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2273         or die "sam2p failed: $!\n";
2274       unlink($png_file);
2275       chdir($curr_dir);
2276
2277       $params{'barcode_file'} = $eps_file;
2278   }
2279
2280   my @filled_in = $self->print_generic( %params );
2281   
2282   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2283                            DIR      => $dir,
2284                            SUFFIX   => '.tex',
2285                            UNLINK   => 0,
2286                          ) or die "can't open temp file: $!\n";
2287   print $fh join('', @filled_in );
2288   close $fh;
2289
2290   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2291   return ($1, $params{'logo_file'}, $params{'barcode_file'});
2292
2293 }
2294
2295 =item invoice_barcode DIR_OR_FALSE
2296
2297 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2298 it is taken as the temp directory where the PNG file will be generated and the
2299 PNG file name is returned. Otherwise, the PNG image itself is returned.
2300
2301 =cut
2302
2303 sub invoice_barcode {
2304     my ($self, $dir) = (shift,shift);
2305     
2306     my $gdbar = new GD::Barcode('Code39',$self->invnum);
2307         die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2308     my $gd = $gdbar->plot(Height => 30);
2309
2310     if($dir) {
2311         my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2312                            DIR      => $dir,
2313                            SUFFIX   => '.png',
2314                            UNLINK   => 0,
2315                          ) or die "can't open temp file: $!\n";
2316         print $bh $gd->png or die "cannot write barcode to file: $!\n";
2317         my $png_file = $bh->filename;
2318         close $bh;
2319         return $png_file;
2320     }
2321     return $gd->png;
2322 }
2323
2324 =item print_generic OPTION => VALUE ...
2325
2326 Internal method - returns a filled-in template for this invoice as a scalar.
2327
2328 See print_ps and print_pdf for methods that return PostScript and PDF output.
2329
2330 Non optional options include 
2331   format - latex, html, template
2332
2333 Optional options include
2334
2335 template - a value used as a suffix for a configuration template
2336
2337 time - a value used to control the printing of overdue messages.  The
2338 default is now.  It isn't the date of the invoice; that's the `_date' field.
2339 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2340 L<Time::Local> and L<Date::Parse> for conversion functions.
2341
2342 cid - 
2343
2344 unsquelch_cdr - overrides any per customer cdr squelching when true
2345
2346 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2347
2348 =cut
2349
2350 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
2351 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2352 # yes: fixed width (dot matrix) text printing will be borked
2353 sub print_generic {
2354   my( $self, %params ) = @_;
2355   my $today = $params{today} ? $params{today} : time;
2356   warn "$me print_generic called on $self with suffix $params{template}\n"
2357     if $DEBUG;
2358
2359   my $format = $params{format};
2360   die "Unknown format: $format"
2361     unless $format =~ /^(latex|html|template)$/;
2362
2363   my $cust_main = $self->cust_main;
2364   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2365     unless $cust_main->payname
2366         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2367
2368   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
2369                      'html'     => [ '<%=', '%>' ],
2370                      'template' => [ '{', '}' ],
2371                    );
2372
2373   warn "$me print_generic creating template\n"
2374     if $DEBUG > 1;
2375
2376   #create the template
2377   my $template = $params{template} ? $params{template} : $self->_agent_template;
2378   my $templatefile = "invoice_$format";
2379   $templatefile .= "_$template"
2380     if length($template) && $conf->exists($templatefile."_$template");
2381   my @invoice_template = map "$_\n", $conf->config($templatefile)
2382     or die "cannot load config data $templatefile";
2383
2384   my $old_latex = '';
2385   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2386     #change this to a die when the old code is removed
2387     warn "old-style invoice template $templatefile; ".
2388          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2389     $old_latex = 'true';
2390     @invoice_template = _translate_old_latex_format(@invoice_template);
2391   } 
2392
2393   warn "$me print_generic creating T:T object\n"
2394     if $DEBUG > 1;
2395
2396   my $text_template = new Text::Template(
2397     TYPE => 'ARRAY',
2398     SOURCE => \@invoice_template,
2399     DELIMITERS => $delimiters{$format},
2400   );
2401
2402   warn "$me print_generic compiling T:T object\n"
2403     if $DEBUG > 1;
2404
2405   $text_template->compile()
2406     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2407
2408
2409   # additional substitution could possibly cause breakage in existing templates
2410   my %convert_maps = ( 
2411     'latex' => {
2412                  'notes'         => sub { map "$_", @_ },
2413                  'footer'        => sub { map "$_", @_ },
2414                  'smallfooter'   => sub { map "$_", @_ },
2415                  'returnaddress' => sub { map "$_", @_ },
2416                  'coupon'        => sub { map "$_", @_ },
2417                  'summary'       => sub { map "$_", @_ },
2418                },
2419     'html'  => {
2420                  'notes' =>
2421                    sub {
2422                      map { 
2423                        s/%%(.*)$/<!-- $1 -->/g;
2424                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2425                        s/\\begin\{enumerate\}/<ol>/g;
2426                        s/\\item /  <li>/g;
2427                        s/\\end\{enumerate\}/<\/ol>/g;
2428                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2429                        s/\\\\\*/<br>/g;
2430                        s/\\dollar ?/\$/g;
2431                        s/\\#/#/g;
2432                        s/~/&nbsp;/g;
2433                        $_;
2434                      }  @_
2435                    },
2436                  'footer' =>
2437                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2438                  'smallfooter' =>
2439                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2440                  'returnaddress' =>
2441                    sub {
2442                      map { 
2443                        s/~/&nbsp;/g;
2444                        s/\\\\\*?\s*$/<BR>/;
2445                        s/\\hyphenation\{[\w\s\-]+}//;
2446                        s/\\([&])/$1/g;
2447                        $_;
2448                      }  @_
2449                    },
2450                  'coupon'        => sub { "" },
2451                  'summary'       => sub { "" },
2452                },
2453     'template' => {
2454                  'notes' =>
2455                    sub {
2456                      map { 
2457                        s/%%.*$//g;
2458                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2459                        s/\\begin\{enumerate\}//g;
2460                        s/\\item /  * /g;
2461                        s/\\end\{enumerate\}//g;
2462                        s/\\textbf\{(.*)\}/$1/g;
2463                        s/\\\\\*/ /;
2464                        s/\\dollar ?/\$/g;
2465                        $_;
2466                      }  @_
2467                    },
2468                  'footer' =>
2469                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2470                  'smallfooter' =>
2471                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2472                  'returnaddress' =>
2473                    sub {
2474                      map { 
2475                        s/~/ /g;
2476                        s/\\\\\*?\s*$/\n/;             # dubious
2477                        s/\\hyphenation\{[\w\s\-]+}//;
2478                        $_;
2479                      }  @_
2480                    },
2481                  'coupon'        => sub { "" },
2482                  'summary'       => sub { "" },
2483                },
2484   );
2485
2486
2487   # hashes for differing output formats
2488   my %nbsps = ( 'latex'    => '~',
2489                 'html'     => '',    # '&nbps;' would be nice
2490                 'template' => '',    # not used
2491               );
2492   my $nbsp = $nbsps{$format};
2493
2494   my %escape_functions = ( 'latex'    => \&_latex_escape,
2495                            'html'     => \&_html_escape_nbsp,#\&encode_entities,
2496                            'template' => sub { shift },
2497                          );
2498   my $escape_function = $escape_functions{$format};
2499   my $escape_function_nonbsp = ($format eq 'html')
2500                                  ? \&_html_escape : $escape_function;
2501
2502   my %date_formats = ( 'latex'    => $date_format_long,
2503                        'html'     => $date_format_long,
2504                        'template' => '%s',
2505                      );
2506   $date_formats{'html'} =~ s/ /&nbsp;/g;
2507
2508   my $date_format = $date_formats{$format};
2509
2510   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
2511                                                },
2512                              'html'     => sub { return '<b>'. shift(). '</b>'
2513                                                },
2514                              'template' => sub { shift },
2515                            );
2516   my $embolden_function = $embolden_functions{$format};
2517
2518   my %newline_tokens = (  'latex'     => '\\\\',
2519                           'html'      => '<br>',
2520                           'template'  => "\n",
2521                         );
2522   my $newline_token = $newline_tokens{$format};
2523
2524   warn "$me generating template variables\n"
2525     if $DEBUG > 1;
2526
2527   # generate template variables
2528   my $returnaddress;
2529   if (
2530          defined( $conf->config_orbase( "invoice_${format}returnaddress",
2531                                         $template
2532                                       )
2533                 )
2534        && length( $conf->config_orbase( "invoice_${format}returnaddress",
2535                                         $template
2536                                       )
2537                 )
2538   ) {
2539
2540     $returnaddress = join("\n",
2541       $conf->config_orbase("invoice_${format}returnaddress", $template)
2542     );
2543
2544   } elsif ( grep /\S/,
2545             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2546
2547     my $convert_map = $convert_maps{$format}{'returnaddress'};
2548     $returnaddress =
2549       join( "\n",
2550             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2551                                                  $template
2552                                                )
2553                          )
2554           );
2555   } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2556
2557     my $convert_map = $convert_maps{$format}{'returnaddress'};
2558     $returnaddress = join( "\n", &$convert_map(
2559                                    map { s/( {2,})/'~' x length($1)/eg;
2560                                          s/$/\\\\\*/;
2561                                          $_
2562                                        }
2563                                      ( $conf->config('company_name', $self->cust_main->agentnum),
2564                                        $conf->config('company_address', $self->cust_main->agentnum),
2565                                      )
2566                                  )
2567                      );
2568
2569   } else {
2570
2571     my $warning = "Couldn't find a return address; ".
2572                   "do you need to set the company_address configuration value?";
2573     warn "$warning\n";
2574     $returnaddress = $nbsp;
2575     #$returnaddress = $warning;
2576
2577   }
2578
2579   warn "$me generating invoice data\n"
2580     if $DEBUG > 1;
2581
2582   my $agentnum = $self->cust_main->agentnum;
2583
2584   my %invoice_data = (
2585
2586     #invoice from info
2587     'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
2588     'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2589     'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2590     'returnaddress'   => $returnaddress,
2591     'agent'           => &$escape_function($cust_main->agent->agent),
2592
2593     #invoice info
2594     'invnum'          => $self->invnum,
2595     'date'            => time2str($date_format, $self->_date),
2596     'today'           => time2str($date_format_long, $today),
2597     'terms'           => $self->terms,
2598     'template'        => $template, #params{'template'},
2599     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
2600     'current_charges' => sprintf("%.2f", $self->charged),
2601     'duedate'         => $self->due_date2str($rdate_format), #date_format?
2602
2603     #customer info
2604     'custnum'         => $cust_main->display_custnum,
2605     'agent_custid'    => &$escape_function($cust_main->agent_custid),
2606     ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2607       payname company address1 address2 city state zip fax
2608     )),
2609
2610     #global config
2611     'ship_enable'     => $conf->exists('invoice-ship_address'),
2612     'unitprices'      => $conf->exists('invoice-unitprice'),
2613     'smallernotes'    => $conf->exists('invoice-smallernotes'),
2614     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
2615     'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2616    
2617     #layout info -- would be fancy to calc some of this and bury the template
2618     #               here in the code
2619     'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2620     'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2621     'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
2622     'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2623     'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2624     'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2625     'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2626     'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2627     'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2628     'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2629
2630     # better hang on to conf_dir for a while (for old templates)
2631     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2632
2633     #these are only used when doing paged plaintext
2634     'page'            => 1,
2635     'total_pages'     => 1,
2636
2637   );
2638   
2639   my $min_sdate = 999999999999;
2640   my $max_edate = 0;
2641   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2642     next unless $cust_bill_pkg->pkgnum > 0;
2643     $min_sdate = $cust_bill_pkg->sdate
2644       if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2645     $max_edate = $cust_bill_pkg->edate
2646       if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2647   }
2648
2649   $invoice_data{'bill_period'} = '';
2650   $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate) 
2651     . " to " . time2str('%e %h', $max_edate)
2652     if ($max_edate != 0 && $min_sdate != 999999999999);
2653
2654   $invoice_data{finance_section} = '';
2655   if ( $conf->config('finance_pkgclass') ) {
2656     my $pkg_class =
2657       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2658     $invoice_data{finance_section} = $pkg_class->categoryname;
2659   } 
2660   $invoice_data{finance_amount} = '0.00';
2661   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2662
2663   my $countrydefault = $conf->config('countrydefault') || 'US';
2664   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2665   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2666     my $method = $prefix.$_;
2667     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2668   }
2669   $invoice_data{'ship_country'} = ''
2670     if ( $invoice_data{'ship_country'} eq $countrydefault );
2671   
2672   $invoice_data{'cid'} = $params{'cid'}
2673     if $params{'cid'};
2674
2675   if ( $cust_main->country eq $countrydefault ) {
2676     $invoice_data{'country'} = '';
2677   } else {
2678     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2679   }
2680
2681   my @address = ();
2682   $invoice_data{'address'} = \@address;
2683   push @address,
2684     $cust_main->payname.
2685       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2686         ? " (P.O. #". $cust_main->payinfo. ")"
2687         : ''
2688       )
2689   ;
2690   push @address, $cust_main->company
2691     if $cust_main->company;
2692   push @address, $cust_main->address1;
2693   push @address, $cust_main->address2
2694     if $cust_main->address2;
2695   push @address,
2696     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2697   push @address, $invoice_data{'country'}
2698     if $invoice_data{'country'};
2699   push @address, ''
2700     while (scalar(@address) < 5);
2701
2702   $invoice_data{'logo_file'} = $params{'logo_file'}
2703     if $params{'logo_file'};
2704   $invoice_data{'barcode_file'} = $params{'barcode_file'}
2705     if $params{'barcode_file'};
2706   $invoice_data{'barcode_img'} = $params{'barcode_img'}
2707     if $params{'barcode_img'};
2708   $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2709     if $params{'barcode_cid'};
2710
2711   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2712 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2713   #my $balance_due = $self->owed + $pr_total - $cr_total;
2714   my $balance_due = $self->owed + $pr_total;
2715   $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2716   $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2717   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2718   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2719
2720   my $summarypage = '';
2721   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2722     $summarypage = 1;
2723   }
2724   $invoice_data{'summarypage'} = $summarypage;
2725
2726   warn "$me substituting variables in notes, footer, smallfooter\n"
2727     if $DEBUG > 1;
2728
2729   my @include = (qw( notes footer smallfooter ));
2730   push @include, 'coupon' unless $params{'no_coupon'};
2731   foreach my $include (@include) {
2732
2733     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2734     my @inc_src;
2735
2736     if ( $conf->exists($inc_file, $agentnum)
2737          && length( $conf->config($inc_file, $agentnum) ) ) {
2738
2739       @inc_src = $conf->config($inc_file, $agentnum);
2740
2741     } else {
2742
2743       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2744
2745       my $convert_map = $convert_maps{$format}{$include};
2746
2747       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2748                        s/--\@\]/$delimiters{$format}[1]/g;
2749                        $_;
2750                      } 
2751                  &$convert_map( $conf->config($inc_file, $agentnum) );
2752
2753     }
2754
2755     my $inc_tt = new Text::Template (
2756       TYPE       => 'ARRAY',
2757       SOURCE     => [ map "$_\n", @inc_src ],
2758       DELIMITERS => $delimiters{$format},
2759     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2760
2761     unless ( $inc_tt->compile() ) {
2762       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2763       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2764       die $error;
2765     }
2766
2767     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2768
2769     $invoice_data{$include} =~ s/\n+$//
2770       if ($format eq 'latex');
2771   }
2772
2773   $invoice_data{'po_line'} =
2774     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2775       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2776       : $nbsp;
2777
2778   my %money_chars = ( 'latex'    => '',
2779                       'html'     => $conf->config('money_char') || '$',
2780                       'template' => '',
2781                     );
2782   my $money_char = $money_chars{$format};
2783
2784   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2785                             'html'     => $conf->config('money_char') || '$',
2786                             'template' => '',
2787                           );
2788   my $other_money_char = $other_money_chars{$format};
2789   $invoice_data{'dollar'} = $other_money_char;
2790
2791   my @detail_items = ();
2792   my @total_items = ();
2793   my @buf = ();
2794   my @sections = ();
2795
2796   $invoice_data{'detail_items'} = \@detail_items;
2797   $invoice_data{'total_items'} = \@total_items;
2798   $invoice_data{'buf'} = \@buf;
2799   $invoice_data{'sections'} = \@sections;
2800
2801   warn "$me generating sections\n"
2802     if $DEBUG > 1;
2803
2804   my $previous_section = { 'description' => 'Previous Charges',
2805                            'subtotal'    => $other_money_char.
2806                                             sprintf('%.2f', $pr_total),
2807                            'summarized'  => $summarypage ? 'Y' : '',
2808                          };
2809   $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
2810     join(' / ', map { $cust_main->balance_date_range(@$_) }
2811                 $self->_prior_month30s
2812         )
2813     if $conf->exists('invoice_include_aging');
2814
2815   my $taxtotal = 0;
2816   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2817                       'subtotal'    => $taxtotal,   # adjusted below
2818                       'summarized'  => $summarypage ? 'Y' : '',
2819                     };
2820   my $tax_weight = _pkg_category($tax_section->{description})
2821                         ? _pkg_category($tax_section->{description})->weight
2822                         : 0;
2823   $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2824   $tax_section->{'sort_weight'} = $tax_weight;
2825
2826
2827   my $adjusttotal = 0;
2828   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2829                          'subtotal'    => 0,   # adjusted below
2830                          'summarized'  => $summarypage ? 'Y' : '',
2831                        };
2832   my $adjust_weight = _pkg_category($adjust_section->{description})
2833                         ? _pkg_category($adjust_section->{description})->weight
2834                         : 0;
2835   $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2836   $adjust_section->{'sort_weight'} = $adjust_weight;
2837
2838   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2839   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2840   $invoice_data{'multisection'} = $multisection;
2841   my $late_sections = [];
2842   my $extra_sections = [];
2843   my $extra_lines = ();
2844   if ( $multisection ) {
2845     ($extra_sections, $extra_lines) =
2846       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2847       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2848
2849     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2850
2851     push @detail_items, @$extra_lines if $extra_lines;
2852     push @sections,
2853       $self->_items_sections( $late_sections,      # this could stand a refactor
2854                               $summarypage,
2855                               $escape_function_nonbsp,
2856                               $extra_sections,
2857                               $format,             #bah
2858                             );
2859     if ($conf->exists('svc_phone_sections')) {
2860       my ($phone_sections, $phone_lines) =
2861         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2862       push @{$late_sections}, @$phone_sections;
2863       push @detail_items, @$phone_lines;
2864     }
2865     if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2866       my ($accountcode_section, $accountcode_lines) =
2867         $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2868       if ( scalar(@$accountcode_lines) ) {
2869           push @{$late_sections}, $accountcode_section;
2870           push @detail_items, @$accountcode_lines;
2871       }
2872     }
2873   }else{
2874     push @sections, { 'description' => '', 'subtotal' => '' };
2875   }
2876
2877   unless (    $conf->exists('disable_previous_balance')
2878            || $conf->exists('previous_balance-summary_only')
2879          )
2880   {
2881
2882     warn "$me adding previous balances\n"
2883       if $DEBUG > 1;
2884
2885     foreach my $line_item ( $self->_items_previous ) {
2886
2887       my $detail = {
2888         ext_description => [],
2889       };
2890       $detail->{'ref'} = $line_item->{'pkgnum'};
2891       $detail->{'quantity'} = 1;
2892       $detail->{'section'} = $previous_section;
2893       $detail->{'description'} = &$escape_function($line_item->{'description'});
2894       if ( exists $line_item->{'ext_description'} ) {
2895         @{$detail->{'ext_description'}} = map {
2896           &$escape_function($_);
2897         } @{$line_item->{'ext_description'}};
2898       }
2899       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2900                             $line_item->{'amount'};
2901       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2902
2903       push @detail_items, $detail;
2904       push @buf, [ $detail->{'description'},
2905                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2906                  ];
2907     }
2908
2909   }
2910   
2911   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2912     push @buf, ['','-----------'];
2913     push @buf, [ 'Total Previous Balance',
2914                  $money_char. sprintf("%10.2f", $pr_total) ];
2915     push @buf, ['',''];
2916   }
2917  
2918   if ( $conf->exists('svc_phone-did-summary') ) {
2919       warn "$me adding DID summary\n"
2920         if $DEBUG > 1;
2921
2922       my ($didsummary,$minutes) = $self->_did_summary;
2923       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2924       push @detail_items, 
2925         { 'description' => $didsummary_desc,
2926             'ext_description' => [ $didsummary, $minutes ],
2927         };
2928   }
2929
2930   foreach my $section (@sections, @$late_sections) {
2931
2932     warn "$me adding section \n". Dumper($section)
2933       if $DEBUG > 1;
2934
2935     # begin some normalization
2936     $section->{'subtotal'} = $section->{'amount'}
2937       if $multisection
2938          && !exists($section->{subtotal})
2939          && exists($section->{amount});
2940
2941     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2942       if ( $invoice_data{finance_section} &&
2943            $section->{'description'} eq $invoice_data{finance_section} );
2944
2945     $section->{'subtotal'} = $other_money_char.
2946                              sprintf('%.2f', $section->{'subtotal'})
2947       if $multisection;
2948
2949     # continue some normalization
2950     $section->{'amount'}   = $section->{'subtotal'}
2951       if $multisection;
2952
2953
2954     if ( $section->{'description'} ) {
2955       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2956                    [ '', '' ],
2957                  );
2958     }
2959
2960     warn "$me   setting options\n"
2961       if $DEBUG > 1;
2962
2963     my $multilocation = scalar($cust_main->cust_location); #too expensive?
2964     my %options = ();
2965     $options{'section'} = $section if $multisection;
2966     $options{'format'} = $format;
2967     $options{'escape_function'} = $escape_function;
2968     $options{'format_function'} = sub { () } unless $unsquelched;
2969     $options{'unsquelched'} = $unsquelched;
2970     $options{'summary_page'} = $summarypage;
2971     $options{'skip_usage'} =
2972       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2973     $options{'multilocation'} = $multilocation;
2974     $options{'multisection'} = $multisection;
2975
2976     warn "$me   searching for line items\n"
2977       if $DEBUG > 1;
2978
2979     foreach my $line_item ( $self->_items_pkg(%options) ) {
2980
2981       warn "$me     adding line item $line_item\n"
2982         if $DEBUG > 1;
2983
2984       my $detail = {
2985         ext_description => [],
2986       };
2987       $detail->{'ref'} = $line_item->{'pkgnum'};
2988       $detail->{'quantity'} = $line_item->{'quantity'};
2989       $detail->{'section'} = $section;
2990       $detail->{'description'} = &$escape_function($line_item->{'description'});
2991       if ( exists $line_item->{'ext_description'} ) {
2992         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2993       }
2994       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2995                               $line_item->{'amount'};
2996       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2997                                  $line_item->{'unit_amount'};
2998       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2999   
3000       push @detail_items, $detail;
3001       push @buf, ( [ $detail->{'description'},
3002                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3003                    ],
3004                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3005                  );
3006     }
3007
3008     if ( $section->{'description'} ) {
3009       push @buf, ( ['','-----------'],
3010                    [ $section->{'description'}. ' sub-total',
3011                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
3012                    ],
3013                    [ '', '' ],
3014                    [ '', '' ],
3015                  );
3016     }
3017   
3018   }
3019   
3020   $invoice_data{current_less_finance} =
3021     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3022
3023   if ( $multisection && !$conf->exists('disable_previous_balance')
3024     || $conf->exists('previous_balance-summary_only') )
3025   {
3026     unshift @sections, $previous_section if $pr_total;
3027   }
3028
3029   warn "$me adding taxes\n"
3030     if $DEBUG > 1;
3031
3032   foreach my $tax ( $self->_items_tax ) {
3033
3034     $taxtotal += $tax->{'amount'};
3035
3036     my $description = &$escape_function( $tax->{'description'} );
3037     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
3038
3039     if ( $multisection ) {
3040
3041       my $money = $old_latex ? '' : $money_char;
3042       push @detail_items, {
3043         ext_description => [],
3044         ref          => '',
3045         quantity     => '',
3046         description  => $description,
3047         amount       => $money. $amount,
3048         product_code => '',
3049         section      => $tax_section,
3050       };
3051
3052     } else {
3053
3054       push @total_items, {
3055         'total_item'   => $description,
3056         'total_amount' => $other_money_char. $amount,
3057       };
3058
3059     }
3060
3061     push @buf,[ $description,
3062                 $money_char. $amount,
3063               ];
3064
3065   }
3066   
3067   if ( $taxtotal ) {
3068     my $total = {};
3069     $total->{'total_item'} = 'Sub-total';
3070     $total->{'total_amount'} =
3071       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3072
3073     if ( $multisection ) {
3074       $tax_section->{'subtotal'} = $other_money_char.
3075                                    sprintf('%.2f', $taxtotal);
3076       $tax_section->{'pretotal'} = 'New charges sub-total '.
3077                                    $total->{'total_amount'};
3078       push @sections, $tax_section if $taxtotal;
3079     }else{
3080       unshift @total_items, $total;
3081     }
3082   }
3083   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3084
3085   push @buf,['','-----------'];
3086   push @buf,[( $conf->exists('disable_previous_balance') 
3087                ? 'Total Charges'
3088                : 'Total New Charges'
3089              ),
3090              $money_char. sprintf("%10.2f",$self->charged) ];
3091   push @buf,['',''];
3092
3093   {
3094     my $total = {};
3095     my $item = 'Total';
3096     $item = $conf->config('previous_balance-exclude_from_total')
3097          || 'Total New Charges'
3098       if $conf->exists('previous_balance-exclude_from_total');
3099     my $amount = $self->charged +
3100                    ( $conf->exists('disable_previous_balance') ||
3101                      $conf->exists('previous_balance-exclude_from_total')
3102                      ? 0
3103                      : $pr_total
3104                    );
3105     $total->{'total_item'} = &$embolden_function($item);
3106     $total->{'total_amount'} =
3107       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
3108     if ( $multisection ) {
3109       if ( $adjust_section->{'sort_weight'} ) {
3110         $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3111           sprintf("%.2f", ($self->billing_balance || 0) );
3112       } else {
3113         $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3114                                         sprintf('%.2f', $self->charged );
3115       } 
3116     }else{
3117       push @total_items, $total;
3118     }
3119     push @buf,['','-----------'];
3120     push @buf,[$item,
3121                $money_char.
3122                sprintf( '%10.2f', $amount )
3123               ];
3124     push @buf,['',''];
3125   }
3126   
3127   unless ( $conf->exists('disable_previous_balance') ) {
3128     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3129   
3130     # credits
3131     my $credittotal = 0;
3132     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3133
3134       my $total;
3135       $total->{'total_item'} = &$escape_function($credit->{'description'});
3136       $credittotal += $credit->{'amount'};
3137       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3138       $adjusttotal += $credit->{'amount'};
3139       if ( $multisection ) {
3140         my $money = $old_latex ? '' : $money_char;
3141         push @detail_items, {
3142           ext_description => [],
3143           ref          => '',
3144           quantity     => '',
3145           description  => &$escape_function($credit->{'description'}),
3146           amount       => $money. $credit->{'amount'},
3147           product_code => '',
3148           section      => $adjust_section,
3149         };
3150       } else {
3151         push @total_items, $total;
3152       }
3153
3154     }
3155     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3156
3157     #credits (again)
3158     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3159       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3160     }
3161
3162     # payments
3163     my $paymenttotal = 0;
3164     foreach my $payment ( $self->_items_payments ) {
3165       my $total = {};
3166       $total->{'total_item'} = &$escape_function($payment->{'description'});
3167       $paymenttotal += $payment->{'amount'};
3168       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3169       $adjusttotal += $payment->{'amount'};
3170       if ( $multisection ) {
3171         my $money = $old_latex ? '' : $money_char;
3172         push @detail_items, {
3173           ext_description => [],
3174           ref          => '',
3175           quantity     => '',
3176           description  => &$escape_function($payment->{'description'}),
3177           amount       => $money. $payment->{'amount'},
3178           product_code => '',
3179           section      => $adjust_section,
3180         };
3181       }else{
3182         push @total_items, $total;
3183       }
3184       push @buf, [ $payment->{'description'},
3185                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
3186                  ];
3187     }
3188     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3189   
3190     if ( $multisection ) {
3191       $adjust_section->{'subtotal'} = $other_money_char.
3192                                       sprintf('%.2f', $adjusttotal);
3193       push @sections, $adjust_section
3194         unless $adjust_section->{sort_weight};
3195     }
3196
3197     { 
3198       my $total;
3199       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3200       $total->{'total_amount'} =
3201         &$embolden_function(
3202           $other_money_char. sprintf('%.2f', $summarypage 
3203                                                ? $self->charged +
3204                                                  $self->billing_balance
3205                                                : $self->owed + $pr_total
3206                                     )
3207         );
3208       if ( $multisection && !$adjust_section->{sort_weight} ) {
3209         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3210                                          $total->{'total_amount'};
3211       }else{
3212         push @total_items, $total;
3213       }
3214       push @buf,['','-----------'];
3215       push @buf,[$self->balance_due_msg, $money_char. 
3216         sprintf("%10.2f", $balance_due ) ];
3217     }
3218
3219     if ( $conf->exists('previous_balance-show_credit')
3220         and $cust_main->balance < 0 ) {
3221       my $credit_total = {
3222         'total_item'    => &$embolden_function($self->credit_balance_msg),
3223         'total_amount'  => &$embolden_function(
3224           $other_money_char. sprintf('%.2f', -$cust_main->balance)
3225         ),
3226       };
3227       if ( $multisection ) {
3228         $adjust_section->{'posttotal'} .= $newline_token .
3229           $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3230       }
3231       else {
3232         push @total_items, $credit_total;
3233       }
3234       push @buf,['','-----------'];
3235       push @buf,[$self->credit_balance_msg, $money_char. 
3236         sprintf("%10.2f", -$cust_main->balance ) ];
3237     }
3238   }
3239
3240   if ( $multisection ) {
3241     if ($conf->exists('svc_phone_sections')) {
3242       my $total;
3243       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3244       $total->{'total_amount'} =
3245         &$embolden_function(
3246           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3247         );
3248       my $last_section = pop @sections;
3249       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3250                                      $total->{'total_amount'};
3251       push @sections, $last_section;
3252     }
3253     push @sections, @$late_sections
3254       if $unsquelched;
3255   }
3256
3257   my @includelist = ();
3258   push @includelist, 'summary' if $summarypage;
3259   foreach my $include ( @includelist ) {
3260
3261     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3262     my @inc_src;
3263
3264     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3265
3266       @inc_src = $conf->config($inc_file, $agentnum);
3267
3268     } else {
3269
3270       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3271
3272       my $convert_map = $convert_maps{$format}{$include};
3273
3274       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3275                        s/--\@\]/$delimiters{$format}[1]/g;
3276                        $_;
3277                      } 
3278                  &$convert_map( $conf->config($inc_file, $agentnum) );
3279
3280     }
3281
3282     my $inc_tt = new Text::Template (
3283       TYPE       => 'ARRAY',
3284       SOURCE     => [ map "$_\n", @inc_src ],
3285       DELIMITERS => $delimiters{$format},
3286     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3287
3288     unless ( $inc_tt->compile() ) {
3289       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3290       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3291       die $error;
3292     }
3293
3294     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3295
3296     $invoice_data{$include} =~ s/\n+$//
3297       if ($format eq 'latex');
3298   }
3299
3300   $invoice_lines = 0;
3301   my $wasfunc = 0;
3302   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3303     /invoice_lines\((\d*)\)/;
3304     $invoice_lines += $1 || scalar(@buf);
3305     $wasfunc=1;
3306   }
3307   die "no invoice_lines() functions in template?"
3308     if ( $format eq 'template' && !$wasfunc );
3309
3310   if ($format eq 'template') {
3311
3312     if ( $invoice_lines ) {
3313       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3314       $invoice_data{'total_pages'}++
3315         if scalar(@buf) % $invoice_lines;
3316     }
3317
3318     #setup subroutine for the template
3319     sub FS::cust_bill::_template::invoice_lines {
3320       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3321       map { 
3322         scalar(@FS::cust_bill::_template::buf)
3323           ? shift @FS::cust_bill::_template::buf
3324           : [ '', '' ];
3325       }
3326       ( 1 .. $lines );
3327     }
3328
3329     my $lines;
3330     my @collect;
3331     while (@buf) {
3332       push @collect, split("\n",
3333         $text_template->fill_in( HASH => \%invoice_data,
3334                                  PACKAGE => 'FS::cust_bill::_template'
3335                                )
3336       );
3337       $FS::cust_bill::_template::page++;
3338     }
3339     map "$_\n", @collect;
3340   }else{
3341     warn "filling in template for invoice ". $self->invnum. "\n"
3342       if $DEBUG;
3343     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3344       if $DEBUG > 1;
3345
3346     $text_template->fill_in(HASH => \%invoice_data);
3347   }
3348 }
3349
3350 # helper routine for generating date ranges
3351 sub _prior_month30s {
3352   my $self = shift;
3353   my @ranges = (
3354    [ 1,       2592000 ], # 0-30 days ago
3355    [ 2592000, 5184000 ], # 30-60 days ago
3356    [ 5184000, 7776000 ], # 60-90 days ago
3357    [ 7776000, 0       ], # 90+   days ago
3358   );
3359
3360   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3361           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3362       ] }
3363   @ranges;
3364 }
3365
3366 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3367
3368 Returns an postscript invoice, as a scalar.
3369
3370 Options can be passed as a hashref (recommended) or as a list of time, template
3371 and then any key/value pairs for any other options.
3372
3373 I<time> an optional value used to control the printing of overdue messages.  The
3374 default is now.  It isn't the date of the invoice; that's the `_date' field.
3375 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3376 L<Time::Local> and L<Date::Parse> for conversion functions.
3377
3378 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3379
3380 =cut
3381
3382 sub print_ps {
3383   my $self = shift;
3384
3385   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3386   my $ps = generate_ps($file);
3387   unlink($logofile);
3388   unlink($barcodefile) if $barcodefile;
3389
3390   $ps;
3391 }
3392
3393 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3394
3395 Returns an PDF invoice, as a scalar.
3396
3397 Options can be passed as a hashref (recommended) or as a list of time, template
3398 and then any key/value pairs for any other options.
3399
3400 I<time> an optional value used to control the printing of overdue messages.  The
3401 default is now.  It isn't the date of the invoice; that's the `_date' field.
3402 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3403 L<Time::Local> and L<Date::Parse> for conversion functions.
3404
3405 I<template>, if specified, is the name of a suffix for alternate invoices.
3406
3407 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3408
3409 =cut
3410
3411 sub print_pdf {
3412   my $self = shift;
3413
3414   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3415   my $pdf = generate_pdf($file);
3416   unlink($logofile);
3417   unlink($barcodefile) if $barcodefile;
3418
3419   $pdf;
3420 }
3421
3422 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3423
3424 Returns an HTML invoice, as a scalar.
3425
3426 I<time> an optional value used to control the printing of overdue messages.  The
3427 default is now.  It isn't the date of the invoice; that's the `_date' field.
3428 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3429 L<Time::Local> and L<Date::Parse> for conversion functions.
3430
3431 I<template>, if specified, is the name of a suffix for alternate invoices.
3432
3433 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3434
3435 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3436 when emailing the invoice as part of a multipart/related MIME email.
3437
3438 =cut
3439
3440 sub print_html {
3441   my $self = shift;
3442   my %params;
3443   if ( ref($_[0]) ) {
3444     %params = %{ shift() }; 
3445   }else{
3446     $params{'time'} = shift;
3447     $params{'template'} = shift;
3448     $params{'cid'} = shift;
3449   }
3450
3451   $params{'format'} = 'html';
3452   
3453   $self->print_generic( %params );
3454 }
3455
3456 # quick subroutine for print_latex
3457 #
3458 # There are ten characters that LaTeX treats as special characters, which
3459 # means that they do not simply typeset themselves: 
3460 #      # $ % & ~ _ ^ \ { }
3461 #
3462 # TeX ignores blanks following an escaped character; if you want a blank (as
3463 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3464
3465 sub _latex_escape {
3466   my $value = shift;
3467   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3468   $value =~ s/([<>])/\$$1\$/g;
3469   $value;
3470 }
3471
3472 sub _html_escape {
3473   my $value = shift;
3474   encode_entities($value);
3475   $value;
3476 }
3477
3478 sub _html_escape_nbsp {
3479   my $value = _html_escape(shift);
3480   $value =~ s/ +/&nbsp;/g;
3481   $value;
3482 }
3483
3484 #utility methods for print_*
3485
3486 sub _translate_old_latex_format {
3487   warn "_translate_old_latex_format called\n"
3488     if $DEBUG; 
3489
3490   my @template = ();
3491   while ( @_ ) {
3492     my $line = shift;
3493   
3494     if ( $line =~ /^%%Detail\s*$/ ) {
3495   
3496       push @template, q![@--!,
3497                       q!  foreach my $_tr_line (@detail_items) {!,
3498                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3499                       q!      $_tr_line->{'description'} .= !, 
3500                       q!        "\\tabularnewline\n~~".!,
3501                       q!        join( "\\tabularnewline\n~~",!,
3502                       q!          @{$_tr_line->{'ext_description'}}!,
3503                       q!        );!,
3504                       q!    }!;
3505
3506       while ( ( my $line_item_line = shift )
3507               !~ /^%%EndDetail\s*$/                            ) {
3508         $line_item_line =~ s/'/\\'/g;    # nice LTS
3509         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3510         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3511         push @template, "    \$OUT .= '$line_item_line';";
3512       }
3513
3514       push @template, '}',
3515                       '--@]';
3516       #' doh, gvim
3517     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3518
3519       push @template, '[@--',
3520                       '  foreach my $_tr_line (@total_items) {';
3521
3522       while ( ( my $total_item_line = shift )
3523               !~ /^%%EndTotalDetails\s*$/                      ) {
3524         $total_item_line =~ s/'/\\'/g;    # nice LTS
3525         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3526         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3527         push @template, "    \$OUT .= '$total_item_line';";
3528       }
3529
3530       push @template, '}',
3531                       '--@]';
3532
3533     } else {
3534       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3535       push @template, $line;  
3536     }
3537   
3538   }
3539
3540   if ($DEBUG) {
3541     warn "$_\n" foreach @template;
3542   }
3543
3544   (@template);
3545 }
3546
3547 sub terms {
3548   my $self = shift;
3549
3550   #check for an invoice-specific override
3551   return $self->invoice_terms if $self->invoice_terms;
3552   
3553   #check for a customer- specific override
3554   my $cust_main = $self->cust_main;
3555   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3556
3557   #use configured default
3558   $conf->config('invoice_default_terms') || '';
3559 }
3560
3561 sub due_date {
3562   my $self = shift;
3563   my $duedate = '';
3564   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3565     $duedate = $self->_date() + ( $1 * 86400 );
3566   }
3567   $duedate;
3568 }
3569
3570 sub due_date2str {
3571   my $self = shift;
3572   $self->due_date ? time2str(shift, $self->due_date) : '';
3573 }
3574
3575 sub balance_due_msg {
3576   my $self = shift;
3577   my $msg = 'Balance Due';
3578   return $msg unless $self->terms;
3579   if ( $self->due_date ) {
3580     $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3581   } elsif ( $self->terms ) {
3582     $msg .= ' - '. $self->terms;
3583   }
3584   $msg;
3585 }
3586
3587 sub balance_due_date {
3588   my $self = shift;
3589   my $duedate = '';
3590   if (    $conf->exists('invoice_default_terms') 
3591        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3592     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3593   }
3594   $duedate;
3595 }
3596
3597 sub credit_balance_msg { 'Credit Balance Remaining' }
3598
3599 =item invnum_date_pretty
3600
3601 Returns a string with the invoice number and date, for example:
3602 "Invoice #54 (3/20/2008)"
3603
3604 =cut
3605
3606 sub invnum_date_pretty {
3607   my $self = shift;
3608   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3609 }
3610
3611 =item _date_pretty
3612
3613 Returns a string with the date, for example: "3/20/2008"
3614
3615 =cut
3616
3617 sub _date_pretty {
3618   my $self = shift;
3619   time2str($date_format, $self->_date);
3620 }
3621
3622 use vars qw(%pkg_category_cache);
3623 sub _items_sections {
3624   my $self = shift;
3625   my $late = shift;
3626   my $summarypage = shift;
3627   my $escape = shift;
3628   my $extra_sections = shift;
3629   my $format = shift;
3630
3631   my %subtotal = ();
3632   my %late_subtotal = ();
3633   my %not_tax = ();
3634
3635   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3636   {
3637
3638       my $usage = $cust_bill_pkg->usage;
3639
3640       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3641         next if ( $display->summary && $summarypage );
3642
3643         my $section = $display->section;
3644         my $type    = $display->type;
3645
3646         $not_tax{$section} = 1
3647           unless $cust_bill_pkg->pkgnum == 0;
3648
3649         if ( $display->post_total && !$summarypage ) {
3650           if (! $type || $type eq 'S') {
3651             $late_subtotal{$section} += $cust_bill_pkg->setup
3652               if $cust_bill_pkg->setup != 0;
3653           }
3654
3655           if (! $type) {
3656             $late_subtotal{$section} += $cust_bill_pkg->recur
3657               if $cust_bill_pkg->recur != 0;
3658           }
3659
3660           if ($type && $type eq 'R') {
3661             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3662               if $cust_bill_pkg->recur != 0;
3663           }
3664           
3665           if ($type && $type eq 'U') {
3666             $late_subtotal{$section} += $usage
3667               unless scalar(@$extra_sections);
3668           }
3669
3670         } else {
3671
3672           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3673
3674           if (! $type || $type eq 'S') {
3675             $subtotal{$section} += $cust_bill_pkg->setup
3676               if $cust_bill_pkg->setup != 0;
3677           }
3678
3679           if (! $type) {
3680             $subtotal{$section} += $cust_bill_pkg->recur
3681               if $cust_bill_pkg->recur != 0;
3682           }
3683
3684           if ($type && $type eq 'R') {
3685             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3686               if $cust_bill_pkg->recur != 0;
3687           }
3688           
3689           if ($type && $type eq 'U') {
3690             $subtotal{$section} += $usage
3691               unless scalar(@$extra_sections);
3692           }
3693
3694         }
3695
3696       }
3697
3698   }
3699
3700   %pkg_category_cache = ();
3701
3702   push @$late, map { { 'description' => &{$escape}($_),
3703                        'subtotal'    => $late_subtotal{$_},
3704                        'post_total'  => 1,
3705                        'sort_weight' => ( _pkg_category($_)
3706                                             ? _pkg_category($_)->weight
3707                                             : 0
3708                                        ),
3709                        ((_pkg_category($_) && _pkg_category($_)->condense)
3710                                            ? $self->_condense_section($format)
3711                                            : ()
3712                        ),
3713                    } }
3714                  sort _sectionsort keys %late_subtotal;
3715
3716   my @sections;
3717   if ( $summarypage ) {
3718     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3719                 map { $_->categoryname } qsearch('pkg_category', {});
3720     push @sections, '' if exists($subtotal{''});
3721   } else {
3722     @sections = keys %subtotal;
3723   }
3724
3725   my @early = map { { 'description' => &{$escape}($_),
3726                       'subtotal'    => $subtotal{$_},
3727                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3728                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3729                       'sort_weight' => ( _pkg_category($_)
3730                                            ? _pkg_category($_)->weight
3731                                            : 0
3732                                        ),
3733                        ((_pkg_category($_) && _pkg_category($_)->condense)
3734                                            ? $self->_condense_section($format)
3735                                            : ()
3736                        ),
3737                     }
3738                   } @sections;
3739   push @early, @$extra_sections if $extra_sections;
3740
3741   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3742
3743 }
3744
3745 #helper subs for above
3746
3747 sub _sectionsort {
3748   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3749 }
3750
3751 sub _pkg_category {
3752   my $categoryname = shift;
3753   $pkg_category_cache{$categoryname} ||=
3754     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3755 }
3756
3757 my %condensed_format = (
3758   'label' => [ qw( Description Qty Amount ) ],
3759   'fields' => [
3760                 sub { shift->{description} },
3761                 sub { shift->{quantity} },
3762                 sub { my($href, %opt) = @_;
3763                       ($opt{dollar} || ''). $href->{amount};
3764                     },
3765               ],
3766   'align'  => [ qw( l r r ) ],
3767   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3768   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3769 );
3770
3771 sub _condense_section {
3772   my ( $self, $format ) = ( shift, shift );
3773   ( 'condensed' => 1,
3774     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3775       qw( description_generator
3776           header_generator
3777           total_generator
3778           total_line_generator
3779         )
3780   );
3781 }
3782
3783 sub _condensed_generator_defaults {
3784   my ( $self, $format ) = ( shift, shift );
3785   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3786 }
3787
3788 my %html_align = (
3789   'c' => 'center',
3790   'l' => 'left',
3791   'r' => 'right',
3792 );
3793
3794 sub _condensed_header_generator {
3795   my ( $self, $format ) = ( shift, shift );
3796
3797   my ( $f, $prefix, $suffix, $separator, $column ) =
3798     _condensed_generator_defaults($format);
3799
3800   if ($format eq 'latex') {
3801     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3802     $suffix = "\\\\\n\\hline";
3803     $separator = "&\n";
3804     $column =
3805       sub { my ($d,$a,$s,$w) = @_;
3806             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3807           };
3808   } elsif ( $format eq 'html' ) {
3809     $prefix = '<th></th>';
3810     $suffix = '';
3811     $separator = '';
3812     $column =
3813       sub { my ($d,$a,$s,$w) = @_;
3814             return qq!<th align="$html_align{$a}">$d</th>!;
3815       };
3816   }
3817
3818   sub {
3819     my @args = @_;
3820     my @result = ();
3821
3822     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3823       push @result,
3824         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3825     }
3826
3827     $prefix. join($separator, @result). $suffix;
3828   };
3829
3830 }
3831
3832 sub _condensed_description_generator {
3833   my ( $self, $format ) = ( shift, shift );
3834
3835   my ( $f, $prefix, $suffix, $separator, $column ) =
3836     _condensed_generator_defaults($format);
3837
3838   my $money_char = '$';
3839   if ($format eq 'latex') {
3840     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3841     $suffix = '\\\\';
3842     $separator = " & \n";
3843     $column =
3844       sub { my ($d,$a,$s,$w) = @_;
3845             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3846           };
3847     $money_char = '\\dollar';
3848   }elsif ( $format eq 'html' ) {
3849     $prefix = '"><td align="center"></td>';
3850     $suffix = '';
3851     $separator = '';
3852     $column =
3853       sub { my ($d,$a,$s,$w) = @_;
3854             return qq!<td align="$html_align{$a}">$d</td>!;
3855       };
3856     #$money_char = $conf->config('money_char') || '$';
3857     $money_char = '';  # this is madness
3858   }
3859
3860   sub {
3861     #my @args = @_;
3862     my $href = shift;
3863     my @result = ();
3864
3865     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3866       my $dollar = '';
3867       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3868       push @result,
3869         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3870                     map { $f->{$_}->[$i] } qw(align span width)
3871                   );
3872     }
3873
3874     $prefix. join( $separator, @result ). $suffix;
3875   };
3876
3877 }
3878
3879 sub _condensed_total_generator {
3880   my ( $self, $format ) = ( shift, shift );
3881
3882   my ( $f, $prefix, $suffix, $separator, $column ) =
3883     _condensed_generator_defaults($format);
3884   my $style = '';
3885
3886   if ($format eq 'latex') {
3887     $prefix = "& ";
3888     $suffix = "\\\\\n";
3889     $separator = " & \n";
3890     $column =
3891       sub { my ($d,$a,$s,$w) = @_;
3892             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3893           };
3894   }elsif ( $format eq 'html' ) {
3895     $prefix = '';
3896     $suffix = '';
3897     $separator = '';
3898     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3899     $column =
3900       sub { my ($d,$a,$s,$w) = @_;
3901             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3902       };
3903   }
3904
3905
3906   sub {
3907     my @args = @_;
3908     my @result = ();
3909
3910     #  my $r = &{$f->{fields}->[$i]}(@args);
3911     #  $r .= ' Total' unless $i;
3912
3913     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3914       push @result,
3915         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3916                     map { $f->{$_}->[$i] } qw(align span width)
3917                   );
3918     }
3919
3920     $prefix. join( $separator, @result ). $suffix;
3921   };
3922
3923 }
3924
3925 =item total_line_generator FORMAT
3926
3927 Returns a coderef used for generation of invoice total line items for this
3928 usage_class.  FORMAT is either html or latex
3929
3930 =cut
3931
3932 # should not be used: will have issues with hash element names (description vs
3933 # total_item and amount vs total_amount -- another array of functions?
3934
3935 sub _condensed_total_line_generator {
3936   my ( $self, $format ) = ( shift, shift );
3937
3938   my ( $f, $prefix, $suffix, $separator, $column ) =
3939     _condensed_generator_defaults($format);
3940   my $style = '';
3941
3942   if ($format eq 'latex') {
3943     $prefix = "& ";
3944     $suffix = "\\\\\n";
3945     $separator = " & \n";
3946     $column =
3947       sub { my ($d,$a,$s,$w) = @_;
3948             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3949           };
3950   }elsif ( $format eq 'html' ) {
3951     $prefix = '';
3952     $suffix = '';
3953     $separator = '';
3954     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3955     $column =
3956       sub { my ($d,$a,$s,$w) = @_;
3957             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3958       };
3959   }
3960
3961
3962   sub {
3963     my @args = @_;
3964     my @result = ();
3965
3966     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3967       push @result,
3968         &{$column}( &{$f->{fields}->[$i]}(@args),
3969                     map { $f->{$_}->[$i] } qw(align span width)
3970                   );
3971     }
3972
3973     $prefix. join( $separator, @result ). $suffix;
3974   };
3975
3976 }
3977
3978 #sub _items_extra_usage_sections {
3979 #  my $self = shift;
3980 #  my $escape = shift;
3981 #
3982 #  my %sections = ();
3983 #
3984 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
3985 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3986 #  {
3987 #    next unless $cust_bill_pkg->pkgnum > 0;
3988 #
3989 #    foreach my $section ( keys %usage_class ) {
3990 #
3991 #      my $usage = $cust_bill_pkg->usage($section);
3992 #
3993 #      next unless $usage && $usage > 0;
3994 #
3995 #      $sections{$section} ||= 0;
3996 #      $sections{$section} += $usage;
3997 #
3998 #    }
3999 #
4000 #  }
4001 #
4002 #  map { { 'description' => &{$escape}($_),
4003 #          'subtotal'    => $sections{$_},
4004 #          'summarized'  => '',
4005 #          'tax_section' => '',
4006 #        }
4007 #      }
4008 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4009 #
4010 #}
4011
4012 sub _items_extra_usage_sections {
4013   my $self = shift;
4014   my $escape = shift;
4015   my $format = shift;
4016
4017   my %sections = ();
4018   my %classnums = ();
4019   my %lines = ();
4020
4021   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4022
4023   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4024   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4025     next unless $cust_bill_pkg->pkgnum > 0;
4026
4027     foreach my $classnum ( keys %usage_class ) {
4028       my $section = $usage_class{$classnum}->classname;
4029       $classnums{$section} = $classnum;
4030
4031       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4032         my $amount = $detail->amount;
4033         next unless $amount && $amount > 0;
4034  
4035         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4036         $sections{$section}{amount} += $amount;  #subtotal
4037         $sections{$section}{calls}++;
4038         $sections{$section}{duration} += $detail->duration;
4039
4040         my $desc = $detail->regionname; 
4041         my $description = $desc;
4042         $description = substr($desc, 0, $maxlength). '...'
4043           if $format eq 'latex' && length($desc) > $maxlength;
4044
4045         $lines{$section}{$desc} ||= {
4046           description     => &{$escape}($description),
4047           #pkgpart         => $part_pkg->pkgpart,
4048           pkgnum          => $cust_bill_pkg->pkgnum,
4049           ref             => '',
4050           amount          => 0,
4051           calls           => 0,
4052           duration        => 0,
4053           #unit_amount     => $cust_bill_pkg->unitrecur,
4054           quantity        => $cust_bill_pkg->quantity,
4055           product_code    => 'N/A',
4056           ext_description => [],
4057         };
4058
4059         $lines{$section}{$desc}{amount} += $amount;
4060         $lines{$section}{$desc}{calls}++;
4061         $lines{$section}{$desc}{duration} += $detail->duration;
4062
4063       }
4064     }
4065   }
4066
4067   my %sectionmap = ();
4068   foreach (keys %sections) {
4069     my $usage_class = $usage_class{$classnums{$_}};
4070     $sectionmap{$_} = { 'description' => &{$escape}($_),
4071                         'amount'    => $sections{$_}{amount},    #subtotal
4072                         'calls'       => $sections{$_}{calls},
4073                         'duration'    => $sections{$_}{duration},
4074                         'summarized'  => '',
4075                         'tax_section' => '',
4076                         'sort_weight' => $usage_class->weight,
4077                         ( $usage_class->format
4078                           ? ( map { $_ => $usage_class->$_($format) }
4079                               qw( description_generator header_generator total_generator total_line_generator )
4080                             )
4081                           : ()
4082                         ), 
4083                       };
4084   }
4085
4086   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4087                  values %sectionmap;
4088
4089   my @lines = ();
4090   foreach my $section ( keys %lines ) {
4091     foreach my $line ( keys %{$lines{$section}} ) {
4092       my $l = $lines{$section}{$line};
4093       $l->{section}     = $sectionmap{$section};
4094       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4095       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4096       push @lines, $l;
4097     }
4098   }
4099
4100   return(\@sections, \@lines);
4101
4102 }
4103
4104 sub _did_summary {
4105     my $self = shift;
4106     my $end = $self->_date;
4107
4108     # start at date of previous invoice + 1 second or 0 if no previous invoice
4109     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4110     $start = 0 if !$start;
4111     $start++;
4112
4113     my $cust_main = $self->cust_main;
4114     my @pkgs = $cust_main->all_pkgs;
4115     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4116         = (0,0,0,0,0);
4117     my @seen = ();
4118     foreach my $pkg ( @pkgs ) {
4119         my @h_cust_svc = $pkg->h_cust_svc($end);
4120         foreach my $h_cust_svc ( @h_cust_svc ) {
4121             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4122             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4123
4124             my $inserted = $h_cust_svc->date_inserted;
4125             my $deleted = $h_cust_svc->date_deleted;
4126             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4127             my $phone_deleted;
4128             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
4129             
4130 # DID either activated or ported in; cannot be both for same DID simultaneously
4131             if ($inserted >= $start && $inserted <= $end && $phone_inserted
4132                 && (!$phone_inserted->lnp_status 
4133                     || $phone_inserted->lnp_status eq ''
4134                     || $phone_inserted->lnp_status eq 'native')) {
4135                 $num_activated++;
4136             }
4137             else { # this one not so clean, should probably move to (h_)svc_phone
4138                  my $phone_portedin = qsearchs( 'h_svc_phone',
4139                       { 'svcnum' => $h_cust_svc->svcnum, 
4140                         'lnp_status' => 'portedin' },  
4141                       FS::h_svc_phone->sql_h_searchs($end),  
4142                     );
4143                  $num_portedin++ if $phone_portedin;
4144             }
4145
4146 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4147             if($deleted >= $start && $deleted <= $end && $phone_deleted
4148                 && (!$phone_deleted->lnp_status 
4149                     || $phone_deleted->lnp_status ne 'portingout')) {
4150                 $num_deactivated++;
4151             } 
4152             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
4153                 && $phone_deleted->lnp_status 
4154                 && $phone_deleted->lnp_status eq 'portingout') {
4155                 $num_portedout++;
4156             }
4157
4158             # increment usage minutes
4159         if ( $phone_inserted ) {
4160             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4161             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4162         }
4163         else {
4164             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4165         }
4166
4167             # don't look at this service again
4168             push @seen, $h_cust_svc->svcnum;
4169         }
4170     }
4171
4172     $minutes = sprintf("%d", $minutes);
4173     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
4174         . "$num_deactivated  Ported-Out: $num_portedout ",
4175             "Total Minutes: $minutes");
4176 }
4177
4178 sub _items_accountcode_cdr {
4179     my $self = shift;
4180     my $escape = shift;
4181     my $format = shift;
4182
4183     my $section = { 'amount'        => 0,
4184                     'calls'         => 0,
4185                     'duration'      => 0,
4186                     'sort_weight'   => '',
4187                     'phonenum'      => '',
4188                     'description'   => 'Usage by Account Code',
4189                     'post_total'    => '',
4190                     'summarized'    => '',
4191                     'header'        => '',
4192                   };
4193     my @lines;
4194     my %accountcodes = ();
4195
4196     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4197         next unless $cust_bill_pkg->pkgnum > 0;
4198
4199         my @header = $cust_bill_pkg->details_header;
4200         next unless scalar(@header);
4201         $section->{'header'} = join(',',@header);
4202
4203         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4204
4205             $section->{'header'} = $detail->formatted('format' => $format)
4206                 if($detail->detail eq $section->{'header'}); 
4207       
4208             my $accountcode = $detail->accountcode;
4209             next unless $accountcode;
4210
4211             my $amount = $detail->amount;
4212             next unless $amount && $amount > 0;
4213
4214             $accountcodes{$accountcode} ||= {
4215                     description => $accountcode,
4216                     pkgnum      => '',
4217                     ref         => '',
4218                     amount      => 0,
4219                     calls       => 0,
4220                     duration    => 0,
4221                     quantity    => '',
4222                     product_code => 'N/A',
4223                     section     => $section,
4224                     ext_description => [ $section->{'header'} ],
4225                     detail_temp => [],
4226             };
4227
4228             $section->{'amount'} += $amount;
4229             $accountcodes{$accountcode}{'amount'} += $amount;
4230             $accountcodes{$accountcode}{calls}++;
4231             $accountcodes{$accountcode}{duration} += $detail->duration;
4232             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4233         }
4234     }
4235
4236     foreach my $l ( values %accountcodes ) {
4237         $l->{amount} = sprintf( "%.2f", $l->{amount} );
4238         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4239         foreach my $sorted_detail ( @sorted_detail ) {
4240             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4241         }
4242         delete $l->{detail_temp};
4243         push @lines, $l;
4244     }
4245
4246     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4247
4248     return ($section,\@sorted_lines);
4249 }
4250
4251 sub _items_svc_phone_sections {
4252   my $self = shift;
4253   my $escape = shift;
4254   my $format = shift;
4255
4256   my %sections = ();
4257   my %classnums = ();
4258   my %lines = ();
4259
4260   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4261
4262   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4263   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4264
4265   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4266     next unless $cust_bill_pkg->pkgnum > 0;
4267
4268     my @header = $cust_bill_pkg->details_header;
4269     next unless scalar(@header);
4270
4271     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4272
4273       my $phonenum = $detail->phonenum;
4274       next unless $phonenum;
4275
4276       my $amount = $detail->amount;
4277       next unless $amount && $amount > 0;
4278
4279       $sections{$phonenum} ||= { 'amount'      => 0,
4280                                  'calls'       => 0,
4281                                  'duration'    => 0,
4282                                  'sort_weight' => -1,
4283                                  'phonenum'    => $phonenum,
4284                                 };
4285       $sections{$phonenum}{amount} += $amount;  #subtotal
4286       $sections{$phonenum}{calls}++;
4287       $sections{$phonenum}{duration} += $detail->duration;
4288
4289       my $desc = $detail->regionname; 
4290       my $description = $desc;
4291       $description = substr($desc, 0, $maxlength). '...'
4292         if $format eq 'latex' && length($desc) > $maxlength;
4293
4294       $lines{$phonenum}{$desc} ||= {
4295         description     => &{$escape}($description),
4296         #pkgpart         => $part_pkg->pkgpart,
4297         pkgnum          => '',
4298         ref             => '',
4299         amount          => 0,
4300         calls           => 0,
4301         duration        => 0,
4302         #unit_amount     => '',
4303         quantity        => '',
4304         product_code    => 'N/A',
4305         ext_description => [],
4306       };
4307
4308       $lines{$phonenum}{$desc}{amount} += $amount;
4309       $lines{$phonenum}{$desc}{calls}++;
4310       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4311
4312       my $line = $usage_class{$detail->classnum}->classname;
4313       $sections{"$phonenum $line"} ||=
4314         { 'amount' => 0,
4315           'calls' => 0,
4316           'duration' => 0,
4317           'sort_weight' => $usage_class{$detail->classnum}->weight,
4318           'phonenum' => $phonenum,
4319           'header'  => [ @header ],
4320         };
4321       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4322       $sections{"$phonenum $line"}{calls}++;
4323       $sections{"$phonenum $line"}{duration} += $detail->duration;
4324
4325       $lines{"$phonenum $line"}{$desc} ||= {
4326         description     => &{$escape}($description),
4327         #pkgpart         => $part_pkg->pkgpart,
4328         pkgnum          => '',
4329         ref             => '',
4330         amount          => 0,
4331         calls           => 0,
4332         duration        => 0,
4333         #unit_amount     => '',
4334         quantity        => '',
4335         product_code    => 'N/A',
4336         ext_description => [],
4337       };
4338
4339       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4340       $lines{"$phonenum $line"}{$desc}{calls}++;
4341       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4342       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4343            $detail->formatted('format' => $format);
4344
4345     }
4346   }
4347
4348   my %sectionmap = ();
4349   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4350   foreach ( keys %sections ) {
4351     my @header = @{ $sections{$_}{header} || [] };
4352     my $usage_simple =
4353       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4354     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4355     my $usage_class = $summary ? $simple : $usage_simple;
4356     my $ending = $summary ? ' usage charges' : '';
4357     my %gen_opt = ();
4358     unless ($summary) {
4359       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4360     }
4361     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4362                         'amount'    => $sections{$_}{amount},    #subtotal
4363                         'calls'       => $sections{$_}{calls},
4364                         'duration'    => $sections{$_}{duration},
4365                         'summarized'  => '',
4366                         'tax_section' => '',
4367                         'phonenum'    => $sections{$_}{phonenum},
4368                         'sort_weight' => $sections{$_}{sort_weight},
4369                         'post_total'  => $summary, #inspire pagebreak
4370                         (
4371                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4372                             qw( description_generator
4373                                 header_generator
4374                                 total_generator
4375                                 total_line_generator
4376                               )
4377                           )
4378                         ), 
4379                       };
4380   }
4381
4382   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4383                         $a->{sort_weight} <=> $b->{sort_weight}
4384                       }
4385                  values %sectionmap;
4386
4387   my @lines = ();
4388   foreach my $section ( keys %lines ) {
4389     foreach my $line ( keys %{$lines{$section}} ) {
4390       my $l = $lines{$section}{$line};
4391       $l->{section}     = $sectionmap{$section};
4392       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4393       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4394       push @lines, $l;
4395     }
4396   }
4397   
4398   if($conf->exists('phone_usage_class_summary')) { 
4399       # this only works with Latex
4400       my @newlines;
4401       my @newsections;
4402
4403       # after this, we'll have only two sections per DID:
4404       # Calls Summary and Calls Detail
4405       foreach my $section ( @sections ) {
4406         if($section->{'post_total'}) {
4407             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4408             $section->{'total_line_generator'} = sub { '' };
4409             $section->{'total_generator'} = sub { '' };
4410             $section->{'header_generator'} = sub { '' };
4411             $section->{'description_generator'} = '';
4412             push @newsections, $section;
4413             my %calls_detail = %$section;
4414             $calls_detail{'post_total'} = '';
4415             $calls_detail{'sort_weight'} = '';
4416             $calls_detail{'description_generator'} = sub { '' };
4417             $calls_detail{'header_generator'} = sub {
4418                 return ' & Date/Time & Called Number & Duration & Price'
4419                     if $format eq 'latex';
4420                 '';
4421             };
4422             $calls_detail{'description'} = 'Calls Detail: '
4423                                                     . $section->{'phonenum'};
4424             push @newsections, \%calls_detail;  
4425         }
4426       }
4427
4428       # after this, each usage class is collapsed/summarized into a single
4429       # line under the Calls Summary section
4430       foreach my $newsection ( @newsections ) {
4431         if($newsection->{'post_total'}) { # this means Calls Summary
4432             foreach my $section ( @sections ) {
4433                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
4434                                 && !$section->{'post_total'});
4435                 my $newdesc = $section->{'description'};
4436                 my $tn = $section->{'phonenum'};
4437                 $newdesc =~ s/$tn//g;
4438                 my $line = {  ext_description => [],
4439                               pkgnum => '',
4440                               ref => '',
4441                               quantity => '',
4442                               calls => $section->{'calls'},
4443                               section => $newsection,
4444                               duration => $section->{'duration'},
4445                               description => $newdesc,
4446                               amount => sprintf("%.2f",$section->{'amount'}),
4447                               product_code => 'N/A',
4448                             };
4449                 push @newlines, $line;
4450             }
4451         }
4452       }
4453
4454       # after this, Calls Details is populated with all CDRs
4455       foreach my $newsection ( @newsections ) {
4456         if(!$newsection->{'post_total'}) { # this means Calls Details
4457             foreach my $line ( @lines ) {
4458                 next unless (scalar(@{$line->{'ext_description'}}) &&
4459                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4460                             );
4461                 my @extdesc = @{$line->{'ext_description'}};
4462                 my @newextdesc;
4463                 foreach my $extdesc ( @extdesc ) {
4464                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4465                     push @newextdesc, $extdesc;
4466                 }
4467                 $line->{'ext_description'} = \@newextdesc;
4468                 $line->{'section'} = $newsection;
4469                 push @newlines, $line;
4470             }
4471         }
4472       }
4473
4474       return(\@newsections, \@newlines);
4475   }
4476
4477   return(\@sections, \@lines);
4478
4479 }
4480
4481 sub _items {
4482   my $self = shift;
4483
4484   #my @display = scalar(@_)
4485   #              ? @_
4486   #              : qw( _items_previous _items_pkg );
4487   #              #: qw( _items_pkg );
4488   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4489   my @display = qw( _items_previous _items_pkg );
4490
4491   my @b = ();
4492   foreach my $display ( @display ) {
4493     push @b, $self->$display(@_);
4494   }
4495   @b;
4496 }
4497
4498 sub _items_previous {
4499   my $self = shift;
4500   my $cust_main = $self->cust_main;
4501   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4502   my @b = ();
4503   foreach ( @pr_cust_bill ) {
4504     my $date = $conf->exists('invoice_show_prior_due_date')
4505                ? 'due '. $_->due_date2str($date_format)
4506                : time2str($date_format, $_->_date);
4507     push @b, {
4508       'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4509       #'pkgpart'     => 'N/A',
4510       'pkgnum'      => 'N/A',
4511       'amount'      => sprintf("%.2f", $_->owed),
4512     };
4513   }
4514   @b;
4515
4516   #{
4517   #    'description'     => 'Previous Balance',
4518   #    #'pkgpart'         => 'N/A',
4519   #    'pkgnum'          => 'N/A',
4520   #    'amount'          => sprintf("%10.2f", $pr_total ),
4521   #    'ext_description' => [ map {
4522   #                                 "Invoice ". $_->invnum.
4523   #                                 " (". time2str("%x",$_->_date). ") ".
4524   #                                 sprintf("%10.2f", $_->owed)
4525   #                         } @pr_cust_bill ],
4526
4527   #};
4528 }
4529
4530 sub _items_pkg {
4531   my $self = shift;
4532   my %options = @_;
4533
4534   warn "$me _items_pkg searching for all package line items\n"
4535     if $DEBUG > 1;
4536
4537   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4538
4539   warn "$me _items_pkg filtering line items\n"
4540     if $DEBUG > 1;
4541   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4542
4543   if ($options{section} && $options{section}->{condensed}) {
4544
4545     warn "$me _items_pkg condensing section\n"
4546       if $DEBUG > 1;
4547
4548     my %itemshash = ();
4549     local $Storable::canonical = 1;
4550     foreach ( @items ) {
4551       my $item = { %$_ };
4552       delete $item->{ref};
4553       delete $item->{ext_description};
4554       my $key = freeze($item);
4555       $itemshash{$key} ||= 0;
4556       $itemshash{$key} ++; # += $item->{quantity};
4557     }
4558     @items = sort { $a->{description} cmp $b->{description} }
4559              map { my $i = thaw($_);
4560                    $i->{quantity} = $itemshash{$_};
4561                    $i->{amount} =
4562                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4563                    $i;
4564                  }
4565              keys %itemshash;
4566   }
4567
4568   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4569     if $DEBUG > 1;
4570
4571   @items;
4572 }
4573
4574 sub _taxsort {
4575   return 0 unless $a->itemdesc cmp $b->itemdesc;
4576   return -1 if $b->itemdesc eq 'Tax';
4577   return 1 if $a->itemdesc eq 'Tax';
4578   return -1 if $b->itemdesc eq 'Other surcharges';
4579   return 1 if $a->itemdesc eq 'Other surcharges';
4580   $a->itemdesc cmp $b->itemdesc;
4581 }
4582
4583 sub _items_tax {
4584   my $self = shift;
4585   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4586   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4587 }
4588
4589 sub _items_cust_bill_pkg {
4590   my $self = shift;
4591   my $cust_bill_pkgs = shift;
4592   my %opt = @_;
4593
4594   my $format = $opt{format} || '';
4595   my $escape_function = $opt{escape_function} || sub { shift };
4596   my $format_function = $opt{format_function} || '';
4597   my $unsquelched = $opt{unsquelched} || '';
4598   my $section = $opt{section}->{description} if $opt{section};
4599   my $summary_page = $opt{summary_page} || '';
4600   my $multilocation = $opt{multilocation} || '';
4601   my $multisection = $opt{multisection} || '';
4602   my $discount_show_always = 0;
4603
4604   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4605
4606   my @b = ();
4607   my ($s, $r, $u) = ( undef, undef, undef );
4608   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4609   {
4610
4611     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4612          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4613       if $DEBUG > 1;
4614
4615     foreach my $display ( grep { defined($section)
4616                                  ? $_->section eq $section
4617                                  : 1
4618                                }
4619                           #grep { !$_->summary || !$summary_page } # bunk!
4620                           grep { !$_->summary || $multisection }
4621                           $cust_bill_pkg->cust_bill_pkg_display
4622                         )
4623     {
4624
4625       warn "$me _items_cust_bill_pkg considering display item $display\n"
4626         if $DEBUG > 1;
4627
4628       my $type = $display->type;
4629
4630       my $desc = $cust_bill_pkg->desc;
4631       $desc = substr($desc, 0, $maxlength). '...'
4632         if $format eq 'latex' && length($desc) > $maxlength;
4633
4634       my %details_opt = ( 'format'          => $format,
4635                           'escape_function' => $escape_function,
4636                           'format_function' => $format_function,
4637                         );
4638
4639       if ( $cust_bill_pkg->pkgnum > 0 ) {
4640
4641         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4642           if $DEBUG > 1;
4643  
4644         my $cust_pkg = $cust_bill_pkg->cust_pkg;
4645
4646         if (    (!$type || $type eq 'S')
4647              && (    $cust_bill_pkg->setup != 0
4648                   || $cust_bill_pkg->setup_show_zero
4649                 )
4650            )
4651          {
4652
4653           warn "$me _items_cust_bill_pkg adding setup\n"
4654             if $DEBUG > 1;
4655
4656           my $description = $desc;
4657           $description .= ' Setup'
4658             if $cust_bill_pkg->recur != 0
4659             || $discount_show_always
4660             || $cust_bill_pkg->recur_show_zero;
4661
4662           my @d = ();
4663           unless ( $cust_pkg->part_pkg->hide_svc_detail
4664                 || $cust_bill_pkg->hidden )
4665           {
4666
4667             push @d, map &{$escape_function}($_),
4668                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
4669               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4670
4671             if ( $multilocation ) {
4672               my $loc = $cust_pkg->location_label;
4673               $loc = substr($loc, 0, $maxlength). '...'
4674                 if $format eq 'latex' && length($loc) > $maxlength;
4675               push @d, &{$escape_function}($loc);
4676             }
4677
4678           }
4679
4680           push @d, $cust_bill_pkg->details(%details_opt)
4681             if $cust_bill_pkg->recur == 0;
4682
4683           if ( $cust_bill_pkg->hidden ) {
4684             $s->{amount}      += $cust_bill_pkg->setup;
4685             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4686             push @{ $s->{ext_description} }, @d;
4687           } else {
4688             $s = {
4689               _is_setup       => 1,
4690               description     => $description,
4691               #pkgpart         => $part_pkg->pkgpart,
4692               pkgnum          => $cust_bill_pkg->pkgnum,
4693               amount          => $cust_bill_pkg->setup,
4694               unit_amount     => $cust_bill_pkg->unitsetup,
4695               quantity        => $cust_bill_pkg->quantity,
4696               ext_description => \@d,
4697             };
4698           };
4699
4700         }
4701
4702         if (    ( !$type || $type eq 'R' || $type eq 'U' )
4703              && (
4704                      $cust_bill_pkg->recur != 0
4705                   || $cust_bill_pkg->setup == 0
4706                   || $discount_show_always
4707                   || $cust_bill_pkg->recur_show_zero
4708                 )
4709            )
4710         {
4711
4712           warn "$me _items_cust_bill_pkg adding recur/usage\n"
4713             if $DEBUG > 1;
4714
4715           my $is_summary = $display->summary;
4716           my $description = ($is_summary && $type && $type eq 'U')
4717                             ? "Usage charges" : $desc;
4718
4719           $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4720                           " - ". time2str($date_format, $cust_bill_pkg->edate).
4721                           ")"
4722             unless $conf->exists('disable_line_item_date_ranges')
4723                 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4724
4725           my @d = ();
4726
4727           #at least until cust_bill_pkg has "past" ranges in addition to
4728           #the "future" sdate/edate ones... see #3032
4729           my @dates = ( $self->_date );
4730           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4731           push @dates, $prev->sdate if $prev;
4732           push @dates, undef if !$prev;
4733
4734           unless ( $cust_pkg->part_pkg->hide_svc_detail
4735                 || $cust_bill_pkg->itemdesc
4736                 || $cust_bill_pkg->hidden
4737                 || $is_summary && $type && $type eq 'U' )
4738           {
4739
4740             warn "$me _items_cust_bill_pkg adding service details\n"
4741               if $DEBUG > 1;
4742
4743             push @d, map &{$escape_function}($_),
4744                          $cust_pkg->h_labels_short(@dates, 'I')
4745                                                    #$cust_bill_pkg->edate,
4746                                                    #$cust_bill_pkg->sdate)
4747               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4748
4749             warn "$me _items_cust_bill_pkg done adding service details\n"
4750               if $DEBUG > 1;
4751
4752             if ( $multilocation ) {
4753               my $loc = $cust_pkg->location_label;
4754               $loc = substr($loc, 0, $maxlength). '...'
4755                 if $format eq 'latex' && length($loc) > $maxlength;
4756               push @d, &{$escape_function}($loc);
4757             }
4758
4759           }
4760
4761           unless ( $is_summary ) {
4762             warn "$me _items_cust_bill_pkg adding details\n"
4763               if $DEBUG > 1;
4764
4765             #instead of omitting details entirely in this case (unwanted side
4766             # effects), just omit CDRs
4767             $details_opt{'format_function'} = sub { () }
4768               if $type && $type eq 'R';
4769
4770             push @d, $cust_bill_pkg->details(%details_opt);
4771           }
4772
4773           warn "$me _items_cust_bill_pkg calculating amount\n"
4774             if $DEBUG > 1;
4775   
4776           my $amount = 0;
4777           if (!$type) {
4778             $amount = $cust_bill_pkg->recur;
4779           } elsif ($type eq 'R') {
4780             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4781           } elsif ($type eq 'U') {
4782             $amount = $cust_bill_pkg->usage;
4783           }
4784   
4785           if ( !$type || $type eq 'R' ) {
4786
4787             warn "$me _items_cust_bill_pkg adding recur\n"
4788               if $DEBUG > 1;
4789
4790             if ( $cust_bill_pkg->hidden ) {
4791               $r->{amount}      += $amount;
4792               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4793               push @{ $r->{ext_description} }, @d;
4794             } else {
4795               $r = {
4796                 description     => $description,
4797                 #pkgpart         => $part_pkg->pkgpart,
4798                 pkgnum          => $cust_bill_pkg->pkgnum,
4799                 amount          => $amount,
4800                 unit_amount     => $cust_bill_pkg->unitrecur,
4801                 quantity        => $cust_bill_pkg->quantity,
4802                 ext_description => \@d,
4803               };
4804             }
4805
4806           } else {  # $type eq 'U'
4807
4808             warn "$me _items_cust_bill_pkg adding usage\n"
4809               if $DEBUG > 1;
4810
4811             if ( $cust_bill_pkg->hidden ) {
4812               $u->{amount}      += $amount;
4813               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4814               push @{ $u->{ext_description} }, @d;
4815             } else {
4816               $u = {
4817                 description     => $description,
4818                 #pkgpart         => $part_pkg->pkgpart,
4819                 pkgnum          => $cust_bill_pkg->pkgnum,
4820                 amount          => $amount,
4821                 unit_amount     => $cust_bill_pkg->unitrecur,
4822                 quantity        => $cust_bill_pkg->quantity,
4823                 ext_description => \@d,
4824               };
4825             }
4826           }
4827
4828         } # recurring or usage with recurring charge
4829
4830       } else { #pkgnum tax or one-shot line item (??)
4831
4832         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4833           if $DEBUG > 1;
4834
4835         if ( $cust_bill_pkg->setup != 0 ) {
4836           push @b, {
4837             'description' => $desc,
4838             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
4839           };
4840         }
4841         if ( $cust_bill_pkg->recur != 0 ) {
4842           push @b, {
4843             'description' => "$desc (".
4844                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4845                              time2str($date_format, $cust_bill_pkg->edate). ')',
4846             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
4847           };
4848         }
4849
4850       }
4851
4852     }
4853
4854     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4855                                 && $conf->exists('discount-show-always'));
4856
4857     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4858       if ( $_ && !$cust_bill_pkg->hidden ) {
4859         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4860         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4861         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4862         push @b, { %$_ }
4863           if $_->{amount} != 0
4864           || $discount_show_always
4865           || ( ! $_->{_is_setup} && $cust_bill_pkg->recur_show_zero )
4866           || (   $_->{_is_setup} && $cust_bill_pkg->setup_show_zero )
4867         ;
4868         $_ = undef;
4869       }
4870     }
4871
4872   }
4873
4874   #foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4875   #  if ( $_  ) {
4876   #    $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4877   #    $_->{amount}      =~ s/^\-0\.00$/0.00/;
4878   #    $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4879   #    push @b, { %$_ }
4880   #      if $_->{amount} != 0
4881   #      || $discount_show_always
4882   #  }
4883   #}
4884
4885   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4886     if $DEBUG > 1;
4887
4888   @b;
4889
4890 }
4891
4892 sub _items_credits {
4893   my( $self, %opt ) = @_;
4894   my $trim_len = $opt{'trim_len'} || 60;
4895
4896   my @b;
4897   #credits
4898   foreach ( $self->cust_credited ) {
4899
4900     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4901
4902     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4903     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4904     $reason = " ($reason) " if $reason;
4905
4906     push @b, {
4907       #'description' => 'Credit ref\#'. $_->crednum.
4908       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
4909       #                 $reason,
4910       'description' => 'Credit applied '.
4911                        time2str($date_format,$_->cust_credit->_date). $reason,
4912       'amount'      => sprintf("%.2f",$_->amount),
4913     };
4914   }
4915
4916   @b;
4917
4918 }
4919
4920 sub _items_payments {
4921   my $self = shift;
4922
4923   my @b;
4924   #get & print payments
4925   foreach ( $self->cust_bill_pay ) {
4926
4927     #something more elaborate if $_->amount ne ->cust_pay->paid ?
4928
4929     push @b, {
4930       'description' => "Payment received ".
4931                        time2str($date_format,$_->cust_pay->_date ),
4932       'amount'      => sprintf("%.2f", $_->amount )
4933     };
4934   }
4935
4936   @b;
4937
4938 }
4939
4940 =item call_details [ OPTION => VALUE ... ]
4941
4942 Returns an array of CSV strings representing the call details for this invoice
4943 The only option available is the boolean prepend_billed_number
4944
4945 =cut
4946
4947 sub call_details {
4948   my ($self, %opt) = @_;
4949
4950   my $format_function = sub { shift };
4951
4952   if ($opt{prepend_billed_number}) {
4953     $format_function = sub {
4954       my $detail = shift;
4955       my $row = shift;
4956
4957       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4958       
4959     };
4960   }
4961
4962   my @details = map { $_->details( 'format_function' => $format_function,
4963                                    'escape_function' => sub{ return() },
4964                                  )
4965                     }
4966                   grep { $_->pkgnum }
4967                   $self->cust_bill_pkg;
4968   my $header = $details[0];
4969   ( $header, grep { $_ ne $header } @details );
4970 }
4971
4972
4973 =back
4974
4975 =head1 SUBROUTINES
4976
4977 =over 4
4978
4979 =item process_reprint
4980
4981 =cut
4982
4983 sub process_reprint {
4984   process_re_X('print', @_);
4985 }
4986
4987 =item process_reemail
4988
4989 =cut
4990
4991 sub process_reemail {
4992   process_re_X('email', @_);
4993 }
4994
4995 =item process_refax
4996
4997 =cut
4998
4999 sub process_refax {
5000   process_re_X('fax', @_);
5001 }
5002
5003 =item process_reftp
5004
5005 =cut
5006
5007 sub process_reftp {
5008   process_re_X('ftp', @_);
5009 }
5010
5011 =item respool
5012
5013 =cut
5014
5015 sub process_respool {
5016   process_re_X('spool', @_);
5017 }
5018
5019 use Storable qw(thaw);
5020 use Data::Dumper;
5021 use MIME::Base64;
5022 sub process_re_X {
5023   my( $method, $job ) = ( shift, shift );
5024   warn "$me process_re_X $method for job $job\n" if $DEBUG;
5025
5026   my $param = thaw(decode_base64(shift));
5027   warn Dumper($param) if $DEBUG;
5028
5029   re_X(
5030     $method,
5031     $job,
5032     %$param,
5033   );
5034
5035 }
5036
5037 sub re_X {
5038   my($method, $job, %param ) = @_;
5039   if ( $DEBUG ) {
5040     warn "re_X $method for job $job with param:\n".
5041          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
5042   }
5043
5044   #some false laziness w/search/cust_bill.html
5045   my $distinct = '';
5046   my $orderby = 'ORDER BY cust_bill._date';
5047
5048   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5049
5050   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5051      
5052   my @cust_bill = qsearch( {
5053     #'select'    => "cust_bill.*",
5054     'table'     => 'cust_bill',
5055     'addl_from' => $addl_from,
5056     'hashref'   => {},
5057     'extra_sql' => $extra_sql,
5058     'order_by'  => $orderby,
5059     'debug' => 1,
5060   } );
5061
5062   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5063
5064   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5065     if $DEBUG;
5066
5067   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5068   foreach my $cust_bill ( @cust_bill ) {
5069     $cust_bill->$method();
5070
5071     if ( $job ) { #progressbar foo
5072       $num++;
5073       if ( time - $min_sec > $last ) {
5074         my $error = $job->update_statustext(
5075           int( 100 * $num / scalar(@cust_bill) )
5076         );
5077         die $error if $error;
5078         $last = time;
5079       }
5080     }
5081
5082   }
5083
5084 }
5085
5086 =back
5087
5088 =head1 CLASS METHODS
5089
5090 =over 4
5091
5092 =item owed_sql
5093
5094 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5095
5096 =cut
5097
5098 sub owed_sql {
5099   my ($class, $start, $end) = @_;
5100   'charged - '. 
5101     $class->paid_sql($start, $end). ' - '. 
5102     $class->credited_sql($start, $end);
5103 }
5104
5105 =item net_sql
5106
5107 Returns an SQL fragment to retreive the net amount (charged minus credited).
5108
5109 =cut
5110
5111 sub net_sql {
5112   my ($class, $start, $end) = @_;
5113   'charged - '. $class->credited_sql($start, $end);
5114 }
5115
5116 =item paid_sql
5117
5118 Returns an SQL fragment to retreive the amount paid against this invoice.
5119
5120 =cut
5121
5122 sub paid_sql {
5123   my ($class, $start, $end) = @_;
5124   $start &&= "AND cust_bill_pay._date <= $start";
5125   $end   &&= "AND cust_bill_pay._date > $end";
5126   $start = '' unless defined($start);
5127   $end   = '' unless defined($end);
5128   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5129        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
5130 }
5131
5132 =item credited_sql
5133
5134 Returns an SQL fragment to retreive the amount credited against this invoice.
5135
5136 =cut
5137
5138 sub credited_sql {
5139   my ($class, $start, $end) = @_;
5140   $start &&= "AND cust_credit_bill._date <= $start";
5141   $end   &&= "AND cust_credit_bill._date >  $end";
5142   $start = '' unless defined($start);
5143   $end   = '' unless defined($end);
5144   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5145        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
5146 }
5147
5148 =item due_date_sql
5149
5150 Returns an SQL fragment to retrieve the due date of an invoice.
5151 Currently only supported on PostgreSQL.
5152
5153 =cut
5154
5155 sub due_date_sql {
5156 'COALESCE(
5157   SUBSTRING(
5158     COALESCE(
5159       cust_bill.invoice_terms,
5160       cust_main.invoice_terms,
5161       \''.($conf->config('invoice_default_terms') || '').'\'
5162     ), E\'Net (\\\\d+)\'
5163   )::INTEGER, 0
5164 ) * 86400 + cust_bill._date'
5165 }
5166
5167 =item search_sql_where HASHREF
5168
5169 Class method which returns an SQL WHERE fragment to search for parameters
5170 specified in HASHREF.  Valid parameters are
5171
5172 =over 4
5173
5174 =item _date
5175
5176 List reference of start date, end date, as UNIX timestamps.
5177
5178 =item invnum_min
5179
5180 =item invnum_max
5181
5182 =item agentnum
5183
5184 =item charged
5185
5186 List reference of charged limits (exclusive).
5187
5188 =item owed
5189
5190 List reference of charged limits (exclusive).
5191
5192 =item open
5193
5194 flag, return open invoices only
5195
5196 =item net
5197
5198 flag, return net invoices only
5199
5200 =item days
5201
5202 =item newest_percust
5203
5204 =back
5205
5206 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5207
5208 =cut
5209
5210 sub search_sql_where {
5211   my($class, $param) = @_;
5212   if ( $DEBUG ) {
5213     warn "$me search_sql_where called with params: \n".
5214          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
5215   }
5216
5217   my @search = ();
5218
5219   #agentnum
5220   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5221     push @search, "cust_main.agentnum = $1";
5222   }
5223
5224   #_date
5225   if ( $param->{_date} ) {
5226     my($beginning, $ending) = @{$param->{_date}};
5227
5228     push @search, "cust_bill._date >= $beginning",
5229                   "cust_bill._date <  $ending";
5230   }
5231
5232   #invnum
5233   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5234     push @search, "cust_bill.invnum >= $1";
5235   }
5236   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5237     push @search, "cust_bill.invnum <= $1";
5238   }
5239
5240   #charged
5241   if ( $param->{charged} ) {
5242     my @charged = ref($param->{charged})
5243                     ? @{ $param->{charged} }
5244                     : ($param->{charged});
5245
5246     push @search, map { s/^charged/cust_bill.charged/; $_; }
5247                       @charged;
5248   }
5249
5250   my $owed_sql = FS::cust_bill->owed_sql;
5251
5252   #owed
5253   if ( $param->{owed} ) {
5254     my @owed = ref($param->{owed})
5255                  ? @{ $param->{owed} }
5256                  : ($param->{owed});
5257     push @search, map { s/^owed/$owed_sql/; $_; }
5258                       @owed;
5259   }
5260
5261   #open/net flags
5262   push @search, "0 != $owed_sql"
5263     if $param->{'open'};
5264   push @search, '0 != '. FS::cust_bill->net_sql
5265     if $param->{'net'};
5266
5267   #days
5268   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5269     if $param->{'days'};
5270
5271   #newest_percust
5272   if ( $param->{'newest_percust'} ) {
5273
5274     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5275     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5276
5277     my @newest_where = map { my $x = $_;
5278                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5279                              $x;
5280                            }
5281                            grep ! /^cust_main./, @search;
5282     my $newest_where = scalar(@newest_where)
5283                          ? ' AND '. join(' AND ', @newest_where)
5284                          : '';
5285
5286
5287     push @search, "cust_bill._date = (
5288       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5289         WHERE newest_cust_bill.custnum = cust_bill.custnum
5290           $newest_where
5291     )";
5292
5293   }
5294
5295   #agent virtualization
5296   my $curuser = $FS::CurrentUser::CurrentUser;
5297   if ( $curuser->username eq 'fs_queue'
5298        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5299     my $username = $1;
5300     my $newuser = qsearchs('access_user', {
5301       'username' => $username,
5302       'disabled' => '',
5303     } );
5304     if ( $newuser ) {
5305       $curuser = $newuser;
5306     } else {
5307       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5308     }
5309   }
5310   push @search, $curuser->agentnums_sql;
5311
5312   join(' AND ', @search );
5313
5314 }
5315
5316 =back
5317
5318 =head1 BUGS
5319
5320 The delete method.
5321
5322 =head1 SEE ALSO
5323
5324 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5325 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5326 documentation.
5327
5328 =cut
5329
5330 1;
5331