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