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