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