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