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