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