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