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