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