2 use base qw( FS::cust_bill::Search FS::Template_Mixin
3 FS::cust_main_Mixin FS::Record
7 use vars qw( $DEBUG $me );
10 use Fcntl qw(:flock); #for spool_csv
12 use List::Util qw(min max sum);
16 use Storable qw( freeze thaw );
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;
28 use FS::cust_credit_bill;
32 use FS::cust_bill_pay;
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;
43 $me = '[FS::cust_bill]';
47 FS::cust_bill - Object methods for cust_bill records
53 $record = new FS::cust_bill \%hash;
54 $record = new FS::cust_bill { 'column' => 'value' };
56 $error = $record->insert;
58 $error = $new_record->replace($old_record);
60 $error = $record->delete;
62 $error = $record->check;
64 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
66 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
68 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
70 @cust_pay_objects = $cust_bill->cust_pay;
72 $tax_amount = $record->tax;
74 @lines = $cust_bill->print_text;
75 @lines = $cust_bill->print_text('time' => $time);
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:
88 =item invnum - primary key (assigned automatically for new invoices)
90 =item custnum - customer (see L<FS::cust_main>)
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.
95 =item charged - amount of this invoice
97 =item invoice_terms - optional terms override for this specific invoice
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.
109 =item previous_balance - the customer's balance immediately after generating
110 the invoice before this one. DEPRECATED.
112 =item printed - formerly used to track the number of times an invoice had
113 been printed; no longer used.
121 =item closed - books closed flag, empty or `Y'
123 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
125 =item agent_invid - legacy invoice number
127 =item promised_date - customer promised payment date, for collection
129 =item pending - invoice is still being generated, empty or 'Y'
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>).
145 sub table { 'cust_bill'; }
146 sub template_conf { 'invoice_'; }
150 my $agentnum = $self->cust_main->agentnum;
151 my $tc = $self->template_conf;
153 $self->conf->exists($tc.'sections', $agentnum) ||
154 $self->conf->exists($tc.'sections_by_location', $agentnum);
157 # should be the ONLY occurrence of "Invoice" in invoice rendering code.
158 # (except email_subject and invnum_date_pretty)
161 $self->conf->config('notice_name') || 'Invoice'
164 sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
165 sub cust_unlinked_msg {
167 "WARNING: can't find cust_main.custnum ". $self->custnum.
168 ' (cust_bill.invnum '. $self->invnum. ')';
173 Adds this invoice to the database ("Posts" the invoice). If there is an error,
174 returns the error, otherwise returns false.
180 warn "$me insert called\n" if $DEBUG;
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';
189 my $oldAutoCommit = $FS::UID::AutoCommit;
190 local $FS::UID::AutoCommit = 0;
193 my $error = $self->SUPER::insert;
195 $dbh->rollback if $oldAutoCommit;
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;
204 $dbh->rollback if $oldAutoCommit;
205 return "can't create invoice line item: $error";
210 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
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).
225 my $reason = scalar(@_) ? shift : '';
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';
234 my $oldAutoCommit = $FS::UID::AutoCommit;
235 local $FS::UID::AutoCommit = 0;
238 my $cust_bill_void = new FS::cust_bill_void ( {
239 map { $_ => $self->get($_) } $self->fields
241 $cust_bill_void->reason($reason);
242 my $error = $cust_bill_void->insert;
244 $dbh->rollback if $oldAutoCommit;
248 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
249 my $error = $cust_bill_pkg->void($reason);
251 $dbh->rollback if $oldAutoCommit;
256 $error = $self->_delete;
258 $dbh->rollback if $oldAutoCommit;
262 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
268 # removed docs entirely and renamed method to _delete to further indicate it is
269 # internal-only and discourage use
273 # DO NOT USE THIS METHOD. Instead, apply a credit against the invoice, or use
274 # the B<void> method.
276 # This is only for internal use by V<void>, which is what you should be using.
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.
285 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
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';
294 my $oldAutoCommit = $FS::UID::AutoCommit;
295 local $FS::UID::AutoCommit = 0;
298 foreach my $table (qw(
306 #cust_event # problematic
308 foreach my $linked ( $self->$table() ) {
309 my $error = $linked->delete;
311 $dbh->rollback if $oldAutoCommit;
318 my $error = $self->SUPER::delete(@_);
320 $dbh->rollback if $oldAutoCommit;
324 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
330 =item replace [ OLD_RECORD ]
332 You can, but probably shouldn't modify invoices...
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.
340 #replace can be inherited from Record.pm
342 # replace_check is now the preferred way to #implement replace data checks
343 # (so $object->replace() works without an argument)
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'};
359 =item add_cc_surcharge
365 sub add_cc_surcharge {
366 my ($self, $pkgnum, $amount) = (shift, shift, shift);
369 my $cust_bill_pkg = new FS::cust_bill_pkg({
370 'invnum' => $self->invnum,
374 $error = $cust_bill_pkg->insert;
375 return $error if $error;
377 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
378 $self->charged($self->charged+$amount);
379 $error = $self->replace;
380 return $error if $error;
382 $self->apply_payments_and_credits;
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
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')
408 return $error if $error;
410 $self->_date(time) unless $self->_date;
412 $self->printed(0) if $self->printed eq '';
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.
426 if ( $self->agent_invid
427 && FS::Conf->new->exists('cust_bill-default_agent_invid') ) {
428 return $self->agent_invid;
430 return $self->invnum;
436 Returns the customer's last invoice before this one.
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',
450 $self->get('previous_bill');
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).
462 # simple memoize; we use this a lot
463 if (!$self->get('previous')) {
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 },
472 foreach ( @cust_bill ) { $total += $_->owed; }
473 $self->set('previous', [$total, @cust_bill]);
475 return @{ $self->get('previous') };
478 =item enable_previous
480 Whether to show the 'Previous Charges' section when printing this invoice.
481 The negation of the 'disable_previous_balance' config setting.
485 sub enable_previous {
487 my $agentnum = $self->cust_main->agentnum;
488 !$self->conf->exists('disable_previous_balance', $agentnum);
493 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
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?
509 =item cust_bill_pkg_pkgnum PKGNUM
511 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
516 sub cust_bill_pkg_pkgnum {
517 my( $self, $pkgnum ) = @_;
519 { 'table' => 'cust_bill_pkg',
520 'hashref' => { 'invnum' => $self->invnum,
523 'order_by' => 'ORDER BY billpkgnum',
530 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
537 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
538 $self->cust_bill_pkg;
540 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
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.
552 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
555 =item open_cust_bill_pkg
557 Returns the open line items for this invoice.
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.
564 # modeled after cust_main::open_cust_bill
565 sub open_cust_bill_pkg {
568 # grep { $_->owed > 0 } $self->cust_bill_pkg
570 my %other = ( 'recur' => 'setup',
571 'setup' => 'recur', );
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;
584 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
588 #false laziness w/cust_pkg.pm
592 'table' => 'cust_event',
593 'addl_from' => 'JOIN part_event USING ( eventpart )',
594 'hashref' => { 'tablenum' => $self->invnum },
595 'extra_sql' => " AND eventtable = 'cust_bill' ",
601 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
605 #false laziness w/cust_pkg.pm
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];
618 Returns the customer (see L<FS::cust_main>) for this invoice.
622 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this invoice
624 Returns a list: an empty list on success or a list of errors.
631 grep { $_->suspend(@_) }
632 grep {! $_->getfield('cancel') }
637 =item cust_suspend_if_balance_over AMOUNT
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.
642 Returns a list: an empty list on success or a list of errors.
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 ) {
652 $cust_main->suspend(@_);
658 Cancel the packages on this invoice. Largely similar to the cust_main version, but does not bother yet with banned payment options
663 my( $self, %opt ) = @_;
665 warn "$me cancel called on cust_bill ". $self->invnum . " with options ".
666 join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
669 return ( 'Access denied' )
670 unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
672 my @pkgs = $self->cust_pkg;
674 if ( !$opt{nobill} && $self->conf->exists('bill_usage_on_cancel') ) {
676 my $error = $self->cust_main->bill( pkg_list => [ @pkgs ], cancel => 1 );
677 warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
682 map { $_->cancel(%opt) }
683 grep { ! $_->getfield('cancel') }
689 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
695 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
696 sort { $a->_date <=> $b->_date }
697 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
702 =item cust_credit_bill
704 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
710 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
711 sort { $a->_date <=> $b->_date }
712 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
716 sub cust_credit_bill {
717 shift->cust_credited(@_);
720 #=item cust_bill_pay_pkgnum PKGNUM
722 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
723 #with matching pkgnum.
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,
737 =item cust_bill_pay_pkg PKGNUM
739 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
740 applied against the matching pkgnum.
744 sub cust_bill_pay_pkg {
745 my( $self, $pkgnum ) = @_;
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",
758 #=item cust_credited_pkgnum PKGNUM
760 #=item cust_credit_bill_pkgnum PKGNUM
762 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
763 #with matching pkgnum.
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,
777 #sub cust_credit_bill_pkgnum {
778 # shift->cust_credited_pkgnum(@_);
781 =item cust_credit_bill_pkg PKGNUM
783 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
784 applied against the matching pkgnum.
788 sub cust_credit_bill_pkg {
789 my( $self, $pkgnum ) = @_;
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",
802 =item cust_bill_batch
804 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
808 sub cust_bill_batch {
810 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
815 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
816 hash keyed by term length.
822 FS::discount_plan->all($self);
827 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
834 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
836 foreach (@taxlines) { $total += $_->setup; }
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>).
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
859 my( $self, $pkgnum ) = @_;
861 #my $balance = $self->charged;
863 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
865 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
866 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
868 $balance = sprintf( "%.2f", $balance);
869 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
875 Returns true if this invoice should be hidden. See the
876 selfservice-hide_invoices-taxclass configuraiton setting.
882 my $conf = $self->conf;
883 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
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;
890 =item apply_payments_and_credits [ OPTION => VALUE ... ]
892 Applies unapplied payments and credits to this invoice.
893 Payments with the no_auto_apply flag set will not be applied.
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.
899 If there is an error, returns the error, otherwise returns false.
903 sub apply_payments_and_credits {
904 my( $self, %options ) = @_;
905 my $conf = $self->conf;
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';
914 my $oldAutoCommit = $FS::UID::AutoCommit;
915 local $FS::UID::AutoCommit = 0;
918 $self->select_for_update; #mutex
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;
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;
932 while ( $self->owed > 0 and ( @payments || @credits ) ) {
935 if ( @payments && @credits ) {
937 #decide which goes first by weight of top (unapplied) line item
939 my @open_lineitems = $self->open_cust_bill_pkg;
942 max( map { $_->part_pkg->pay_weight || 0 }
947 my $max_credit_weight =
948 max( map { $_->part_pkg->credit_weight || 0 }
954 #if both are the same... payments first? it has to be something
955 if ( $max_pay_weight >= $max_credit_weight ) {
961 } elsif ( @payments ) {
963 } elsif ( @credits ) {
966 die "guru meditation #12 and 35";
970 if ( $app eq 'pay' ) {
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;
978 } elsif ( $app eq 'credit' ) {
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;
987 die "guru meditation #12 and 35";
991 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
992 warn "owed_pkgnum ". $app->pkgnum;
993 $owed = $self->owed_pkgnum($app->pkgnum);
997 next unless $owed > 0;
999 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
1000 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
1002 $app->invnum( $self->invnum );
1004 my $error = $app->insert(%options);
1006 $dbh->rollback if $oldAutoCommit;
1007 return "Error inserting ". $app->table. " record: $error";
1009 die $error if $error;
1013 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1020 Sends this invoice to the destinations configured for this customer: sends
1021 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1023 Options can be passed as a hashref. Positional parameters are no longer
1026 I<template>: a suffix for alternate invoices
1028 I<agentnum>: obsolete, now does nothing.
1030 I<from> overrides the default email invoice From: address.
1032 I<amount>: obsolete, does nothing
1034 I<notice_name> overrides "Invoice" as the name of the sent document
1035 (templates from 10/2009 or newer required).
1037 I<lpr> overrides the system 'lpr' option as the command to print a document
1038 from standard input.
1044 my $opt = ref($_[0]) ? $_[0] : +{ @_ };
1045 my $conf = $self->conf;
1047 my $cust_main = $self->cust_main;
1049 my @invoicing_list = $cust_main->invoicing_list;
1052 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1053 && ! $cust_main->invoice_noemail;
1056 if grep { $_ eq 'POST' } @invoicing_list; #postal
1058 #this has never been used post-$ORIGINAL_ISP afaik
1059 $self->fax_invoice($opt)
1060 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1068 my $opt = shift || {};
1069 if ($opt and !ref($opt)) {
1070 die ref($self). '->email called with positional parameters';
1073 my $conf = $self->conf;
1075 my $from = delete $opt->{from};
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 );
1081 my @invoicing_list = $self->cust_main->invoicing_list_emailonly;
1083 if ( ! @invoicing_list ) { #no recipients
1084 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1085 die 'No recipients for customer #'. $self->custnum;
1087 #default: better to notify this person than silence
1088 @invoicing_list = ($from);
1092 $self->SUPER::email( {
1094 'to' => \@invoicing_list,
1100 #this stays here for now because its explicitly used as
1101 # FS::cust_bill::queueable_email
1102 sub queueable_email {
1105 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1106 or die "invalid invoice number: " . $opt{invnum};
1109 $self->set('mode', $opt{mode});
1112 my %args = map {$_ => $opt{$_}}
1114 qw( from notice_name no_coupon template );
1116 my $error = $self->email( \%args );
1117 die $error if $error;
1123 my $conf = $self->conf;
1125 #my $template = scalar(@_) ? shift : '';
1128 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
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;
1137 eval qq("$subject");
1140 =item lpr_data HASHREF
1142 Returns the postscript or plaintext for this invoice as an arrayref.
1144 Options must be passed as a hashref. Positional parameters are no longer
1147 I<template>, if specified, is the name of a suffix for alternate invoices.
1149 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
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";
1162 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1163 [ $self->$method( $opt ) ];
1168 Prints this invoice.
1170 Options must be passed as a hashref.
1172 I<template>, if specified, is the name of a suffix for alternate invoices.
1174 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
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";
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);
1194 $self->lpr_data($opt),
1195 'agentnum' => $self->cust_main->agentnum,
1201 =item fax_invoice HASHREF
1205 Options must be passed as a hashref.
1207 I<template>, if specified, is the name of a suffix for alternate invoices.
1209 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
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";
1222 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1223 unless $conf->exists('invoice_latex');
1225 my $dialstring = $self->cust_main->getfield('fax');
1228 my $error = send_fax( 'docdata' => $self->lpr_data($opt),
1229 'dialstring' => $dialstring,
1231 die $error if $error;
1235 =item batch_invoice [ HASHREF ]
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.
1240 HASHREF may contain any options to be passed to C<print_pdf>.
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,
1251 return $cust_bill_batch->insert($opt);
1254 =item get_open_batch
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
1262 sub get_open_bill_batch {
1264 my $conf = $self->conf;
1265 my $hashref = { status => 'O' };
1266 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1267 ? $self->cust_main->agentnum
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;
1277 =item ftp_invoice [ TEMPLATENAME ]
1279 Sends this invoice data via FTP.
1281 TEMPLATENAME is unused?
1287 my $conf = $self->conf;
1288 my $template = scalar(@_) ? shift : '';
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'),
1300 =item spool_invoice [ TEMPLATENAME ]
1302 Spools this invoice data (see L<FS::spool_csv>)
1304 TEMPLATENAME is unused?
1310 my $conf = $self->conf;
1311 my $template = scalar(@_) ? shift : '';
1314 'format' => $conf->config('cust_bill-spoolformat'),
1315 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1319 =item send_csv OPTION => VALUE, ...
1321 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1325 protocol - currently only "ftp"
1331 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1332 and YYMMDDHHMMSS is a timestamp.
1334 See L</print_csv> for a description of the output format.
1339 my($self, %opt) = @_;
1343 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1344 mkdir $spooldir, 0700 unless -d $spooldir;
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";
1350 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1352 open(CSV, ">$file") or die "can't open $file: $!";
1360 if ( $opt{protocol} eq 'ftp' ) {
1361 eval "use Net::FTP;";
1363 $net = Net::FTP->new($opt{server}) or die @$;
1365 die "unknown protocol: $opt{protocol}";
1368 $net->login( $opt{username}, $opt{password} )
1369 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1371 $net->binary or die "can't set binary mode";
1373 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1375 $net->put($file) or die "can't put $file: $!";
1385 Spools CSV invoice data.
1391 =item format - any of FS::Misc::::Invoicing::spool_formats
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>).
1397 =item agent_spools - if set to a true value, will spool to per-agent files
1398 rather than a single global file
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
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.
1407 =item time - the "current time". Controls the printing of past due messages
1415 my($self, %opt) = @_;
1417 my $time = $opt{'time'} || time;
1418 my $cust_main = $self->cust_main;
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;
1427 if ( $opt{'balanceover'} ) {
1429 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1432 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1433 mkdir $spooldir, 0700 unless -d $spooldir;
1435 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', $time);
1438 if ( $opt{'agent_spools'} ) {
1439 $file = 'agentnum'.$cust_main->agentnum;
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
1449 if ( lc($opt{'format'}) eq 'billco' ) {
1453 $file = "$spooldir/$file.csv";
1455 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
1457 open(CSV, ">>$file") or die "can't open $file: $!";
1458 flock(CSV, LOCK_EX);
1463 if ( lc($opt{'format'}) eq 'billco' ) {
1465 flock(CSV, LOCK_UN);
1468 $file =~ s/-header.csv$/-detail.csv/;
1470 open(CSV,">>$file") or die "can't open $file: $!";
1471 flock(CSV, LOCK_EX);
1475 print CSV $detail if defined($detail);
1477 flock(CSV, LOCK_UN);
1484 =item print_csv OPTION => VALUE, ...
1486 Returns CSV data for this invoice.
1490 format - 'default', 'billco', 'oneline', 'bridgestone'
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.
1496 If I<format> is not specified or "default", the fields of the CSV file are as
1499 record_type, invnum, custnum, _date, charged, first, last, company, address1,
1500 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1504 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
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.
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>)
1514 =item invnum - invoice number
1516 =item custnum - customer number
1518 =item _date - invoice date
1520 =item charged - total invoice amount
1522 =item first - customer first name
1524 =item last - customer first name
1526 =item company - company name
1528 =item address1 - address line 1
1530 =item address2 - address line 1
1540 =item pkg - line item description
1542 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1544 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1546 =item sdate - start date for recurring fee
1548 =item edate - end date for recurring fee
1552 If I<format> is "billco", the fields of the header CSV file are as follows:
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 +-------+-------------------------------+------------+------+-------+
1589 If I<format> is "billco", the fields of the detail CSV file are as follows:
1591 FORMAT FOR DETAIL FILE
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
1605 If format is 'oneline', there is no detail file. Each invoice has a
1606 header line only, with the fields:
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.
1612 and then, for each line item, three columns containing the package number,
1613 description, and amount.
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:
1618 Customer number (in display format), date, name (first last), company,
1619 address 1, address 2, city, state, zip.
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.
1628 my($self, %opt) = @_;
1630 eval "use Text::CSV_XS";
1633 my $cust_main = $self->cust_main;
1635 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1636 my $format = lc($opt{'format'});
1638 my $time = $opt{'time'} || time;
1640 my $tracctnum = ''; #leaking out from billco-specific sections :/
1641 if ( $format eq 'billco' ) {
1644 $self->conf->config('billco-account_num', $cust_main->agentnum);
1646 $tracctnum = $account_num eq 'display_custnum'
1647 ? $cust_main->display_custnum
1648 : $opt{'tracctnum'};
1651 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1653 my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
1655 my( $previous_balance, @unused ) = $self->previous; #previous balance
1657 my $pmt_cr_applied = 0;
1658 $pmt_cr_applied += $_->{'amount'}
1659 foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
1661 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
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
1679 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1682 $duedate, # 14 | Bill Due Date CHAR 10
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
1701 } elsif ( $format eq 'oneline' ) { #name
1703 my ($previous_balance) = $self->previous;
1704 $previous_balance = sprintf('%.2f', $previous_balance);
1705 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1711 $self->_items_pkg, #_items_nontax? no sections or anything
1716 $cust_main->agentnum,
1717 $cust_main->agent->agent,
1721 $cust_main->company,
1722 $cust_main->address1,
1723 $cust_main->address2,
1729 time2str("%x", $self->_date),
1734 $self->due_date2str("%x"),
1739 } elsif ( $format eq 'bridgestone' ) {
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;
1745 my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
1749 "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
1751 $cust_main->display_custnum,
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),
1764 } elsif ( $format eq 'ics' ) {
1766 my $bill = $cust_main->bill_location;
1767 my $zip = $bill->zip;
1771 if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
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);
1783 if ( $self->due_date and $time >= $self->due_date ) {
1784 $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
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
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
1812 $past_due, #PAST_MSG
1816 my %svc_class = ('' => ''); # maybe cache this more persistently?
1818 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1820 my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
1821 my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
1825 my @dates = ( $self->_date, undef );
1826 if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
1827 $dates[1] = $prev->sdate; #questionable
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
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)
1840 $svc_class{$svcpart} = $part_svc_class ?
1841 $part_svc_class->classname :
1845 my @h_label = $cust_svc->label(@dates, 'I');
1846 push @details, sprintf('01%-9s%-20s%-47s',
1848 $svc_class{$svcpart},
1851 } #foreach $cust_svc
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);
1859 push @details, sprintf('02%-6s%-60s%-10s',
1862 sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
1864 } #foreach $cust_bill_pkg
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 ) {
1870 } elsif ( scalar(@details) < 58 ) {
1876 return join('', $header, @details, "\n");
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 ) ),
1889 ) or die "can't create csv";
1892 my $header = $csv->string. "\n";
1895 if ( lc($opt{'format'}) eq 'billco' ) {
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.
1903 my @items = ( $self->_items_pkg(%items_opt),
1904 $self->_items_fee(%items_opt) );
1905 foreach my $item (@items) {
1907 my $description = $item->{'description'};
1908 if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
1909 $description .= ': ' . $item->{ext_description}[0];
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
1925 $detail .= $csv->string. "\n";
1929 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
1935 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1937 my($pkg, $setup, $recur, $sdate, $edate);
1938 if ( $cust_bill_pkg->pkgnum ) {
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 )
1945 ( $cust_bill_pkg->recur != 0
1946 ? sprintf("%.2f", $cust_bill_pkg->recur )
1948 ( $cust_bill_pkg->sdate
1949 ? time2str("%x", $cust_bill_pkg->sdate)
1951 ($cust_bill_pkg->edate
1952 ? time2str("%x", $cust_bill_pkg->edate)
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 ) = ( '', '' );
1966 ( map { '' } (1..11) ),
1967 ($pkg, $setup, $recur, $sdate, $edate)
1968 ) or die "can't create csv";
1970 $detail .= $csv->string. "\n";
1976 ( $header, $detail );
1981 croak 'cust_bill->comp is deprecated (COMP payments are deprecated)';
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.
1995 $self->realtime_bop( 'CC', @_ );
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.
2009 $self->realtime_bop( 'ECHECK', @_ );
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.
2023 $self->realtime_bop( 'LEC', @_ );
2027 my( $self, $method ) = (shift,shift);
2028 my $conf = $self->conf;
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;
2037 my $description = 'Internet Services';
2038 if ( $conf->exists('business-onlinepayment-description') ) {
2039 my $dtempl = $conf->config('business-onlinepayment-description');
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
2049 $description = eval qq("$dtempl");
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
2057 'apply_to_invoice' => 1,
2060 #this changes application behavior: auto payments
2061 #triggered against a specific invoice are now applied
2062 #to that invoice instead of oldest open.
2068 =item batch_card OPTION => VALUE...
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.
2077 my ($self, %options) = @_;
2078 my $cust_main = $self->cust_main;
2080 $options{invnum} = $self->invnum;
2082 $cust_main->batch_card(%options);
2085 sub _agent_template {
2087 $self->cust_main->agent_template;
2090 sub _agent_invoice_from {
2092 $self->cust_main->agent_invoice_from;
2095 =item invoice_barcode DIR_OR_FALSE
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.
2103 sub invoice_barcode {
2104 my ($self, $dir) = (shift,shift);
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);
2111 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
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;
2124 =item invnum_date_pretty
2126 Returns a string with the invoice number and date, for example:
2127 "Invoice #54 (3/20/2008)".
2129 Intended for back-end context, with regard to translation and date formatting.
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 {
2138 #$self->mt('Invoice #').
2139 'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
2140 $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
2143 #sub _items_extra_usage_sections {
2145 # my $escape = shift;
2147 # my %sections = ();
2149 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
2150 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2152 # next unless $cust_bill_pkg->pkgnum > 0;
2154 # foreach my $section ( keys %usage_class ) {
2156 # my $usage = $cust_bill_pkg->usage($section);
2158 # next unless $usage && $usage > 0;
2160 # $sections{$section} ||= 0;
2161 # $sections{$section} += $usage;
2167 # map { { 'description' => &{$escape}($_),
2168 # 'subtotal' => $sections{$_},
2169 # 'summarized' => '',
2170 # 'tax_section' => '',
2173 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
2177 sub _items_extra_usage_sections {
2179 my $conf = $self->conf;
2187 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
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;
2193 foreach my $classnum ( keys %usage_class ) {
2194 my $section = $usage_class{$classnum}->classname;
2195 $classnums{$section} = $classnum;
2197 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
2198 my $amount = $detail->amount;
2199 next unless $amount && $amount > 0;
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;
2206 my $desc = $detail->regionname;
2207 my $description = $desc;
2208 $description = substr($desc, 0, $maxlength). '...'
2209 if $format eq 'latex' && length($desc) > $maxlength;
2211 $lines{$section}{$desc} ||= {
2212 description => &{$escape}($description),
2213 #pkgpart => $part_pkg->pkgpart,
2214 pkgnum => $cust_bill_pkg->pkgnum,
2219 #unit_amount => $cust_bill_pkg->unitrecur,
2220 quantity => $cust_bill_pkg->quantity,
2221 product_code => 'N/A',
2222 ext_description => [],
2225 $lines{$section}{$desc}{amount} += $amount;
2226 $lines{$section}{$desc}{calls}++;
2227 $lines{$section}{$desc}{duration} += $detail->duration;
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},
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 )
2252 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
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} );
2266 return(\@sections, \@lines);
2272 my $end = $self->_date;
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;
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)
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';
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);
2294 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
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')) {
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),
2310 $num_portedin++ if $phone_portedin;
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')) {
2319 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
2320 && $phone_deleted->lnp_status
2321 && $phone_deleted->lnp_status eq 'portingout') {
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;
2331 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
2334 # don't look at this service again
2335 push @seen, $h_cust_svc->svcnum;
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");
2345 sub _items_accountcode_cdr {
2350 my $section = { 'amount' => 0,
2353 'sort_weight' => '',
2355 'description' => 'Usage by Account Code',
2361 my %accountcodes = ();
2363 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2364 next unless $cust_bill_pkg->pkgnum > 0;
2366 my @header = $cust_bill_pkg->details_header;
2367 next unless scalar(@header);
2368 $section->{'header'} = join(',',@header);
2370 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2372 $section->{'header'} = $detail->formatted('format' => $format)
2373 if($detail->detail eq $section->{'header'});
2375 my $accountcode = $detail->accountcode;
2376 next unless $accountcode;
2378 my $amount = $detail->amount;
2379 next unless $amount && $amount > 0;
2381 $accountcodes{$accountcode} ||= {
2382 description => $accountcode,
2389 product_code => 'N/A',
2390 section => $section,
2391 ext_description => [ $section->{'header'} ],
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;
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);
2409 delete $l->{detail_temp};
2413 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
2415 return ($section,\@sorted_lines);
2418 sub _items_svc_phone_sections {
2420 my $conf = $self->conf;
2428 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2430 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2431 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
2433 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2434 next unless $cust_bill_pkg->pkgnum > 0;
2436 my @header = $cust_bill_pkg->details_header;
2437 next unless scalar(@header);
2439 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2441 my $phonenum = $detail->phonenum;
2442 next unless $phonenum;
2444 my $amount = $detail->amount;
2445 next unless $amount && $amount > 0;
2447 $sections{$phonenum} ||= { 'amount' => 0,
2450 'sort_weight' => -1,
2451 'phonenum' => $phonenum,
2453 $sections{$phonenum}{amount} += $amount; #subtotal
2454 $sections{$phonenum}{calls}++;
2455 $sections{$phonenum}{duration} += $detail->duration;
2457 my $desc = $detail->regionname;
2458 my $description = $desc;
2459 $description = substr($desc, 0, $maxlength). '...'
2460 if $format eq 'latex' && length($desc) > $maxlength;
2462 $lines{$phonenum}{$desc} ||= {
2463 description => &{$escape}($description),
2464 #pkgpart => $part_pkg->pkgpart,
2472 product_code => 'N/A',
2473 ext_description => [],
2476 $lines{$phonenum}{$desc}{amount} += $amount;
2477 $lines{$phonenum}{$desc}{calls}++;
2478 $lines{$phonenum}{$desc}{duration} += $detail->duration;
2480 my $line = $usage_class{$detail->classnum}->classname;
2481 $sections{"$phonenum $line"} ||=
2485 'sort_weight' => $usage_class{$detail->classnum}->weight,
2486 'phonenum' => $phonenum,
2487 'header' => [ @header ],
2489 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
2490 $sections{"$phonenum $line"}{calls}++;
2491 $sections{"$phonenum $line"}{duration} += $detail->duration;
2493 $lines{"$phonenum $line"}{$desc} ||= {
2494 description => &{$escape}($description),
2495 #pkgpart => $part_pkg->pkgpart,
2503 product_code => 'N/A',
2504 ext_description => [],
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);
2516 my %sectionmap = ();
2517 my $simple = new FS::usage_class { format => 'simple' }; #bleh
2518 foreach ( keys %sections ) {
2519 my @header = @{ $sections{$_}{header} || [] };
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' : '';
2527 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
2529 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
2530 'amount' => $sections{$_}{amount}, #subtotal
2531 'calls' => $sections{$_}{calls},
2532 'duration' => $sections{$_}{duration},
2534 'tax_section' => '',
2535 'phonenum' => $sections{$_}{phonenum},
2536 'sort_weight' => $sections{$_}{sort_weight},
2537 'post_total' => $summary, #inspire pagebreak
2539 ( map { $_ => $usage_class->$_($format, %gen_opt) }
2540 qw( description_generator
2543 total_line_generator
2550 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
2551 $a->{sort_weight} <=> $b->{sort_weight}
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} );
2566 if($conf->exists('phone_usage_class_summary')) {
2567 # this only works with Latex
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';
2590 $calls_detail{'description'} = 'Calls Detail: '
2591 . $section->{'phonenum'};
2592 push @newsections, \%calls_detail;
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 => [],
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',
2617 push @newlines, $line;
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'}
2629 my @extdesc = @{$line->{'ext_description'}};
2631 foreach my $extdesc ( @extdesc ) {
2632 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
2633 push @newextdesc, $extdesc;
2635 $line->{'ext_description'} = \@newextdesc;
2636 $line->{'section'} = $newsection;
2637 push @newlines, $line;
2642 return(\@newsections, \@newlines);
2645 return(\@sections, \@lines);
2649 =sub _items_usage_class_summary OPTIONS
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'.
2655 OPTIONS can include 'escape' (a function to escape the descriptions).
2659 sub _items_usage_class_summary {
2663 my $escape = $opt{escape} || sub { $_[0] };
2664 my $invnum = $self->invnum;
2665 my @classes = qsearch({
2666 'table' => 'usage_class',
2667 'select' => 'classnum, classname, SUM(amount) AS amount',
2668 'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
2669 ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
2670 'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
2671 ' GROUP BY classnum, classname, weight'.
2672 ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
2673 ' ORDER BY weight ASC',
2677 description => &{$escape}($self->mt('Usage Summary')),
2681 foreach my $class (@classes) {
2683 'description' => &{$escape}($class->classname),
2684 'amount' => sprintf('%.2f', $class->amount),
2685 'usage_classnum' => $class->classnum,
2686 'section' => $section,
2692 sub _items_previous {
2694 my $conf = $self->conf;
2695 my $cust_main = $self->cust_main;
2696 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2698 foreach ( @pr_cust_bill ) {
2699 my $date = $conf->exists('invoice_show_prior_due_date')
2700 ? 'due '. $_->due_date2str('short')
2701 : $self->time2str_local('short', $_->_date);
2703 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
2704 #'pkgpart' => 'N/A',
2706 'amount' => sprintf("%.2f", $_->owed),
2712 # 'description' => 'Previous Balance',
2713 # #'pkgpart' => 'N/A',
2714 # 'pkgnum' => 'N/A',
2715 # 'amount' => sprintf("%10.2f", $pr_total ),
2716 # 'ext_description' => [ map {
2717 # "Invoice ". $_->invnum.
2718 # " (". time2str("%x",$_->_date). ") ".
2719 # sprintf("%10.2f", $_->owed)
2720 # } @pr_cust_bill ],
2725 sub _items_credits {
2726 my( $self, %opt ) = @_;
2727 my $trim_len = $opt{'trim_len'} || 40;
2732 if ( $self->conf->exists('previous_balance-payments_since') ) {
2733 if ( $opt{'template'} eq 'statement' ) {
2734 # then the current bill is a "statement" (i.e. an invoice sent as
2735 # a payment receipt)
2736 # and in that case we want to see payments on or after THIS invoice
2737 @objects = qsearch('cust_credit', {
2738 'custnum' => $self->custnum,
2739 '_date' => {op => '>=', value => $self->_date},
2743 $date = $self->previous_bill->_date if $self->previous_bill;
2744 @objects = qsearch('cust_credit', {
2745 'custnum' => $self->custnum,
2746 '_date' => {op => '>=', value => $date},
2750 @objects = $self->cust_credited;
2753 foreach my $obj ( @objects ) {
2754 my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
2756 my $reason = substr($cust_credit->reason, 0, $trim_len);
2757 $reason .= '...' if length($reason) < length($cust_credit->reason);
2758 $reason = " ($reason) " if $reason;
2761 #'description' => 'Credit ref\#'. $_->crednum.
2762 # " (". time2str("%x",$_->cust_credit->_date) .")".
2764 'description' => $self->mt('Credit applied').' '.
2765 $self->time2str_local('short', $obj->_date). $reason,
2766 'amount' => sprintf("%.2f",$obj->amount),
2774 sub _items_payments {
2779 my $detailed = $self->conf->exists('invoice_payment_details');
2781 if ( $self->conf->exists('previous_balance-payments_since') ) {
2782 # then show payments dated on/after the previous bill...
2783 if ( $opt{'template'} eq 'statement' ) {
2784 # then the current bill is a "statement" (i.e. an invoice sent as
2785 # a payment receipt)
2786 # and in that case we want to see payments on or after THIS invoice
2787 @objects = qsearch('cust_pay', {
2788 'custnum' => $self->custnum,
2789 '_date' => {op => '>=', value => $self->_date},
2792 # the normal case: payments on or after the previous invoice
2794 $date = $self->previous_bill->_date if $self->previous_bill;
2795 @objects = qsearch('cust_pay', {
2796 'custnum' => $self->custnum,
2797 '_date' => {op => '>=', value => $date},
2799 # and before the current bill...
2800 @objects = grep { $_->_date < $self->_date } @objects;
2803 @objects = $self->cust_bill_pay;
2806 foreach my $obj (@objects) {
2807 my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
2808 my $desc = $self->mt('Payment received').' '.
2809 $self->time2str_local('short', $cust_pay->_date );
2810 $desc .= $self->mt(' via ') .
2811 $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
2815 'description' => $desc,
2816 'amount' => sprintf("%.2f", $obj->amount )
2826 my $conf = $self->conf;
2829 my ($pr_total) = $self->previous;
2830 my ($previous_charges_desc, $new_charges_desc, $new_charges_amount);
2832 if ( $conf->exists('previous_balance-exclude_from_total') ) {
2833 # can we do some caching on this stuff? it's going to change infrequently
2835 $previous_charges_desc = $self->mt(
2836 $conf->config('previous_balance-text') || 'Previous Balance'
2839 # then return separate lines for previous balance and total new charges
2842 { total_item => $previous_charges_desc,
2843 total_amount => sprintf('%.2f',$pr_total)
2846 $new_charges_desc = $self->mt(
2847 $conf->config('previous_balance-text-total_new_charges')
2848 || 'Total New Charges'
2851 $new_charges_amount = $self->charged;
2855 $new_charges_desc = $self->mt('Total Charges');
2856 $new_charges_amount = sprintf('%.2f',$self->charged + $pr_total);
2860 if ( $conf->exists('invoice_show_prior_due_date') ) {
2861 # then the due date should be shown with Total New Charges,
2862 # and should NOT be shown with the Balance Due message.
2863 if ( $self->due_date ) {
2864 # localize the "Please pay by" message and the date itself
2865 # (grammar issues with this, yeah)
2866 $new_charges_desc .= ' - ' . $self->mt('Please pay by') . ' ' .
2867 $self->due_date2str('short');
2868 } elsif ( $self->terms ) {
2869 # phrases like "due on receipt" should be localized
2870 $new_charges_desc .= ' - ' . $self->mt($self->terms);
2875 { total_item => $new_charges_desc,
2876 total_amount => $new_charges_amount,
2884 =item call_details [ OPTION => VALUE ... ]
2886 Returns an array of CSV strings representing the call details for this invoice
2887 The only option available is the boolean prepend_billed_number
2892 my ($self, %opt) = @_;
2894 my $format_function = sub { shift };
2896 if ($opt{prepend_billed_number}) {
2897 $format_function = sub {
2901 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
2906 my @details = map { $_->details( 'format_function' => $format_function,
2907 'escape_function' => sub{ return() },
2911 $self->cust_bill_pkg;
2912 my $header = $details[0];
2913 ( $header, grep { $_ ne $header } @details );
2923 =item process_reprint
2927 sub process_reprint {
2928 process_re_X('print', @_);
2931 =item process_reemail
2935 sub process_reemail {
2936 process_re_X('email', @_);
2944 process_re_X('fax', @_);
2952 process_re_X('ftp', @_);
2959 sub process_respool {
2960 process_re_X('spool', @_);
2965 my( $method, $job ) = ( shift, shift );
2966 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2969 warn Dumper($param) if $DEBUG;
2979 # this is called from search/cust_bill.html and given all its search
2980 # parameters, so it needs to perform the same search.
2983 # spool_invoice ftp_invoice fax_invoice print_invoice
2984 my($method, $job, %param ) = @_;
2986 warn "re_X $method for job $job with param:\n".
2987 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2990 #some false laziness w/search/cust_bill.html
2991 $param{'order_by'} = 'cust_bill._date';
2993 my $query = FS::cust_bill->search(\%param);
2994 delete $query->{'count_query'};
2995 delete $query->{'count_addl'};
2997 $query->{debug} = 1; # was in here before, is obviously useful
2999 my @cust_bill = qsearch( $query );
3001 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3003 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3006 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3007 foreach my $cust_bill ( @cust_bill ) {
3008 $cust_bill->$method();
3010 if ( $job ) { #progressbar foo
3012 if ( time - $min_sec > $last ) {
3013 my $error = $job->update_statustext(
3014 int( 100 * $num / scalar(@cust_bill) )
3016 die $error if $error;
3027 +{ ( map { $_=>$self->$_ } $self->fields ),
3028 'owed' => $self->owed,
3029 #XXX last payment applied date
3035 =head1 CLASS METHODS
3041 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3046 my ($class, $start, $end) = @_;
3048 $class->paid_sql($start, $end). ' - '.
3049 $class->credited_sql($start, $end);
3054 Returns an SQL fragment to retreive the net amount (charged minus credited).
3059 my ($class, $start, $end) = @_;
3060 'charged - '. $class->credited_sql($start, $end);
3065 Returns an SQL fragment to retreive the amount paid against this invoice.
3070 my ($class, $start, $end) = @_;
3071 $start &&= "AND cust_bill_pay._date <= $start";
3072 $end &&= "AND cust_bill_pay._date > $end";
3073 $start = '' unless defined($start);
3074 $end = '' unless defined($end);
3075 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3076 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
3081 Returns an SQL fragment to retreive the amount credited against this invoice.
3086 my ($class, $start, $end) = @_;
3087 $start &&= "AND cust_credit_bill._date <= $start";
3088 $end &&= "AND cust_credit_bill._date > $end";
3089 $start = '' unless defined($start);
3090 $end = '' unless defined($end);
3091 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3092 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
3097 Returns an SQL fragment to retrieve the due date of an invoice.
3098 Currently only supported on PostgreSQL.
3103 die "don't use: doesn't account for agent-specific invoice_default_terms";
3105 #we're passed a $conf but not a specific customer (that's in the query), so
3106 # to make this work we'd need an agentnum-aware "condition_sql_conf" like
3107 # "condition_sql_option" that retreives a conf value with SQL in an agent-
3110 my $conf = new FS::Conf;
3114 cust_bill.invoice_terms,
3115 cust_main.invoice_terms,
3116 \''.($conf->config('invoice_default_terms') || '').'\'
3117 ), E\'Net (\\\\d+)\'
3119 ) * 86400 + cust_bill._date'
3130 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3131 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base