invoice batch download fix, #11871
[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         if !$multisection;
2862   }
2863
2864   foreach my $section (@sections, @$late_sections) {
2865
2866     warn "$me adding section \n". Dumper($section)
2867       if $DEBUG > 1;
2868
2869     # begin some normalization
2870     $section->{'subtotal'} = $section->{'amount'}
2871       if $multisection
2872          && !exists($section->{subtotal})
2873          && exists($section->{amount});
2874
2875     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2876       if ( $invoice_data{finance_section} &&
2877            $section->{'description'} eq $invoice_data{finance_section} );
2878
2879     $section->{'subtotal'} = $other_money_char.
2880                              sprintf('%.2f', $section->{'subtotal'})
2881       if $multisection;
2882
2883     # continue some normalization
2884     $section->{'amount'}   = $section->{'subtotal'}
2885       if $multisection;
2886
2887
2888     if ( $section->{'description'} ) {
2889       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2890                    [ '', '' ],
2891                  );
2892     }
2893
2894     warn "$me   setting options\n"
2895       if $DEBUG > 1;
2896
2897     my $multilocation = scalar($cust_main->cust_location); #too expensive?
2898     my %options = ();
2899     $options{'section'} = $section if $multisection;
2900     $options{'format'} = $format;
2901     $options{'escape_function'} = $escape_function;
2902     $options{'format_function'} = sub { () } unless $unsquelched;
2903     $options{'unsquelched'} = $unsquelched;
2904     $options{'summary_page'} = $summarypage;
2905     $options{'skip_usage'} =
2906       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2907     $options{'multilocation'} = $multilocation;
2908     $options{'multisection'} = $multisection;
2909
2910     warn "$me   searching for line items\n"
2911       if $DEBUG > 1;
2912
2913     foreach my $line_item ( $self->_items_pkg(%options) ) {
2914
2915       warn "$me     adding line item $line_item\n"
2916         if $DEBUG > 1;
2917
2918       my $detail = {
2919         ext_description => [],
2920       };
2921       $detail->{'ref'} = $line_item->{'pkgnum'};
2922       $detail->{'quantity'} = $line_item->{'quantity'};
2923       $detail->{'section'} = $section;
2924       $detail->{'description'} = &$escape_function($line_item->{'description'});
2925       if ( exists $line_item->{'ext_description'} ) {
2926         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2927       }
2928       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2929                               $line_item->{'amount'};
2930       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2931                                  $line_item->{'unit_amount'};
2932       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2933   
2934       push @detail_items, $detail;
2935       push @buf, ( [ $detail->{'description'},
2936                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2937                    ],
2938                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2939                  );
2940     }
2941
2942     if ( $section->{'description'} ) {
2943       push @buf, ( ['','-----------'],
2944                    [ $section->{'description'}. ' sub-total',
2945                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2946                    ],
2947                    [ '', '' ],
2948                    [ '', '' ],
2949                  );
2950     }
2951   
2952   }
2953   
2954   $invoice_data{current_less_finance} =
2955     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2956
2957   if ( $multisection && !$conf->exists('disable_previous_balance')
2958     || $conf->exists('previous_balance-summary_only') )
2959   {
2960     unshift @sections, $previous_section if $pr_total;
2961   }
2962
2963   warn "$me adding taxes\n"
2964     if $DEBUG > 1;
2965
2966   foreach my $tax ( $self->_items_tax ) {
2967
2968     $taxtotal += $tax->{'amount'};
2969
2970     my $description = &$escape_function( $tax->{'description'} );
2971     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2972
2973     if ( $multisection ) {
2974
2975       my $money = $old_latex ? '' : $money_char;
2976       push @detail_items, {
2977         ext_description => [],
2978         ref          => '',
2979         quantity     => '',
2980         description  => $description,
2981         amount       => $money. $amount,
2982         product_code => '',
2983         section      => $tax_section,
2984       };
2985
2986     } else {
2987
2988       push @total_items, {
2989         'total_item'   => $description,
2990         'total_amount' => $other_money_char. $amount,
2991       };
2992
2993     }
2994
2995     push @buf,[ $description,
2996                 $money_char. $amount,
2997               ];
2998
2999   }
3000   
3001   if ( $taxtotal ) {
3002     my $total = {};
3003     $total->{'total_item'} = 'Sub-total';
3004     $total->{'total_amount'} =
3005       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3006
3007     if ( $multisection ) {
3008       $tax_section->{'subtotal'} = $other_money_char.
3009                                    sprintf('%.2f', $taxtotal);
3010       $tax_section->{'pretotal'} = 'New charges sub-total '.
3011                                    $total->{'total_amount'};
3012       push @sections, $tax_section if $taxtotal;
3013     }else{
3014       unshift @total_items, $total;
3015     }
3016   }
3017   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3018
3019   push @buf,['','-----------'];
3020   push @buf,[( $conf->exists('disable_previous_balance') 
3021                ? 'Total Charges'
3022                : 'Total New Charges'
3023              ),
3024              $money_char. sprintf("%10.2f",$self->charged) ];
3025   push @buf,['',''];
3026
3027   {
3028     my $total = {};
3029     my $item = 'Total';
3030     $item = $conf->config('previous_balance-exclude_from_total')
3031          || 'Total New Charges'
3032       if $conf->exists('previous_balance-exclude_from_total');
3033     my $amount = $self->charged +
3034                    ( $conf->exists('disable_previous_balance') ||
3035                      $conf->exists('previous_balance-exclude_from_total')
3036                      ? 0
3037                      : $pr_total
3038                    );
3039     $total->{'total_item'} = &$embolden_function($item);
3040     $total->{'total_amount'} =
3041       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
3042     if ( $multisection ) {
3043       if ( $adjust_section->{'sort_weight'} ) {
3044         $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3045           sprintf("%.2f", ($self->billing_balance || 0) );
3046       } else {
3047         $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3048                                         sprintf('%.2f', $self->charged );
3049       } 
3050     }else{
3051       push @total_items, $total;
3052     }
3053     push @buf,['','-----------'];
3054     push @buf,[$item,
3055                $money_char.
3056                sprintf( '%10.2f', $amount )
3057               ];
3058     push @buf,['',''];
3059   }
3060   
3061   unless ( $conf->exists('disable_previous_balance') ) {
3062     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3063   
3064     # credits
3065     my $credittotal = 0;
3066     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3067
3068       my $total;
3069       $total->{'total_item'} = &$escape_function($credit->{'description'});
3070       $credittotal += $credit->{'amount'};
3071       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3072       $adjusttotal += $credit->{'amount'};
3073       if ( $multisection ) {
3074         my $money = $old_latex ? '' : $money_char;
3075         push @detail_items, {
3076           ext_description => [],
3077           ref          => '',
3078           quantity     => '',
3079           description  => &$escape_function($credit->{'description'}),
3080           amount       => $money. $credit->{'amount'},
3081           product_code => '',
3082           section      => $adjust_section,
3083         };
3084       } else {
3085         push @total_items, $total;
3086       }
3087
3088     }
3089     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3090
3091     #credits (again)
3092     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3093       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3094     }
3095
3096     # payments
3097     my $paymenttotal = 0;
3098     foreach my $payment ( $self->_items_payments ) {
3099       my $total = {};
3100       $total->{'total_item'} = &$escape_function($payment->{'description'});
3101       $paymenttotal += $payment->{'amount'};
3102       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3103       $adjusttotal += $payment->{'amount'};
3104       if ( $multisection ) {
3105         my $money = $old_latex ? '' : $money_char;
3106         push @detail_items, {
3107           ext_description => [],
3108           ref          => '',
3109           quantity     => '',
3110           description  => &$escape_function($payment->{'description'}),
3111           amount       => $money. $payment->{'amount'},
3112           product_code => '',
3113           section      => $adjust_section,
3114         };
3115       }else{
3116         push @total_items, $total;
3117       }
3118       push @buf, [ $payment->{'description'},
3119                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
3120                  ];
3121     }
3122     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3123   
3124     if ( $multisection ) {
3125       $adjust_section->{'subtotal'} = $other_money_char.
3126                                       sprintf('%.2f', $adjusttotal);
3127       push @sections, $adjust_section
3128         unless $adjust_section->{sort_weight};
3129     }
3130
3131     { 
3132       my $total;
3133       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3134       $total->{'total_amount'} =
3135         &$embolden_function(
3136           $other_money_char. sprintf('%.2f', $summarypage 
3137                                                ? $self->charged +
3138                                                  $self->billing_balance
3139                                                : $self->owed + $pr_total
3140                                     )
3141         );
3142       if ( $multisection && !$adjust_section->{sort_weight} ) {
3143         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3144                                          $total->{'total_amount'};
3145       }else{
3146         push @total_items, $total;
3147       }
3148       push @buf,['','-----------'];
3149       push @buf,[$self->balance_due_msg, $money_char. 
3150         sprintf("%10.2f", $balance_due ) ];
3151     }
3152
3153     if ( $conf->exists('previous_balance-show_credit')
3154         and $cust_main->balance < 0 ) {
3155       my $credit_total = {
3156         'total_item'    => &$embolden_function($self->credit_balance_msg),
3157         'total_amount'  => &$embolden_function(
3158           $other_money_char. sprintf('%.2f', -$cust_main->balance)
3159         ),
3160       };
3161       if ( $multisection ) {
3162         $adjust_section->{'posttotal'} .= $newline_token .
3163           $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3164       }
3165       else {
3166         push @total_items, $credit_total;
3167       }
3168       push @buf,['','-----------'];
3169       push @buf,[$self->credit_balance_msg, $money_char. 
3170         sprintf("%10.2f", -$cust_main->balance ) ];
3171     }
3172   }
3173
3174   if ( $multisection ) {
3175     if ($conf->exists('svc_phone_sections')) {
3176       my $total;
3177       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3178       $total->{'total_amount'} =
3179         &$embolden_function(
3180           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3181         );
3182       my $last_section = pop @sections;
3183       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3184                                      $total->{'total_amount'};
3185       push @sections, $last_section;
3186     }
3187     push @sections, @$late_sections
3188       if $unsquelched;
3189   }
3190
3191   my @includelist = ();
3192   push @includelist, 'summary' if $summarypage;
3193   foreach my $include ( @includelist ) {
3194
3195     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3196     my @inc_src;
3197
3198     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3199
3200       @inc_src = $conf->config($inc_file, $agentnum);
3201
3202     } else {
3203
3204       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3205
3206       my $convert_map = $convert_maps{$format}{$include};
3207
3208       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3209                        s/--\@\]/$delimiters{$format}[1]/g;
3210                        $_;
3211                      } 
3212                  &$convert_map( $conf->config($inc_file, $agentnum) );
3213
3214     }
3215
3216     my $inc_tt = new Text::Template (
3217       TYPE       => 'ARRAY',
3218       SOURCE     => [ map "$_\n", @inc_src ],
3219       DELIMITERS => $delimiters{$format},
3220     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3221
3222     unless ( $inc_tt->compile() ) {
3223       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3224       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3225       die $error;
3226     }
3227
3228     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3229
3230     $invoice_data{$include} =~ s/\n+$//
3231       if ($format eq 'latex');
3232   }
3233
3234   $invoice_lines = 0;
3235   my $wasfunc = 0;
3236   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3237     /invoice_lines\((\d*)\)/;
3238     $invoice_lines += $1 || scalar(@buf);
3239     $wasfunc=1;
3240   }
3241   die "no invoice_lines() functions in template?"
3242     if ( $format eq 'template' && !$wasfunc );
3243
3244   if ($format eq 'template') {
3245
3246     if ( $invoice_lines ) {
3247       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3248       $invoice_data{'total_pages'}++
3249         if scalar(@buf) % $invoice_lines;
3250     }
3251
3252     #setup subroutine for the template
3253     sub FS::cust_bill::_template::invoice_lines {
3254       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3255       map { 
3256         scalar(@FS::cust_bill::_template::buf)
3257           ? shift @FS::cust_bill::_template::buf
3258           : [ '', '' ];
3259       }
3260       ( 1 .. $lines );
3261     }
3262
3263     my $lines;
3264     my @collect;
3265     while (@buf) {
3266       push @collect, split("\n",
3267         $text_template->fill_in( HASH => \%invoice_data,
3268                                  PACKAGE => 'FS::cust_bill::_template'
3269                                )
3270       );
3271       $FS::cust_bill::_template::page++;
3272     }
3273     map "$_\n", @collect;
3274   }else{
3275     warn "filling in template for invoice ". $self->invnum. "\n"
3276       if $DEBUG;
3277     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3278       if $DEBUG > 1;
3279
3280     $text_template->fill_in(HASH => \%invoice_data);
3281   }
3282 }
3283
3284 # helper routine for generating date ranges
3285 sub _prior_month30s {
3286   my $self = shift;
3287   my @ranges = (
3288    [ 1,       2592000 ], # 0-30 days ago
3289    [ 2592000, 5184000 ], # 30-60 days ago
3290    [ 5184000, 7776000 ], # 60-90 days ago
3291    [ 7776000, 0       ], # 90+   days ago
3292   );
3293
3294   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3295           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3296       ] }
3297   @ranges;
3298 }
3299
3300 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3301
3302 Returns an postscript invoice, as a scalar.
3303
3304 Options can be passed as a hashref (recommended) or as a list of time, template
3305 and then any key/value pairs for any other options.
3306
3307 I<time> an optional value used to control the printing of overdue messages.  The
3308 default is now.  It isn't the date of the invoice; that's the `_date' field.
3309 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3310 L<Time::Local> and L<Date::Parse> for conversion functions.
3311
3312 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3313
3314 =cut
3315
3316 sub print_ps {
3317   my $self = shift;
3318
3319   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3320   my $ps = generate_ps($file);
3321   unlink($logofile);
3322   unlink($barcodefile) if $barcodefile;
3323
3324   $ps;
3325 }
3326
3327 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3328
3329 Returns an PDF invoice, as a scalar.
3330
3331 Options can be passed as a hashref (recommended) or as a list of time, template
3332 and then any key/value pairs for any other options.
3333
3334 I<time> an optional value used to control the printing of overdue messages.  The
3335 default is now.  It isn't the date of the invoice; that's the `_date' field.
3336 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3337 L<Time::Local> and L<Date::Parse> for conversion functions.
3338
3339 I<template>, if specified, is the name of a suffix for alternate invoices.
3340
3341 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3342
3343 =cut
3344
3345 sub print_pdf {
3346   my $self = shift;
3347
3348   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3349   my $pdf = generate_pdf($file);
3350   unlink($logofile);
3351   unlink($barcodefile) if $barcodefile;
3352
3353   $pdf;
3354 }
3355
3356 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3357
3358 Returns an HTML invoice, as a scalar.
3359
3360 I<time> an optional value used to control the printing of overdue messages.  The
3361 default is now.  It isn't the date of the invoice; that's the `_date' field.
3362 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3363 L<Time::Local> and L<Date::Parse> for conversion functions.
3364
3365 I<template>, if specified, is the name of a suffix for alternate invoices.
3366
3367 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3368
3369 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3370 when emailing the invoice as part of a multipart/related MIME email.
3371
3372 =cut
3373
3374 sub print_html {
3375   my $self = shift;
3376   my %params;
3377   if ( ref($_[0]) ) {
3378     %params = %{ shift() }; 
3379   }else{
3380     $params{'time'} = shift;
3381     $params{'template'} = shift;
3382     $params{'cid'} = shift;
3383   }
3384
3385   $params{'format'} = 'html';
3386   
3387   $self->print_generic( %params );
3388 }
3389
3390 # quick subroutine for print_latex
3391 #
3392 # There are ten characters that LaTeX treats as special characters, which
3393 # means that they do not simply typeset themselves: 
3394 #      # $ % & ~ _ ^ \ { }
3395 #
3396 # TeX ignores blanks following an escaped character; if you want a blank (as
3397 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3398
3399 sub _latex_escape {
3400   my $value = shift;
3401   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3402   $value =~ s/([<>])/\$$1\$/g;
3403   $value;
3404 }
3405
3406 sub _html_escape {
3407   my $value = shift;
3408   encode_entities($value);
3409   $value;
3410 }
3411
3412 sub _html_escape_nbsp {
3413   my $value = _html_escape(shift);
3414   $value =~ s/ +/&nbsp;/g;
3415   $value;
3416 }
3417
3418 #utility methods for print_*
3419
3420 sub _translate_old_latex_format {
3421   warn "_translate_old_latex_format called\n"
3422     if $DEBUG; 
3423
3424   my @template = ();
3425   while ( @_ ) {
3426     my $line = shift;
3427   
3428     if ( $line =~ /^%%Detail\s*$/ ) {
3429   
3430       push @template, q![@--!,
3431                       q!  foreach my $_tr_line (@detail_items) {!,
3432                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3433                       q!      $_tr_line->{'description'} .= !, 
3434                       q!        "\\tabularnewline\n~~".!,
3435                       q!        join( "\\tabularnewline\n~~",!,
3436                       q!          @{$_tr_line->{'ext_description'}}!,
3437                       q!        );!,
3438                       q!    }!;
3439
3440       while ( ( my $line_item_line = shift )
3441               !~ /^%%EndDetail\s*$/                            ) {
3442         $line_item_line =~ s/'/\\'/g;    # nice LTS
3443         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3444         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3445         push @template, "    \$OUT .= '$line_item_line';";
3446       }
3447
3448       push @template, '}',
3449                       '--@]';
3450       #' doh, gvim
3451     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3452
3453       push @template, '[@--',
3454                       '  foreach my $_tr_line (@total_items) {';
3455
3456       while ( ( my $total_item_line = shift )
3457               !~ /^%%EndTotalDetails\s*$/                      ) {
3458         $total_item_line =~ s/'/\\'/g;    # nice LTS
3459         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3460         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3461         push @template, "    \$OUT .= '$total_item_line';";
3462       }
3463
3464       push @template, '}',
3465                       '--@]';
3466
3467     } else {
3468       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3469       push @template, $line;  
3470     }
3471   
3472   }
3473
3474   if ($DEBUG) {
3475     warn "$_\n" foreach @template;
3476   }
3477
3478   (@template);
3479 }
3480
3481 sub terms {
3482   my $self = shift;
3483
3484   #check for an invoice-specific override
3485   return $self->invoice_terms if $self->invoice_terms;
3486   
3487   #check for a customer- specific override
3488   my $cust_main = $self->cust_main;
3489   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3490
3491   #use configured default
3492   $conf->config('invoice_default_terms') || '';
3493 }
3494
3495 sub due_date {
3496   my $self = shift;
3497   my $duedate = '';
3498   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3499     $duedate = $self->_date() + ( $1 * 86400 );
3500   }
3501   $duedate;
3502 }
3503
3504 sub due_date2str {
3505   my $self = shift;
3506   $self->due_date ? time2str(shift, $self->due_date) : '';
3507 }
3508
3509 sub balance_due_msg {
3510   my $self = shift;
3511   my $msg = 'Balance Due';
3512   return $msg unless $self->terms;
3513   if ( $self->due_date ) {
3514     $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3515   } elsif ( $self->terms ) {
3516     $msg .= ' - '. $self->terms;
3517   }
3518   $msg;
3519 }
3520
3521 sub balance_due_date {
3522   my $self = shift;
3523   my $duedate = '';
3524   if (    $conf->exists('invoice_default_terms') 
3525        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3526     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3527   }
3528   $duedate;
3529 }
3530
3531 sub credit_balance_msg { 'Credit Balance Remaining' }
3532
3533 =item invnum_date_pretty
3534
3535 Returns a string with the invoice number and date, for example:
3536 "Invoice #54 (3/20/2008)"
3537
3538 =cut
3539
3540 sub invnum_date_pretty {
3541   my $self = shift;
3542   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3543 }
3544
3545 =item _date_pretty
3546
3547 Returns a string with the date, for example: "3/20/2008"
3548
3549 =cut
3550
3551 sub _date_pretty {
3552   my $self = shift;
3553   time2str($date_format, $self->_date);
3554 }
3555
3556 use vars qw(%pkg_category_cache);
3557 sub _items_sections {
3558   my $self = shift;
3559   my $late = shift;
3560   my $summarypage = shift;
3561   my $escape = shift;
3562   my $extra_sections = shift;
3563   my $format = shift;
3564
3565   my %subtotal = ();
3566   my %late_subtotal = ();
3567   my %not_tax = ();
3568
3569   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3570   {
3571
3572       my $usage = $cust_bill_pkg->usage;
3573
3574       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3575         next if ( $display->summary && $summarypage );
3576
3577         my $section = $display->section;
3578         my $type    = $display->type;
3579
3580         $not_tax{$section} = 1
3581           unless $cust_bill_pkg->pkgnum == 0;
3582
3583         if ( $display->post_total && !$summarypage ) {
3584           if (! $type || $type eq 'S') {
3585             $late_subtotal{$section} += $cust_bill_pkg->setup
3586               if $cust_bill_pkg->setup != 0;
3587           }
3588
3589           if (! $type) {
3590             $late_subtotal{$section} += $cust_bill_pkg->recur
3591               if $cust_bill_pkg->recur != 0;
3592           }
3593
3594           if ($type && $type eq 'R') {
3595             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3596               if $cust_bill_pkg->recur != 0;
3597           }
3598           
3599           if ($type && $type eq 'U') {
3600             $late_subtotal{$section} += $usage
3601               unless scalar(@$extra_sections);
3602           }
3603
3604         } else {
3605
3606           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3607
3608           if (! $type || $type eq 'S') {
3609             $subtotal{$section} += $cust_bill_pkg->setup
3610               if $cust_bill_pkg->setup != 0;
3611           }
3612
3613           if (! $type) {
3614             $subtotal{$section} += $cust_bill_pkg->recur
3615               if $cust_bill_pkg->recur != 0;
3616           }
3617
3618           if ($type && $type eq 'R') {
3619             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3620               if $cust_bill_pkg->recur != 0;
3621           }
3622           
3623           if ($type && $type eq 'U') {
3624             $subtotal{$section} += $usage
3625               unless scalar(@$extra_sections);
3626           }
3627
3628         }
3629
3630       }
3631
3632   }
3633
3634   %pkg_category_cache = ();
3635
3636   push @$late, map { { 'description' => &{$escape}($_),
3637                        'subtotal'    => $late_subtotal{$_},
3638                        'post_total'  => 1,
3639                        'sort_weight' => ( _pkg_category($_)
3640                                             ? _pkg_category($_)->weight
3641                                             : 0
3642                                        ),
3643                        ((_pkg_category($_) && _pkg_category($_)->condense)
3644                                            ? $self->_condense_section($format)
3645                                            : ()
3646                        ),
3647                    } }
3648                  sort _sectionsort keys %late_subtotal;
3649
3650   my @sections;
3651   if ( $summarypage ) {
3652     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3653                 map { $_->categoryname } qsearch('pkg_category', {});
3654     push @sections, '' if exists($subtotal{''});
3655   } else {
3656     @sections = keys %subtotal;
3657   }
3658
3659   my @early = map { { 'description' => &{$escape}($_),
3660                       'subtotal'    => $subtotal{$_},
3661                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3662                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3663                       'sort_weight' => ( _pkg_category($_)
3664                                            ? _pkg_category($_)->weight
3665                                            : 0
3666                                        ),
3667                        ((_pkg_category($_) && _pkg_category($_)->condense)
3668                                            ? $self->_condense_section($format)
3669                                            : ()
3670                        ),
3671                     }
3672                   } @sections;
3673   push @early, @$extra_sections if $extra_sections;
3674
3675   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3676
3677 }
3678
3679 #helper subs for above
3680
3681 sub _sectionsort {
3682   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3683 }
3684
3685 sub _pkg_category {
3686   my $categoryname = shift;
3687   $pkg_category_cache{$categoryname} ||=
3688     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3689 }
3690
3691 my %condensed_format = (
3692   'label' => [ qw( Description Qty Amount ) ],
3693   'fields' => [
3694                 sub { shift->{description} },
3695                 sub { shift->{quantity} },
3696                 sub { my($href, %opt) = @_;
3697                       ($opt{dollar} || ''). $href->{amount};
3698                     },
3699               ],
3700   'align'  => [ qw( l r r ) ],
3701   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3702   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3703 );
3704
3705 sub _condense_section {
3706   my ( $self, $format ) = ( shift, shift );
3707   ( 'condensed' => 1,
3708     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3709       qw( description_generator
3710           header_generator
3711           total_generator
3712           total_line_generator
3713         )
3714   );
3715 }
3716
3717 sub _condensed_generator_defaults {
3718   my ( $self, $format ) = ( shift, shift );
3719   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3720 }
3721
3722 my %html_align = (
3723   'c' => 'center',
3724   'l' => 'left',
3725   'r' => 'right',
3726 );
3727
3728 sub _condensed_header_generator {
3729   my ( $self, $format ) = ( shift, shift );
3730
3731   my ( $f, $prefix, $suffix, $separator, $column ) =
3732     _condensed_generator_defaults($format);
3733
3734   if ($format eq 'latex') {
3735     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3736     $suffix = "\\\\\n\\hline";
3737     $separator = "&\n";
3738     $column =
3739       sub { my ($d,$a,$s,$w) = @_;
3740             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3741           };
3742   } elsif ( $format eq 'html' ) {
3743     $prefix = '<th></th>';
3744     $suffix = '';
3745     $separator = '';
3746     $column =
3747       sub { my ($d,$a,$s,$w) = @_;
3748             return qq!<th align="$html_align{$a}">$d</th>!;
3749       };
3750   }
3751
3752   sub {
3753     my @args = @_;
3754     my @result = ();
3755
3756     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3757       push @result,
3758         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3759     }
3760
3761     $prefix. join($separator, @result). $suffix;
3762   };
3763
3764 }
3765
3766 sub _condensed_description_generator {
3767   my ( $self, $format ) = ( shift, shift );
3768
3769   my ( $f, $prefix, $suffix, $separator, $column ) =
3770     _condensed_generator_defaults($format);
3771
3772   my $money_char = '$';
3773   if ($format eq 'latex') {
3774     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3775     $suffix = '\\\\';
3776     $separator = " & \n";
3777     $column =
3778       sub { my ($d,$a,$s,$w) = @_;
3779             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3780           };
3781     $money_char = '\\dollar';
3782   }elsif ( $format eq 'html' ) {
3783     $prefix = '"><td align="center"></td>';
3784     $suffix = '';
3785     $separator = '';
3786     $column =
3787       sub { my ($d,$a,$s,$w) = @_;
3788             return qq!<td align="$html_align{$a}">$d</td>!;
3789       };
3790     #$money_char = $conf->config('money_char') || '$';
3791     $money_char = '';  # this is madness
3792   }
3793
3794   sub {
3795     #my @args = @_;
3796     my $href = shift;
3797     my @result = ();
3798
3799     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3800       my $dollar = '';
3801       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3802       push @result,
3803         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3804                     map { $f->{$_}->[$i] } qw(align span width)
3805                   );
3806     }
3807
3808     $prefix. join( $separator, @result ). $suffix;
3809   };
3810
3811 }
3812
3813 sub _condensed_total_generator {
3814   my ( $self, $format ) = ( shift, shift );
3815
3816   my ( $f, $prefix, $suffix, $separator, $column ) =
3817     _condensed_generator_defaults($format);
3818   my $style = '';
3819
3820   if ($format eq 'latex') {
3821     $prefix = "& ";
3822     $suffix = "\\\\\n";
3823     $separator = " & \n";
3824     $column =
3825       sub { my ($d,$a,$s,$w) = @_;
3826             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3827           };
3828   }elsif ( $format eq 'html' ) {
3829     $prefix = '';
3830     $suffix = '';
3831     $separator = '';
3832     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3833     $column =
3834       sub { my ($d,$a,$s,$w) = @_;
3835             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3836       };
3837   }
3838
3839
3840   sub {
3841     my @args = @_;
3842     my @result = ();
3843
3844     #  my $r = &{$f->{fields}->[$i]}(@args);
3845     #  $r .= ' Total' unless $i;
3846
3847     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3848       push @result,
3849         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3850                     map { $f->{$_}->[$i] } qw(align span width)
3851                   );
3852     }
3853
3854     $prefix. join( $separator, @result ). $suffix;
3855   };
3856
3857 }
3858
3859 =item total_line_generator FORMAT
3860
3861 Returns a coderef used for generation of invoice total line items for this
3862 usage_class.  FORMAT is either html or latex
3863
3864 =cut
3865
3866 # should not be used: will have issues with hash element names (description vs
3867 # total_item and amount vs total_amount -- another array of functions?
3868
3869 sub _condensed_total_line_generator {
3870   my ( $self, $format ) = ( shift, shift );
3871
3872   my ( $f, $prefix, $suffix, $separator, $column ) =
3873     _condensed_generator_defaults($format);
3874   my $style = '';
3875
3876   if ($format eq 'latex') {
3877     $prefix = "& ";
3878     $suffix = "\\\\\n";
3879     $separator = " & \n";
3880     $column =
3881       sub { my ($d,$a,$s,$w) = @_;
3882             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3883           };
3884   }elsif ( $format eq 'html' ) {
3885     $prefix = '';
3886     $suffix = '';
3887     $separator = '';
3888     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3889     $column =
3890       sub { my ($d,$a,$s,$w) = @_;
3891             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3892       };
3893   }
3894
3895
3896   sub {
3897     my @args = @_;
3898     my @result = ();
3899
3900     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3901       push @result,
3902         &{$column}( &{$f->{fields}->[$i]}(@args),
3903                     map { $f->{$_}->[$i] } qw(align span width)
3904                   );
3905     }
3906
3907     $prefix. join( $separator, @result ). $suffix;
3908   };
3909
3910 }
3911
3912 #sub _items_extra_usage_sections {
3913 #  my $self = shift;
3914 #  my $escape = shift;
3915 #
3916 #  my %sections = ();
3917 #
3918 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
3919 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3920 #  {
3921 #    next unless $cust_bill_pkg->pkgnum > 0;
3922 #
3923 #    foreach my $section ( keys %usage_class ) {
3924 #
3925 #      my $usage = $cust_bill_pkg->usage($section);
3926 #
3927 #      next unless $usage && $usage > 0;
3928 #
3929 #      $sections{$section} ||= 0;
3930 #      $sections{$section} += $usage;
3931 #
3932 #    }
3933 #
3934 #  }
3935 #
3936 #  map { { 'description' => &{$escape}($_),
3937 #          'subtotal'    => $sections{$_},
3938 #          'summarized'  => '',
3939 #          'tax_section' => '',
3940 #        }
3941 #      }
3942 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3943 #
3944 #}
3945
3946 sub _items_extra_usage_sections {
3947   my $self = shift;
3948   my $escape = shift;
3949   my $format = shift;
3950
3951   my %sections = ();
3952   my %classnums = ();
3953   my %lines = ();
3954
3955   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3956   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3957     next unless $cust_bill_pkg->pkgnum > 0;
3958
3959     foreach my $classnum ( keys %usage_class ) {
3960       my $section = $usage_class{$classnum}->classname;
3961       $classnums{$section} = $classnum;
3962
3963       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3964         my $amount = $detail->amount;
3965         next unless $amount && $amount > 0;
3966  
3967         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3968         $sections{$section}{amount} += $amount;  #subtotal
3969         $sections{$section}{calls}++;
3970         $sections{$section}{duration} += $detail->duration;
3971
3972         my $desc = $detail->regionname; 
3973         my $description = $desc;
3974         $description = substr($desc, 0, 50). '...'
3975           if $format eq 'latex' && length($desc) > 50;
3976
3977         $lines{$section}{$desc} ||= {
3978           description     => &{$escape}($description),
3979           #pkgpart         => $part_pkg->pkgpart,
3980           pkgnum          => $cust_bill_pkg->pkgnum,
3981           ref             => '',
3982           amount          => 0,
3983           calls           => 0,
3984           duration        => 0,
3985           #unit_amount     => $cust_bill_pkg->unitrecur,
3986           quantity        => $cust_bill_pkg->quantity,
3987           product_code    => 'N/A',
3988           ext_description => [],
3989         };
3990
3991         $lines{$section}{$desc}{amount} += $amount;
3992         $lines{$section}{$desc}{calls}++;
3993         $lines{$section}{$desc}{duration} += $detail->duration;
3994
3995       }
3996     }
3997   }
3998
3999   my %sectionmap = ();
4000   foreach (keys %sections) {
4001     my $usage_class = $usage_class{$classnums{$_}};
4002     $sectionmap{$_} = { 'description' => &{$escape}($_),
4003                         'amount'    => $sections{$_}{amount},    #subtotal
4004                         'calls'       => $sections{$_}{calls},
4005                         'duration'    => $sections{$_}{duration},
4006                         'summarized'  => '',
4007                         'tax_section' => '',
4008                         'sort_weight' => $usage_class->weight,
4009                         ( $usage_class->format
4010                           ? ( map { $_ => $usage_class->$_($format) }
4011                               qw( description_generator header_generator total_generator total_line_generator )
4012                             )
4013                           : ()
4014                         ), 
4015                       };
4016   }
4017
4018   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4019                  values %sectionmap;
4020
4021   my @lines = ();
4022   foreach my $section ( keys %lines ) {
4023     foreach my $line ( keys %{$lines{$section}} ) {
4024       my $l = $lines{$section}{$line};
4025       $l->{section}     = $sectionmap{$section};
4026       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4027       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4028       push @lines, $l;
4029     }
4030   }
4031
4032   return(\@sections, \@lines);
4033
4034 }
4035
4036 sub _did_summary {
4037     my $self = shift;
4038     my $end = $self->_date;
4039
4040     # start at date of previous invoice + 1 second or 0 if no previous invoice
4041     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4042     $start = 0 if !$start;
4043     $start++;
4044
4045     my $cust_main = $self->cust_main;
4046     my @pkgs = $cust_main->all_pkgs;
4047     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4048         = (0,0,0,0,0);
4049     my @seen = ();
4050     foreach my $pkg ( @pkgs ) {
4051         my @h_cust_svc = $pkg->h_cust_svc($end);
4052         foreach my $h_cust_svc ( @h_cust_svc ) {
4053             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4054             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4055
4056             my $inserted = $h_cust_svc->date_inserted;
4057             my $deleted = $h_cust_svc->date_deleted;
4058             my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4059             my $phone_deleted;
4060             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
4061             
4062 # DID either activated or ported in; cannot be both for same DID simultaneously
4063             if ($inserted >= $start && $inserted <= $end && $phone_inserted
4064                 && (!$phone_inserted->lnp_status 
4065                     || $phone_inserted->lnp_status eq ''
4066                     || $phone_inserted->lnp_status eq 'native')) {
4067                 $num_activated++;
4068             }
4069             else { # this one not so clean, should probably move to (h_)svc_phone
4070                  my $phone_portedin = qsearchs( 'h_svc_phone',
4071                       { 'svcnum' => $h_cust_svc->svcnum, 
4072                         'lnp_status' => 'portedin' },  
4073                       FS::h_svc_phone->sql_h_searchs($end),  
4074                     );
4075                  $num_portedin++ if $phone_portedin;
4076             }
4077
4078 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4079             if($deleted >= $start && $deleted <= $end && $phone_deleted
4080                 && (!$phone_deleted->lnp_status 
4081                     || $phone_deleted->lnp_status ne 'portingout')) {
4082                 $num_deactivated++;
4083             } 
4084             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
4085                 && $phone_deleted->lnp_status 
4086                 && $phone_deleted->lnp_status eq 'portingout') {
4087                 $num_portedout++;
4088             }
4089
4090             # increment usage minutes
4091             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4092             foreach my $cdr ( @cdrs ) {
4093                 $minutes += $cdr->billsec/60;
4094             }
4095
4096             # don't look at this service again
4097             push @seen, $h_cust_svc->svcnum;
4098         }
4099     }
4100
4101     $minutes = sprintf("%d", $minutes);
4102     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
4103         . "$num_deactivated  Ported-Out: $num_portedout ",
4104             "Total Minutes: $minutes");
4105 }
4106
4107 sub _items_svc_phone_sections {
4108   my $self = shift;
4109   my $escape = shift;
4110   my $format = shift;
4111
4112   my %sections = ();
4113   my %classnums = ();
4114   my %lines = ();
4115
4116   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4117   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4118
4119   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4120     next unless $cust_bill_pkg->pkgnum > 0;
4121
4122     my @header = $cust_bill_pkg->details_header;
4123     next unless scalar(@header);
4124
4125     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4126
4127       my $phonenum = $detail->phonenum;
4128       next unless $phonenum;
4129
4130       my $amount = $detail->amount;
4131       next unless $amount && $amount > 0;
4132
4133       $sections{$phonenum} ||= { 'amount'      => 0,
4134                                  'calls'       => 0,
4135                                  'duration'    => 0,
4136                                  'sort_weight' => -1,
4137                                  'phonenum'    => $phonenum,
4138                                 };
4139       $sections{$phonenum}{amount} += $amount;  #subtotal
4140       $sections{$phonenum}{calls}++;
4141       $sections{$phonenum}{duration} += $detail->duration;
4142
4143       my $desc = $detail->regionname; 
4144       my $description = $desc;
4145       $description = substr($desc, 0, 50). '...'
4146         if $format eq 'latex' && length($desc) > 50;
4147
4148       $lines{$phonenum}{$desc} ||= {
4149         description     => &{$escape}($description),
4150         #pkgpart         => $part_pkg->pkgpart,
4151         pkgnum          => '',
4152         ref             => '',
4153         amount          => 0,
4154         calls           => 0,
4155         duration        => 0,
4156         #unit_amount     => '',
4157         quantity        => '',
4158         product_code    => 'N/A',
4159         ext_description => [],
4160       };
4161
4162       $lines{$phonenum}{$desc}{amount} += $amount;
4163       $lines{$phonenum}{$desc}{calls}++;
4164       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4165
4166       my $line = $usage_class{$detail->classnum}->classname;
4167       $sections{"$phonenum $line"} ||=
4168         { 'amount' => 0,
4169           'calls' => 0,
4170           'duration' => 0,
4171           'sort_weight' => $usage_class{$detail->classnum}->weight,
4172           'phonenum' => $phonenum,
4173           'header'  => [ @header ],
4174         };
4175       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4176       $sections{"$phonenum $line"}{calls}++;
4177       $sections{"$phonenum $line"}{duration} += $detail->duration;
4178
4179       $lines{"$phonenum $line"}{$desc} ||= {
4180         description     => &{$escape}($description),
4181         #pkgpart         => $part_pkg->pkgpart,
4182         pkgnum          => '',
4183         ref             => '',
4184         amount          => 0,
4185         calls           => 0,
4186         duration        => 0,
4187         #unit_amount     => '',
4188         quantity        => '',
4189         product_code    => 'N/A',
4190         ext_description => [],
4191       };
4192
4193       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4194       $lines{"$phonenum $line"}{$desc}{calls}++;
4195       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4196       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4197            $detail->formatted('format' => $format);
4198
4199     }
4200   }
4201
4202   my %sectionmap = ();
4203   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4204   foreach ( keys %sections ) {
4205     my @header = @{ $sections{$_}{header} || [] };
4206     my $usage_simple =
4207       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4208     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4209     my $usage_class = $summary ? $simple : $usage_simple;
4210     my $ending = $summary ? ' usage charges' : '';
4211     my %gen_opt = ();
4212     unless ($summary) {
4213       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4214     }
4215     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4216                         'amount'    => $sections{$_}{amount},    #subtotal
4217                         'calls'       => $sections{$_}{calls},
4218                         'duration'    => $sections{$_}{duration},
4219                         'summarized'  => '',
4220                         'tax_section' => '',
4221                         'phonenum'    => $sections{$_}{phonenum},
4222                         'sort_weight' => $sections{$_}{sort_weight},
4223                         'post_total'  => $summary, #inspire pagebreak
4224                         (
4225                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4226                             qw( description_generator
4227                                 header_generator
4228                                 total_generator
4229                                 total_line_generator
4230                               )
4231                           )
4232                         ), 
4233                       };
4234   }
4235
4236   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4237                         $a->{sort_weight} <=> $b->{sort_weight}
4238                       }
4239                  values %sectionmap;
4240
4241   my @lines = ();
4242   foreach my $section ( keys %lines ) {
4243     foreach my $line ( keys %{$lines{$section}} ) {
4244       my $l = $lines{$section}{$line};
4245       $l->{section}     = $sectionmap{$section};
4246       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4247       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4248       push @lines, $l;
4249     }
4250   }
4251   
4252   if($conf->exists('phone_usage_class_summary')) { 
4253       # this only works with Latex
4254       my @newlines;
4255       my @newsections;
4256
4257       # after this, we'll have only two sections per DID:
4258       # Calls Summary and Calls Detail
4259       foreach my $section ( @sections ) {
4260         if($section->{'post_total'}) {
4261             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4262             $section->{'total_line_generator'} = sub { '' };
4263             $section->{'total_generator'} = sub { '' };
4264             $section->{'header_generator'} = sub { '' };
4265             $section->{'description_generator'} = '';
4266             push @newsections, $section;
4267             my %calls_detail = %$section;
4268             $calls_detail{'post_total'} = '';
4269             $calls_detail{'sort_weight'} = '';
4270             $calls_detail{'description_generator'} = sub { '' };
4271             $calls_detail{'header_generator'} = sub {
4272                 return ' & Date/Time & Called Number & Duration & Price'
4273                     if $format eq 'latex';
4274                 '';
4275             };
4276             $calls_detail{'description'} = 'Calls Detail: '
4277                                                     . $section->{'phonenum'};
4278             push @newsections, \%calls_detail;  
4279         }
4280       }
4281
4282       # after this, each usage class is collapsed/summarized into a single
4283       # line under the Calls Summary section
4284       foreach my $newsection ( @newsections ) {
4285         if($newsection->{'post_total'}) { # this means Calls Summary
4286             foreach my $section ( @sections ) {
4287                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
4288                                 && !$section->{'post_total'});
4289                 my $newdesc = $section->{'description'};
4290                 my $tn = $section->{'phonenum'};
4291                 $newdesc =~ s/$tn//g;
4292                 my $line = {  ext_description => [],
4293                               pkgnum => '',
4294                               ref => '',
4295                               quantity => '',
4296                               calls => $section->{'calls'},
4297                               section => $newsection,
4298                               duration => $section->{'duration'},
4299                               description => $newdesc,
4300                               amount => sprintf("%.2f",$section->{'amount'}),
4301                               product_code => 'N/A',
4302                             };
4303                 push @newlines, $line;
4304             }
4305         }
4306       }
4307
4308       # after this, Calls Details is populated with all CDRs
4309       foreach my $newsection ( @newsections ) {
4310         if(!$newsection->{'post_total'}) { # this means Calls Details
4311             foreach my $line ( @lines ) {
4312                 next unless (scalar(@{$line->{'ext_description'}}) &&
4313                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4314                             );
4315                 my @extdesc = @{$line->{'ext_description'}};
4316                 my @newextdesc;
4317                 foreach my $extdesc ( @extdesc ) {
4318                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4319                     push @newextdesc, $extdesc;
4320                 }
4321                 $line->{'ext_description'} = \@newextdesc;
4322                 $line->{'section'} = $newsection;
4323                 push @newlines, $line;
4324             }
4325         }
4326       }
4327
4328       return(\@newsections, \@newlines);
4329   }
4330
4331   return(\@sections, \@lines);
4332
4333 }
4334
4335 sub _items {
4336   my $self = shift;
4337
4338   #my @display = scalar(@_)
4339   #              ? @_
4340   #              : qw( _items_previous _items_pkg );
4341   #              #: qw( _items_pkg );
4342   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4343   my @display = qw( _items_previous _items_pkg );
4344
4345   my @b = ();
4346   foreach my $display ( @display ) {
4347     push @b, $self->$display(@_);
4348   }
4349   @b;
4350 }
4351
4352 sub _items_previous {
4353   my $self = shift;
4354   my $cust_main = $self->cust_main;
4355   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4356   my @b = ();
4357   foreach ( @pr_cust_bill ) {
4358     my $date = $conf->exists('invoice_show_prior_due_date')
4359                ? 'due '. $_->due_date2str($date_format)
4360                : time2str($date_format, $_->_date);
4361     push @b, {
4362       'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4363       #'pkgpart'     => 'N/A',
4364       'pkgnum'      => 'N/A',
4365       'amount'      => sprintf("%.2f", $_->owed),
4366     };
4367   }
4368   @b;
4369
4370   #{
4371   #    'description'     => 'Previous Balance',
4372   #    #'pkgpart'         => 'N/A',
4373   #    'pkgnum'          => 'N/A',
4374   #    'amount'          => sprintf("%10.2f", $pr_total ),
4375   #    'ext_description' => [ map {
4376   #                                 "Invoice ". $_->invnum.
4377   #                                 " (". time2str("%x",$_->_date). ") ".
4378   #                                 sprintf("%10.2f", $_->owed)
4379   #                         } @pr_cust_bill ],
4380
4381   #};
4382 }
4383
4384 sub _items_pkg {
4385   my $self = shift;
4386   my %options = @_;
4387
4388   warn "$me _items_pkg searching for all package line items\n"
4389     if $DEBUG > 1;
4390
4391   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4392
4393   warn "$me _items_pkg filtering line items\n"
4394     if $DEBUG > 1;
4395   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4396
4397   if ($options{section} && $options{section}->{condensed}) {
4398
4399     warn "$me _items_pkg condensing section\n"
4400       if $DEBUG > 1;
4401
4402     my %itemshash = ();
4403     local $Storable::canonical = 1;
4404     foreach ( @items ) {
4405       my $item = { %$_ };
4406       delete $item->{ref};
4407       delete $item->{ext_description};
4408       my $key = freeze($item);
4409       $itemshash{$key} ||= 0;
4410       $itemshash{$key} ++; # += $item->{quantity};
4411     }
4412     @items = sort { $a->{description} cmp $b->{description} }
4413              map { my $i = thaw($_);
4414                    $i->{quantity} = $itemshash{$_};
4415                    $i->{amount} =
4416                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4417                    $i;
4418                  }
4419              keys %itemshash;
4420   }
4421
4422   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4423     if $DEBUG > 1;
4424
4425   @items;
4426 }
4427
4428 sub _taxsort {
4429   return 0 unless $a->itemdesc cmp $b->itemdesc;
4430   return -1 if $b->itemdesc eq 'Tax';
4431   return 1 if $a->itemdesc eq 'Tax';
4432   return -1 if $b->itemdesc eq 'Other surcharges';
4433   return 1 if $a->itemdesc eq 'Other surcharges';
4434   $a->itemdesc cmp $b->itemdesc;
4435 }
4436
4437 sub _items_tax {
4438   my $self = shift;
4439   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4440   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4441 }
4442
4443 sub _items_cust_bill_pkg {
4444   my $self = shift;
4445   my $cust_bill_pkgs = shift;
4446   my %opt = @_;
4447
4448   my $format = $opt{format} || '';
4449   my $escape_function = $opt{escape_function} || sub { shift };
4450   my $format_function = $opt{format_function} || '';
4451   my $unsquelched = $opt{unsquelched} || '';
4452   my $section = $opt{section}->{description} if $opt{section};
4453   my $summary_page = $opt{summary_page} || '';
4454   my $multilocation = $opt{multilocation} || '';
4455   my $multisection = $opt{multisection} || '';
4456   my $discount_show_always = 0;
4457
4458   my @b = ();
4459   my ($s, $r, $u) = ( undef, undef, undef );
4460   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4461   {
4462
4463     warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4464       if $DEBUG > 1;
4465
4466     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4467                                 && $conf->exists('discount-show-always'));
4468
4469     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4470       if ( $_ && !$cust_bill_pkg->hidden ) {
4471         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4472         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4473         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4474         push @b, { %$_ }
4475           unless ( $_->{amount} == 0 && !$discount_show_always );
4476         $_ = undef;
4477       }
4478     }
4479
4480     foreach my $display ( grep { defined($section)
4481                                  ? $_->section eq $section
4482                                  : 1
4483                                }
4484                           #grep { !$_->summary || !$summary_page } # bunk!
4485                           grep { !$_->summary || $multisection }
4486                           $cust_bill_pkg->cust_bill_pkg_display
4487                         )
4488     {
4489
4490       warn "$me _items_cust_bill_pkg considering display item $display\n"
4491         if $DEBUG > 1;
4492
4493       my $type = $display->type;
4494
4495       my $desc = $cust_bill_pkg->desc;
4496       $desc = substr($desc, 0, 50). '...'
4497         if $format eq 'latex' && length($desc) > 50;
4498
4499       my %details_opt = ( 'format'          => $format,
4500                           'escape_function' => $escape_function,
4501                           'format_function' => $format_function,
4502                         );
4503
4504       if ( $cust_bill_pkg->pkgnum > 0 ) {
4505
4506         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4507           if $DEBUG > 1;
4508  
4509         my $cust_pkg = $cust_bill_pkg->cust_pkg;
4510
4511         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4512
4513           warn "$me _items_cust_bill_pkg adding setup\n"
4514             if $DEBUG > 1;
4515
4516           my $description = $desc;
4517           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4518
4519           my @d = ();
4520           unless ( $cust_pkg->part_pkg->hide_svc_detail
4521                 || $cust_bill_pkg->hidden )
4522           {
4523
4524             push @d, map &{$escape_function}($_),
4525                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
4526               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4527
4528             if ( $multilocation ) {
4529               my $loc = $cust_pkg->location_label;
4530               $loc = substr($loc, 0, 50). '...'
4531                 if $format eq 'latex' && length($loc) > 50;
4532               push @d, &{$escape_function}($loc);
4533             }
4534
4535           }
4536
4537           push @d, $cust_bill_pkg->details(%details_opt)
4538             if $cust_bill_pkg->recur == 0;
4539
4540           if ( $cust_bill_pkg->hidden ) {
4541             $s->{amount}      += $cust_bill_pkg->setup;
4542             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4543             push @{ $s->{ext_description} }, @d;
4544           } else {
4545             $s = {
4546               description     => $description,
4547               #pkgpart         => $part_pkg->pkgpart,
4548               pkgnum          => $cust_bill_pkg->pkgnum,
4549               amount          => $cust_bill_pkg->setup,
4550               unit_amount     => $cust_bill_pkg->unitsetup,
4551               quantity        => $cust_bill_pkg->quantity,
4552               ext_description => \@d,
4553             };
4554           };
4555
4556         }
4557
4558         if ( ( $cust_bill_pkg->recur != 0  || $cust_bill_pkg->setup == 0 || 
4559                 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4560              ( !$type || $type eq 'R' || $type eq 'U' )
4561            )
4562         {
4563
4564           warn "$me _items_cust_bill_pkg adding recur/usage\n"
4565             if $DEBUG > 1;
4566
4567           my $is_summary = $display->summary;
4568           my $description = ($is_summary && $type && $type eq 'U')
4569                             ? "Usage charges" : $desc;
4570
4571           $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4572                           " - ". time2str($date_format, $cust_bill_pkg->edate).
4573                           ")"
4574             unless $conf->exists('disable_line_item_date_ranges');
4575
4576           my @d = ();
4577
4578           #at least until cust_bill_pkg has "past" ranges in addition to
4579           #the "future" sdate/edate ones... see #3032
4580           my @dates = ( $self->_date );
4581           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4582           push @dates, $prev->sdate if $prev;
4583           push @dates, undef if !$prev;
4584
4585           unless ( $cust_pkg->part_pkg->hide_svc_detail
4586                 || $cust_bill_pkg->itemdesc
4587                 || $cust_bill_pkg->hidden
4588                 || $is_summary && $type && $type eq 'U' )
4589           {
4590
4591             warn "$me _items_cust_bill_pkg adding service details\n"
4592               if $DEBUG > 1;
4593
4594             push @d, map &{$escape_function}($_),
4595                          $cust_pkg->h_labels_short(@dates, 'I')
4596                                                    #$cust_bill_pkg->edate,
4597                                                    #$cust_bill_pkg->sdate)
4598               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4599
4600             warn "$me _items_cust_bill_pkg done adding service details\n"
4601               if $DEBUG > 1;
4602
4603             if ( $multilocation ) {
4604               my $loc = $cust_pkg->location_label;
4605               $loc = substr($loc, 0, 50). '...'
4606                 if $format eq 'latex' && length($loc) > 50;
4607               push @d, &{$escape_function}($loc);
4608             }
4609
4610           }
4611
4612           unless ( $is_summary ) {
4613             warn "$me _items_cust_bill_pkg adding details\n"
4614               if $DEBUG > 1;
4615
4616             #instead of omitting details entirely in this case (unwanted side
4617             # effects), just omit CDRs
4618             $details_opt{'format_function'} = sub { () }
4619               if $type && $type eq 'R';
4620
4621             push @d, $cust_bill_pkg->details(%details_opt);
4622           }
4623
4624           warn "$me _items_cust_bill_pkg calculating amount\n"
4625             if $DEBUG > 1;
4626   
4627           my $amount = 0;
4628           if (!$type) {
4629             $amount = $cust_bill_pkg->recur;
4630           } elsif ($type eq 'R') {
4631             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4632           } elsif ($type eq 'U') {
4633             $amount = $cust_bill_pkg->usage;
4634           }
4635   
4636           if ( !$type || $type eq 'R' ) {
4637
4638             warn "$me _items_cust_bill_pkg adding recur\n"
4639               if $DEBUG > 1;
4640
4641             if ( $cust_bill_pkg->hidden ) {
4642               $r->{amount}      += $amount;
4643               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4644               push @{ $r->{ext_description} }, @d;
4645             } else {
4646               $r = {
4647                 description     => $description,
4648                 #pkgpart         => $part_pkg->pkgpart,
4649                 pkgnum          => $cust_bill_pkg->pkgnum,
4650                 amount          => $amount,
4651                 unit_amount     => $cust_bill_pkg->unitrecur,
4652                 quantity        => $cust_bill_pkg->quantity,
4653                 ext_description => \@d,
4654               };
4655             }
4656
4657           } else {  # $type eq 'U'
4658
4659             warn "$me _items_cust_bill_pkg adding usage\n"
4660               if $DEBUG > 1;
4661
4662             if ( $cust_bill_pkg->hidden ) {
4663               $u->{amount}      += $amount;
4664               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4665               push @{ $u->{ext_description} }, @d;
4666             } else {
4667               $u = {
4668                 description     => $description,
4669                 #pkgpart         => $part_pkg->pkgpart,
4670                 pkgnum          => $cust_bill_pkg->pkgnum,
4671                 amount          => $amount,
4672                 unit_amount     => $cust_bill_pkg->unitrecur,
4673                 quantity        => $cust_bill_pkg->quantity,
4674                 ext_description => \@d,
4675               };
4676             }
4677
4678           }
4679
4680         } # recurring or usage with recurring charge
4681
4682       } else { #pkgnum tax or one-shot line item (??)
4683
4684         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4685           if $DEBUG > 1;
4686
4687         if ( $cust_bill_pkg->setup != 0 ) {
4688           push @b, {
4689             'description' => $desc,
4690             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
4691           };
4692         }
4693         if ( $cust_bill_pkg->recur != 0 ) {
4694           push @b, {
4695             'description' => "$desc (".
4696                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4697                              time2str($date_format, $cust_bill_pkg->edate). ')',
4698             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
4699           };
4700         }
4701
4702       }
4703
4704     }
4705
4706   }
4707
4708   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4709     if $DEBUG > 1;
4710
4711   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4712     if ( $_  ) {
4713       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4714       $_->{amount}      =~ s/^\-0\.00$/0.00/;
4715       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4716       push @b, { %$_ }
4717         unless ( $_->{amount} == 0 && !$discount_show_always );
4718     }
4719   }
4720
4721   @b;
4722
4723 }
4724
4725 sub _items_credits {
4726   my( $self, %opt ) = @_;
4727   my $trim_len = $opt{'trim_len'} || 60;
4728
4729   my @b;
4730   #credits
4731   foreach ( $self->cust_credited ) {
4732
4733     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4734
4735     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4736     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4737     $reason = " ($reason) " if $reason;
4738
4739     push @b, {
4740       #'description' => 'Credit ref\#'. $_->crednum.
4741       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
4742       #                 $reason,
4743       'description' => 'Credit applied '.
4744                        time2str($date_format,$_->cust_credit->_date). $reason,
4745       'amount'      => sprintf("%.2f",$_->amount),
4746     };
4747   }
4748
4749   @b;
4750
4751 }
4752
4753 sub _items_payments {
4754   my $self = shift;
4755
4756   my @b;
4757   #get & print payments
4758   foreach ( $self->cust_bill_pay ) {
4759
4760     #something more elaborate if $_->amount ne ->cust_pay->paid ?
4761
4762     push @b, {
4763       'description' => "Payment received ".
4764                        time2str($date_format,$_->cust_pay->_date ),
4765       'amount'      => sprintf("%.2f", $_->amount )
4766     };
4767   }
4768
4769   @b;
4770
4771 }
4772
4773 =item call_details [ OPTION => VALUE ... ]
4774
4775 Returns an array of CSV strings representing the call details for this invoice
4776 The only option available is the boolean prepend_billed_number
4777
4778 =cut
4779
4780 sub call_details {
4781   my ($self, %opt) = @_;
4782
4783   my $format_function = sub { shift };
4784
4785   if ($opt{prepend_billed_number}) {
4786     $format_function = sub {
4787       my $detail = shift;
4788       my $row = shift;
4789
4790       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4791       
4792     };
4793   }
4794
4795   my @details = map { $_->details( 'format_function' => $format_function,
4796                                    'escape_function' => sub{ return() },
4797                                  )
4798                     }
4799                   grep { $_->pkgnum }
4800                   $self->cust_bill_pkg;
4801   my $header = $details[0];
4802   ( $header, grep { $_ ne $header } @details );
4803 }
4804
4805
4806 =back
4807
4808 =head1 SUBROUTINES
4809
4810 =over 4
4811
4812 =item process_reprint
4813
4814 =cut
4815
4816 sub process_reprint {
4817   process_re_X('print', @_);
4818 }
4819
4820 =item process_reemail
4821
4822 =cut
4823
4824 sub process_reemail {
4825   process_re_X('email', @_);
4826 }
4827
4828 =item process_refax
4829
4830 =cut
4831
4832 sub process_refax {
4833   process_re_X('fax', @_);
4834 }
4835
4836 =item process_reftp
4837
4838 =cut
4839
4840 sub process_reftp {
4841   process_re_X('ftp', @_);
4842 }
4843
4844 =item respool
4845
4846 =cut
4847
4848 sub process_respool {
4849   process_re_X('spool', @_);
4850 }
4851
4852 use Storable qw(thaw);
4853 use Data::Dumper;
4854 use MIME::Base64;
4855 sub process_re_X {
4856   my( $method, $job ) = ( shift, shift );
4857   warn "$me process_re_X $method for job $job\n" if $DEBUG;
4858
4859   my $param = thaw(decode_base64(shift));
4860   warn Dumper($param) if $DEBUG;
4861
4862   re_X(
4863     $method,
4864     $job,
4865     %$param,
4866   );
4867
4868 }
4869
4870 sub re_X {
4871   my($method, $job, %param ) = @_;
4872   if ( $DEBUG ) {
4873     warn "re_X $method for job $job with param:\n".
4874          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
4875   }
4876
4877   #some false laziness w/search/cust_bill.html
4878   my $distinct = '';
4879   my $orderby = 'ORDER BY cust_bill._date';
4880
4881   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4882
4883   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4884      
4885   my @cust_bill = qsearch( {
4886     #'select'    => "cust_bill.*",
4887     'table'     => 'cust_bill',
4888     'addl_from' => $addl_from,
4889     'hashref'   => {},
4890     'extra_sql' => $extra_sql,
4891     'order_by'  => $orderby,
4892     'debug' => 1,
4893   } );
4894
4895   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4896
4897   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4898     if $DEBUG;
4899
4900   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4901   foreach my $cust_bill ( @cust_bill ) {
4902     $cust_bill->$method();
4903
4904     if ( $job ) { #progressbar foo
4905       $num++;
4906       if ( time - $min_sec > $last ) {
4907         my $error = $job->update_statustext(
4908           int( 100 * $num / scalar(@cust_bill) )
4909         );
4910         die $error if $error;
4911         $last = time;
4912       }
4913     }
4914
4915   }
4916
4917 }
4918
4919 =back
4920
4921 =head1 CLASS METHODS
4922
4923 =over 4
4924
4925 =item owed_sql
4926
4927 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4928
4929 =cut
4930
4931 sub owed_sql {
4932   my ($class, $start, $end) = @_;
4933   'charged - '. 
4934     $class->paid_sql($start, $end). ' - '. 
4935     $class->credited_sql($start, $end);
4936 }
4937
4938 =item net_sql
4939
4940 Returns an SQL fragment to retreive the net amount (charged minus credited).
4941
4942 =cut
4943
4944 sub net_sql {
4945   my ($class, $start, $end) = @_;
4946   'charged - '. $class->credited_sql($start, $end);
4947 }
4948
4949 =item paid_sql
4950
4951 Returns an SQL fragment to retreive the amount paid against this invoice.
4952
4953 =cut
4954
4955 sub paid_sql {
4956   my ($class, $start, $end) = @_;
4957   $start &&= "AND cust_bill_pay._date <= $start";
4958   $end   &&= "AND cust_bill_pay._date > $end";
4959   $start = '' unless defined($start);
4960   $end   = '' unless defined($end);
4961   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4962        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
4963 }
4964
4965 =item credited_sql
4966
4967 Returns an SQL fragment to retreive the amount credited against this invoice.
4968
4969 =cut
4970
4971 sub credited_sql {
4972   my ($class, $start, $end) = @_;
4973   $start &&= "AND cust_credit_bill._date <= $start";
4974   $end   &&= "AND cust_credit_bill._date >  $end";
4975   $start = '' unless defined($start);
4976   $end   = '' unless defined($end);
4977   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4978        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
4979 }
4980
4981 =item due_date_sql
4982
4983 Returns an SQL fragment to retrieve the due date of an invoice.
4984 Currently only supported on PostgreSQL.
4985
4986 =cut
4987
4988 sub due_date_sql {
4989 'COALESCE(
4990   SUBSTRING(
4991     COALESCE(
4992       cust_bill.invoice_terms,
4993       cust_main.invoice_terms,
4994       \''.($conf->config('invoice_default_terms') || '').'\'
4995     ), E\'Net (\\\\d+)\'
4996   )::INTEGER, 0
4997 ) * 86400 + cust_bill._date'
4998 }
4999
5000 =item search_sql_where HASHREF
5001
5002 Class method which returns an SQL WHERE fragment to search for parameters
5003 specified in HASHREF.  Valid parameters are
5004
5005 =over 4
5006
5007 =item _date
5008
5009 List reference of start date, end date, as UNIX timestamps.
5010
5011 =item invnum_min
5012
5013 =item invnum_max
5014
5015 =item agentnum
5016
5017 =item charged
5018
5019 List reference of charged limits (exclusive).
5020
5021 =item owed
5022
5023 List reference of charged limits (exclusive).
5024
5025 =item open
5026
5027 flag, return open invoices only
5028
5029 =item net
5030
5031 flag, return net invoices only
5032
5033 =item days
5034
5035 =item newest_percust
5036
5037 =back
5038
5039 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5040
5041 =cut
5042
5043 sub search_sql_where {
5044   my($class, $param) = @_;
5045   if ( $DEBUG ) {
5046     warn "$me search_sql_where called with params: \n".
5047          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
5048   }
5049
5050   my @search = ();
5051
5052   #agentnum
5053   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5054     push @search, "cust_main.agentnum = $1";
5055   }
5056
5057   #_date
5058   if ( $param->{_date} ) {
5059     my($beginning, $ending) = @{$param->{_date}};
5060
5061     push @search, "cust_bill._date >= $beginning",
5062                   "cust_bill._date <  $ending";
5063   }
5064
5065   #invnum
5066   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5067     push @search, "cust_bill.invnum >= $1";
5068   }
5069   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5070     push @search, "cust_bill.invnum <= $1";
5071   }
5072
5073   #charged
5074   if ( $param->{charged} ) {
5075     my @charged = ref($param->{charged})
5076                     ? @{ $param->{charged} }
5077                     : ($param->{charged});
5078
5079     push @search, map { s/^charged/cust_bill.charged/; $_; }
5080                       @charged;
5081   }
5082
5083   my $owed_sql = FS::cust_bill->owed_sql;
5084
5085   #owed
5086   if ( $param->{owed} ) {
5087     my @owed = ref($param->{owed})
5088                  ? @{ $param->{owed} }
5089                  : ($param->{owed});
5090     push @search, map { s/^owed/$owed_sql/; $_; }
5091                       @owed;
5092   }
5093
5094   #open/net flags
5095   push @search, "0 != $owed_sql"
5096     if $param->{'open'};
5097   push @search, '0 != '. FS::cust_bill->net_sql
5098     if $param->{'net'};
5099
5100   #days
5101   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5102     if $param->{'days'};
5103
5104   #newest_percust
5105   if ( $param->{'newest_percust'} ) {
5106
5107     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5108     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5109
5110     my @newest_where = map { my $x = $_;
5111                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5112                              $x;
5113                            }
5114                            grep ! /^cust_main./, @search;
5115     my $newest_where = scalar(@newest_where)
5116                          ? ' AND '. join(' AND ', @newest_where)
5117                          : '';
5118
5119
5120     push @search, "cust_bill._date = (
5121       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5122         WHERE newest_cust_bill.custnum = cust_bill.custnum
5123           $newest_where
5124     )";
5125
5126   }
5127
5128   #agent virtualization
5129   my $curuser = $FS::CurrentUser::CurrentUser;
5130   if ( $curuser->username eq 'fs_queue'
5131        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5132     my $username = $1;
5133     my $newuser = qsearchs('access_user', {
5134       'username' => $username,
5135       'disabled' => '',
5136     } );
5137     if ( $newuser ) {
5138       $curuser = $newuser;
5139     } else {
5140       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5141     }
5142   }
5143   push @search, $curuser->agentnums_sql;
5144
5145   join(' AND ', @search );
5146
5147 }
5148
5149 =back
5150
5151 =head1 BUGS
5152
5153 The delete method.
5154
5155 =head1 SEE ALSO
5156
5157 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5158 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5159 documentation.
5160
5161 =cut
5162
5163 1;
5164