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