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