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