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