invoice configurations, #24723
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::Record );
3
4 use strict;
5 use vars qw( $DEBUG $me $date_format );
6              # but NOT $conf
7 use Fcntl qw(:flock); #for spool_csv
8 use Cwd;
9 use List::Util qw(min max sum);
10 use Date::Format;
11 use File::Temp 0.14;
12 use HTML::Entities;
13 use Storable qw( freeze thaw );
14 use GD::Barcode;
15 use FS::UID qw( datasrc );
16 use FS::Misc qw( send_email send_fax do_print );
17 use FS::Record qw( qsearch qsearchs dbh );
18 use FS::cust_main;
19 use FS::cust_statement;
20 use FS::cust_bill_pkg;
21 use FS::cust_bill_pkg_display;
22 use FS::cust_bill_pkg_detail;
23 use FS::cust_credit;
24 use FS::cust_pay;
25 use FS::cust_pkg;
26 use FS::cust_credit_bill;
27 use FS::pay_batch;
28 use FS::cust_pay_batch;
29 use FS::cust_bill_event;
30 use FS::cust_event;
31 use FS::part_pkg;
32 use FS::cust_bill_pay;
33 use FS::cust_bill_pay_batch;
34 use FS::part_bill_event;
35 use FS::payby;
36 use FS::bill_batch;
37 use FS::cust_bill_batch;
38 use FS::cust_bill_pay_pkg;
39 use FS::cust_credit_bill_pkg;
40 use FS::discount_plan;
41 use FS::cust_bill_void;
42 use FS::L10N;
43
44 $DEBUG = 0;
45 $me = '[FS::cust_bill]';
46
47 #ask FS::UID to run this stuff for us later
48 FS::UID->install_callback( sub { 
49   my $conf = new FS::Conf; #global
50   $date_format      = $conf->config('date_format')      || '%x'; #/YY
51 } );
52
53 =head1 NAME
54
55 FS::cust_bill - Object methods for cust_bill records
56
57 =head1 SYNOPSIS
58
59   use FS::cust_bill;
60
61   $record = new FS::cust_bill \%hash;
62   $record = new FS::cust_bill { 'column' => 'value' };
63
64   $error = $record->insert;
65
66   $error = $new_record->replace($old_record);
67
68   $error = $record->delete;
69
70   $error = $record->check;
71
72   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
73
74   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
75
76   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
77
78   @cust_pay_objects = $cust_bill->cust_pay;
79
80   $tax_amount = $record->tax;
81
82   @lines = $cust_bill->print_text;
83   @lines = $cust_bill->print_text('time' => $time);
84
85 =head1 DESCRIPTION
86
87 An FS::cust_bill object represents an invoice; a declaration that a customer
88 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
89 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
90 following fields are currently supported:
91
92 Regular fields
93
94 =over 4
95
96 =item invnum - primary key (assigned automatically for new invoices)
97
98 =item custnum - customer (see L<FS::cust_main>)
99
100 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
101 L<Time::Local> and L<Date::Parse> for conversion functions.
102
103 =item charged - amount of this invoice
104
105 =item invoice_terms - optional terms override for this specific invoice
106
107 =back
108
109 Customer info at invoice generation time
110
111 =over 4
112
113 =item billing_balance - the customer's balance at the time the invoice was 
114 generated (not including charges on this invoice)
115
116 =item previous_balance - the billing_balance of this customer's previous 
117 invoice plus the charges on that invoice
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 =item promised_date - customer promised payment date, for collection
140
141 =back
142
143 =head1 METHODS
144
145 =over 4
146
147 =item new HASHREF
148
149 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
150 Invoices are normally created by calling the bill method of a customer object
151 (see L<FS::cust_main>).
152
153 =cut
154
155 sub table { 'cust_bill'; }
156
157 # should be the ONLY occurrence of "Invoice" in invoice rendering code.
158 # (except email_subject and invnum_date_pretty)
159 sub notice_name {
160   my $self = shift;
161   $self->conf->config('notice_name') || 'Invoice'
162 }
163
164 sub cust_linked { $_[0]->cust_main_custnum; } 
165 sub cust_unlinked_msg {
166   my $self = shift;
167   "WARNING: can't find cust_main.custnum ". $self->custnum.
168   ' (cust_bill.invnum '. $self->invnum. ')';
169 }
170
171 =item insert
172
173 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
174 returns the error, otherwise returns false.
175
176 =cut
177
178 sub insert {
179   my $self = shift;
180   warn "$me insert called\n" if $DEBUG;
181
182   local $SIG{HUP} = 'IGNORE';
183   local $SIG{INT} = 'IGNORE';
184   local $SIG{QUIT} = 'IGNORE';
185   local $SIG{TERM} = 'IGNORE';
186   local $SIG{TSTP} = 'IGNORE';
187   local $SIG{PIPE} = 'IGNORE';
188
189   my $oldAutoCommit = $FS::UID::AutoCommit;
190   local $FS::UID::AutoCommit = 0;
191   my $dbh = dbh;
192
193   my $error = $self->SUPER::insert;
194   if ( $error ) {
195     $dbh->rollback if $oldAutoCommit;
196     return $error;
197   }
198
199   if ( $self->get('cust_bill_pkg') ) {
200     foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
201       $cust_bill_pkg->invnum($self->invnum);
202       my $error = $cust_bill_pkg->insert;
203       if ( $error ) {
204         $dbh->rollback if $oldAutoCommit;
205         return "can't create invoice line item: $error";
206       }
207     }
208   }
209
210   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
211   '';
212
213 }
214
215 =item void
216
217 Voids this invoice: deletes the invoice and adds a record of the voided invoice
218 to the FS::cust_bill_void table (and related tables starting from
219 FS::cust_bill_pkg_void).
220
221 =cut
222
223 sub void {
224   my $self = shift;
225   my $reason = scalar(@_) ? shift : '';
226
227   local $SIG{HUP} = 'IGNORE';
228   local $SIG{INT} = 'IGNORE';
229   local $SIG{QUIT} = 'IGNORE';
230   local $SIG{TERM} = 'IGNORE';
231   local $SIG{TSTP} = 'IGNORE';
232   local $SIG{PIPE} = 'IGNORE';
233
234   my $oldAutoCommit = $FS::UID::AutoCommit;
235   local $FS::UID::AutoCommit = 0;
236   my $dbh = dbh;
237
238   my $cust_bill_void = new FS::cust_bill_void ( {
239     map { $_ => $self->get($_) } $self->fields
240   } );
241   $cust_bill_void->reason($reason);
242   my $error = $cust_bill_void->insert;
243   if ( $error ) {
244     $dbh->rollback if $oldAutoCommit;
245     return $error;
246   }
247
248   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
249     my $error = $cust_bill_pkg->void($reason);
250     if ( $error ) {
251       $dbh->rollback if $oldAutoCommit;
252       return $error;
253     }
254   }
255
256   $error = $self->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 delete
269
270 This method now works but you probably shouldn't use it.  Instead, apply a
271 credit against the invoice, or use the new void method.
272
273 Using this method to delete invoices outright is really, really bad.  There
274 would be no record you ever posted this invoice, and there are no check to
275 make sure charged = 0 or that there are no associated cust_bill_pkg records.
276
277 Really, don't use it.
278
279 =cut
280
281 sub delete {
282   my $self = shift;
283   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
284
285   local $SIG{HUP} = 'IGNORE';
286   local $SIG{INT} = 'IGNORE';
287   local $SIG{QUIT} = 'IGNORE';
288   local $SIG{TERM} = 'IGNORE';
289   local $SIG{TSTP} = 'IGNORE';
290   local $SIG{PIPE} = 'IGNORE';
291
292   my $oldAutoCommit = $FS::UID::AutoCommit;
293   local $FS::UID::AutoCommit = 0;
294   my $dbh = dbh;
295
296   foreach my $table (qw(
297     cust_bill_event
298     cust_event
299     cust_credit_bill
300     cust_bill_pay
301     cust_pay_batch
302     cust_bill_pay_batch
303     cust_bill_batch
304     cust_bill_pkg
305   )) {
306
307     foreach my $linked ( $self->$table() ) {
308       my $error = $linked->delete;
309       if ( $error ) {
310         $dbh->rollback if $oldAutoCommit;
311         return $error;
312       }
313     }
314
315   }
316
317   my $error = $self->SUPER::delete(@_);
318   if ( $error ) {
319     $dbh->rollback if $oldAutoCommit;
320     return $error;
321   }
322
323   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
324
325   '';
326
327 }
328
329 =item replace [ OLD_RECORD ]
330
331 You can, but probably shouldn't modify invoices...
332
333 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
334 supplied, replaces this record.  If there is an error, returns the error,
335 otherwise returns false.
336
337 =cut
338
339 #replace can be inherited from Record.pm
340
341 # replace_check is now the preferred way to #implement replace data checks
342 # (so $object->replace() works without an argument)
343
344 sub replace_check {
345   my( $new, $old ) = ( shift, shift );
346   return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
347   #return "Can't change _date!" unless $old->_date eq $new->_date;
348   return "Can't change _date" unless $old->_date == $new->_date;
349   return "Can't change charged" unless $old->charged == $new->charged
350                                     || $old->charged == 0
351                                     || $new->{'Hash'}{'cc_surcharge_replace_hack'};
352
353   '';
354 }
355
356
357 =item add_cc_surcharge
358
359 Giant hack
360
361 =cut
362
363 sub add_cc_surcharge {
364     my ($self, $pkgnum, $amount) = (shift, shift, shift);
365
366     my $error;
367     my $cust_bill_pkg = new FS::cust_bill_pkg({
368                                     'invnum' => $self->invnum,
369                                     'pkgnum' => $pkgnum,
370                                     'setup' => $amount,
371                         });
372     $error = $cust_bill_pkg->insert;
373     return $error if $error;
374
375     $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
376     $self->charged($self->charged+$amount);
377     $error = $self->replace;
378     return $error if $error;
379
380     $self->apply_payments_and_credits;
381 }
382
383
384 =item check
385
386 Checks all fields to make sure this is a valid invoice.  If there is an error,
387 returns the error, otherwise returns false.  Called by the insert and replace
388 methods.
389
390 =cut
391
392 sub check {
393   my $self = shift;
394
395   my $error =
396     $self->ut_numbern('invnum')
397     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
398     || $self->ut_numbern('_date')
399     || $self->ut_money('charged')
400     || $self->ut_numbern('printed')
401     || $self->ut_enum('closed', [ '', 'Y' ])
402     || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
403     || $self->ut_numbern('agent_invid') #varchar?
404   ;
405   return $error if $error;
406
407   $self->_date(time) unless $self->_date;
408
409   $self->printed(0) if $self->printed eq '';
410
411   $self->SUPER::check;
412 }
413
414 =item display_invnum
415
416 Returns the displayed invoice number for this invoice: agent_invid if
417 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
418
419 =cut
420
421 sub display_invnum {
422   my $self = shift;
423   my $conf = $self->conf;
424   if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
425     return $self->agent_invid;
426   } else {
427     return $self->invnum;
428   }
429 }
430
431 =item previous_bill
432
433 Returns the customer's last invoice before this one.
434
435 =cut
436
437 sub previous_bill {
438   my $self = shift;
439   if ( !$self->get('previous_bill') ) {
440     $self->set('previous_bill', qsearchs({
441           'table'     => 'cust_bill',
442           'hashref'   => { 'custnum'  => $self->custnum,
443                            '_date'    => { op=>'<', value=>$self->_date } },
444           'order_by'  => 'ORDER BY _date DESC LIMIT 1',
445     }) );
446   }
447   $self->get('previous_bill');
448 }
449
450 =item previous
451
452 Returns a list consisting of the total previous balance for this customer, 
453 followed by the previous outstanding invoices (as FS::cust_bill objects also).
454
455 =cut
456
457 sub previous {
458   my $self = shift;
459   my $total = 0;
460   my @cust_bill = sort { $a->_date <=> $b->_date }
461     grep { $_->owed != 0 }
462       qsearch( 'cust_bill', { 'custnum' => $self->custnum,
463                               #'_date'   => { op=>'<', value=>$self->_date },
464                               'invnum'   => { op=>'<', value=>$self->invnum },
465                             } ) 
466   ;
467   foreach ( @cust_bill ) { $total += $_->owed; }
468   $total, @cust_bill;
469 }
470
471 =item enable_previous
472
473 Whether to show the 'Previous Charges' section when printing this invoice.
474 The negation of the 'disable_previous_balance' config setting.
475
476 =cut
477
478 sub enable_previous {
479   my $self = shift;
480   my $agentnum = $self->cust_main->agentnum;
481   !$self->conf->exists('disable_previous_balance', $agentnum);
482 }
483
484 =item cust_bill_pkg
485
486 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
487
488 =cut
489
490 sub cust_bill_pkg {
491   my $self = shift;
492   qsearch(
493     { 'table'    => 'cust_bill_pkg',
494       'hashref'  => { 'invnum' => $self->invnum },
495       'order_by' => 'ORDER BY billpkgnum',
496     }
497   );
498 }
499
500 =item cust_bill_pkg_pkgnum PKGNUM
501
502 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
503 specified pkgnum.
504
505 =cut
506
507 sub cust_bill_pkg_pkgnum {
508   my( $self, $pkgnum ) = @_;
509   qsearch(
510     { 'table'    => 'cust_bill_pkg',
511       'hashref'  => { 'invnum' => $self->invnum,
512                       'pkgnum' => $pkgnum,
513                     },
514       'order_by' => 'ORDER BY billpkgnum',
515     }
516   );
517 }
518
519 =item cust_pkg
520
521 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
522 this invoice.
523
524 =cut
525
526 sub cust_pkg {
527   my $self = shift;
528   my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
529                      $self->cust_bill_pkg;
530   my %saw = ();
531   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
532 }
533
534 =item no_auto
535
536 Returns true if any of the packages (or their definitions) corresponding to the
537 line items for this invoice have the no_auto flag set.
538
539 =cut
540
541 sub no_auto {
542   my $self = shift;
543   grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
544 }
545
546 =item open_cust_bill_pkg
547
548 Returns the open line items for this invoice.
549
550 Note that cust_bill_pkg with both setup and recur fees are returned as two
551 separate line items, each with only one fee.
552
553 =cut
554
555 # modeled after cust_main::open_cust_bill
556 sub open_cust_bill_pkg {
557   my $self = shift;
558
559   # grep { $_->owed > 0 } $self->cust_bill_pkg
560
561   my %other = ( 'recur' => 'setup',
562                 'setup' => 'recur', );
563   my @open = ();
564   foreach my $field ( qw( recur setup )) {
565     push @open, map  { $_->set( $other{$field}, 0 ); $_; }
566                 grep { $_->owed($field) > 0 }
567                 $self->cust_bill_pkg;
568   }
569
570   @open;
571 }
572
573 =item cust_bill_event
574
575 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
576
577 =cut
578
579 sub cust_bill_event {
580   my $self = shift;
581   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
582 }
583
584 =item num_cust_bill_event
585
586 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
587
588 =cut
589
590 sub num_cust_bill_event {
591   my $self = shift;
592   my $sql =
593     "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
594   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
595   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
596   $sth->fetchrow_arrayref->[0];
597 }
598
599 =item cust_event
600
601 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
602
603 =cut
604
605 #false laziness w/cust_pkg.pm
606 sub cust_event {
607   my $self = shift;
608   qsearch({
609     'table'     => 'cust_event',
610     'addl_from' => 'JOIN part_event USING ( eventpart )',
611     'hashref'   => { 'tablenum' => $self->invnum },
612     'extra_sql' => " AND eventtable = 'cust_bill' ",
613   });
614 }
615
616 =item num_cust_event
617
618 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
619
620 =cut
621
622 #false laziness w/cust_pkg.pm
623 sub num_cust_event {
624   my $self = shift;
625   my $sql =
626     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
627     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
628   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
629   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
630   $sth->fetchrow_arrayref->[0];
631 }
632
633 =item cust_main
634
635 Returns the customer (see L<FS::cust_main>) for this invoice.
636
637 =cut
638
639 sub cust_main {
640   my $self = shift;
641   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
642 }
643
644 =item cust_suspend_if_balance_over AMOUNT
645
646 Suspends the customer associated with this invoice if the total amount owed on
647 this invoice and all older invoices is greater than the specified amount.
648
649 Returns a list: an empty list on success or a list of errors.
650
651 =cut
652
653 sub cust_suspend_if_balance_over {
654   my( $self, $amount ) = ( shift, shift );
655   my $cust_main = $self->cust_main;
656   if ( $cust_main->total_owed_date($self->_date) < $amount ) {
657     return ();
658   } else {
659     $cust_main->suspend(@_);
660   }
661 }
662
663 =item cust_credit
664
665 Depreciated.  See the cust_credited method.
666
667  #Returns a list consisting of the total previous credited (see
668  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
669  #outstanding credits (FS::cust_credit objects).
670
671 =cut
672
673 sub cust_credit {
674   use Carp;
675   croak "FS::cust_bill->cust_credit depreciated; see ".
676         "FS::cust_bill->cust_credit_bill";
677   #my $self = shift;
678   #my $total = 0;
679   #my @cust_credit = sort { $a->_date <=> $b->_date }
680   #  grep { $_->credited != 0 && $_->_date < $self->_date }
681   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
682   #;
683   #foreach (@cust_credit) { $total += $_->credited; }
684   #$total, @cust_credit;
685 }
686
687 =item cust_pay
688
689 Depreciated.  See the cust_bill_pay method.
690
691 #Returns all payments (see L<FS::cust_pay>) for this invoice.
692
693 =cut
694
695 sub cust_pay {
696   use Carp;
697   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
698   #my $self = shift;
699   #sort { $a->_date <=> $b->_date }
700   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
701   #;
702 }
703
704 sub cust_pay_batch {
705   my $self = shift;
706   qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
707 }
708
709 sub cust_bill_pay_batch {
710   my $self = shift;
711   qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
712 }
713
714 =item cust_bill_pay
715
716 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
717
718 =cut
719
720 sub cust_bill_pay {
721   my $self = shift;
722   map { $_ } #return $self->num_cust_bill_pay unless wantarray;
723   sort { $a->_date <=> $b->_date }
724     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
725 }
726
727 =item cust_credited
728
729 =item cust_credit_bill
730
731 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
732
733 =cut
734
735 sub cust_credited {
736   my $self = shift;
737   map { $_ } #return $self->num_cust_credit_bill unless wantarray;
738   sort { $a->_date <=> $b->_date }
739     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
740   ;
741 }
742
743 sub cust_credit_bill {
744   shift->cust_credited(@_);
745 }
746
747 #=item cust_bill_pay_pkgnum PKGNUM
748 #
749 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
750 #with matching pkgnum.
751 #
752 #=cut
753 #
754 #sub cust_bill_pay_pkgnum {
755 #  my( $self, $pkgnum ) = @_;
756 #  map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
757 #  sort { $a->_date <=> $b->_date }
758 #    qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
759 #                                'pkgnum' => $pkgnum,
760 #                              }
761 #           );
762 #}
763
764 =item cust_bill_pay_pkg PKGNUM
765
766 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
767 applied against the matching pkgnum.
768
769 =cut
770
771 sub cust_bill_pay_pkg {
772   my( $self, $pkgnum ) = @_;
773
774   qsearch({
775     'select'    => 'cust_bill_pay_pkg.*',
776     'table'     => 'cust_bill_pay_pkg',
777     'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
778                    ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
779     'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
780                    "   AND cust_bill_pkg.pkgnum = $pkgnum",
781   });
782
783 }
784
785 #=item cust_credited_pkgnum PKGNUM
786 #
787 #=item cust_credit_bill_pkgnum PKGNUM
788 #
789 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
790 #with matching pkgnum.
791 #
792 #=cut
793 #
794 #sub cust_credited_pkgnum {
795 #  my( $self, $pkgnum ) = @_;
796 #  map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
797 #  sort { $a->_date <=> $b->_date }
798 #    qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
799 #                                   'pkgnum' => $pkgnum,
800 #                                 }
801 #           );
802 #}
803 #
804 #sub cust_credit_bill_pkgnum {
805 #  shift->cust_credited_pkgnum(@_);
806 #}
807
808 =item cust_credit_bill_pkg PKGNUM
809
810 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
811 applied against the matching pkgnum.
812
813 =cut
814
815 sub cust_credit_bill_pkg {
816   my( $self, $pkgnum ) = @_;
817
818   qsearch({
819     'select'    => 'cust_credit_bill_pkg.*',
820     'table'     => 'cust_credit_bill_pkg',
821     'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
822                    ' LEFT JOIN cust_bill_pkg    USING ( billpkgnum    ) ',
823     'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
824                    "   AND cust_bill_pkg.pkgnum = $pkgnum",
825   });
826
827 }
828
829 =item cust_bill_batch
830
831 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
832
833 =cut
834
835 sub cust_bill_batch {
836   my $self = shift;
837   qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
838 }
839
840 =item discount_plans
841
842 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a 
843 hash keyed by term length.
844
845 =cut
846
847 sub discount_plans {
848   my $self = shift;
849   FS::discount_plan->all($self);
850 }
851
852 =item tax
853
854 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
855
856 =cut
857
858 sub tax {
859   my $self = shift;
860   my $total = 0;
861   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
862                                              'pkgnum' => 0 } );
863   foreach (@taxlines) { $total += $_->setup; }
864   $total;
865 }
866
867 =item owed
868
869 Returns the amount owed (still outstanding) on this invoice, which is charged
870 minus all payment applications (see L<FS::cust_bill_pay>) and credit
871 applications (see L<FS::cust_credit_bill>).
872
873 =cut
874
875 sub owed {
876   my $self = shift;
877   my $balance = $self->charged;
878   $balance -= $_->amount foreach ( $self->cust_bill_pay );
879   $balance -= $_->amount foreach ( $self->cust_credited );
880   $balance = sprintf( "%.2f", $balance);
881   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
882   $balance;
883 }
884
885 sub owed_pkgnum {
886   my( $self, $pkgnum ) = @_;
887
888   #my $balance = $self->charged;
889   my $balance = 0;
890   $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
891
892   $balance -= $_->amount            for $self->cust_bill_pay_pkg($pkgnum);
893   $balance -= $_->amount            for $self->cust_credit_bill_pkg($pkgnum);
894
895   $balance = sprintf( "%.2f", $balance);
896   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
897   $balance;
898 }
899
900 =item hide
901
902 Returns true if this invoice should be hidden.  See the
903 selfservice-hide_invoices-taxclass configuraiton setting.
904
905 =cut
906
907 sub hide {
908   my $self = shift;
909   my $conf = $self->conf;
910   my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
911     or return '';
912   my @cust_bill_pkg = $self->cust_bill_pkg;
913   my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
914   ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
915 }
916
917 =item apply_payments_and_credits [ OPTION => VALUE ... ]
918
919 Applies unapplied payments and credits to this invoice.
920
921 A hash of optional arguments may be passed.  Currently "manual" is supported.
922 If true, a payment receipt is sent instead of a statement when
923 'payment_receipt_email' configuration option is set.
924
925 If there is an error, returns the error, otherwise returns false.
926
927 =cut
928
929 sub apply_payments_and_credits {
930   my( $self, %options ) = @_;
931   my $conf = $self->conf;
932
933   local $SIG{HUP} = 'IGNORE';
934   local $SIG{INT} = 'IGNORE';
935   local $SIG{QUIT} = 'IGNORE';
936   local $SIG{TERM} = 'IGNORE';
937   local $SIG{TSTP} = 'IGNORE';
938   local $SIG{PIPE} = 'IGNORE';
939
940   my $oldAutoCommit = $FS::UID::AutoCommit;
941   local $FS::UID::AutoCommit = 0;
942   my $dbh = dbh;
943
944   $self->select_for_update; #mutex
945
946   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
947   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
948
949   if ( $conf->exists('pkg-balances') ) {
950     # limit @payments & @credits to those w/ a pkgnum grepped from $self
951     my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
952     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
953     @credits  = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
954   }
955
956   while ( $self->owed > 0 and ( @payments || @credits ) ) {
957
958     my $app = '';
959     if ( @payments && @credits ) {
960
961       #decide which goes first by weight of top (unapplied) line item
962
963       my @open_lineitems = $self->open_cust_bill_pkg;
964
965       my $max_pay_weight =
966         max( map  { $_->part_pkg->pay_weight || 0 }
967              grep { $_ }
968              map  { $_->cust_pkg }
969                   @open_lineitems
970            );
971       my $max_credit_weight =
972         max( map  { $_->part_pkg->credit_weight || 0 }
973              grep { $_ } 
974              map  { $_->cust_pkg }
975                   @open_lineitems
976            );
977
978       #if both are the same... payments first?  it has to be something
979       if ( $max_pay_weight >= $max_credit_weight ) {
980         $app = 'pay';
981       } else {
982         $app = 'credit';
983       }
984     
985     } elsif ( @payments ) {
986       $app = 'pay';
987     } elsif ( @credits ) {
988       $app = 'credit';
989     } else {
990       die "guru meditation #12 and 35";
991     }
992
993     my $unapp_amount;
994     if ( $app eq 'pay' ) {
995
996       my $payment = shift @payments;
997       $unapp_amount = $payment->unapplied;
998       $app = new FS::cust_bill_pay { 'paynum'  => $payment->paynum };
999       $app->pkgnum( $payment->pkgnum )
1000         if $conf->exists('pkg-balances') && $payment->pkgnum;
1001
1002     } elsif ( $app eq 'credit' ) {
1003
1004       my $credit = shift @credits;
1005       $unapp_amount = $credit->credited;
1006       $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
1007       $app->pkgnum( $credit->pkgnum )
1008         if $conf->exists('pkg-balances') && $credit->pkgnum;
1009
1010     } else {
1011       die "guru meditation #12 and 35";
1012     }
1013
1014     my $owed;
1015     if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
1016       warn "owed_pkgnum ". $app->pkgnum;
1017       $owed = $self->owed_pkgnum($app->pkgnum);
1018     } else {
1019       $owed = $self->owed;
1020     }
1021     next unless $owed > 0;
1022
1023     warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
1024     $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
1025
1026     $app->invnum( $self->invnum );
1027
1028     my $error = $app->insert(%options);
1029     if ( $error ) {
1030       $dbh->rollback if $oldAutoCommit;
1031       return "Error inserting ". $app->table. " record: $error";
1032     }
1033     die $error if $error;
1034
1035   }
1036
1037   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1038   ''; #no error
1039
1040 }
1041
1042 =item generate_email OPTION => VALUE ...
1043
1044 Options:
1045
1046 =over 4
1047
1048 =item from
1049
1050 sender address, required
1051
1052 =item template
1053
1054 alternate template name, optional
1055
1056 =item print_text
1057
1058 text attachment arrayref, optional
1059
1060 =item subject
1061
1062 email subject, optional
1063
1064 =item notice_name
1065
1066 notice name instead of "Invoice", optional
1067
1068 =back
1069
1070 Returns an argument list to be passed to L<FS::Misc::send_email>.
1071
1072 =cut
1073
1074 use MIME::Entity;
1075
1076 sub generate_email {
1077
1078   my $self = shift;
1079   my %args = @_;
1080   my $conf = $self->conf;
1081
1082   my $me = '[FS::cust_bill::generate_email]';
1083
1084   my %return = (
1085     'from'      => $args{'from'},
1086     'subject'   => ($args{'subject'} || $self->email_subject),
1087   );
1088
1089   $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
1090
1091   my $cust_main = $self->cust_main;
1092
1093   if (ref($args{'to'}) eq 'ARRAY') {
1094     $return{'to'} = $args{'to'};
1095   } else {
1096     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1097                            $cust_main->invoicing_list
1098                     ];
1099   }
1100
1101   if ( $conf->exists('invoice_html') ) {
1102
1103     warn "$me creating HTML/text multipart message"
1104       if $DEBUG;
1105
1106     $return{'nobody'} = 1;
1107
1108     my $alternative = build MIME::Entity
1109       'Type'        => 'multipart/alternative',
1110       #'Encoding'    => '7bit',
1111       'Disposition' => 'inline'
1112     ;
1113
1114     my $data;
1115     if ( $conf->exists('invoice_email_pdf')
1116          and scalar($conf->config('invoice_email_pdf_note')) ) {
1117
1118       warn "$me using 'invoice_email_pdf_note' in multipart message"
1119         if $DEBUG;
1120       $data = [ map { $_ . "\n" }
1121                     $conf->config('invoice_email_pdf_note')
1122               ];
1123
1124     } else {
1125
1126       warn "$me not using 'invoice_email_pdf_note' in multipart message"
1127         if $DEBUG;
1128       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1129         $data = $args{'print_text'};
1130       } else {
1131         $data = [ $self->print_text(\%args) ];
1132       }
1133
1134     }
1135
1136     $alternative->attach(
1137       'Type'        => 'text/plain',
1138       'Encoding'    => 'quoted-printable',
1139       #'Encoding'    => '7bit',
1140       'Data'        => $data,
1141       'Disposition' => 'inline',
1142     );
1143
1144
1145     my $htmldata;
1146     my $image = '';
1147     my $barcode = '';
1148     if ( $conf->exists('invoice_email_pdf')
1149          and scalar($conf->config('invoice_email_pdf_note')) ) {
1150
1151       $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1152
1153     } else {
1154
1155       $args{'from'} =~ /\@([\w\.\-]+)/;
1156       my $from = $1 || 'example.com';
1157       my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1158
1159       my $logo;
1160       my $agentnum = $cust_main->agentnum;
1161       if ( defined($args{'template'}) && length($args{'template'})
1162            && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1163          )
1164       {
1165         $logo = 'logo_'. $args{'template'}. '.png';
1166       } else {
1167         $logo = "logo.png";
1168       }
1169       my $image_data = $conf->config_binary( $logo, $agentnum);
1170
1171       $image = build MIME::Entity
1172         'Type'       => 'image/png',
1173         'Encoding'   => 'base64',
1174         'Data'       => $image_data,
1175         'Filename'   => 'logo.png',
1176         'Content-ID' => "<$content_id>",
1177       ;
1178    
1179       if ($conf->exists('invoice-barcode')) {
1180         my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1181         $barcode = build MIME::Entity
1182           'Type'       => 'image/png',
1183           'Encoding'   => 'base64',
1184           'Data'       => $self->invoice_barcode(0),
1185           'Filename'   => 'barcode.png',
1186           'Content-ID' => "<$barcode_content_id>",
1187         ;
1188         $args{'barcode_cid'} = $barcode_content_id;
1189       }
1190
1191       $htmldata = $self->print_html({ 'cid'=>$content_id, %args });
1192     }
1193
1194     $alternative->attach(
1195       'Type'        => 'text/html',
1196       'Encoding'    => 'quoted-printable',
1197       'Data'        => [ '<html>',
1198                          '  <head>',
1199                          '    <title>',
1200                          '      '. encode_entities($return{'subject'}), 
1201                          '    </title>',
1202                          '  </head>',
1203                          '  <body bgcolor="#e8e8e8">',
1204                          $htmldata,
1205                          '  </body>',
1206                          '</html>',
1207                        ],
1208       'Disposition' => 'inline',
1209       #'Filename'    => 'invoice.pdf',
1210     );
1211
1212
1213     my @otherparts = ();
1214     if ( $cust_main->email_csv_cdr ) {
1215
1216       push @otherparts, build MIME::Entity
1217         'Type'        => 'text/csv',
1218         'Encoding'    => '7bit',
1219         'Data'        => [ map { "$_\n" }
1220                              $self->call_details('prepend_billed_number' => 1)
1221                          ],
1222         'Disposition' => 'attachment',
1223         'Filename'    => 'usage-'. $self->invnum. '.csv',
1224       ;
1225
1226     }
1227
1228     if ( $conf->exists('invoice_email_pdf') ) {
1229
1230       #attaching pdf too:
1231       # multipart/mixed
1232       #   multipart/related
1233       #     multipart/alternative
1234       #       text/plain
1235       #       text/html
1236       #     image/png
1237       #   application/pdf
1238
1239       my $related = build MIME::Entity 'Type'     => 'multipart/related',
1240                                        'Encoding' => '7bit';
1241
1242       #false laziness w/Misc::send_email
1243       $related->head->replace('Content-type',
1244         $related->mime_type.
1245         '; boundary="'. $related->head->multipart_boundary. '"'.
1246         '; type=multipart/alternative'
1247       );
1248
1249       $related->add_part($alternative);
1250
1251       $related->add_part($image) if $image;
1252
1253       my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
1254
1255       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1256
1257     } else {
1258
1259       #no other attachment:
1260       # multipart/related
1261       #   multipart/alternative
1262       #     text/plain
1263       #     text/html
1264       #   image/png
1265
1266       $return{'content-type'} = 'multipart/related';
1267       if ($conf->exists('invoice-barcode') && $barcode) {
1268         $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1269       } else {
1270         $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1271       }
1272       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1273       #$return{'disposition'} = 'inline';
1274
1275     }
1276   
1277   } else {
1278
1279     if ( $conf->exists('invoice_email_pdf') ) {
1280       warn "$me creating PDF attachment"
1281         if $DEBUG;
1282
1283       #mime parts arguments a la MIME::Entity->build().
1284       $return{'mimeparts'} = [
1285         { $self->mimebuild_pdf(\%args) }
1286       ];
1287     }
1288   
1289     if ( $conf->exists('invoice_email_pdf')
1290          and scalar($conf->config('invoice_email_pdf_note')) ) {
1291
1292       warn "$me using 'invoice_email_pdf_note'"
1293         if $DEBUG;
1294       $return{'body'} = [ map { $_ . "\n" }
1295                               $conf->config('invoice_email_pdf_note')
1296                         ];
1297
1298     } else {
1299
1300       warn "$me not using 'invoice_email_pdf_note'"
1301         if $DEBUG;
1302       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1303         $return{'body'} = $args{'print_text'};
1304       } else {
1305         $return{'body'} = [ $self->print_text(\%args) ];
1306       }
1307
1308     }
1309
1310   }
1311
1312   %return;
1313
1314 }
1315
1316 =item mimebuild_pdf
1317
1318 Returns a list suitable for passing to MIME::Entity->build(), representing
1319 this invoice as PDF attachment.
1320
1321 =cut
1322
1323 sub mimebuild_pdf {
1324   my $self = shift;
1325   (
1326     'Type'        => 'application/pdf',
1327     'Encoding'    => 'base64',
1328     'Data'        => [ $self->print_pdf(@_) ],
1329     'Disposition' => 'attachment',
1330     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
1331   );
1332 }
1333
1334 =item send HASHREF
1335
1336 Sends this invoice to the destinations configured for this customer: sends
1337 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
1338
1339 Options can be passed as a hashref.  Positional parameters are no longer
1340 allowed.
1341
1342 I<template>: a suffix for alternate invoices
1343
1344 I<agentnum>: obsolete, now does nothing.
1345
1346 I<invoice_from> overrides the default email invoice From: address.
1347
1348 I<amount>: obsolete, does nothing
1349
1350 I<notice_name> overrides "Invoice" as the name of the sent document 
1351 (templates from 10/2009 or newer required).
1352
1353 I<lpr> overrides the system 'lpr' option as the command to print a document
1354 from standard input.
1355
1356 =cut
1357
1358 sub send {
1359   my $self = shift;
1360   my $opt = ref($_[0]) ? $_[0] : +{ @_ };
1361   my $conf = $self->conf;
1362
1363   my $cust_main = $self->cust_main;
1364
1365   my @invoicing_list = $cust_main->invoicing_list;
1366
1367   $self->email($opt)
1368     if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1369     && ! $self->invoice_noemail;
1370
1371   $self->print($opt)
1372     if grep { $_ eq 'POST' } @invoicing_list; #postal
1373
1374   #this has never been used post-$ORIGINAL_ISP afaik
1375   $self->fax_invoice($opt)
1376     if grep { $_ eq 'FAX' } @invoicing_list; #fax
1377
1378   '';
1379
1380 }
1381
1382 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] 
1383
1384 Sends this invoice to the customer's email destination(s).
1385
1386 Options must be passed as a hashref.  Positional parameters are no longer
1387 allowed.
1388
1389 I<template>, if specified, is the name of a suffix for alternate invoices.
1390
1391 I<invoice_from>, if specified, overrides the default email invoice From: 
1392 address.
1393
1394 I<notice_name> is the name of the sent document.
1395
1396 =cut
1397
1398 sub queueable_email {
1399   my %opt = @_;
1400
1401   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1402     or die "invalid invoice number: " . $opt{invnum};
1403
1404   my %args = map {$_ => $opt{$_}} 
1405              grep { $opt{$_} }
1406               qw( invoice_from notice_name no_coupon template );
1407
1408   my $error = $self->email( \%args );
1409   die $error if $error;
1410
1411 }
1412
1413 sub email {
1414   my $self = shift;
1415   return if $self->hide;
1416   my $conf = $self->conf;
1417   my $opt = shift;
1418   if ($opt and !ref($opt)) {
1419     die "FS::cust_bill::email called with positional parameters";
1420   }
1421
1422   my $template = $opt->{template};
1423   my $from = delete $opt->{invoice_from};
1424
1425   # this is where we set the From: address
1426   $from ||= $self->_agent_invoice_from ||    #XXX should go away
1427             $conf->config('invoice_from', $self->cust_main->agentnum );
1428
1429   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
1430                             $self->cust_main->invoicing_list;
1431
1432   if ( ! @invoicing_list ) { #no recipients
1433     if ( $conf->exists('cust_bill-no_recipients-error') ) {
1434       die 'No recipients for customer #'. $self->custnum;
1435     } else {
1436       #default: better to notify this person than silence
1437       @invoicing_list = ($from);
1438     }
1439   }
1440
1441   # this is where we set the Subject:
1442   my $subject = $self->email_subject($template);
1443
1444   my $error = send_email(
1445     $self->generate_email(
1446       'from'        => $from,
1447       'to'          => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1448       'subject'     => $subject,
1449       %$opt, # template, etc.
1450     )
1451   );
1452   die "can't email invoice: $error\n" if $error;
1453   #die "$error\n" if $error;
1454
1455 }
1456
1457 sub email_subject {
1458   my $self = shift;
1459   my $conf = $self->conf;
1460
1461   #my $template = scalar(@_) ? shift : '';
1462   #per-template?
1463
1464   my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1465                 || 'Invoice';
1466
1467   my $cust_main = $self->cust_main;
1468   my $name = $cust_main->name;
1469   my $name_short = $cust_main->name_short;
1470   my $invoice_number = $self->invnum;
1471   my $invoice_date = $self->_date_pretty;
1472
1473   eval qq("$subject");
1474 }
1475
1476 =item lpr_data HASHREF
1477
1478 Returns the postscript or plaintext for this invoice as an arrayref.
1479
1480 Options must be passed as a hashref.  Positional parameters are no longer 
1481 allowed.
1482
1483 I<template>, if specified, is the name of a suffix for alternate invoices.
1484
1485 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1486
1487 =cut
1488
1489 sub lpr_data {
1490   my $self = shift;
1491   my $conf = $self->conf;
1492   my $opt = shift;
1493   if ($opt and !ref($opt)) {
1494     # nobody does this anyway
1495     die "FS::cust_bill::lpr_data called with positional parameters";
1496   }
1497
1498   my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1499   [ $self->$method( $opt ) ];
1500 }
1501
1502 =item print HASHREF
1503
1504 Prints this invoice.
1505
1506 Options must be passed as a hashref.
1507
1508 I<template>, if specified, is the name of a suffix for alternate invoices.
1509
1510 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1511
1512 =cut
1513
1514 sub print {
1515   my $self = shift;
1516   return if $self->hide;
1517   my $conf = $self->conf;
1518   my $opt = shift;
1519   if ($opt and !ref($opt)) {
1520     die "FS::cust_bill::print called with positional parameters";
1521   }
1522
1523   my $lpr = delete $opt->{lpr};
1524   if($conf->exists('invoice_print_pdf')) {
1525     # Add the invoice to the current batch.
1526     $self->batch_invoice($opt);
1527   }
1528   else {
1529     do_print(
1530       $self->lpr_data($opt),
1531       'agentnum' => $self->cust_main->agentnum,
1532       'lpr'      => $lpr,
1533     );
1534   }
1535 }
1536
1537 =item fax_invoice HASHREF
1538
1539 Faxes this invoice.
1540
1541 Options must be passed as a hashref.
1542
1543 I<template>, if specified, is the name of a suffix for alternate invoices.
1544
1545 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1546
1547 =cut
1548
1549 sub fax_invoice {
1550   my $self = shift;
1551   return if $self->hide;
1552   my $conf = $self->conf;
1553   my $opt = shift;
1554   if ($opt and !ref($opt)) {
1555     die "FS::cust_bill::fax_invoice called with positional parameters";
1556   }
1557
1558   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1559     unless $conf->exists('invoice_latex');
1560
1561   my $dialstring = $self->cust_main->getfield('fax');
1562   #Check $dialstring?
1563
1564   my $error = send_fax( 'docdata'    => $self->lpr_data($opt),
1565                         'dialstring' => $dialstring,
1566                       );
1567   die $error if $error;
1568
1569 }
1570
1571 =item batch_invoice [ HASHREF ]
1572
1573 Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
1574 isn't an open batch, one will be created.
1575
1576 =cut
1577
1578 sub batch_invoice {
1579   my ($self, $opt) = @_;
1580   my $bill_batch = $self->get_open_bill_batch;
1581   my $cust_bill_batch = FS::cust_bill_batch->new({
1582       batchnum => $bill_batch->batchnum,
1583       invnum   => $self->invnum,
1584   });
1585   return $cust_bill_batch->insert($opt);
1586 }
1587
1588 =item get_open_batch
1589
1590 Returns the currently open batch as an FS::bill_batch object, creating a new
1591 one if necessary.  (A per-agent batch if invoice_print_pdf-spoolagent is
1592 enabled)
1593
1594 =cut
1595
1596 sub get_open_bill_batch {
1597   my $self = shift;
1598   my $conf = $self->conf;
1599   my $hashref = { status => 'O' };
1600   $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1601                              ? $self->cust_main->agentnum
1602                              : '';
1603   my $batch = qsearchs('bill_batch', $hashref);
1604   return $batch if $batch;
1605   $batch = FS::bill_batch->new($hashref);
1606   my $error = $batch->insert;
1607   die $error if $error;
1608   return $batch;
1609 }
1610
1611 =item ftp_invoice [ TEMPLATENAME ] 
1612
1613 Sends this invoice data via FTP.
1614
1615 TEMPLATENAME is unused?
1616
1617 =cut
1618
1619 sub ftp_invoice {
1620   my $self = shift;
1621   my $conf = $self->conf;
1622   my $template = scalar(@_) ? shift : '';
1623
1624   $self->send_csv(
1625     'protocol'   => 'ftp',
1626     'server'     => $conf->config('cust_bill-ftpserver'),
1627     'username'   => $conf->config('cust_bill-ftpusername'),
1628     'password'   => $conf->config('cust_bill-ftppassword'),
1629     'dir'        => $conf->config('cust_bill-ftpdir'),
1630     'format'     => $conf->config('cust_bill-ftpformat'),
1631   );
1632 }
1633
1634 =item spool_invoice [ TEMPLATENAME ] 
1635
1636 Spools this invoice data (see L<FS::spool_csv>)
1637
1638 TEMPLATENAME is unused?
1639
1640 =cut
1641
1642 sub spool_invoice {
1643   my $self = shift;
1644   my $conf = $self->conf;
1645   my $template = scalar(@_) ? shift : '';
1646
1647   $self->spool_csv(
1648     'format'       => $conf->config('cust_bill-spoolformat'),
1649     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1650   );
1651 }
1652
1653 =item send_csv OPTION => VALUE, ...
1654
1655 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1656
1657 Options are:
1658
1659 protocol - currently only "ftp"
1660 server
1661 username
1662 password
1663 dir
1664
1665 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1666 and YYMMDDHHMMSS is a timestamp.
1667
1668 See L</print_csv> for a description of the output format.
1669
1670 =cut
1671
1672 sub send_csv {
1673   my($self, %opt) = @_;
1674
1675   #create file(s)
1676
1677   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1678   mkdir $spooldir, 0700 unless -d $spooldir;
1679
1680   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1681   my $file = "$spooldir/$tracctnum.csv";
1682   
1683   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1684
1685   open(CSV, ">$file") or die "can't open $file: $!";
1686   print CSV $header;
1687
1688   print CSV $detail;
1689
1690   close CSV;
1691
1692   my $net;
1693   if ( $opt{protocol} eq 'ftp' ) {
1694     eval "use Net::FTP;";
1695     die $@ if $@;
1696     $net = Net::FTP->new($opt{server}) or die @$;
1697   } else {
1698     die "unknown protocol: $opt{protocol}";
1699   }
1700
1701   $net->login( $opt{username}, $opt{password} )
1702     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1703
1704   $net->binary or die "can't set binary mode";
1705
1706   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1707
1708   $net->put($file) or die "can't put $file: $!";
1709
1710   $net->quit;
1711
1712   unlink $file;
1713
1714 }
1715
1716 =item spool_csv
1717
1718 Spools CSV invoice data.
1719
1720 Options are:
1721
1722 =over 4
1723
1724 =item format - any of FS::Misc::::Invoicing::spool_formats
1725
1726 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
1727 customer has the corresponding invoice destinations set (see
1728 L<FS::cust_main_invoice>).
1729
1730 =item agent_spools - if set to a true value, will spool to per-agent files
1731 rather than a single global file
1732
1733 =item upload_targetnum - if set to a target (see L<FS::upload_target>), will
1734 append to that spool.  L<FS::Cron::upload> will then send the spool file to
1735 that destination.
1736
1737 =item balanceover - if set, only spools the invoice if the total amount owed on
1738 this invoice and all older invoices is greater than the specified amount.
1739
1740 =item time - the "current time".  Controls the printing of past due messages
1741 in the ICS format.
1742
1743 =back
1744
1745 =cut
1746
1747 sub spool_csv {
1748   my($self, %opt) = @_;
1749
1750   my $time = $opt{'time'} || time;
1751   my $cust_main = $self->cust_main;
1752
1753   if ( $opt{'dest'} ) {
1754     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1755                              $cust_main->invoicing_list;
1756     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1757                      || ! keys %invoicing_list;
1758   }
1759
1760   if ( $opt{'balanceover'} ) {
1761     return 'N/A'
1762       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1763   }
1764
1765   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1766   mkdir $spooldir, 0700 unless -d $spooldir;
1767
1768   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', $time);
1769
1770   my $file;
1771   if ( $opt{'agent_spools'} ) {
1772     $file = 'agentnum'.$cust_main->agentnum;
1773   } else {
1774     $file = 'spool';
1775   }
1776
1777   if ( $opt{'upload_targetnum'} ) {
1778     $spooldir .= '/target'.$opt{'upload_targetnum'};
1779     mkdir $spooldir, 0700 unless -d $spooldir;
1780   } # otherwise it just goes into export.xxx/cust_bill
1781
1782   if ( lc($opt{'format'}) eq 'billco' ) {
1783     $file .= '-header';
1784   }
1785
1786   $file = "$spooldir/$file.csv";
1787   
1788   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
1789
1790   open(CSV, ">>$file") or die "can't open $file: $!";
1791   flock(CSV, LOCK_EX);
1792   seek(CSV, 0, 2);
1793
1794   print CSV $header;
1795
1796   if ( lc($opt{'format'}) eq 'billco' ) {
1797
1798     flock(CSV, LOCK_UN);
1799     close CSV;
1800
1801     $file =~ s/-header.csv$/-detail.csv/;
1802
1803     open(CSV,">>$file") or die "can't open $file: $!";
1804     flock(CSV, LOCK_EX);
1805     seek(CSV, 0, 2);
1806   }
1807
1808   print CSV $detail if defined($detail);
1809
1810   flock(CSV, LOCK_UN);
1811   close CSV;
1812
1813   return '';
1814
1815 }
1816
1817 =item print_csv OPTION => VALUE, ...
1818
1819 Returns CSV data for this invoice.
1820
1821 Options are:
1822
1823 format - 'default', 'billco', 'oneline', 'bridgestone'
1824
1825 Returns a list consisting of two scalars.  The first is a single line of CSV
1826 header information for this invoice.  The second is one or more lines of CSV
1827 detail information for this invoice.
1828
1829 If I<format> is not specified or "default", the fields of the CSV file are as
1830 follows:
1831
1832 record_type, invnum, custnum, _date, charged, first, last, company, address1, 
1833 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1834
1835 =over 4
1836
1837 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1838
1839 B<record_type> is C<cust_bill> for the initial header line only.  The
1840 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1841 fields are filled in.
1842
1843 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1844 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1845 are filled in.
1846
1847 =item invnum - invoice number
1848
1849 =item custnum - customer number
1850
1851 =item _date - invoice date
1852
1853 =item charged - total invoice amount
1854
1855 =item first - customer first name
1856
1857 =item last - customer first name
1858
1859 =item company - company name
1860
1861 =item address1 - address line 1
1862
1863 =item address2 - address line 1
1864
1865 =item city
1866
1867 =item state
1868
1869 =item zip
1870
1871 =item country
1872
1873 =item pkg - line item description
1874
1875 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1876
1877 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1878
1879 =item sdate - start date for recurring fee
1880
1881 =item edate - end date for recurring fee
1882
1883 =back
1884
1885 If I<format> is "billco", the fields of the header CSV file are as follows:
1886
1887   +-------------------------------------------------------------------+
1888   |                        FORMAT HEADER FILE                         |
1889   |-------------------------------------------------------------------|
1890   | Field | Description                   | Name       | Type | Width |
1891   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1892   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1893   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1894   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1895   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1896   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1897   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1898   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1899   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1900   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1901   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1902   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1903   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1904   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1905   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1906   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1907   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1908   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1909   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1910   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1911   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1912   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1913   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1914   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1915   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1916   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1917   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1918   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1919   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1920   +-------+-------------------------------+------------+------+-------+
1921
1922 If I<format> is "billco", the fields of the detail CSV file are as follows:
1923
1924                                   FORMAT FOR DETAIL FILE
1925         |                            |           |      |
1926   Field | Description                | Name      | Type | Width
1927   1     | N/A-Leave Empty            | RC        | CHAR |     2
1928   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1929   3     | Account Number             | TRACCTNUM | CHAR |    15
1930   4     | Invoice Number             | TRINVOICE | CHAR |    15
1931   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1932   6     | Transaction Detail         | DETAILS   | CHAR |   100
1933   7     | Amount                     | AMT       | NUM* |     9
1934   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1935   9     | Grouping Code              | GROUP     | CHAR |     2
1936   10    | User Defined               | ACCT CODE | CHAR |    15
1937
1938 If format is 'oneline', there is no detail file.  Each invoice has a 
1939 header line only, with the fields:
1940
1941 Agent number, agent name, customer number, first name, last name, address
1942 line 1, address line 2, city, state, zip, invoice date, invoice number,
1943 amount charged, amount due, previous balance, due date.
1944
1945 and then, for each line item, three columns containing the package number,
1946 description, and amount.
1947
1948 If format is 'bridgestone', there is no detail file.  Each invoice has a 
1949 header line with the following fields in a fixed-width format:
1950
1951 Customer number (in display format), date, name (first last), company,
1952 address 1, address 2, city, state, zip.
1953
1954 This is a mailing list format, and has no per-invoice fields.  To avoid
1955 sending redundant notices, the spooling event should have a "once" or 
1956 "once_percust_every" condition.
1957
1958 =cut
1959
1960 sub print_csv {
1961   my($self, %opt) = @_;
1962   
1963   eval "use Text::CSV_XS";
1964   die $@ if $@;
1965
1966   my $cust_main = $self->cust_main;
1967
1968   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1969   my $format = lc($opt{'format'});
1970
1971   my $time = $opt{'time'} || time;
1972
1973   if ( $format eq 'billco' ) {
1974
1975     my $taxtotal = 0;
1976     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1977
1978     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1979
1980     my( $previous_balance, @unused ) = $self->previous; #previous balance
1981
1982     my $pmt_cr_applied = 0;
1983     $pmt_cr_applied += $_->{'amount'}
1984       foreach ( $self->_items_payments, $self->_items_credits ) ;
1985
1986     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1987
1988     $csv->combine(
1989       '',                         #  1 | N/A-Leave Empty               CHAR   2
1990       '',                         #  2 | N/A-Leave Empty               CHAR  15
1991       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1992       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1993       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1994       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1995       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1996       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1997       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1998       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1999       '',                         # 10 | Ancillary Billing Information CHAR  30
2000       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
2001       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
2002
2003       # XXX ?
2004       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
2005
2006       # XXX ?
2007       $duedate,                   # 14 | Bill Due Date                 CHAR  10
2008
2009       $previous_balance,          # 15 | Previous Balance              NUM*   9
2010       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
2011       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
2012       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
2013       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
2014       '',                         # 20 | 30 Day Aging                  NUM*   9
2015       '',                         # 21 | 60 Day Aging                  NUM*   9
2016       '',                         # 22 | 90 Day Aging                  NUM*   9
2017       'N',                        # 23 | Y/N                           CHAR   1
2018       '',                         # 24 | Remittance automation         CHAR 100
2019       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
2020       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
2021       '0',                        # 27 | Federal Tax***                NUM*   9
2022       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
2023       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
2024     );
2025
2026   } elsif ( $format eq 'oneline' ) { #name
2027   
2028     my ($previous_balance) = $self->previous; 
2029     $previous_balance = sprintf('%.2f', $previous_balance);
2030     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2031     my @items = map {
2032                       $_->{pkgnum},
2033                       $_->{description},
2034                       $_->{amount}
2035                     }
2036                   $self->_items_pkg, #_items_nontax?  no sections or anything
2037                                      # with this format
2038                   $self->_items_tax;
2039
2040     $csv->combine(
2041       $cust_main->agentnum,
2042       $cust_main->agent->agent,
2043       $self->custnum,
2044       $cust_main->first,
2045       $cust_main->last,
2046       $cust_main->company,
2047       $cust_main->address1,
2048       $cust_main->address2,
2049       $cust_main->city,
2050       $cust_main->state,
2051       $cust_main->zip,
2052
2053       # invoice fields
2054       time2str("%x", $self->_date),
2055       $self->invnum,
2056       $self->charged,
2057       $totaldue,
2058       $previous_balance,
2059       $self->due_date2str("%x"),
2060
2061       @items,
2062     );
2063
2064   } elsif ( $format eq 'bridgestone' ) {
2065
2066     # bypass the CSV stuff and just return this
2067     my $longdate = time2str('%B %d, %Y', $time); #current time, right?
2068     my $zip = $cust_main->zip;
2069     $zip =~ s/\D//;
2070     my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
2071       || '';
2072     return (
2073       sprintf(
2074         "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
2075         $prefix,
2076         $cust_main->display_custnum,
2077         $longdate,
2078         uc(substr($cust_main->contact_firstlast,0,30)),
2079         uc(substr($cust_main->company          ,0,30)),
2080         uc(substr($cust_main->address1         ,0,30)),
2081         uc(substr($cust_main->address2         ,0,30)),
2082         uc(substr($cust_main->city             ,0,20)),
2083         uc($cust_main->state),
2084         $zip
2085       ),
2086       '' #detail
2087       );
2088
2089   } elsif ( $format eq 'ics' ) {
2090
2091     my $bill = $cust_main->bill_location;
2092     my $zip = $bill->zip;
2093     my $zip4 = '';
2094
2095     $zip =~ s/\D//;
2096     if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
2097       $zip = $1;
2098       $zip4 = $2;
2099     }
2100
2101     # minor false laziness with print_generic
2102     my ($previous_balance) = $self->previous;
2103     my $balance_due = $self->owed + $previous_balance;
2104     my $payment_total = sum(0, map { $_->{'amount'} } $self->_items_payments);
2105     my $credit_total  = sum(0, map { $_->{'amount'} } $self->_items_credits);
2106
2107     my $past_due = '';
2108     if ( $self->due_date and $time >= $self->due_date ) {
2109       $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
2110     }
2111
2112     # again, bypass CSV
2113     my $header = sprintf(
2114       '%-10s%-30s%-48s%-2s%-50s%-30s%-30s%-25s%-2s%-5s%-4s%-8s%-8s%-10s%-10s%-10s%-10s%-10s%-10s%-480s%-35s',
2115       $cust_main->display_custnum, #BID
2116       uc($cust_main->first), #FNAME
2117       uc($cust_main->last), #LNAME
2118       '00', #BATCH, should this ever be anything else?
2119       uc($cust_main->company), #COMP
2120       uc($bill->address1), #STREET1
2121       uc($bill->address2), #STREET2
2122       uc($bill->city), #CITY
2123       uc($bill->state), #STATE
2124       $zip,
2125       $zip4,
2126       time2str('%Y%m%d', $self->_date), #BILL_DATE
2127       $self->due_date2str('%Y%m%d'), #DUE_DATE,
2128       ( map {sprintf('%0.2f', $_)}
2129         $balance_due, #AMNT_DUE
2130         $previous_balance, #PREV_BAL
2131         $payment_total, #PYMT_RCVD
2132         $credit_total, #CREDITS
2133         $previous_balance, #BEG_BAL--is this correct?
2134         $self->charged, #NEW_CHRG
2135       ),
2136       'img01', #MRKT_MSG?
2137       $past_due, #PAST_MSG
2138     );
2139
2140     my @details;
2141     my %svc_class = ('' => ''); # maybe cache this more persistently?
2142
2143     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2144
2145       my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
2146       my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
2147
2148       if ( $cust_pkg ) {
2149
2150         my @dates = ( $self->_date, undef );
2151         if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
2152           $dates[1] = $prev->sdate; #questionable
2153         }
2154
2155         # generate an 01 detail for each service
2156         my @svcs = $cust_pkg->h_cust_svc(@dates, 'I');
2157         foreach my $cust_svc ( @svcs ) {
2158           $show_pkgnum = ''; # hide it if we're showing svcnums
2159
2160           my $svcpart = $cust_svc->svcpart;
2161           if (!exists($svc_class{$svcpart})) {
2162             my $classnum = $cust_svc->part_svc->classnum;
2163             my $part_svc_class = FS::part_svc_class->by_key($classnum)
2164               if $classnum;
2165             $svc_class{$svcpart} = $part_svc_class ? 
2166                                    $part_svc_class->classname :
2167                                    '';
2168           }
2169
2170           my @h_label = $cust_svc->label(@dates, 'I');
2171           push @details, sprintf('01%-9s%-20s%-47s',
2172             $cust_svc->svcnum,
2173             $svc_class{$svcpart},
2174             $h_label[1],
2175           );
2176         } #foreach $cust_svc
2177       } #if $cust_pkg
2178
2179       my $desc = $cust_bill_pkg->desc; # itemdesc or part_pkg.pkg
2180       if ($cust_bill_pkg->recur > 0) {
2181         $desc .= ' '.time2str('%d-%b-%Y', $cust_bill_pkg->sdate).' to '.
2182                      time2str('%d-%b-%Y', $cust_bill_pkg->edate - 86400);
2183       }
2184       push @details, sprintf('02%-6s%-60s%-10s',
2185         $show_pkgnum,
2186         $desc,
2187         sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
2188       );
2189     } #foreach $cust_bill_pkg
2190
2191     # Tag this row so that we know whether this is one page (1), two pages
2192     # (2), # or "big" (B).  The tag will be stripped off before uploading.
2193     if ( scalar(@details) < 12 ) {
2194       push @details, '1';
2195     } elsif ( scalar(@details) < 58 ) {
2196       push @details, '2';
2197     } else {
2198       push @details, 'B';
2199     }
2200
2201     return join('', $header, @details, "\n");
2202
2203   } else { # default
2204   
2205     $csv->combine(
2206       'cust_bill',
2207       $self->invnum,
2208       $self->custnum,
2209       time2str("%x", $self->_date),
2210       sprintf("%.2f", $self->charged),
2211       ( map { $cust_main->getfield($_) }
2212           qw( first last company address1 address2 city state zip country ) ),
2213       map { '' } (1..5),
2214     ) or die "can't create csv";
2215   }
2216
2217   my $header = $csv->string. "\n";
2218
2219   my $detail = '';
2220   if ( lc($opt{'format'}) eq 'billco' ) {
2221
2222     my $lineseq = 0;
2223     foreach my $item ( $self->_items_pkg ) {
2224
2225       $csv->combine(
2226         '',                     #  1 | N/A-Leave Empty            CHAR   2
2227         '',                     #  2 | N/A-Leave Empty            CHAR  15
2228         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
2229         $self->invnum,          #  4 | Invoice Number             CHAR  15
2230         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
2231         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
2232         $item->{'amount'},      #  7 | Amount                     NUM*   9
2233         '',                     #  8 | Line Format Control**      CHAR   2
2234         '',                     #  9 | Grouping Code              CHAR   2
2235         '',                     # 10 | User Defined               CHAR  15
2236       );
2237
2238       $detail .= $csv->string. "\n";
2239
2240     }
2241
2242   } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2243
2244     #do nothing
2245
2246   } else {
2247
2248     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2249
2250       my($pkg, $setup, $recur, $sdate, $edate);
2251       if ( $cust_bill_pkg->pkgnum ) {
2252       
2253         ($pkg, $setup, $recur, $sdate, $edate) = (
2254           $cust_bill_pkg->part_pkg->pkg,
2255           ( $cust_bill_pkg->setup != 0
2256             ? sprintf("%.2f", $cust_bill_pkg->setup )
2257             : '' ),
2258           ( $cust_bill_pkg->recur != 0
2259             ? sprintf("%.2f", $cust_bill_pkg->recur )
2260             : '' ),
2261           ( $cust_bill_pkg->sdate 
2262             ? time2str("%x", $cust_bill_pkg->sdate)
2263             : '' ),
2264           ($cust_bill_pkg->edate 
2265             ?time2str("%x", $cust_bill_pkg->edate)
2266             : '' ),
2267         );
2268   
2269       } else { #pkgnum tax
2270         next unless $cust_bill_pkg->setup != 0;
2271         $pkg = $cust_bill_pkg->desc;
2272         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2273         ( $sdate, $edate ) = ( '', '' );
2274       }
2275   
2276       $csv->combine(
2277         'cust_bill_pkg',
2278         $self->invnum,
2279         ( map { '' } (1..11) ),
2280         ($pkg, $setup, $recur, $sdate, $edate)
2281       ) or die "can't create csv";
2282
2283       $detail .= $csv->string. "\n";
2284
2285     }
2286
2287   }
2288
2289   ( $header, $detail );
2290
2291 }
2292
2293 =item comp
2294
2295 Pays this invoice with a compliemntary payment.  If there is an error,
2296 returns the error, otherwise returns false.
2297
2298 =cut
2299
2300 sub comp {
2301   my $self = shift;
2302   my $cust_pay = new FS::cust_pay ( {
2303     'invnum'   => $self->invnum,
2304     'paid'     => $self->owed,
2305     '_date'    => '',
2306     'payby'    => 'COMP',
2307     'payinfo'  => $self->cust_main->payinfo,
2308     'paybatch' => '',
2309   } );
2310   $cust_pay->insert;
2311 }
2312
2313 =item realtime_card
2314
2315 Attempts to pay this invoice with a credit card payment via a
2316 Business::OnlinePayment realtime gateway.  See
2317 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2318 for supported processors.
2319
2320 =cut
2321
2322 sub realtime_card {
2323   my $self = shift;
2324   $self->realtime_bop( 'CC', @_ );
2325 }
2326
2327 =item realtime_ach
2328
2329 Attempts to pay this invoice with an electronic check (ACH) payment via a
2330 Business::OnlinePayment realtime gateway.  See
2331 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2332 for supported processors.
2333
2334 =cut
2335
2336 sub realtime_ach {
2337   my $self = shift;
2338   $self->realtime_bop( 'ECHECK', @_ );
2339 }
2340
2341 =item realtime_lec
2342
2343 Attempts to pay this invoice with phone bill (LEC) payment via a
2344 Business::OnlinePayment realtime gateway.  See
2345 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2346 for supported processors.
2347
2348 =cut
2349
2350 sub realtime_lec {
2351   my $self = shift;
2352   $self->realtime_bop( 'LEC', @_ );
2353 }
2354
2355 sub realtime_bop {
2356   my( $self, $method ) = (shift,shift);
2357   my $conf = $self->conf;
2358   my %opt = @_;
2359
2360   my $cust_main = $self->cust_main;
2361   my $balance = $cust_main->balance;
2362   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2363   $amount = sprintf("%.2f", $amount);
2364   return "not run (balance $balance)" unless $amount > 0;
2365
2366   my $description = 'Internet Services';
2367   if ( $conf->exists('business-onlinepayment-description') ) {
2368     my $dtempl = $conf->config('business-onlinepayment-description');
2369
2370     my $agent_obj = $cust_main->agent
2371       or die "can't retreive agent for $cust_main (agentnum ".
2372              $cust_main->agentnum. ")";
2373     my $agent = $agent_obj->agent;
2374     my $pkgs = join(', ',
2375       map { $_->part_pkg->pkg }
2376         grep { $_->pkgnum } $self->cust_bill_pkg
2377     );
2378     $description = eval qq("$dtempl");
2379   }
2380
2381   $cust_main->realtime_bop($method, $amount,
2382     'description' => $description,
2383     'invnum'      => $self->invnum,
2384 #this didn't do what we want, it just calls apply_payments_and_credits
2385 #    'apply'       => 1,
2386     'apply_to_invoice' => 1,
2387     %opt,
2388  #what we want:
2389  #this changes application behavior: auto payments
2390                         #triggered against a specific invoice are now applied
2391                         #to that invoice instead of oldest open.
2392                         #seem okay to me...
2393   );
2394
2395 }
2396
2397 =item batch_card OPTION => VALUE...
2398
2399 Adds a payment for this invoice to the pending credit card batch (see
2400 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2401 runs the payment using a realtime gateway.
2402
2403 =cut
2404
2405 sub batch_card {
2406   my ($self, %options) = @_;
2407   my $cust_main = $self->cust_main;
2408
2409   $options{invnum} = $self->invnum;
2410   
2411   $cust_main->batch_card(%options);
2412 }
2413
2414 sub _agent_template {
2415   my $self = shift;
2416   $self->cust_main->agent_template;
2417 }
2418
2419 sub _agent_invoice_from {
2420   my $self = shift;
2421   $self->cust_main->agent_invoice_from;
2422 }
2423
2424 =item invoice_barcode DIR_OR_FALSE
2425
2426 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2427 it is taken as the temp directory where the PNG file will be generated and the
2428 PNG file name is returned. Otherwise, the PNG image itself is returned.
2429
2430 =cut
2431
2432 sub invoice_barcode {
2433     my ($self, $dir) = (shift,shift);
2434     
2435     my $gdbar = new GD::Barcode('Code39',$self->invnum);
2436         die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2437     my $gd = $gdbar->plot(Height => 30);
2438
2439     if($dir) {
2440         my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2441                            DIR      => $dir,
2442                            SUFFIX   => '.png',
2443                            UNLINK   => 0,
2444                          ) or die "can't open temp file: $!\n";
2445         print $bh $gd->png or die "cannot write barcode to file: $!\n";
2446         my $png_file = $bh->filename;
2447         close $bh;
2448         return $png_file;
2449     }
2450     return $gd->png;
2451 }
2452
2453 =item invnum_date_pretty
2454
2455 Returns a string with the invoice number and date, for example:
2456 "Invoice #54 (3/20/2008)"
2457
2458 =cut
2459
2460 sub invnum_date_pretty {
2461   my $self = shift;
2462   $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
2463 }
2464
2465 #sub _items_extra_usage_sections {
2466 #  my $self = shift;
2467 #  my $escape = shift;
2468 #
2469 #  my %sections = ();
2470 #
2471 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
2472 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2473 #  {
2474 #    next unless $cust_bill_pkg->pkgnum > 0;
2475 #
2476 #    foreach my $section ( keys %usage_class ) {
2477 #
2478 #      my $usage = $cust_bill_pkg->usage($section);
2479 #
2480 #      next unless $usage && $usage > 0;
2481 #
2482 #      $sections{$section} ||= 0;
2483 #      $sections{$section} += $usage;
2484 #
2485 #    }
2486 #
2487 #  }
2488 #
2489 #  map { { 'description' => &{$escape}($_),
2490 #          'subtotal'    => $sections{$_},
2491 #          'summarized'  => '',
2492 #          'tax_section' => '',
2493 #        }
2494 #      }
2495 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
2496 #
2497 #}
2498
2499 sub _items_extra_usage_sections {
2500   my $self = shift;
2501   my $conf = $self->conf;
2502   my $escape = shift;
2503   my $format = shift;
2504
2505   my %sections = ();
2506   my %classnums = ();
2507   my %lines = ();
2508
2509   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2510
2511   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2512   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2513     next unless $cust_bill_pkg->pkgnum > 0;
2514
2515     foreach my $classnum ( keys %usage_class ) {
2516       my $section = $usage_class{$classnum}->classname;
2517       $classnums{$section} = $classnum;
2518
2519       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
2520         my $amount = $detail->amount;
2521         next unless $amount && $amount > 0;
2522  
2523         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
2524         $sections{$section}{amount} += $amount;  #subtotal
2525         $sections{$section}{calls}++;
2526         $sections{$section}{duration} += $detail->duration;
2527
2528         my $desc = $detail->regionname; 
2529         my $description = $desc;
2530         $description = substr($desc, 0, $maxlength). '...'
2531           if $format eq 'latex' && length($desc) > $maxlength;
2532
2533         $lines{$section}{$desc} ||= {
2534           description     => &{$escape}($description),
2535           #pkgpart         => $part_pkg->pkgpart,
2536           pkgnum          => $cust_bill_pkg->pkgnum,
2537           ref             => '',
2538           amount          => 0,
2539           calls           => 0,
2540           duration        => 0,
2541           #unit_amount     => $cust_bill_pkg->unitrecur,
2542           quantity        => $cust_bill_pkg->quantity,
2543           product_code    => 'N/A',
2544           ext_description => [],
2545         };
2546
2547         $lines{$section}{$desc}{amount} += $amount;
2548         $lines{$section}{$desc}{calls}++;
2549         $lines{$section}{$desc}{duration} += $detail->duration;
2550
2551       }
2552     }
2553   }
2554
2555   my %sectionmap = ();
2556   foreach (keys %sections) {
2557     my $usage_class = $usage_class{$classnums{$_}};
2558     $sectionmap{$_} = { 'description' => &{$escape}($_),
2559                         'amount'    => $sections{$_}{amount},    #subtotal
2560                         'calls'       => $sections{$_}{calls},
2561                         'duration'    => $sections{$_}{duration},
2562                         'summarized'  => '',
2563                         'tax_section' => '',
2564                         'sort_weight' => $usage_class->weight,
2565                         ( $usage_class->format
2566                           ? ( map { $_ => $usage_class->$_($format) }
2567                               qw( description_generator header_generator total_generator total_line_generator )
2568                             )
2569                           : ()
2570                         ), 
2571                       };
2572   }
2573
2574   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
2575                  values %sectionmap;
2576
2577   my @lines = ();
2578   foreach my $section ( keys %lines ) {
2579     foreach my $line ( keys %{$lines{$section}} ) {
2580       my $l = $lines{$section}{$line};
2581       $l->{section}     = $sectionmap{$section};
2582       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2583       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2584       push @lines, $l;
2585     }
2586   }
2587
2588   return(\@sections, \@lines);
2589
2590 }
2591
2592 sub _did_summary {
2593     my $self = shift;
2594     my $end = $self->_date;
2595
2596     # start at date of previous invoice + 1 second or 0 if no previous invoice
2597     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
2598     $start = 0 if !$start;
2599     $start++;
2600
2601     my $cust_main = $self->cust_main;
2602     my @pkgs = $cust_main->all_pkgs;
2603     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
2604         = (0,0,0,0,0);
2605     my @seen = ();
2606     foreach my $pkg ( @pkgs ) {
2607         my @h_cust_svc = $pkg->h_cust_svc($end);
2608         foreach my $h_cust_svc ( @h_cust_svc ) {
2609             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
2610             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
2611
2612             my $inserted = $h_cust_svc->date_inserted;
2613             my $deleted = $h_cust_svc->date_deleted;
2614             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
2615             my $phone_deleted;
2616             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
2617             
2618 # DID either activated or ported in; cannot be both for same DID simultaneously
2619             if ($inserted >= $start && $inserted <= $end && $phone_inserted
2620                 && (!$phone_inserted->lnp_status 
2621                     || $phone_inserted->lnp_status eq ''
2622                     || $phone_inserted->lnp_status eq 'native')) {
2623                 $num_activated++;
2624             }
2625             else { # this one not so clean, should probably move to (h_)svc_phone
2626                  my $phone_portedin = qsearchs( 'h_svc_phone',
2627                       { 'svcnum' => $h_cust_svc->svcnum, 
2628                         'lnp_status' => 'portedin' },  
2629                       FS::h_svc_phone->sql_h_searchs($end),  
2630                     );
2631                  $num_portedin++ if $phone_portedin;
2632             }
2633
2634 # DID either deactivated or ported out; cannot be both for same DID simultaneously
2635             if($deleted >= $start && $deleted <= $end && $phone_deleted
2636                 && (!$phone_deleted->lnp_status 
2637                     || $phone_deleted->lnp_status ne 'portingout')) {
2638                 $num_deactivated++;
2639             } 
2640             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
2641                 && $phone_deleted->lnp_status 
2642                 && $phone_deleted->lnp_status eq 'portingout') {
2643                 $num_portedout++;
2644             }
2645
2646             # increment usage minutes
2647         if ( $phone_inserted ) {
2648             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
2649             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
2650         }
2651         else {
2652             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
2653         }
2654
2655             # don't look at this service again
2656             push @seen, $h_cust_svc->svcnum;
2657         }
2658     }
2659
2660     $minutes = sprintf("%d", $minutes);
2661     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
2662         . "$num_deactivated  Ported-Out: $num_portedout ",
2663             "Total Minutes: $minutes");
2664 }
2665
2666 sub _items_accountcode_cdr {
2667     my $self = shift;
2668     my $escape = shift;
2669     my $format = shift;
2670
2671     my $section = { 'amount'        => 0,
2672                     'calls'         => 0,
2673                     'duration'      => 0,
2674                     'sort_weight'   => '',
2675                     'phonenum'      => '',
2676                     'description'   => 'Usage by Account Code',
2677                     'post_total'    => '',
2678                     'summarized'    => '',
2679                     'header'        => '',
2680                   };
2681     my @lines;
2682     my %accountcodes = ();
2683
2684     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2685         next unless $cust_bill_pkg->pkgnum > 0;
2686
2687         my @header = $cust_bill_pkg->details_header;
2688         next unless scalar(@header);
2689         $section->{'header'} = join(',',@header);
2690
2691         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2692
2693             $section->{'header'} = $detail->formatted('format' => $format)
2694                 if($detail->detail eq $section->{'header'}); 
2695       
2696             my $accountcode = $detail->accountcode;
2697             next unless $accountcode;
2698
2699             my $amount = $detail->amount;
2700             next unless $amount && $amount > 0;
2701
2702             $accountcodes{$accountcode} ||= {
2703                     description => $accountcode,
2704                     pkgnum      => '',
2705                     ref         => '',
2706                     amount      => 0,
2707                     calls       => 0,
2708                     duration    => 0,
2709                     quantity    => '',
2710                     product_code => 'N/A',
2711                     section     => $section,
2712                     ext_description => [ $section->{'header'} ],
2713                     detail_temp => [],
2714             };
2715
2716             $section->{'amount'} += $amount;
2717             $accountcodes{$accountcode}{'amount'} += $amount;
2718             $accountcodes{$accountcode}{calls}++;
2719             $accountcodes{$accountcode}{duration} += $detail->duration;
2720             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
2721         }
2722     }
2723
2724     foreach my $l ( values %accountcodes ) {
2725         $l->{amount} = sprintf( "%.2f", $l->{amount} );
2726         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
2727         foreach my $sorted_detail ( @sorted_detail ) {
2728             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
2729         }
2730         delete $l->{detail_temp};
2731         push @lines, $l;
2732     }
2733
2734     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
2735
2736     return ($section,\@sorted_lines);
2737 }
2738
2739 sub _items_svc_phone_sections {
2740   my $self = shift;
2741   my $conf = $self->conf;
2742   my $escape = shift;
2743   my $format = shift;
2744
2745   my %sections = ();
2746   my %classnums = ();
2747   my %lines = ();
2748
2749   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2750
2751   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2752   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
2753
2754   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2755     next unless $cust_bill_pkg->pkgnum > 0;
2756
2757     my @header = $cust_bill_pkg->details_header;
2758     next unless scalar(@header);
2759
2760     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2761
2762       my $phonenum = $detail->phonenum;
2763       next unless $phonenum;
2764
2765       my $amount = $detail->amount;
2766       next unless $amount && $amount > 0;
2767
2768       $sections{$phonenum} ||= { 'amount'      => 0,
2769                                  'calls'       => 0,
2770                                  'duration'    => 0,
2771                                  'sort_weight' => -1,
2772                                  'phonenum'    => $phonenum,
2773                                 };
2774       $sections{$phonenum}{amount} += $amount;  #subtotal
2775       $sections{$phonenum}{calls}++;
2776       $sections{$phonenum}{duration} += $detail->duration;
2777
2778       my $desc = $detail->regionname; 
2779       my $description = $desc;
2780       $description = substr($desc, 0, $maxlength). '...'
2781         if $format eq 'latex' && length($desc) > $maxlength;
2782
2783       $lines{$phonenum}{$desc} ||= {
2784         description     => &{$escape}($description),
2785         #pkgpart         => $part_pkg->pkgpart,
2786         pkgnum          => '',
2787         ref             => '',
2788         amount          => 0,
2789         calls           => 0,
2790         duration        => 0,
2791         #unit_amount     => '',
2792         quantity        => '',
2793         product_code    => 'N/A',
2794         ext_description => [],
2795       };
2796
2797       $lines{$phonenum}{$desc}{amount} += $amount;
2798       $lines{$phonenum}{$desc}{calls}++;
2799       $lines{$phonenum}{$desc}{duration} += $detail->duration;
2800
2801       my $line = $usage_class{$detail->classnum}->classname;
2802       $sections{"$phonenum $line"} ||=
2803         { 'amount' => 0,
2804           'calls' => 0,
2805           'duration' => 0,
2806           'sort_weight' => $usage_class{$detail->classnum}->weight,
2807           'phonenum' => $phonenum,
2808           'header'  => [ @header ],
2809         };
2810       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
2811       $sections{"$phonenum $line"}{calls}++;
2812       $sections{"$phonenum $line"}{duration} += $detail->duration;
2813
2814       $lines{"$phonenum $line"}{$desc} ||= {
2815         description     => &{$escape}($description),
2816         #pkgpart         => $part_pkg->pkgpart,
2817         pkgnum          => '',
2818         ref             => '',
2819         amount          => 0,
2820         calls           => 0,
2821         duration        => 0,
2822         #unit_amount     => '',
2823         quantity        => '',
2824         product_code    => 'N/A',
2825         ext_description => [],
2826       };
2827
2828       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
2829       $lines{"$phonenum $line"}{$desc}{calls}++;
2830       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
2831       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
2832            $detail->formatted('format' => $format);
2833
2834     }
2835   }
2836
2837   my %sectionmap = ();
2838   my $simple = new FS::usage_class { format => 'simple' }; #bleh
2839   foreach ( keys %sections ) {
2840     my @header = @{ $sections{$_}{header} || [] };
2841     my $usage_simple =
2842       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
2843     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
2844     my $usage_class = $summary ? $simple : $usage_simple;
2845     my $ending = $summary ? ' usage charges' : '';
2846     my %gen_opt = ();
2847     unless ($summary) {
2848       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
2849     }
2850     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
2851                         'amount'    => $sections{$_}{amount},    #subtotal
2852                         'calls'       => $sections{$_}{calls},
2853                         'duration'    => $sections{$_}{duration},
2854                         'summarized'  => '',
2855                         'tax_section' => '',
2856                         'phonenum'    => $sections{$_}{phonenum},
2857                         'sort_weight' => $sections{$_}{sort_weight},
2858                         'post_total'  => $summary, #inspire pagebreak
2859                         (
2860                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
2861                             qw( description_generator
2862                                 header_generator
2863                                 total_generator
2864                                 total_line_generator
2865                               )
2866                           )
2867                         ), 
2868                       };
2869   }
2870
2871   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
2872                         $a->{sort_weight} <=> $b->{sort_weight}
2873                       }
2874                  values %sectionmap;
2875
2876   my @lines = ();
2877   foreach my $section ( keys %lines ) {
2878     foreach my $line ( keys %{$lines{$section}} ) {
2879       my $l = $lines{$section}{$line};
2880       $l->{section}     = $sectionmap{$section};
2881       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2882       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2883       push @lines, $l;
2884     }
2885   }
2886   
2887   if($conf->exists('phone_usage_class_summary')) { 
2888       # this only works with Latex
2889       my @newlines;
2890       my @newsections;
2891
2892       # after this, we'll have only two sections per DID:
2893       # Calls Summary and Calls Detail
2894       foreach my $section ( @sections ) {
2895         if($section->{'post_total'}) {
2896             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
2897             $section->{'total_line_generator'} = sub { '' };
2898             $section->{'total_generator'} = sub { '' };
2899             $section->{'header_generator'} = sub { '' };
2900             $section->{'description_generator'} = '';
2901             push @newsections, $section;
2902             my %calls_detail = %$section;
2903             $calls_detail{'post_total'} = '';
2904             $calls_detail{'sort_weight'} = '';
2905             $calls_detail{'description_generator'} = sub { '' };
2906             $calls_detail{'header_generator'} = sub {
2907                 return ' & Date/Time & Called Number & Duration & Price'
2908                     if $format eq 'latex';
2909                 '';
2910             };
2911             $calls_detail{'description'} = 'Calls Detail: '
2912                                                     . $section->{'phonenum'};
2913             push @newsections, \%calls_detail;  
2914         }
2915       }
2916
2917       # after this, each usage class is collapsed/summarized into a single
2918       # line under the Calls Summary section
2919       foreach my $newsection ( @newsections ) {
2920         if($newsection->{'post_total'}) { # this means Calls Summary
2921             foreach my $section ( @sections ) {
2922                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
2923                                 && !$section->{'post_total'});
2924                 my $newdesc = $section->{'description'};
2925                 my $tn = $section->{'phonenum'};
2926                 $newdesc =~ s/$tn//g;
2927                 my $line = {  ext_description => [],
2928                               pkgnum => '',
2929                               ref => '',
2930                               quantity => '',
2931                               calls => $section->{'calls'},
2932                               section => $newsection,
2933                               duration => $section->{'duration'},
2934                               description => $newdesc,
2935                               amount => sprintf("%.2f",$section->{'amount'}),
2936                               product_code => 'N/A',
2937                             };
2938                 push @newlines, $line;
2939             }
2940         }
2941       }
2942
2943       # after this, Calls Details is populated with all CDRs
2944       foreach my $newsection ( @newsections ) {
2945         if(!$newsection->{'post_total'}) { # this means Calls Details
2946             foreach my $line ( @lines ) {
2947                 next unless (scalar(@{$line->{'ext_description'}}) &&
2948                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
2949                             );
2950                 my @extdesc = @{$line->{'ext_description'}};
2951                 my @newextdesc;
2952                 foreach my $extdesc ( @extdesc ) {
2953                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
2954                     push @newextdesc, $extdesc;
2955                 }
2956                 $line->{'ext_description'} = \@newextdesc;
2957                 $line->{'section'} = $newsection;
2958                 push @newlines, $line;
2959             }
2960         }
2961       }
2962
2963       return(\@newsections, \@newlines);
2964   }
2965
2966   return(\@sections, \@lines);
2967
2968 }
2969
2970 sub _items_previous {
2971   my $self = shift;
2972   my $conf = $self->conf;
2973   my $cust_main = $self->cust_main;
2974   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2975   my @b = ();
2976   foreach ( @pr_cust_bill ) {
2977     my $date = $conf->exists('invoice_show_prior_due_date')
2978                ? 'due '. $_->due_date2str($date_format)
2979                : time2str($date_format, $_->_date);
2980     push @b, {
2981       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
2982       #'pkgpart'     => 'N/A',
2983       'pkgnum'      => 'N/A',
2984       'amount'      => sprintf("%.2f", $_->owed),
2985     };
2986   }
2987   @b;
2988
2989   #{
2990   #    'description'     => 'Previous Balance',
2991   #    #'pkgpart'         => 'N/A',
2992   #    'pkgnum'          => 'N/A',
2993   #    'amount'          => sprintf("%10.2f", $pr_total ),
2994   #    'ext_description' => [ map {
2995   #                                 "Invoice ". $_->invnum.
2996   #                                 " (". time2str("%x",$_->_date). ") ".
2997   #                                 sprintf("%10.2f", $_->owed)
2998   #                         } @pr_cust_bill ],
2999
3000   #};
3001 }
3002
3003 sub _items_credits {
3004   my( $self, %opt ) = @_;
3005   my $trim_len = $opt{'trim_len'} || 60;
3006
3007   my @b;
3008   #credits
3009   my @objects;
3010   if ( $self->conf->exists('previous_balance-payments_since') ) {
3011     my $date = 0;
3012     $date = $self->previous_bill->_date if $self->previous_bill;
3013     @objects = qsearch('cust_credit', {
3014         'custnum' => $self->custnum,
3015         '_date'   => {op => '>=', value => $date},
3016       });
3017       # hard to do this in the qsearch...
3018     @objects = grep { $_->_date < $self->_date } @objects;
3019   } else {
3020     @objects = $self->cust_credited;
3021   }
3022
3023   foreach my $obj ( @objects ) {
3024     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
3025
3026     my $reason = substr($cust_credit->reason, 0, $trim_len);
3027     $reason .= '...' if length($reason) < length($cust_credit->reason);
3028     $reason = " ($reason) " if $reason;
3029
3030     push @b, {
3031       #'description' => 'Credit ref\#'. $_->crednum.
3032       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
3033       #                 $reason,
3034       'description' => $self->mt('Credit applied').' '.
3035                        time2str($date_format,$obj->_date). $reason,
3036       'amount'      => sprintf("%.2f",$obj->amount),
3037     };
3038   }
3039
3040   @b;
3041
3042 }
3043
3044 sub _items_payments {
3045   my $self = shift;
3046
3047   my @b;
3048   my $detailed = $self->conf->exists('invoice_payment_details');
3049   my @objects;
3050   if ( $self->conf->exists('previous_balance-payments_since') ) {
3051     my $date = 0;
3052     $date = $self->previous_bill->_date if $self->previous_bill;
3053     @objects = qsearch('cust_pay', {
3054         'custnum' => $self->custnum,
3055         '_date'   => {op => '>=', value => $date},
3056       });
3057     @objects = grep { $_->_date < $self->_date } @objects;
3058   } else {
3059     @objects = $self->cust_bill_pay;
3060   }
3061
3062   foreach my $obj (@objects) {
3063     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
3064     my $desc = $self->mt('Payment received').' '.
3065                time2str($date_format, $cust_pay->_date );
3066     $desc .= $self->mt(' via ' . $cust_pay->payby_payinfo_pretty)
3067       if $detailed;
3068
3069     push @b, {
3070       'description' => $desc,
3071       'amount'      => sprintf("%.2f", $obj->amount )
3072     };
3073   }
3074
3075   @b;
3076
3077 }
3078
3079 =item call_details [ OPTION => VALUE ... ]
3080
3081 Returns an array of CSV strings representing the call details for this invoice
3082 The only option available is the boolean prepend_billed_number
3083
3084 =cut
3085
3086 sub call_details {
3087   my ($self, %opt) = @_;
3088
3089   my $format_function = sub { shift };
3090
3091   if ($opt{prepend_billed_number}) {
3092     $format_function = sub {
3093       my $detail = shift;
3094       my $row = shift;
3095
3096       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3097       
3098     };
3099   }
3100
3101   my @details = map { $_->details( 'format_function' => $format_function,
3102                                    'escape_function' => sub{ return() },
3103                                  )
3104                     }
3105                   grep { $_->pkgnum }
3106                   $self->cust_bill_pkg;
3107   my $header = $details[0];
3108   ( $header, grep { $_ ne $header } @details );
3109 }
3110
3111
3112 =back
3113
3114 =head1 SUBROUTINES
3115
3116 =over 4
3117
3118 =item process_reprint
3119
3120 =cut
3121
3122 sub process_reprint {
3123   process_re_X('print', @_);
3124 }
3125
3126 =item process_reemail
3127
3128 =cut
3129
3130 sub process_reemail {
3131   process_re_X('email', @_);
3132 }
3133
3134 =item process_refax
3135
3136 =cut
3137
3138 sub process_refax {
3139   process_re_X('fax', @_);
3140 }
3141
3142 =item process_reftp
3143
3144 =cut
3145
3146 sub process_reftp {
3147   process_re_X('ftp', @_);
3148 }
3149
3150 =item respool
3151
3152 =cut
3153
3154 sub process_respool {
3155   process_re_X('spool', @_);
3156 }
3157
3158 use Storable qw(thaw);
3159 use Data::Dumper;
3160 use MIME::Base64;
3161 sub process_re_X {
3162   my( $method, $job ) = ( shift, shift );
3163   warn "$me process_re_X $method for job $job\n" if $DEBUG;
3164
3165   my $param = thaw(decode_base64(shift));
3166   warn Dumper($param) if $DEBUG;
3167
3168   re_X(
3169     $method,
3170     $job,
3171     %$param,
3172   );
3173
3174 }
3175
3176 sub re_X {
3177   # spool_invoice ftp_invoice fax_invoice print_invoice
3178   my($method, $job, %param ) = @_;
3179   if ( $DEBUG ) {
3180     warn "re_X $method for job $job with param:\n".
3181          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
3182   }
3183
3184   #some false laziness w/search/cust_bill.html
3185   my $distinct = '';
3186   my $orderby = 'ORDER BY cust_bill._date';
3187
3188   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
3189
3190   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3191      
3192   my @cust_bill = qsearch( {
3193     #'select'    => "cust_bill.*",
3194     'table'     => 'cust_bill',
3195     'addl_from' => $addl_from,
3196     'hashref'   => {},
3197     'extra_sql' => $extra_sql,
3198     'order_by'  => $orderby,
3199     'debug' => 1,
3200   } );
3201
3202   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3203
3204   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3205     if $DEBUG;
3206
3207   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3208   foreach my $cust_bill ( @cust_bill ) {
3209     $cust_bill->$method();
3210
3211     if ( $job ) { #progressbar foo
3212       $num++;
3213       if ( time - $min_sec > $last ) {
3214         my $error = $job->update_statustext(
3215           int( 100 * $num / scalar(@cust_bill) )
3216         );
3217         die $error if $error;
3218         $last = time;
3219       }
3220     }
3221
3222   }
3223
3224 }
3225
3226 =back
3227
3228 =head1 CLASS METHODS
3229
3230 =over 4
3231
3232 =item owed_sql
3233
3234 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3235
3236 =cut
3237
3238 sub owed_sql {
3239   my ($class, $start, $end) = @_;
3240   'charged - '. 
3241     $class->paid_sql($start, $end). ' - '. 
3242     $class->credited_sql($start, $end);
3243 }
3244
3245 =item net_sql
3246
3247 Returns an SQL fragment to retreive the net amount (charged minus credited).
3248
3249 =cut
3250
3251 sub net_sql {
3252   my ($class, $start, $end) = @_;
3253   'charged - '. $class->credited_sql($start, $end);
3254 }
3255
3256 =item paid_sql
3257
3258 Returns an SQL fragment to retreive the amount paid against this invoice.
3259
3260 =cut
3261
3262 sub paid_sql {
3263   my ($class, $start, $end) = @_;
3264   $start &&= "AND cust_bill_pay._date <= $start";
3265   $end   &&= "AND cust_bill_pay._date > $end";
3266   $start = '' unless defined($start);
3267   $end   = '' unless defined($end);
3268   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3269        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
3270 }
3271
3272 =item credited_sql
3273
3274 Returns an SQL fragment to retreive the amount credited against this invoice.
3275
3276 =cut
3277
3278 sub credited_sql {
3279   my ($class, $start, $end) = @_;
3280   $start &&= "AND cust_credit_bill._date <= $start";
3281   $end   &&= "AND cust_credit_bill._date >  $end";
3282   $start = '' unless defined($start);
3283   $end   = '' unless defined($end);
3284   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3285        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
3286 }
3287
3288 =item due_date_sql
3289
3290 Returns an SQL fragment to retrieve the due date of an invoice.
3291 Currently only supported on PostgreSQL.
3292
3293 =cut
3294
3295 sub due_date_sql {
3296   my $conf = new FS::Conf;
3297 'COALESCE(
3298   SUBSTRING(
3299     COALESCE(
3300       cust_bill.invoice_terms,
3301       cust_main.invoice_terms,
3302       \''.($conf->config('invoice_default_terms') || '').'\'
3303     ), E\'Net (\\\\d+)\'
3304   )::INTEGER, 0
3305 ) * 86400 + cust_bill._date'
3306 }
3307
3308 =item search_sql_where HASHREF
3309
3310 Class method which returns an SQL WHERE fragment to search for parameters
3311 specified in HASHREF.  Valid parameters are
3312
3313 =over 4
3314
3315 =item _date
3316
3317 List reference of start date, end date, as UNIX timestamps.
3318
3319 =item invnum_min
3320
3321 =item invnum_max
3322
3323 =item agentnum
3324
3325 =item charged
3326
3327 List reference of charged limits (exclusive).
3328
3329 =item owed
3330
3331 List reference of charged limits (exclusive).
3332
3333 =item open
3334
3335 flag, return open invoices only
3336
3337 =item net
3338
3339 flag, return net invoices only
3340
3341 =item days
3342
3343 =item newest_percust
3344
3345 =back
3346
3347 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3348
3349 =cut
3350
3351 sub search_sql_where {
3352   my($class, $param) = @_;
3353   if ( $DEBUG ) {
3354     warn "$me search_sql_where called with params: \n".
3355          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
3356   }
3357
3358   my @search = ();
3359
3360   #agentnum
3361   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3362     push @search, "cust_main.agentnum = $1";
3363   }
3364
3365   #refnum
3366   if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
3367     push @search, "cust_main.refnum = $1";
3368   }
3369
3370   #custnum
3371   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
3372     push @search, "cust_bill.custnum = $1";
3373   }
3374
3375   #customer classnum
3376   if ( $param->{'cust_classnum'} ) {
3377     my $classnums = $param->{'cust_classnum'};
3378     $classnums = [ $classnums ] if !ref($classnums);
3379     $classnums = [ grep /^\d+$/, @$classnums ];
3380     push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
3381       if @$classnums;
3382   }
3383
3384   #_date
3385   if ( $param->{_date} ) {
3386     my($beginning, $ending) = @{$param->{_date}};
3387
3388     push @search, "cust_bill._date >= $beginning",
3389                   "cust_bill._date <  $ending";
3390   }
3391
3392   #invnum
3393   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3394     push @search, "cust_bill.invnum >= $1";
3395   }
3396   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3397     push @search, "cust_bill.invnum <= $1";
3398   }
3399
3400   #charged
3401   if ( $param->{charged} ) {
3402     my @charged = ref($param->{charged})
3403                     ? @{ $param->{charged} }
3404                     : ($param->{charged});
3405
3406     push @search, map { s/^charged/cust_bill.charged/; $_; }
3407                       @charged;
3408   }
3409
3410   my $owed_sql = FS::cust_bill->owed_sql;
3411
3412   #owed
3413   if ( $param->{owed} ) {
3414     my @owed = ref($param->{owed})
3415                  ? @{ $param->{owed} }
3416                  : ($param->{owed});
3417     push @search, map { s/^owed/$owed_sql/; $_; }
3418                       @owed;
3419   }
3420
3421   #open/net flags
3422   push @search, "0 != $owed_sql"
3423     if $param->{'open'};
3424   push @search, '0 != '. FS::cust_bill->net_sql
3425     if $param->{'net'};
3426
3427   #days
3428   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3429     if $param->{'days'};
3430
3431   #newest_percust
3432   if ( $param->{'newest_percust'} ) {
3433
3434     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3435     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3436
3437     my @newest_where = map { my $x = $_;
3438                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
3439                              $x;
3440                            }
3441                            grep ! /^cust_main./, @search;
3442     my $newest_where = scalar(@newest_where)
3443                          ? ' AND '. join(' AND ', @newest_where)
3444                          : '';
3445
3446
3447     push @search, "cust_bill._date = (
3448       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3449         WHERE newest_cust_bill.custnum = cust_bill.custnum
3450           $newest_where
3451     )";
3452
3453   }
3454
3455   #promised_date - also has an option to accept nulls
3456   if ( $param->{promised_date} ) {
3457     my($beginning, $ending, $null) = @{$param->{promised_date}};
3458
3459     push @search, "(( cust_bill.promised_date >= $beginning AND ".
3460                     "cust_bill.promised_date <  $ending )" .
3461                     ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
3462   }
3463
3464   #agent virtualization
3465   my $curuser = $FS::CurrentUser::CurrentUser;
3466   if ( $curuser->username eq 'fs_queue'
3467        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3468     my $username = $1;
3469     my $newuser = qsearchs('access_user', {
3470       'username' => $username,
3471       'disabled' => '',
3472     } );
3473     if ( $newuser ) {
3474       $curuser = $newuser;
3475     } else {
3476       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3477     }
3478   }
3479   push @search, $curuser->agentnums_sql;
3480
3481   join(' AND ', @search );
3482
3483 }
3484
3485 =back
3486
3487 =head1 BUGS
3488
3489 The delete method.
3490
3491 =head1 SEE ALSO
3492
3493 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3494 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3495 documentation.
3496
3497 =cut
3498
3499 1;
3500