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