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