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