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