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