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