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