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