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