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