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