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