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