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