bb6949a7368f2bd71f7dda85d906f0f4bf6f41a2
[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_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 send HASHREF
1035
1036 Sends this invoice to the destinations configured for this customer: sends
1037 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
1038
1039 Options can be passed as a hashref.  Positional parameters are no longer
1040 allowed.
1041
1042 I<template>: a suffix for alternate invoices
1043
1044 I<agentnum>: obsolete, now does nothing.
1045
1046 I<from> overrides the default email invoice From: address.
1047
1048 I<amount>: obsolete, does nothing
1049
1050 I<notice_name> overrides "Invoice" as the name of the sent document 
1051 (templates from 10/2009 or newer required).
1052
1053 I<lpr> overrides the system 'lpr' option as the command to print a document
1054 from standard input.
1055
1056 =cut
1057
1058 sub send {
1059   my $self = shift;
1060   my $opt = ref($_[0]) ? $_[0] : +{ @_ };
1061   my $conf = $self->conf;
1062
1063   my $cust_main = $self->cust_main;
1064
1065   my @invoicing_list = $cust_main->invoicing_list;
1066
1067   $self->email($opt)
1068     if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1069     && ! $cust_main->invoice_noemail;
1070
1071   $self->print($opt)
1072     if grep { $_ eq 'POST' } @invoicing_list; #postal
1073
1074   #this has never been used post-$ORIGINAL_ISP afaik
1075   $self->fax_invoice($opt)
1076     if grep { $_ eq 'FAX' } @invoicing_list; #fax
1077
1078   '';
1079
1080 }
1081
1082 sub email {
1083   my $self = shift;
1084   my $opt = shift || {};
1085   if ($opt and !ref($opt)) {
1086     die ref($self). '->email called with positional parameters';
1087   }
1088
1089   my $conf = $self->conf;
1090
1091   my $from = delete $opt->{from};
1092
1093   # this is where we set the From: address
1094   $from ||= $self->_agent_invoice_from ||    #XXX should go away
1095             $conf->invoice_from_full( $self->cust_main->agentnum );
1096
1097   my @invoicing_list = $self->cust_main->invoicing_list_emailonly;
1098
1099   if ( ! @invoicing_list ) { #no recipients
1100     if ( $conf->exists('cust_bill-no_recipients-error') ) {
1101       die 'No recipients for customer #'. $self->custnum;
1102     } else {
1103       #default: better to notify this person than silence
1104       @invoicing_list = ($from);
1105     }
1106   }
1107
1108   $self->SUPER::email( {
1109     'from' => $from,
1110     'to'   => \@invoicing_list,
1111     %$opt,
1112   });
1113
1114 }
1115
1116 #this stays here for now because its explicitly used as
1117 # FS::cust_bill::queueable_email
1118 sub queueable_email {
1119   my %opt = @_;
1120
1121   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1122     or die "invalid invoice number: " . $opt{invnum};
1123
1124   if ( $opt{mode} ) {
1125     $self->set('mode', $opt{mode});
1126   }
1127
1128   my %args = map {$_ => $opt{$_}} 
1129              grep { $opt{$_} }
1130               qw( from notice_name no_coupon template );
1131
1132   my $error = $self->email( \%args );
1133   die $error if $error;
1134
1135 }
1136
1137 sub email_subject {
1138   my $self = shift;
1139   my $conf = $self->conf;
1140
1141   #my $template = scalar(@_) ? shift : '';
1142   #per-template?
1143
1144   my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1145                 || 'Invoice';
1146
1147   my $cust_main = $self->cust_main;
1148   my $name = $cust_main->name;
1149   my $name_short = $cust_main->name_short;
1150   my $invoice_number = $self->invnum;
1151   my $invoice_date = $self->_date_pretty;
1152
1153   eval qq("$subject");
1154 }
1155
1156 =item lpr_data HASHREF
1157
1158 Returns the postscript or plaintext for this invoice as an arrayref.
1159
1160 Options must be passed as a hashref.  Positional parameters are no longer 
1161 allowed.
1162
1163 I<template>, if specified, is the name of a suffix for alternate invoices.
1164
1165 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1166
1167 =cut
1168
1169 sub lpr_data {
1170   my $self = shift;
1171   my $conf = $self->conf;
1172   my $opt = shift || {};
1173   if ($opt and !ref($opt)) {
1174     # nobody does this anyway
1175     die "FS::cust_bill::lpr_data called with positional parameters";
1176   }
1177
1178   my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1179   [ $self->$method( $opt ) ];
1180 }
1181
1182 =item print HASHREF
1183
1184 Prints this invoice.
1185
1186 Options must be passed as a hashref.
1187
1188 I<template>, if specified, is the name of a suffix for alternate invoices.
1189
1190 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1191
1192 =cut
1193
1194 sub print {
1195   my $self = shift;
1196   return if $self->hide;
1197   my $conf = $self->conf;
1198   my $opt = shift || {};
1199   if ($opt and !ref($opt)) {
1200     die "FS::cust_bill::print called with positional parameters";
1201   }
1202
1203   my $lpr = delete $opt->{lpr};
1204   if($conf->exists('invoice_print_pdf')) {
1205     # Add the invoice to the current batch.
1206     $self->batch_invoice($opt);
1207   }
1208   else {
1209     do_print(
1210       $self->lpr_data($opt),
1211       'agentnum' => $self->cust_main->agentnum,
1212       'lpr'      => $lpr,
1213     );
1214   }
1215 }
1216
1217 =item fax_invoice HASHREF
1218
1219 Faxes this invoice.
1220
1221 Options must be passed as a hashref.
1222
1223 I<template>, if specified, is the name of a suffix for alternate invoices.
1224
1225 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1226
1227 =cut
1228
1229 sub fax_invoice {
1230   my $self = shift;
1231   return if $self->hide;
1232   my $conf = $self->conf;
1233   my $opt = shift || {};
1234   if ($opt and !ref($opt)) {
1235     die "FS::cust_bill::fax_invoice called with positional parameters";
1236   }
1237
1238   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1239     unless $conf->exists('invoice_latex');
1240
1241   my $dialstring = $self->cust_main->getfield('fax');
1242   #Check $dialstring?
1243
1244   my $error = send_fax( 'docdata'    => $self->lpr_data($opt),
1245                         'dialstring' => $dialstring,
1246                       );
1247   die $error if $error;
1248
1249 }
1250
1251 =item batch_invoice [ HASHREF ]
1252
1253 Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
1254 isn't an open batch, one will be created.
1255
1256 HASHREF may contain any options to be passed to C<print_pdf>.
1257
1258 =cut
1259
1260 sub batch_invoice {
1261   my ($self, $opt) = @_;
1262   my $bill_batch = $self->get_open_bill_batch;
1263   my $cust_bill_batch = FS::cust_bill_batch->new({
1264       batchnum => $bill_batch->batchnum,
1265       invnum   => $self->invnum,
1266   });
1267   return $cust_bill_batch->insert($opt);
1268 }
1269
1270 =item get_open_batch
1271
1272 Returns the currently open batch as an FS::bill_batch object, creating a new
1273 one if necessary.  (A per-agent batch if invoice_print_pdf-spoolagent is
1274 enabled)
1275
1276 =cut
1277
1278 sub get_open_bill_batch {
1279   my $self = shift;
1280   my $conf = $self->conf;
1281   my $hashref = { status => 'O' };
1282   $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1283                              ? $self->cust_main->agentnum
1284                              : '';
1285   my $batch = qsearchs('bill_batch', $hashref);
1286   return $batch if $batch;
1287   $batch = FS::bill_batch->new($hashref);
1288   my $error = $batch->insert;
1289   die $error if $error;
1290   return $batch;
1291 }
1292
1293 =item ftp_invoice [ TEMPLATENAME ] 
1294
1295 Sends this invoice data via FTP.
1296
1297 TEMPLATENAME is unused?
1298
1299 =cut
1300
1301 sub ftp_invoice {
1302   my $self = shift;
1303   my $conf = $self->conf;
1304   my $template = scalar(@_) ? shift : '';
1305
1306   $self->send_csv(
1307     'protocol'   => 'ftp',
1308     'server'     => $conf->config('cust_bill-ftpserver'),
1309     'username'   => $conf->config('cust_bill-ftpusername'),
1310     'password'   => $conf->config('cust_bill-ftppassword'),
1311     'dir'        => $conf->config('cust_bill-ftpdir'),
1312     'format'     => $conf->config('cust_bill-ftpformat'),
1313   );
1314 }
1315
1316 =item spool_invoice [ TEMPLATENAME ] 
1317
1318 Spools this invoice data (see L<FS::spool_csv>)
1319
1320 TEMPLATENAME is unused?
1321
1322 =cut
1323
1324 sub spool_invoice {
1325   my $self = shift;
1326   my $conf = $self->conf;
1327   my $template = scalar(@_) ? shift : '';
1328
1329   $self->spool_csv(
1330     'format'       => $conf->config('cust_bill-spoolformat'),
1331     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1332   );
1333 }
1334
1335 =item send_csv OPTION => VALUE, ...
1336
1337 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1338
1339 Options are:
1340
1341 protocol - currently only "ftp"
1342 server
1343 username
1344 password
1345 dir
1346
1347 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1348 and YYMMDDHHMMSS is a timestamp.
1349
1350 See L</print_csv> for a description of the output format.
1351
1352 =cut
1353
1354 sub send_csv {
1355   my($self, %opt) = @_;
1356
1357   #create file(s)
1358
1359   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1360   mkdir $spooldir, 0700 unless -d $spooldir;
1361
1362   # don't localize dates here, they're a defined format
1363   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1364   my $file = "$spooldir/$tracctnum.csv";
1365   
1366   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1367
1368   open(CSV, ">$file") or die "can't open $file: $!";
1369   print CSV $header;
1370
1371   print CSV $detail;
1372
1373   close CSV;
1374
1375   my $net;
1376   if ( $opt{protocol} eq 'ftp' ) {
1377     eval "use Net::FTP;";
1378     die $@ if $@;
1379     $net = Net::FTP->new($opt{server}) or die @$;
1380   } else {
1381     die "unknown protocol: $opt{protocol}";
1382   }
1383
1384   $net->login( $opt{username}, $opt{password} )
1385     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1386
1387   $net->binary or die "can't set binary mode";
1388
1389   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1390
1391   $net->put($file) or die "can't put $file: $!";
1392
1393   $net->quit;
1394
1395   unlink $file;
1396
1397 }
1398
1399 =item spool_csv
1400
1401 Spools CSV invoice data.
1402
1403 Options are:
1404
1405 =over 4
1406
1407 =item format - any of FS::Misc::::Invoicing::spool_formats
1408
1409 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
1410 customer has the corresponding invoice destinations set (see
1411 L<FS::cust_main_invoice>).
1412
1413 =item agent_spools - if set to a true value, will spool to per-agent files
1414 rather than a single global file
1415
1416 =item upload_targetnum - if set to a target (see L<FS::upload_target>), will
1417 append to that spool.  L<FS::Cron::upload> will then send the spool file to
1418 that destination.
1419
1420 =item balanceover - if set, only spools the invoice if the total amount owed on
1421 this invoice and all older invoices is greater than the specified amount.
1422
1423 =item time - the "current time".  Controls the printing of past due messages
1424 in the ICS format.
1425
1426 =back
1427
1428 =cut
1429
1430 sub spool_csv {
1431   my($self, %opt) = @_;
1432
1433   my $time = $opt{'time'} || time;
1434   my $cust_main = $self->cust_main;
1435
1436   if ( $opt{'dest'} ) {
1437     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1438                              $cust_main->invoicing_list;
1439     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1440                      || ! keys %invoicing_list;
1441   }
1442
1443   if ( $opt{'balanceover'} ) {
1444     return 'N/A'
1445       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1446   }
1447
1448   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1449   mkdir $spooldir, 0700 unless -d $spooldir;
1450
1451   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', $time);
1452
1453   my $file;
1454   if ( $opt{'agent_spools'} ) {
1455     $file = 'agentnum'.$cust_main->agentnum;
1456   } else {
1457     $file = 'spool';
1458   }
1459
1460   if ( $opt{'upload_targetnum'} ) {
1461     $spooldir .= '/target'.$opt{'upload_targetnum'};
1462     mkdir $spooldir, 0700 unless -d $spooldir;
1463   } # otherwise it just goes into export.xxx/cust_bill
1464
1465   if ( lc($opt{'format'}) eq 'billco' ) {
1466     $file .= '-header';
1467   }
1468
1469   $file = "$spooldir/$file.csv";
1470   
1471   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
1472
1473   open(CSV, ">>$file") or die "can't open $file: $!";
1474   flock(CSV, LOCK_EX);
1475   seek(CSV, 0, 2);
1476
1477   print CSV $header;
1478
1479   if ( lc($opt{'format'}) eq 'billco' ) {
1480
1481     flock(CSV, LOCK_UN);
1482     close CSV;
1483
1484     $file =~ s/-header.csv$/-detail.csv/;
1485
1486     open(CSV,">>$file") or die "can't open $file: $!";
1487     flock(CSV, LOCK_EX);
1488     seek(CSV, 0, 2);
1489   }
1490
1491   print CSV $detail if defined($detail);
1492
1493   flock(CSV, LOCK_UN);
1494   close CSV;
1495
1496   return '';
1497
1498 }
1499
1500 =item print_csv OPTION => VALUE, ...
1501
1502 Returns CSV data for this invoice.
1503
1504 Options are:
1505
1506 format - 'default', 'billco', 'oneline', 'bridgestone'
1507
1508 Returns a list consisting of two scalars.  The first is a single line of CSV
1509 header information for this invoice.  The second is one or more lines of CSV
1510 detail information for this invoice.
1511
1512 If I<format> is not specified or "default", the fields of the CSV file are as
1513 follows:
1514
1515 record_type, invnum, custnum, _date, charged, first, last, company, address1, 
1516 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1517
1518 =over 4
1519
1520 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1521
1522 B<record_type> is C<cust_bill> for the initial header line only.  The
1523 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1524 fields are filled in.
1525
1526 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1527 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1528 are filled in.
1529
1530 =item invnum - invoice number
1531
1532 =item custnum - customer number
1533
1534 =item _date - invoice date
1535
1536 =item charged - total invoice amount
1537
1538 =item first - customer first name
1539
1540 =item last - customer first name
1541
1542 =item company - company name
1543
1544 =item address1 - address line 1
1545
1546 =item address2 - address line 1
1547
1548 =item city
1549
1550 =item state
1551
1552 =item zip
1553
1554 =item country
1555
1556 =item pkg - line item description
1557
1558 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1559
1560 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1561
1562 =item sdate - start date for recurring fee
1563
1564 =item edate - end date for recurring fee
1565
1566 =back
1567
1568 If I<format> is "billco", the fields of the header CSV file are as follows:
1569
1570   +-------------------------------------------------------------------+
1571   |                        FORMAT HEADER FILE                         |
1572   |-------------------------------------------------------------------|
1573   | Field | Description                   | Name       | Type | Width |
1574   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1575   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1576   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1577   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1578   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1579   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1580   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1581   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1582   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1583   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1584   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1585   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1586   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1587   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1588   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1589   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1590   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1591   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1592   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1593   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1594   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1595   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1596   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1597   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1598   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1599   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1600   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1601   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1602   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1603   +-------+-------------------------------+------------+------+-------+
1604
1605 If I<format> is "billco", the fields of the detail CSV file are as follows:
1606
1607                                   FORMAT FOR DETAIL FILE
1608         |                            |           |      |
1609   Field | Description                | Name      | Type | Width
1610   1     | N/A-Leave Empty            | RC        | CHAR |     2
1611   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1612   3     | Account Number             | TRACCTNUM | CHAR |    15
1613   4     | Invoice Number             | TRINVOICE | CHAR |    15
1614   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1615   6     | Transaction Detail         | DETAILS   | CHAR |   100
1616   7     | Amount                     | AMT       | NUM* |     9
1617   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1618   9     | Grouping Code              | GROUP     | CHAR |     2
1619   10    | User Defined               | ACCT CODE | CHAR |    15
1620
1621 If format is 'oneline', there is no detail file.  Each invoice has a 
1622 header line only, with the fields:
1623
1624 Agent number, agent name, customer number, first name, last name, address
1625 line 1, address line 2, city, state, zip, invoice date, invoice number,
1626 amount charged, amount due, previous balance, due date.
1627
1628 and then, for each line item, three columns containing the package number,
1629 description, and amount.
1630
1631 If format is 'bridgestone', there is no detail file.  Each invoice has a 
1632 header line with the following fields in a fixed-width format:
1633
1634 Customer number (in display format), date, name (first last), company,
1635 address 1, address 2, city, state, zip.
1636
1637 This is a mailing list format, and has no per-invoice fields.  To avoid
1638 sending redundant notices, the spooling event should have a "once" or 
1639 "once_percust_every" condition.
1640
1641 =cut
1642
1643 sub print_csv {
1644   my($self, %opt) = @_;
1645   
1646   eval "use Text::CSV_XS";
1647   die $@ if $@;
1648
1649   my $cust_main = $self->cust_main;
1650
1651   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1652   my $format = lc($opt{'format'});
1653
1654   my $time = $opt{'time'} || time;
1655
1656   my $tracctnum = ''; #leaking out from billco-specific sections :/
1657   if ( $format eq 'billco' ) {
1658
1659     my $account_num =
1660       $self->conf->config('billco-account_num', $cust_main->agentnum);
1661
1662     $tracctnum = $account_num eq 'display_custnum'
1663                    ? $cust_main->display_custnum
1664                    : $opt{'tracctnum'};
1665
1666     my $taxtotal = 0;
1667     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1668
1669     my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
1670
1671     my( $previous_balance, @unused ) = $self->previous; #previous balance
1672
1673     my $pmt_cr_applied = 0;
1674     $pmt_cr_applied += $_->{'amount'}
1675       foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
1676
1677     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1678
1679     $csv->combine(
1680       '',                         #  1 | N/A-Leave Empty               CHAR   2
1681       '',                         #  2 | N/A-Leave Empty               CHAR  15
1682       $tracctnum,                 #  3 | Transaction Account No        CHAR  15
1683       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1684       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1685       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1686       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1687       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1688       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1689       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1690       '',                         # 10 | Ancillary Billing Information CHAR  30
1691       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1692       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1693
1694       # XXX ?
1695       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1696
1697       # XXX ?
1698       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1699
1700       $previous_balance,          # 15 | Previous Balance              NUM*   9
1701       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1702       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1703       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1704       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1705       '',                         # 20 | 30 Day Aging                  NUM*   9
1706       '',                         # 21 | 60 Day Aging                  NUM*   9
1707       '',                         # 22 | 90 Day Aging                  NUM*   9
1708       'N',                        # 23 | Y/N                           CHAR   1
1709       '',                         # 24 | Remittance automation         CHAR 100
1710       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1711       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1712       '0',                        # 27 | Federal Tax***                NUM*   9
1713       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1714       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1715     );
1716
1717   } elsif ( $format eq 'oneline' ) { #name
1718   
1719     my ($previous_balance) = $self->previous; 
1720     $previous_balance = sprintf('%.2f', $previous_balance);
1721     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1722     my @items = map {
1723                       $_->{pkgnum},
1724                       $_->{description},
1725                       $_->{amount}
1726                     }
1727                   $self->_items_pkg, #_items_nontax?  no sections or anything
1728                                      # with this format
1729                   $self->_items_tax;
1730
1731     $csv->combine(
1732       $cust_main->agentnum,
1733       $cust_main->agent->agent,
1734       $self->custnum,
1735       $cust_main->first,
1736       $cust_main->last,
1737       $cust_main->company,
1738       $cust_main->address1,
1739       $cust_main->address2,
1740       $cust_main->city,
1741       $cust_main->state,
1742       $cust_main->zip,
1743
1744       # invoice fields
1745       time2str("%x", $self->_date),
1746       $self->invnum,
1747       $self->charged,
1748       $totaldue,
1749       $previous_balance,
1750       $self->due_date2str("%x"),
1751
1752       @items,
1753     );
1754
1755   } elsif ( $format eq 'bridgestone' ) {
1756
1757     # bypass the CSV stuff and just return this
1758     my $longdate = time2str('%B %d, %Y', $time); #current time, right?
1759     my $zip = $cust_main->zip;
1760     $zip =~ s/\D//;
1761     my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
1762       || '';
1763     return (
1764       sprintf(
1765         "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
1766         $prefix,
1767         $cust_main->display_custnum,
1768         $longdate,
1769         uc(substr($cust_main->contact_firstlast,0,30)),
1770         uc(substr($cust_main->company          ,0,30)),
1771         uc(substr($cust_main->address1         ,0,30)),
1772         uc(substr($cust_main->address2         ,0,30)),
1773         uc(substr($cust_main->city             ,0,20)),
1774         uc($cust_main->state),
1775         $zip
1776       ),
1777       '' #detail
1778       );
1779
1780   } elsif ( $format eq 'ics' ) {
1781
1782     my $bill = $cust_main->bill_location;
1783     my $zip = $bill->zip;
1784     my $zip4 = '';
1785
1786     $zip =~ s/\D//;
1787     if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
1788       $zip = $1;
1789       $zip4 = $2;
1790     }
1791
1792     # minor false laziness with print_generic
1793     my ($previous_balance) = $self->previous;
1794     my $balance_due = $self->owed + $previous_balance;
1795     my $payment_total = sum(0, map { $_->{'amount'} } $self->_items_payments);
1796     my $credit_total  = sum(0, map { $_->{'amount'} } $self->_items_credits);
1797
1798     my $past_due = '';
1799     if ( $self->due_date and $time >= $self->due_date ) {
1800       $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
1801     }
1802
1803     # again, bypass CSV
1804     my $header = sprintf(
1805       '%-10s%-30s%-48s%-2s%-50s%-30s%-30s%-25s%-2s%-5s%-4s%-8s%-8s%-10s%-10s%-10s%-10s%-10s%-10s%-480s%-35s',
1806       $cust_main->display_custnum, #BID
1807       uc($cust_main->first), #FNAME
1808       uc($cust_main->last), #LNAME
1809       '00', #BATCH, should this ever be anything else?
1810       uc($cust_main->company), #COMP
1811       uc($bill->address1), #STREET1
1812       uc($bill->address2), #STREET2
1813       uc($bill->city), #CITY
1814       uc($bill->state), #STATE
1815       $zip,
1816       $zip4,
1817       time2str('%Y%m%d', $self->_date), #BILL_DATE
1818       $self->due_date2str('%Y%m%d'), #DUE_DATE,
1819       ( map {sprintf('%0.2f', $_)}
1820         $balance_due, #AMNT_DUE
1821         $previous_balance, #PREV_BAL
1822         $payment_total, #PYMT_RCVD
1823         $credit_total, #CREDITS
1824         $previous_balance, #BEG_BAL--is this correct?
1825         $self->charged, #NEW_CHRG
1826       ),
1827       'img01', #MRKT_MSG?
1828       $past_due, #PAST_MSG
1829     );
1830
1831     my @details;
1832     my %svc_class = ('' => ''); # maybe cache this more persistently?
1833
1834     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1835
1836       my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
1837       my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
1838
1839       if ( $cust_pkg ) {
1840
1841         my @dates = ( $self->_date, undef );
1842         if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
1843           $dates[1] = $prev->sdate; #questionable
1844         }
1845
1846         # generate an 01 detail for each service
1847         my @svcs = $cust_pkg->h_cust_svc(@dates, 'I');
1848         foreach my $cust_svc ( @svcs ) {
1849           $show_pkgnum = ''; # hide it if we're showing svcnums
1850
1851           my $svcpart = $cust_svc->svcpart;
1852           if (!exists($svc_class{$svcpart})) {
1853             my $classnum = $cust_svc->part_svc->classnum;
1854             my $part_svc_class = FS::part_svc_class->by_key($classnum)
1855               if $classnum;
1856             $svc_class{$svcpart} = $part_svc_class ? 
1857                                    $part_svc_class->classname :
1858                                    '';
1859           }
1860
1861           my @h_label = $cust_svc->label(@dates, 'I');
1862           push @details, sprintf('01%-9s%-20s%-47s',
1863             $cust_svc->svcnum,
1864             $svc_class{$svcpart},
1865             $h_label[1],
1866           );
1867         } #foreach $cust_svc
1868       } #if $cust_pkg
1869
1870       my $desc = $cust_bill_pkg->desc; # itemdesc or part_pkg.pkg
1871       if ($cust_bill_pkg->recur > 0) {
1872         $desc .= ' '.time2str('%d-%b-%Y', $cust_bill_pkg->sdate).' to '.
1873                      time2str('%d-%b-%Y', $cust_bill_pkg->edate - 86400);
1874       }
1875       push @details, sprintf('02%-6s%-60s%-10s',
1876         $show_pkgnum,
1877         $desc,
1878         sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
1879       );
1880     } #foreach $cust_bill_pkg
1881
1882     # Tag this row so that we know whether this is one page (1), two pages
1883     # (2), # or "big" (B).  The tag will be stripped off before uploading.
1884     if ( scalar(@details) < 12 ) {
1885       push @details, '1';
1886     } elsif ( scalar(@details) < 58 ) {
1887       push @details, '2';
1888     } else {
1889       push @details, 'B';
1890     }
1891
1892     return join('', $header, @details, "\n");
1893
1894   } else { # default
1895   
1896     $csv->combine(
1897       'cust_bill',
1898       $self->invnum,
1899       $self->custnum,
1900       time2str("%x", $self->_date),
1901       sprintf("%.2f", $self->charged),
1902       ( map { $cust_main->getfield($_) }
1903           qw( first last company address1 address2 city state zip country ) ),
1904       map { '' } (1..5),
1905     ) or die "can't create csv";
1906   }
1907
1908   my $header = $csv->string. "\n";
1909
1910   my $detail = '';
1911   if ( lc($opt{'format'}) eq 'billco' ) {
1912
1913     my $lineseq = 0;
1914     my %items_opt = ( format => 'template',
1915                       escape_function => sub { shift } );
1916     # I don't know what characters billco actually tolerates in spool entries.
1917     # Text::CSV will take care of delimiters, though.
1918
1919     my @items = ( $self->_items_pkg(%items_opt),
1920                   $self->_items_fee(%items_opt) );
1921     foreach my $item (@items) {
1922
1923       my $description = $item->{'description'};
1924       if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
1925         $description .= ': ' . $item->{ext_description}[0];
1926       }
1927
1928       $csv->combine(
1929         '',                     #  1 | N/A-Leave Empty            CHAR   2
1930         '',                     #  2 | N/A-Leave Empty            CHAR  15
1931         $tracctnum,             #  3 | Account Number             CHAR  15
1932         $self->invnum,          #  4 | Invoice Number             CHAR  15
1933         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1934         $description,           #  6 | Transaction Detail         CHAR 100
1935         $item->{'amount'},      #  7 | Amount                     NUM*   9
1936         '',                     #  8 | Line Format Control**      CHAR   2
1937         '',                     #  9 | Grouping Code              CHAR   2
1938         '',                     # 10 | User Defined               CHAR  15
1939       );
1940
1941       $detail .= $csv->string. "\n";
1942
1943     }
1944
1945   } elsif ( lc($opt{'format'}) eq 'oneline' ) {
1946
1947     #do nothing
1948
1949   } else {
1950
1951     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1952
1953       my($pkg, $setup, $recur, $sdate, $edate);
1954       if ( $cust_bill_pkg->pkgnum ) {
1955       
1956         ($pkg, $setup, $recur, $sdate, $edate) = (
1957           $cust_bill_pkg->part_pkg->pkg,
1958           ( $cust_bill_pkg->setup != 0
1959             ? sprintf("%.2f", $cust_bill_pkg->setup )
1960             : '' ),
1961           ( $cust_bill_pkg->recur != 0
1962             ? sprintf("%.2f", $cust_bill_pkg->recur )
1963             : '' ),
1964           ( $cust_bill_pkg->sdate 
1965             ? time2str("%x", $cust_bill_pkg->sdate)
1966             : '' ),
1967           ($cust_bill_pkg->edate 
1968             ? time2str("%x", $cust_bill_pkg->edate)
1969             : '' ),
1970         );
1971   
1972       } else { #pkgnum tax
1973         next unless $cust_bill_pkg->setup != 0;
1974         $pkg = $cust_bill_pkg->desc;
1975         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1976         ( $sdate, $edate ) = ( '', '' );
1977       }
1978   
1979       $csv->combine(
1980         'cust_bill_pkg',
1981         $self->invnum,
1982         ( map { '' } (1..11) ),
1983         ($pkg, $setup, $recur, $sdate, $edate)
1984       ) or die "can't create csv";
1985
1986       $detail .= $csv->string. "\n";
1987
1988     }
1989
1990   }
1991
1992   ( $header, $detail );
1993
1994 }
1995
1996 =item comp
1997
1998 Pays this invoice with a compliemntary payment.  If there is an error,
1999 returns the error, otherwise returns false.
2000
2001 =cut
2002
2003 sub comp {
2004   my $self = shift;
2005   my $cust_pay = new FS::cust_pay ( {
2006     'invnum'   => $self->invnum,
2007     'paid'     => $self->owed,
2008     '_date'    => '',
2009     'payby'    => 'COMP',
2010     'payinfo'  => $self->cust_main->payinfo,
2011     'paybatch' => '',
2012   } );
2013   $cust_pay->insert;
2014 }
2015
2016 =item realtime_card
2017
2018 Attempts to pay this invoice with a credit card payment via a
2019 Business::OnlinePayment realtime gateway.  See
2020 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2021 for supported processors.
2022
2023 =cut
2024
2025 sub realtime_card {
2026   my $self = shift;
2027   $self->realtime_bop( 'CC', @_ );
2028 }
2029
2030 =item realtime_ach
2031
2032 Attempts to pay this invoice with an electronic check (ACH) payment via a
2033 Business::OnlinePayment realtime gateway.  See
2034 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2035 for supported processors.
2036
2037 =cut
2038
2039 sub realtime_ach {
2040   my $self = shift;
2041   $self->realtime_bop( 'ECHECK', @_ );
2042 }
2043
2044 =item realtime_lec
2045
2046 Attempts to pay this invoice with phone bill (LEC) payment via a
2047 Business::OnlinePayment realtime gateway.  See
2048 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2049 for supported processors.
2050
2051 =cut
2052
2053 sub realtime_lec {
2054   my $self = shift;
2055   $self->realtime_bop( 'LEC', @_ );
2056 }
2057
2058 sub realtime_bop {
2059   my( $self, $method ) = (shift,shift);
2060   my $conf = $self->conf;
2061   my %opt = @_;
2062
2063   my $cust_main = $self->cust_main;
2064   my $balance = $cust_main->balance;
2065   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2066   $amount = sprintf("%.2f", $amount);
2067   return "not run (balance $balance)" unless $amount > 0;
2068
2069   my $description = 'Internet Services';
2070   if ( $conf->exists('business-onlinepayment-description') ) {
2071     my $dtempl = $conf->config('business-onlinepayment-description');
2072
2073     my $agent_obj = $cust_main->agent
2074       or die "can't retreive agent for $cust_main (agentnum ".
2075              $cust_main->agentnum. ")";
2076     my $agent = $agent_obj->agent;
2077     my $pkgs = join(', ',
2078       map { $_->part_pkg->pkg }
2079         grep { $_->pkgnum } $self->cust_bill_pkg
2080     );
2081     $description = eval qq("$dtempl");
2082   }
2083
2084   $cust_main->realtime_bop($method, $amount,
2085     'description' => $description,
2086     'invnum'      => $self->invnum,
2087 #this didn't do what we want, it just calls apply_payments_and_credits
2088 #    'apply'       => 1,
2089     'apply_to_invoice' => 1,
2090     %opt,
2091  #what we want:
2092  #this changes application behavior: auto payments
2093                         #triggered against a specific invoice are now applied
2094                         #to that invoice instead of oldest open.
2095                         #seem okay to me...
2096   );
2097
2098 }
2099
2100 =item batch_card OPTION => VALUE...
2101
2102 Adds a payment for this invoice to the pending credit card batch (see
2103 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2104 runs the payment using a realtime gateway.
2105
2106 =cut
2107
2108 sub batch_card {
2109   my ($self, %options) = @_;
2110   my $cust_main = $self->cust_main;
2111
2112   $options{invnum} = $self->invnum;
2113   
2114   $cust_main->batch_card(%options);
2115 }
2116
2117 sub _agent_template {
2118   my $self = shift;
2119   $self->cust_main->agent_template;
2120 }
2121
2122 sub _agent_invoice_from {
2123   my $self = shift;
2124   $self->cust_main->agent_invoice_from;
2125 }
2126
2127 =item invoice_barcode DIR_OR_FALSE
2128
2129 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2130 it is taken as the temp directory where the PNG file will be generated and the
2131 PNG file name is returned. Otherwise, the PNG image itself is returned.
2132
2133 =cut
2134
2135 sub invoice_barcode {
2136     my ($self, $dir) = (shift,shift);
2137     
2138     my $gdbar = new GD::Barcode('Code39',$self->invnum);
2139         die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2140     my $gd = $gdbar->plot(Height => 30);
2141
2142     if($dir) {
2143         my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2144                            DIR      => $dir,
2145                            SUFFIX   => '.png',
2146                            UNLINK   => 0,
2147                          ) or die "can't open temp file: $!\n";
2148         print $bh $gd->png or die "cannot write barcode to file: $!\n";
2149         my $png_file = $bh->filename;
2150         close $bh;
2151         return $png_file;
2152     }
2153     return $gd->png;
2154 }
2155
2156 =item invnum_date_pretty
2157
2158 Returns a string with the invoice number and date, for example:
2159 "Invoice #54 (3/20/2008)".
2160
2161 Intended for back-end context, with regard to translation and date formatting.
2162
2163 =cut
2164
2165 #note: this uses _date_pretty_unlocalized because _date_pretty is too expensive
2166 # for backend use (and also does the wrong thing, localizing for end customer
2167 # instead of backoffice configured date format)
2168 sub invnum_date_pretty {
2169   my $self = shift;
2170   #$self->mt('Invoice #').
2171   'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
2172     $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
2173 }
2174
2175 #sub _items_extra_usage_sections {
2176 #  my $self = shift;
2177 #  my $escape = shift;
2178 #
2179 #  my %sections = ();
2180 #
2181 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
2182 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2183 #  {
2184 #    next unless $cust_bill_pkg->pkgnum > 0;
2185 #
2186 #    foreach my $section ( keys %usage_class ) {
2187 #
2188 #      my $usage = $cust_bill_pkg->usage($section);
2189 #
2190 #      next unless $usage && $usage > 0;
2191 #
2192 #      $sections{$section} ||= 0;
2193 #      $sections{$section} += $usage;
2194 #
2195 #    }
2196 #
2197 #  }
2198 #
2199 #  map { { 'description' => &{$escape}($_),
2200 #          'subtotal'    => $sections{$_},
2201 #          'summarized'  => '',
2202 #          'tax_section' => '',
2203 #        }
2204 #      }
2205 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
2206 #
2207 #}
2208
2209 sub _items_extra_usage_sections {
2210   my $self = shift;
2211   my $conf = $self->conf;
2212   my $escape = shift;
2213   my $format = shift;
2214
2215   my %sections = ();
2216   my %classnums = ();
2217   my %lines = ();
2218
2219   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2220
2221   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2222   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2223     next unless $cust_bill_pkg->pkgnum > 0;
2224
2225     foreach my $classnum ( keys %usage_class ) {
2226       my $section = $usage_class{$classnum}->classname;
2227       $classnums{$section} = $classnum;
2228
2229       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
2230         my $amount = $detail->amount;
2231         next unless $amount && $amount > 0;
2232  
2233         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
2234         $sections{$section}{amount} += $amount;  #subtotal
2235         $sections{$section}{calls}++;
2236         $sections{$section}{duration} += $detail->duration;
2237
2238         my $desc = $detail->regionname; 
2239         my $description = $desc;
2240         $description = substr($desc, 0, $maxlength). '...'
2241           if $format eq 'latex' && length($desc) > $maxlength;
2242
2243         $lines{$section}{$desc} ||= {
2244           description     => &{$escape}($description),
2245           #pkgpart         => $part_pkg->pkgpart,
2246           pkgnum          => $cust_bill_pkg->pkgnum,
2247           ref             => '',
2248           amount          => 0,
2249           calls           => 0,
2250           duration        => 0,
2251           #unit_amount     => $cust_bill_pkg->unitrecur,
2252           quantity        => $cust_bill_pkg->quantity,
2253           product_code    => 'N/A',
2254           ext_description => [],
2255         };
2256
2257         $lines{$section}{$desc}{amount} += $amount;
2258         $lines{$section}{$desc}{calls}++;
2259         $lines{$section}{$desc}{duration} += $detail->duration;
2260
2261       }
2262     }
2263   }
2264
2265   my %sectionmap = ();
2266   foreach (keys %sections) {
2267     my $usage_class = $usage_class{$classnums{$_}};
2268     $sectionmap{$_} = { 'description' => &{$escape}($_),
2269                         'amount'    => $sections{$_}{amount},    #subtotal
2270                         'calls'       => $sections{$_}{calls},
2271                         'duration'    => $sections{$_}{duration},
2272                         'summarized'  => '',
2273                         'tax_section' => '',
2274                         'sort_weight' => $usage_class->weight,
2275                         ( $usage_class->format
2276                           ? ( map { $_ => $usage_class->$_($format) }
2277                               qw( description_generator header_generator total_generator total_line_generator )
2278                             )
2279                           : ()
2280                         ), 
2281                       };
2282   }
2283
2284   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
2285                  values %sectionmap;
2286
2287   my @lines = ();
2288   foreach my $section ( keys %lines ) {
2289     foreach my $line ( keys %{$lines{$section}} ) {
2290       my $l = $lines{$section}{$line};
2291       $l->{section}     = $sectionmap{$section};
2292       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2293       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2294       push @lines, $l;
2295     }
2296   }
2297
2298   return(\@sections, \@lines);
2299
2300 }
2301
2302 sub _did_summary {
2303     my $self = shift;
2304     my $end = $self->_date;
2305
2306     # start at date of previous invoice + 1 second or 0 if no previous invoice
2307     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
2308     $start = 0 if !$start;
2309     $start++;
2310
2311     my $cust_main = $self->cust_main;
2312     my @pkgs = $cust_main->all_pkgs;
2313     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
2314         = (0,0,0,0,0);
2315     my @seen = ();
2316     foreach my $pkg ( @pkgs ) {
2317         my @h_cust_svc = $pkg->h_cust_svc($end);
2318         foreach my $h_cust_svc ( @h_cust_svc ) {
2319             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
2320             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
2321
2322             my $inserted = $h_cust_svc->date_inserted;
2323             my $deleted = $h_cust_svc->date_deleted;
2324             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
2325             my $phone_deleted;
2326             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
2327             
2328 # DID either activated or ported in; cannot be both for same DID simultaneously
2329             if ($inserted >= $start && $inserted <= $end && $phone_inserted
2330                 && (!$phone_inserted->lnp_status 
2331                     || $phone_inserted->lnp_status eq ''
2332                     || $phone_inserted->lnp_status eq 'native')) {
2333                 $num_activated++;
2334             }
2335             else { # this one not so clean, should probably move to (h_)svc_phone
2336                  my $phone_portedin = qsearchs( 'h_svc_phone',
2337                       { 'svcnum' => $h_cust_svc->svcnum, 
2338                         'lnp_status' => 'portedin' },  
2339                       FS::h_svc_phone->sql_h_searchs($end),  
2340                     );
2341                  $num_portedin++ if $phone_portedin;
2342             }
2343
2344 # DID either deactivated or ported out; cannot be both for same DID simultaneously
2345             if($deleted >= $start && $deleted <= $end && $phone_deleted
2346                 && (!$phone_deleted->lnp_status 
2347                     || $phone_deleted->lnp_status ne 'portingout')) {
2348                 $num_deactivated++;
2349             } 
2350             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
2351                 && $phone_deleted->lnp_status 
2352                 && $phone_deleted->lnp_status eq 'portingout') {
2353                 $num_portedout++;
2354             }
2355
2356             # increment usage minutes
2357         if ( $phone_inserted ) {
2358             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
2359             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
2360         }
2361         else {
2362             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
2363         }
2364
2365             # don't look at this service again
2366             push @seen, $h_cust_svc->svcnum;
2367         }
2368     }
2369
2370     $minutes = sprintf("%d", $minutes);
2371     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
2372         . "$num_deactivated  Ported-Out: $num_portedout ",
2373             "Total Minutes: $minutes");
2374 }
2375
2376 sub _items_accountcode_cdr {
2377     my $self = shift;
2378     my $escape = shift;
2379     my $format = shift;
2380
2381     my $section = { 'amount'        => 0,
2382                     'calls'         => 0,
2383                     'duration'      => 0,
2384                     'sort_weight'   => '',
2385                     'phonenum'      => '',
2386                     'description'   => 'Usage by Account Code',
2387                     'post_total'    => '',
2388                     'summarized'    => '',
2389                     'header'        => '',
2390                   };
2391     my @lines;
2392     my %accountcodes = ();
2393
2394     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2395         next unless $cust_bill_pkg->pkgnum > 0;
2396
2397         my @header = $cust_bill_pkg->details_header;
2398         next unless scalar(@header);
2399         $section->{'header'} = join(',',@header);
2400
2401         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2402
2403             $section->{'header'} = $detail->formatted('format' => $format)
2404                 if($detail->detail eq $section->{'header'}); 
2405       
2406             my $accountcode = $detail->accountcode;
2407             next unless $accountcode;
2408
2409             my $amount = $detail->amount;
2410             next unless $amount && $amount > 0;
2411
2412             $accountcodes{$accountcode} ||= {
2413                     description => $accountcode,
2414                     pkgnum      => '',
2415                     ref         => '',
2416                     amount      => 0,
2417                     calls       => 0,
2418                     duration    => 0,
2419                     quantity    => '',
2420                     product_code => 'N/A',
2421                     section     => $section,
2422                     ext_description => [ $section->{'header'} ],
2423                     detail_temp => [],
2424             };
2425
2426             $section->{'amount'} += $amount;
2427             $accountcodes{$accountcode}{'amount'} += $amount;
2428             $accountcodes{$accountcode}{calls}++;
2429             $accountcodes{$accountcode}{duration} += $detail->duration;
2430             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
2431         }
2432     }
2433
2434     foreach my $l ( values %accountcodes ) {
2435         $l->{amount} = sprintf( "%.2f", $l->{amount} );
2436         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
2437         foreach my $sorted_detail ( @sorted_detail ) {
2438             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
2439         }
2440         delete $l->{detail_temp};
2441         push @lines, $l;
2442     }
2443
2444     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
2445
2446     return ($section,\@sorted_lines);
2447 }
2448
2449 sub _items_svc_phone_sections {
2450   my $self = shift;
2451   my $conf = $self->conf;
2452   my $escape = shift;
2453   my $format = shift;
2454
2455   my %sections = ();
2456   my %classnums = ();
2457   my %lines = ();
2458
2459   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2460
2461   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2462   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
2463
2464   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2465     next unless $cust_bill_pkg->pkgnum > 0;
2466
2467     my @header = $cust_bill_pkg->details_header;
2468     next unless scalar(@header);
2469
2470     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2471
2472       my $phonenum = $detail->phonenum;
2473       next unless $phonenum;
2474
2475       my $amount = $detail->amount;
2476       next unless $amount && $amount > 0;
2477
2478       $sections{$phonenum} ||= { 'amount'      => 0,
2479                                  'calls'       => 0,
2480                                  'duration'    => 0,
2481                                  'sort_weight' => -1,
2482                                  'phonenum'    => $phonenum,
2483                                 };
2484       $sections{$phonenum}{amount} += $amount;  #subtotal
2485       $sections{$phonenum}{calls}++;
2486       $sections{$phonenum}{duration} += $detail->duration;
2487
2488       my $desc = $detail->regionname; 
2489       my $description = $desc;
2490       $description = substr($desc, 0, $maxlength). '...'
2491         if $format eq 'latex' && length($desc) > $maxlength;
2492
2493       $lines{$phonenum}{$desc} ||= {
2494         description     => &{$escape}($description),
2495         #pkgpart         => $part_pkg->pkgpart,
2496         pkgnum          => '',
2497         ref             => '',
2498         amount          => 0,
2499         calls           => 0,
2500         duration        => 0,
2501         #unit_amount     => '',
2502         quantity        => '',
2503         product_code    => 'N/A',
2504         ext_description => [],
2505       };
2506
2507       $lines{$phonenum}{$desc}{amount} += $amount;
2508       $lines{$phonenum}{$desc}{calls}++;
2509       $lines{$phonenum}{$desc}{duration} += $detail->duration;
2510
2511       my $line = $usage_class{$detail->classnum}->classname;
2512       $sections{"$phonenum $line"} ||=
2513         { 'amount' => 0,
2514           'calls' => 0,
2515           'duration' => 0,
2516           'sort_weight' => $usage_class{$detail->classnum}->weight,
2517           'phonenum' => $phonenum,
2518           'header'  => [ @header ],
2519         };
2520       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
2521       $sections{"$phonenum $line"}{calls}++;
2522       $sections{"$phonenum $line"}{duration} += $detail->duration;
2523
2524       $lines{"$phonenum $line"}{$desc} ||= {
2525         description     => &{$escape}($description),
2526         #pkgpart         => $part_pkg->pkgpart,
2527         pkgnum          => '',
2528         ref             => '',
2529         amount          => 0,
2530         calls           => 0,
2531         duration        => 0,
2532         #unit_amount     => '',
2533         quantity        => '',
2534         product_code    => 'N/A',
2535         ext_description => [],
2536       };
2537
2538       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
2539       $lines{"$phonenum $line"}{$desc}{calls}++;
2540       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
2541       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
2542            $detail->formatted('format' => $format);
2543
2544     }
2545   }
2546
2547   my %sectionmap = ();
2548   my $simple = new FS::usage_class { format => 'simple' }; #bleh
2549   foreach ( keys %sections ) {
2550     my @header = @{ $sections{$_}{header} || [] };
2551     my $usage_simple =
2552       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
2553     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
2554     my $usage_class = $summary ? $simple : $usage_simple;
2555     my $ending = $summary ? ' usage charges' : '';
2556     my %gen_opt = ();
2557     unless ($summary) {
2558       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
2559     }
2560     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
2561                         'amount'    => $sections{$_}{amount},    #subtotal
2562                         'calls'       => $sections{$_}{calls},
2563                         'duration'    => $sections{$_}{duration},
2564                         'summarized'  => '',
2565                         'tax_section' => '',
2566                         'phonenum'    => $sections{$_}{phonenum},
2567                         'sort_weight' => $sections{$_}{sort_weight},
2568                         'post_total'  => $summary, #inspire pagebreak
2569                         (
2570                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
2571                             qw( description_generator
2572                                 header_generator
2573                                 total_generator
2574                                 total_line_generator
2575                               )
2576                           )
2577                         ), 
2578                       };
2579   }
2580
2581   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
2582                         $a->{sort_weight} <=> $b->{sort_weight}
2583                       }
2584                  values %sectionmap;
2585
2586   my @lines = ();
2587   foreach my $section ( keys %lines ) {
2588     foreach my $line ( keys %{$lines{$section}} ) {
2589       my $l = $lines{$section}{$line};
2590       $l->{section}     = $sectionmap{$section};
2591       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2592       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2593       push @lines, $l;
2594     }
2595   }
2596   
2597   if($conf->exists('phone_usage_class_summary')) { 
2598       # this only works with Latex
2599       my @newlines;
2600       my @newsections;
2601
2602       # after this, we'll have only two sections per DID:
2603       # Calls Summary and Calls Detail
2604       foreach my $section ( @sections ) {
2605         if($section->{'post_total'}) {
2606             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
2607             $section->{'total_line_generator'} = sub { '' };
2608             $section->{'total_generator'} = sub { '' };
2609             $section->{'header_generator'} = sub { '' };
2610             $section->{'description_generator'} = '';
2611             push @newsections, $section;
2612             my %calls_detail = %$section;
2613             $calls_detail{'post_total'} = '';
2614             $calls_detail{'sort_weight'} = '';
2615             $calls_detail{'description_generator'} = sub { '' };
2616             $calls_detail{'header_generator'} = sub {
2617                 return ' & Date/Time & Called Number & Duration & Price'
2618                     if $format eq 'latex';
2619                 '';
2620             };
2621             $calls_detail{'description'} = 'Calls Detail: '
2622                                                     . $section->{'phonenum'};
2623             push @newsections, \%calls_detail;  
2624         }
2625       }
2626
2627       # after this, each usage class is collapsed/summarized into a single
2628       # line under the Calls Summary section
2629       foreach my $newsection ( @newsections ) {
2630         if($newsection->{'post_total'}) { # this means Calls Summary
2631             foreach my $section ( @sections ) {
2632                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
2633                                 && !$section->{'post_total'});
2634                 my $newdesc = $section->{'description'};
2635                 my $tn = $section->{'phonenum'};
2636                 $newdesc =~ s/$tn//g;
2637                 my $line = {  ext_description => [],
2638                               pkgnum => '',
2639                               ref => '',
2640                               quantity => '',
2641                               calls => $section->{'calls'},
2642                               section => $newsection,
2643                               duration => $section->{'duration'},
2644                               description => $newdesc,
2645                               amount => sprintf("%.2f",$section->{'amount'}),
2646                               product_code => 'N/A',
2647                             };
2648                 push @newlines, $line;
2649             }
2650         }
2651       }
2652
2653       # after this, Calls Details is populated with all CDRs
2654       foreach my $newsection ( @newsections ) {
2655         if(!$newsection->{'post_total'}) { # this means Calls Details
2656             foreach my $line ( @lines ) {
2657                 next unless (scalar(@{$line->{'ext_description'}}) &&
2658                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
2659                             );
2660                 my @extdesc = @{$line->{'ext_description'}};
2661                 my @newextdesc;
2662                 foreach my $extdesc ( @extdesc ) {
2663                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
2664                     push @newextdesc, $extdesc;
2665                 }
2666                 $line->{'ext_description'} = \@newextdesc;
2667                 $line->{'section'} = $newsection;
2668                 push @newlines, $line;
2669             }
2670         }
2671       }
2672
2673       return(\@newsections, \@newlines);
2674   }
2675
2676   return(\@sections, \@lines);
2677
2678 }
2679
2680 =sub _items_usage_class_summary OPTIONS
2681
2682 Returns a list of detail items summarizing the usage charges on this 
2683 invoice.  Each one will have 'amount', 'description' (the usage charge name),
2684 and 'usage_classnum'.
2685
2686 OPTIONS can include 'escape' (a function to escape the descriptions).
2687
2688 =cut
2689
2690 sub _items_usage_class_summary {
2691   my $self = shift;
2692   my %opt = @_;
2693
2694   my $escape = $opt{escape} || sub { $_[0] };
2695   my $invnum = $self->invnum;
2696   my @classes = qsearch({
2697       'table'     => 'usage_class',
2698       'select'    => 'classnum, classname, SUM(amount) AS amount',
2699       'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
2700                      ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
2701       'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
2702                      ' GROUP BY classnum, classname, weight'.
2703                      ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
2704                      ' ORDER BY weight ASC',
2705   });
2706   my @l;
2707   my $section = {
2708     description   => &{$escape}($self->mt('Usage Summary')),
2709     no_subtotal   => 1,
2710     usage_section => 1,
2711   };
2712   foreach my $class (@classes) {
2713     push @l, {
2714       'description'     => &{$escape}($class->classname),
2715       'amount'          => sprintf('%.2f', $class->amount),
2716       'usage_classnum'  => $class->classnum,
2717       'section'         => $section,
2718     };
2719   }
2720   return @l;
2721 }
2722
2723 sub _items_previous {
2724   my $self = shift;
2725   my $conf = $self->conf;
2726   my $cust_main = $self->cust_main;
2727   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2728   my @b = ();
2729   foreach ( @pr_cust_bill ) {
2730     my $date = $conf->exists('invoice_show_prior_due_date')
2731                ? 'due '. $_->due_date2str('short')
2732                : $self->time2str_local('short', $_->_date);
2733     push @b, {
2734       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
2735       #'pkgpart'     => 'N/A',
2736       'pkgnum'      => 'N/A',
2737       'amount'      => sprintf("%.2f", $_->owed),
2738     };
2739   }
2740   @b;
2741
2742   #{
2743   #    'description'     => 'Previous Balance',
2744   #    #'pkgpart'         => 'N/A',
2745   #    'pkgnum'          => 'N/A',
2746   #    'amount'          => sprintf("%10.2f", $pr_total ),
2747   #    'ext_description' => [ map {
2748   #                                 "Invoice ". $_->invnum.
2749   #                                 " (". time2str("%x",$_->_date). ") ".
2750   #                                 sprintf("%10.2f", $_->owed)
2751   #                         } @pr_cust_bill ],
2752
2753   #};
2754 }
2755
2756 sub _items_credits {
2757   my( $self, %opt ) = @_;
2758   my $trim_len = $opt{'trim_len'} || 40;
2759
2760   my @b;
2761   #credits
2762   my @objects;
2763   if ( $self->conf->exists('previous_balance-payments_since') ) {
2764     if ( $opt{'template'} eq 'statement' ) {
2765       # then the current bill is a "statement" (i.e. an invoice sent as
2766       # a payment receipt)
2767       # and in that case we want to see payments on or after THIS invoice
2768       @objects = qsearch('cust_credit', {
2769           'custnum' => $self->custnum,
2770           '_date'   => {op => '>=', value => $self->_date},
2771       });
2772     } else {
2773       my $date = 0;
2774       $date = $self->previous_bill->_date if $self->previous_bill;
2775       @objects = qsearch('cust_credit', {
2776           'custnum' => $self->custnum,
2777           '_date'   => {op => '>=', value => $date},
2778       });
2779     }
2780   } else {
2781     @objects = $self->cust_credited;
2782   }
2783
2784   foreach my $obj ( @objects ) {
2785     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
2786
2787     my $reason = substr($cust_credit->reason, 0, $trim_len);
2788     $reason .= '...' if length($reason) < length($cust_credit->reason);
2789     $reason = " ($reason) " if $reason;
2790
2791     push @b, {
2792       #'description' => 'Credit ref\#'. $_->crednum.
2793       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2794       #                 $reason,
2795       'description' => $self->mt('Credit applied').' '.
2796                        $self->time2str_local('short', $obj->_date). $reason,
2797       'amount'      => sprintf("%.2f",$obj->amount),
2798     };
2799   }
2800
2801   @b;
2802
2803 }
2804
2805 sub _items_payments {
2806   my $self = shift;
2807   my %opt = @_;
2808
2809   my @b;
2810   my $detailed = $self->conf->exists('invoice_payment_details');
2811   my @objects;
2812   if ( $self->conf->exists('previous_balance-payments_since') ) {
2813     # then show payments dated on/after the previous bill...
2814     if ( $opt{'template'} eq 'statement' ) {
2815       # then the current bill is a "statement" (i.e. an invoice sent as
2816       # a payment receipt)
2817       # and in that case we want to see payments on or after THIS invoice
2818       @objects = qsearch('cust_pay', {
2819           'custnum' => $self->custnum,
2820           '_date'   => {op => '>=', value => $self->_date},
2821       });
2822     } else {
2823       # the normal case: payments on or after the previous invoice
2824       my $date = 0;
2825       $date = $self->previous_bill->_date if $self->previous_bill;
2826       @objects = qsearch('cust_pay', {
2827         'custnum' => $self->custnum,
2828         '_date'   => {op => '>=', value => $date},
2829       });
2830       # and before the current bill...
2831       @objects = grep { $_->_date < $self->_date } @objects;
2832     }
2833   } else {
2834     @objects = $self->cust_bill_pay;
2835   }
2836
2837   foreach my $obj (@objects) {
2838     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
2839     my $desc = $self->mt('Payment received').' '.
2840                $self->time2str_local('short', $cust_pay->_date );
2841     $desc .= $self->mt(' via ') .
2842              $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
2843       if $detailed;
2844
2845     push @b, {
2846       'description' => $desc,
2847       'amount'      => sprintf("%.2f", $obj->amount )
2848     };
2849   }
2850
2851   @b;
2852
2853 }
2854
2855 =item call_details [ OPTION => VALUE ... ]
2856
2857 Returns an array of CSV strings representing the call details for this invoice
2858 The only option available is the boolean prepend_billed_number
2859
2860 =cut
2861
2862 sub call_details {
2863   my ($self, %opt) = @_;
2864
2865   my $format_function = sub { shift };
2866
2867   if ($opt{prepend_billed_number}) {
2868     $format_function = sub {
2869       my $detail = shift;
2870       my $row = shift;
2871
2872       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
2873       
2874     };
2875   }
2876
2877   my @details = map { $_->details( 'format_function' => $format_function,
2878                                    'escape_function' => sub{ return() },
2879                                  )
2880                     }
2881                   grep { $_->pkgnum }
2882                   $self->cust_bill_pkg;
2883   my $header = $details[0];
2884   ( $header, grep { $_ ne $header } @details );
2885 }
2886
2887
2888 =back
2889
2890 =head1 SUBROUTINES
2891
2892 =over 4
2893
2894 =item process_reprint
2895
2896 =cut
2897
2898 sub process_reprint {
2899   process_re_X('print', @_);
2900 }
2901
2902 =item process_reemail
2903
2904 =cut
2905
2906 sub process_reemail {
2907   process_re_X('email', @_);
2908 }
2909
2910 =item process_refax
2911
2912 =cut
2913
2914 sub process_refax {
2915   process_re_X('fax', @_);
2916 }
2917
2918 =item process_reftp
2919
2920 =cut
2921
2922 sub process_reftp {
2923   process_re_X('ftp', @_);
2924 }
2925
2926 =item respool
2927
2928 =cut
2929
2930 sub process_respool {
2931   process_re_X('spool', @_);
2932 }
2933
2934 use Storable qw(thaw);
2935 use Data::Dumper;
2936 use MIME::Base64;
2937 sub process_re_X {
2938   my( $method, $job ) = ( shift, shift );
2939   warn "$me process_re_X $method for job $job\n" if $DEBUG;
2940
2941   my $param = thaw(decode_base64(shift));
2942   warn Dumper($param) if $DEBUG;
2943
2944   re_X(
2945     $method,
2946     $job,
2947     %$param,
2948   );
2949
2950 }
2951
2952 sub re_X {
2953   # spool_invoice ftp_invoice fax_invoice print_invoice
2954   my($method, $job, %param ) = @_;
2955   if ( $DEBUG ) {
2956     warn "re_X $method for job $job with param:\n".
2957          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2958   }
2959
2960   #some false laziness w/search/cust_bill.html
2961   my $distinct = '';
2962   my $orderby = 'ORDER BY cust_bill._date';
2963
2964   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
2965
2966   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2967      
2968   my @cust_bill = qsearch( {
2969     #'select'    => "cust_bill.*",
2970     'table'     => 'cust_bill',
2971     'addl_from' => $addl_from,
2972     'hashref'   => {},
2973     'extra_sql' => $extra_sql,
2974     'order_by'  => $orderby,
2975     'debug' => 1,
2976   } );
2977
2978   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
2979
2980   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2981     if $DEBUG;
2982
2983   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2984   foreach my $cust_bill ( @cust_bill ) {
2985     $cust_bill->$method();
2986
2987     if ( $job ) { #progressbar foo
2988       $num++;
2989       if ( time - $min_sec > $last ) {
2990         my $error = $job->update_statustext(
2991           int( 100 * $num / scalar(@cust_bill) )
2992         );
2993         die $error if $error;
2994         $last = time;
2995       }
2996     }
2997
2998   }
2999
3000 }
3001
3002 =back
3003
3004 =head1 CLASS METHODS
3005
3006 =over 4
3007
3008 =item owed_sql
3009
3010 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3011
3012 =cut
3013
3014 sub owed_sql {
3015   my ($class, $start, $end) = @_;
3016   'charged - '. 
3017     $class->paid_sql($start, $end). ' - '. 
3018     $class->credited_sql($start, $end);
3019 }
3020
3021 =item net_sql
3022
3023 Returns an SQL fragment to retreive the net amount (charged minus credited).
3024
3025 =cut
3026
3027 sub net_sql {
3028   my ($class, $start, $end) = @_;
3029   'charged - '. $class->credited_sql($start, $end);
3030 }
3031
3032 =item paid_sql
3033
3034 Returns an SQL fragment to retreive the amount paid against this invoice.
3035
3036 =cut
3037
3038 sub paid_sql {
3039   my ($class, $start, $end) = @_;
3040   $start &&= "AND cust_bill_pay._date <= $start";
3041   $end   &&= "AND cust_bill_pay._date > $end";
3042   $start = '' unless defined($start);
3043   $end   = '' unless defined($end);
3044   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3045        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
3046 }
3047
3048 =item credited_sql
3049
3050 Returns an SQL fragment to retreive the amount credited against this invoice.
3051
3052 =cut
3053
3054 sub credited_sql {
3055   my ($class, $start, $end) = @_;
3056   $start &&= "AND cust_credit_bill._date <= $start";
3057   $end   &&= "AND cust_credit_bill._date >  $end";
3058   $start = '' unless defined($start);
3059   $end   = '' unless defined($end);
3060   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3061        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
3062 }
3063
3064 =item due_date_sql
3065
3066 Returns an SQL fragment to retrieve the due date of an invoice.
3067 Currently only supported on PostgreSQL.
3068
3069 =cut
3070
3071 sub due_date_sql {
3072   die "don't use: doesn't account for agent-specific invoice_default_terms";
3073
3074   #we're passed a $conf but not a specific customer (that's in the query), so
3075   # to make this work we'd need an agentnum-aware "condition_sql_conf" like
3076   # "condition_sql_option" that retreives a conf value with SQL in an agent-
3077   # aware fashion
3078
3079   my $conf = new FS::Conf;
3080 'COALESCE(
3081   SUBSTRING(
3082     COALESCE(
3083       cust_bill.invoice_terms,
3084       cust_main.invoice_terms,
3085       \''.($conf->config('invoice_default_terms') || '').'\'
3086     ), E\'Net (\\\\d+)\'
3087   )::INTEGER, 0
3088 ) * 86400 + cust_bill._date'
3089 }
3090
3091 =back
3092
3093 =head1 BUGS
3094
3095 The delete method.
3096
3097 =head1 SEE ALSO
3098
3099 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3100 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3101 documentation.
3102
3103 =cut
3104
3105 1;
3106