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