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