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;
270 This method now works but you probably shouldn't use it. Instead, apply a
271 credit against the invoice, or use the new void method.
273 Using this method to delete invoices outright is really, really bad. There
274 would be no record you ever posted this invoice, and there are no check to
275 make sure charged = 0 or that there are no associated cust_bill_pkg records.
277 Really, don't use it.
283 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
285 local $SIG{HUP} = 'IGNORE';
286 local $SIG{INT} = 'IGNORE';
287 local $SIG{QUIT} = 'IGNORE';
288 local $SIG{TERM} = 'IGNORE';
289 local $SIG{TSTP} = 'IGNORE';
290 local $SIG{PIPE} = 'IGNORE';
292 my $oldAutoCommit = $FS::UID::AutoCommit;
293 local $FS::UID::AutoCommit = 0;
296 foreach my $table (qw(
304 #cust_event # problematic
306 foreach my $linked ( $self->$table() ) {
307 my $error = $linked->delete;
309 $dbh->rollback if $oldAutoCommit;
316 my $error = $self->SUPER::delete(@_);
318 $dbh->rollback if $oldAutoCommit;
322 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
328 =item replace [ OLD_RECORD ]
330 You can, but probably shouldn't modify invoices...
332 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
333 supplied, replaces this record. If there is an error, returns the error,
334 otherwise returns false.
338 #replace can be inherited from Record.pm
340 # replace_check is now the preferred way to #implement replace data checks
341 # (so $object->replace() works without an argument)
344 my( $new, $old ) = ( shift, shift );
345 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
346 #return "Can't change _date!" unless $old->_date eq $new->_date;
347 return "Can't change _date" unless $old->_date == $new->_date;
348 return "Can't change charged" unless $old->charged == $new->charged
349 || $old->pending eq 'Y'
350 || $old->charged == 0
351 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
357 =item add_cc_surcharge
363 sub add_cc_surcharge {
364 my ($self, $pkgnum, $amount) = (shift, shift, shift);
367 my $cust_bill_pkg = new FS::cust_bill_pkg({
368 'invnum' => $self->invnum,
372 $error = $cust_bill_pkg->insert;
373 return $error if $error;
375 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
376 $self->charged($self->charged+$amount);
377 $error = $self->replace;
378 return $error if $error;
380 $self->apply_payments_and_credits;
386 Checks all fields to make sure this is a valid invoice. If there is an error,
387 returns the error, otherwise returns false. Called by the insert and replace
396 $self->ut_numbern('invnum')
397 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
398 || $self->ut_numbern('_date')
399 || $self->ut_money('charged')
400 || $self->ut_numbern('printed')
401 || $self->ut_enum('closed', [ '', 'Y' ])
402 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
403 || $self->ut_numbern('agent_invid') #varchar?
404 || $self->ut_flag('pending')
406 return $error if $error;
408 $self->_date(time) unless $self->_date;
410 $self->printed(0) if $self->printed eq '';
417 Returns the displayed invoice number for this invoice: agent_invid if
418 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
424 if ( $self->agent_invid
425 && FS::Conf->new->exists('cust_bill-default_agent_invid') ) {
426 return $self->agent_invid;
428 return $self->invnum;
434 Returns the customer's last invoice before this one.
440 if ( !$self->get('previous_bill') ) {
441 $self->set('previous_bill', qsearchs({
442 'table' => 'cust_bill',
443 'hashref' => { 'custnum' => $self->custnum,
444 '_date' => { op=>'<', value=>$self->_date } },
445 'order_by' => 'ORDER BY _date DESC LIMIT 1',
448 $self->get('previous_bill');
453 Returns a list consisting of the total previous balance for this customer,
454 followed by the previous outstanding invoices (as FS::cust_bill objects also).
460 # simple memoize; we use this a lot
461 if (!$self->get('previous')) {
463 my @cust_bill = sort { $a->_date <=> $b->_date }
464 grep { $_->owed != 0 }
465 qsearch( 'cust_bill', { 'custnum' => $self->custnum,
466 #'_date' => { op=>'<', value=>$self->_date },
467 'invnum' => { op=>'<', value=>$self->invnum },
470 foreach ( @cust_bill ) { $total += $_->owed; }
471 $self->set('previous', [$total, @cust_bill]);
473 return @{ $self->get('previous') };
476 =item enable_previous
478 Whether to show the 'Previous Charges' section when printing this invoice.
479 The negation of the 'disable_previous_balance' config setting.
483 sub enable_previous {
485 my $agentnum = $self->cust_main->agentnum;
486 !$self->conf->exists('disable_previous_balance', $agentnum);
491 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
498 { 'table' => 'cust_bill_pkg',
499 'hashref' => { 'invnum' => $self->invnum },
500 'order_by' => 'ORDER BY billpkgnum', #important? otherwise we could use
501 # the AUTLOADED FK search. or should
502 # that default to ORDER by the pkey?
507 =item cust_bill_pkg_pkgnum PKGNUM
509 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
514 sub cust_bill_pkg_pkgnum {
515 my( $self, $pkgnum ) = @_;
517 { 'table' => 'cust_bill_pkg',
518 'hashref' => { 'invnum' => $self->invnum,
521 'order_by' => 'ORDER BY billpkgnum',
528 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
535 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
536 $self->cust_bill_pkg;
538 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
543 Returns true if any of the packages (or their definitions) corresponding to the
544 line items for this invoice have the no_auto flag set.
550 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
553 =item open_cust_bill_pkg
555 Returns the open line items for this invoice.
557 Note that cust_bill_pkg with both setup and recur fees are returned as two
558 separate line items, each with only one fee.
562 # modeled after cust_main::open_cust_bill
563 sub open_cust_bill_pkg {
566 # grep { $_->owed > 0 } $self->cust_bill_pkg
568 my %other = ( 'recur' => 'setup',
569 'setup' => 'recur', );
571 foreach my $field ( qw( recur setup )) {
572 push @open, map { $_->set( $other{$field}, 0 ); $_; }
573 grep { $_->owed($field) > 0 }
574 $self->cust_bill_pkg;
582 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
586 #false laziness w/cust_pkg.pm
590 'table' => 'cust_event',
591 'addl_from' => 'JOIN part_event USING ( eventpart )',
592 'hashref' => { 'tablenum' => $self->invnum },
593 'extra_sql' => " AND eventtable = 'cust_bill' ",
599 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
603 #false laziness w/cust_pkg.pm
607 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
608 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
609 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
610 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
611 $sth->fetchrow_arrayref->[0];
616 Returns the customer (see L<FS::cust_main>) for this invoice.
620 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this invoice
622 Returns a list: an empty list on success or a list of errors.
629 grep { $_->suspend(@_) }
630 grep {! $_->getfield('cancel') }
635 =item cust_suspend_if_balance_over AMOUNT
637 Suspends the customer associated with this invoice if the total amount owed on
638 this invoice and all older invoices is greater than the specified amount.
640 Returns a list: an empty list on success or a list of errors.
644 sub cust_suspend_if_balance_over {
645 my( $self, $amount ) = ( shift, shift );
646 my $cust_main = $self->cust_main;
647 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
650 $cust_main->suspend(@_);
656 Cancel the packages on this invoice. Largely similar to the cust_main version, but does not bother yet with banned payment options
661 my( $self, %opt ) = @_;
663 warn "$me cancel called on cust_bill ". $self->invnum . " with options ".
664 join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
667 return ( 'Access denied' )
668 unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
670 my @pkgs = $self->cust_pkg;
672 if ( !$opt{nobill} && $self->conf->exists('bill_usage_on_cancel') ) {
674 my $error = $self->cust_main->bill( pkg_list => [ @pkgs ], cancel => 1 );
675 warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
680 map { $_->cancel(%opt) }
681 grep { ! $_->getfield('cancel') }
687 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
693 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
694 sort { $a->_date <=> $b->_date }
695 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
700 =item cust_credit_bill
702 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
708 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
709 sort { $a->_date <=> $b->_date }
710 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
714 sub cust_credit_bill {
715 shift->cust_credited(@_);
718 #=item cust_bill_pay_pkgnum PKGNUM
720 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
721 #with matching pkgnum.
725 #sub cust_bill_pay_pkgnum {
726 # my( $self, $pkgnum ) = @_;
727 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
728 # sort { $a->_date <=> $b->_date }
729 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
730 # 'pkgnum' => $pkgnum,
735 =item cust_bill_pay_pkg PKGNUM
737 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
738 applied against the matching pkgnum.
742 sub cust_bill_pay_pkg {
743 my( $self, $pkgnum ) = @_;
746 'select' => 'cust_bill_pay_pkg.*',
747 'table' => 'cust_bill_pay_pkg',
748 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
749 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
750 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
751 " AND cust_bill_pkg.pkgnum = $pkgnum",
756 #=item cust_credited_pkgnum PKGNUM
758 #=item cust_credit_bill_pkgnum PKGNUM
760 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
761 #with matching pkgnum.
765 #sub cust_credited_pkgnum {
766 # my( $self, $pkgnum ) = @_;
767 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
768 # sort { $a->_date <=> $b->_date }
769 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
770 # 'pkgnum' => $pkgnum,
775 #sub cust_credit_bill_pkgnum {
776 # shift->cust_credited_pkgnum(@_);
779 =item cust_credit_bill_pkg PKGNUM
781 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
782 applied against the matching pkgnum.
786 sub cust_credit_bill_pkg {
787 my( $self, $pkgnum ) = @_;
790 'select' => 'cust_credit_bill_pkg.*',
791 'table' => 'cust_credit_bill_pkg',
792 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
793 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
794 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
795 " AND cust_bill_pkg.pkgnum = $pkgnum",
800 =item cust_bill_batch
802 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
806 sub cust_bill_batch {
808 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
813 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
814 hash keyed by term length.
820 FS::discount_plan->all($self);
825 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
832 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
834 foreach (@taxlines) { $total += $_->setup; }
840 Returns the amount owed (still outstanding) on this invoice, which is charged
841 minus all payment applications (see L<FS::cust_bill_pay>) and credit
842 applications (see L<FS::cust_credit_bill>).
848 my $balance = $self->charged;
849 $balance -= $_->amount foreach ( $self->cust_bill_pay );
850 $balance -= $_->amount foreach ( $self->cust_credited );
851 $balance = sprintf( "%.2f", $balance);
852 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
857 my( $self, $pkgnum ) = @_;
859 #my $balance = $self->charged;
861 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
863 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
864 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
866 $balance = sprintf( "%.2f", $balance);
867 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
873 Returns true if this invoice should be hidden. See the
874 selfservice-hide_invoices-taxclass configuraiton setting.
880 my $conf = $self->conf;
881 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
883 my @cust_bill_pkg = $self->cust_bill_pkg;
884 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
885 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
888 =item apply_payments_and_credits [ OPTION => VALUE ... ]
890 Applies unapplied payments and credits to this invoice.
892 A hash of optional arguments may be passed. Currently "manual" is supported.
893 If true, a payment receipt is sent instead of a statement when
894 'payment_receipt_email' configuration option is set.
896 If there is an error, returns the error, otherwise returns false.
900 sub apply_payments_and_credits {
901 my( $self, %options ) = @_;
902 my $conf = $self->conf;
904 local $SIG{HUP} = 'IGNORE';
905 local $SIG{INT} = 'IGNORE';
906 local $SIG{QUIT} = 'IGNORE';
907 local $SIG{TERM} = 'IGNORE';
908 local $SIG{TSTP} = 'IGNORE';
909 local $SIG{PIPE} = 'IGNORE';
911 my $oldAutoCommit = $FS::UID::AutoCommit;
912 local $FS::UID::AutoCommit = 0;
915 $self->select_for_update; #mutex
917 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
918 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
920 if ( $conf->exists('pkg-balances') ) {
921 # limit @payments & @credits to those w/ a pkgnum grepped from $self
922 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
923 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
924 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
927 while ( $self->owed > 0 and ( @payments || @credits ) ) {
930 if ( @payments && @credits ) {
932 #decide which goes first by weight of top (unapplied) line item
934 my @open_lineitems = $self->open_cust_bill_pkg;
937 max( map { $_->part_pkg->pay_weight || 0 }
942 my $max_credit_weight =
943 max( map { $_->part_pkg->credit_weight || 0 }
949 #if both are the same... payments first? it has to be something
950 if ( $max_pay_weight >= $max_credit_weight ) {
956 } elsif ( @payments ) {
958 } elsif ( @credits ) {
961 die "guru meditation #12 and 35";
965 if ( $app eq 'pay' ) {
967 my $payment = shift @payments;
968 $unapp_amount = $payment->unapplied;
969 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
970 $app->pkgnum( $payment->pkgnum )
971 if $conf->exists('pkg-balances') && $payment->pkgnum;
973 } elsif ( $app eq 'credit' ) {
975 my $credit = shift @credits;
976 $unapp_amount = $credit->credited;
977 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
978 $app->pkgnum( $credit->pkgnum )
979 if $conf->exists('pkg-balances') && $credit->pkgnum;
982 die "guru meditation #12 and 35";
986 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
987 warn "owed_pkgnum ". $app->pkgnum;
988 $owed = $self->owed_pkgnum($app->pkgnum);
992 next unless $owed > 0;
994 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
995 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
997 $app->invnum( $self->invnum );
999 my $error = $app->insert(%options);
1001 $dbh->rollback if $oldAutoCommit;
1002 return "Error inserting ". $app->table. " record: $error";
1004 die $error if $error;
1008 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1015 Sends this invoice to the destinations configured for this customer: sends
1016 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1018 Options can be passed as a hashref. Positional parameters are no longer
1021 I<template>: a suffix for alternate invoices
1023 I<agentnum>: obsolete, now does nothing.
1025 I<from> overrides the default email invoice From: address.
1027 I<amount>: obsolete, does nothing
1029 I<notice_name> overrides "Invoice" as the name of the sent document
1030 (templates from 10/2009 or newer required).
1032 I<lpr> overrides the system 'lpr' option as the command to print a document
1033 from standard input.
1039 my $opt = ref($_[0]) ? $_[0] : +{ @_ };
1040 my $conf = $self->conf;
1042 my $cust_main = $self->cust_main;
1044 my @invoicing_list = $cust_main->invoicing_list;
1047 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1048 && ! $cust_main->invoice_noemail;
1051 if grep { $_ eq 'POST' } @invoicing_list; #postal
1053 #this has never been used post-$ORIGINAL_ISP afaik
1054 $self->fax_invoice($opt)
1055 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1063 my $opt = shift || {};
1064 if ($opt and !ref($opt)) {
1065 die ref($self). '->email called with positional parameters';
1068 my $conf = $self->conf;
1070 my $from = delete $opt->{from};
1072 # this is where we set the From: address
1073 $from ||= $self->_agent_invoice_from || #XXX should go away
1074 $conf->invoice_from_full( $self->cust_main->agentnum );
1076 my @invoicing_list = $self->cust_main->invoicing_list_emailonly;
1078 if ( ! @invoicing_list ) { #no recipients
1079 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1080 die 'No recipients for customer #'. $self->custnum;
1082 #default: better to notify this person than silence
1083 @invoicing_list = ($from);
1087 $self->SUPER::email( {
1089 'to' => \@invoicing_list,
1095 #this stays here for now because its explicitly used as
1096 # FS::cust_bill::queueable_email
1097 sub queueable_email {
1100 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1101 or die "invalid invoice number: " . $opt{invnum};
1104 $self->set('mode', $opt{mode});
1107 my %args = map {$_ => $opt{$_}}
1109 qw( from notice_name no_coupon template );
1111 my $error = $self->email( \%args );
1112 die $error if $error;
1118 my $conf = $self->conf;
1120 #my $template = scalar(@_) ? shift : '';
1123 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1126 my $cust_main = $self->cust_main;
1127 my $name = $cust_main->name;
1128 my $name_short = $cust_main->name_short;
1129 my $invoice_number = $self->invnum;
1130 my $invoice_date = $self->_date_pretty;
1132 eval qq("$subject");
1135 =item lpr_data HASHREF
1137 Returns the postscript or plaintext for this invoice as an arrayref.
1139 Options must be passed as a hashref. Positional parameters are no longer
1142 I<template>, if specified, is the name of a suffix for alternate invoices.
1144 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1150 my $conf = $self->conf;
1151 my $opt = shift || {};
1152 if ($opt and !ref($opt)) {
1153 # nobody does this anyway
1154 die "FS::cust_bill::lpr_data called with positional parameters";
1157 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1158 [ $self->$method( $opt ) ];
1163 Prints this invoice.
1165 Options must be passed as a hashref.
1167 I<template>, if specified, is the name of a suffix for alternate invoices.
1169 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1175 return if $self->hide;
1176 my $conf = $self->conf;
1177 my $opt = shift || {};
1178 if ($opt and !ref($opt)) {
1179 die "FS::cust_bill::print called with positional parameters";
1182 my $lpr = delete $opt->{lpr};
1183 if($conf->exists('invoice_print_pdf')) {
1184 # Add the invoice to the current batch.
1185 $self->batch_invoice($opt);
1189 $self->lpr_data($opt),
1190 'agentnum' => $self->cust_main->agentnum,
1196 =item fax_invoice HASHREF
1200 Options must be passed as a hashref.
1202 I<template>, if specified, is the name of a suffix for alternate invoices.
1204 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1210 return if $self->hide;
1211 my $conf = $self->conf;
1212 my $opt = shift || {};
1213 if ($opt and !ref($opt)) {
1214 die "FS::cust_bill::fax_invoice called with positional parameters";
1217 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1218 unless $conf->exists('invoice_latex');
1220 my $dialstring = $self->cust_main->getfield('fax');
1223 my $error = send_fax( 'docdata' => $self->lpr_data($opt),
1224 'dialstring' => $dialstring,
1226 die $error if $error;
1230 =item batch_invoice [ HASHREF ]
1232 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1233 isn't an open batch, one will be created.
1235 HASHREF may contain any options to be passed to C<print_pdf>.
1240 my ($self, $opt) = @_;
1241 my $bill_batch = $self->get_open_bill_batch;
1242 my $cust_bill_batch = FS::cust_bill_batch->new({
1243 batchnum => $bill_batch->batchnum,
1244 invnum => $self->invnum,
1246 return $cust_bill_batch->insert($opt);
1249 =item get_open_batch
1251 Returns the currently open batch as an FS::bill_batch object, creating a new
1252 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1257 sub get_open_bill_batch {
1259 my $conf = $self->conf;
1260 my $hashref = { status => 'O' };
1261 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1262 ? $self->cust_main->agentnum
1264 my $batch = qsearchs('bill_batch', $hashref);
1265 return $batch if $batch;
1266 $batch = FS::bill_batch->new($hashref);
1267 my $error = $batch->insert;
1268 die $error if $error;
1272 =item ftp_invoice [ TEMPLATENAME ]
1274 Sends this invoice data via FTP.
1276 TEMPLATENAME is unused?
1282 my $conf = $self->conf;
1283 my $template = scalar(@_) ? shift : '';
1286 'protocol' => 'ftp',
1287 'server' => $conf->config('cust_bill-ftpserver'),
1288 'username' => $conf->config('cust_bill-ftpusername'),
1289 'password' => $conf->config('cust_bill-ftppassword'),
1290 'dir' => $conf->config('cust_bill-ftpdir'),
1291 'format' => $conf->config('cust_bill-ftpformat'),
1295 =item spool_invoice [ TEMPLATENAME ]
1297 Spools this invoice data (see L<FS::spool_csv>)
1299 TEMPLATENAME is unused?
1305 my $conf = $self->conf;
1306 my $template = scalar(@_) ? shift : '';
1309 'format' => $conf->config('cust_bill-spoolformat'),
1310 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1314 =item send_csv OPTION => VALUE, ...
1316 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1320 protocol - currently only "ftp"
1326 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1327 and YYMMDDHHMMSS is a timestamp.
1329 See L</print_csv> for a description of the output format.
1334 my($self, %opt) = @_;
1338 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1339 mkdir $spooldir, 0700 unless -d $spooldir;
1341 # don't localize dates here, they're a defined format
1342 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1343 my $file = "$spooldir/$tracctnum.csv";
1345 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1347 open(CSV, ">$file") or die "can't open $file: $!";
1355 if ( $opt{protocol} eq 'ftp' ) {
1356 eval "use Net::FTP;";
1358 $net = Net::FTP->new($opt{server}) or die @$;
1360 die "unknown protocol: $opt{protocol}";
1363 $net->login( $opt{username}, $opt{password} )
1364 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1366 $net->binary or die "can't set binary mode";
1368 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1370 $net->put($file) or die "can't put $file: $!";
1380 Spools CSV invoice data.
1386 =item format - any of FS::Misc::::Invoicing::spool_formats
1388 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
1389 customer has the corresponding invoice destinations set (see
1390 L<FS::cust_main_invoice>).
1392 =item agent_spools - if set to a true value, will spool to per-agent files
1393 rather than a single global file
1395 =item upload_targetnum - if set to a target (see L<FS::upload_target>), will
1396 append to that spool. L<FS::Cron::upload> will then send the spool file to
1399 =item balanceover - if set, only spools the invoice if the total amount owed on
1400 this invoice and all older invoices is greater than the specified amount.
1402 =item time - the "current time". Controls the printing of past due messages
1410 my($self, %opt) = @_;
1412 my $time = $opt{'time'} || time;
1413 my $cust_main = $self->cust_main;
1415 if ( $opt{'dest'} ) {
1416 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1417 $cust_main->invoicing_list;
1418 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1419 || ! keys %invoicing_list;
1422 if ( $opt{'balanceover'} ) {
1424 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1427 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1428 mkdir $spooldir, 0700 unless -d $spooldir;
1430 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', $time);
1433 if ( $opt{'agent_spools'} ) {
1434 $file = 'agentnum'.$cust_main->agentnum;
1439 if ( $opt{'upload_targetnum'} ) {
1440 $spooldir .= '/target'.$opt{'upload_targetnum'};
1441 mkdir $spooldir, 0700 unless -d $spooldir;
1442 } # otherwise it just goes into export.xxx/cust_bill
1444 if ( lc($opt{'format'}) eq 'billco' ) {
1448 $file = "$spooldir/$file.csv";
1450 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
1452 open(CSV, ">>$file") or die "can't open $file: $!";
1453 flock(CSV, LOCK_EX);
1458 if ( lc($opt{'format'}) eq 'billco' ) {
1460 flock(CSV, LOCK_UN);
1463 $file =~ s/-header.csv$/-detail.csv/;
1465 open(CSV,">>$file") or die "can't open $file: $!";
1466 flock(CSV, LOCK_EX);
1470 print CSV $detail if defined($detail);
1472 flock(CSV, LOCK_UN);
1479 =item print_csv OPTION => VALUE, ...
1481 Returns CSV data for this invoice.
1485 format - 'default', 'billco', 'oneline', 'bridgestone'
1487 Returns a list consisting of two scalars. The first is a single line of CSV
1488 header information for this invoice. The second is one or more lines of CSV
1489 detail information for this invoice.
1491 If I<format> is not specified or "default", the fields of the CSV file are as
1494 record_type, invnum, custnum, _date, charged, first, last, company, address1,
1495 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1499 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1501 B<record_type> is C<cust_bill> for the initial header line only. The
1502 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1503 fields are filled in.
1505 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1506 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1509 =item invnum - invoice number
1511 =item custnum - customer number
1513 =item _date - invoice date
1515 =item charged - total invoice amount
1517 =item first - customer first name
1519 =item last - customer first name
1521 =item company - company name
1523 =item address1 - address line 1
1525 =item address2 - address line 1
1535 =item pkg - line item description
1537 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1539 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1541 =item sdate - start date for recurring fee
1543 =item edate - end date for recurring fee
1547 If I<format> is "billco", the fields of the header CSV file are as follows:
1549 +-------------------------------------------------------------------+
1550 | FORMAT HEADER FILE |
1551 |-------------------------------------------------------------------|
1552 | Field | Description | Name | Type | Width |
1553 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1554 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1555 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1556 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1557 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1558 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1559 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1560 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1561 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1562 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1563 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1564 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1565 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1566 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1567 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1568 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1569 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1570 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1571 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1572 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1573 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1574 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1575 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1576 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1577 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1578 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1579 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1580 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1581 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1582 +-------+-------------------------------+------------+------+-------+
1584 If I<format> is "billco", the fields of the detail CSV file are as follows:
1586 FORMAT FOR DETAIL FILE
1588 Field | Description | Name | Type | Width
1589 1 | N/A-Leave Empty | RC | CHAR | 2
1590 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1591 3 | Account Number | TRACCTNUM | CHAR | 15
1592 4 | Invoice Number | TRINVOICE | CHAR | 15
1593 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1594 6 | Transaction Detail | DETAILS | CHAR | 100
1595 7 | Amount | AMT | NUM* | 9
1596 8 | Line Format Control** | LNCTRL | CHAR | 2
1597 9 | Grouping Code | GROUP | CHAR | 2
1598 10 | User Defined | ACCT CODE | CHAR | 15
1600 If format is 'oneline', there is no detail file. Each invoice has a
1601 header line only, with the fields:
1603 Agent number, agent name, customer number, first name, last name, address
1604 line 1, address line 2, city, state, zip, invoice date, invoice number,
1605 amount charged, amount due, previous balance, due date.
1607 and then, for each line item, three columns containing the package number,
1608 description, and amount.
1610 If format is 'bridgestone', there is no detail file. Each invoice has a
1611 header line with the following fields in a fixed-width format:
1613 Customer number (in display format), date, name (first last), company,
1614 address 1, address 2, city, state, zip.
1616 This is a mailing list format, and has no per-invoice fields. To avoid
1617 sending redundant notices, the spooling event should have a "once" or
1618 "once_percust_every" condition.
1623 my($self, %opt) = @_;
1625 eval "use Text::CSV_XS";
1628 my $cust_main = $self->cust_main;
1630 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1631 my $format = lc($opt{'format'});
1633 my $time = $opt{'time'} || time;
1635 my $tracctnum = ''; #leaking out from billco-specific sections :/
1636 if ( $format eq 'billco' ) {
1639 $self->conf->config('billco-account_num', $cust_main->agentnum);
1641 $tracctnum = $account_num eq 'display_custnum'
1642 ? $cust_main->display_custnum
1643 : $opt{'tracctnum'};
1646 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1648 my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
1650 my( $previous_balance, @unused ) = $self->previous; #previous balance
1652 my $pmt_cr_applied = 0;
1653 $pmt_cr_applied += $_->{'amount'}
1654 foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
1656 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1659 '', # 1 | N/A-Leave Empty CHAR 2
1660 '', # 2 | N/A-Leave Empty CHAR 15
1661 $tracctnum, # 3 | Transaction Account No CHAR 15
1662 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1663 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1664 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1665 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1666 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1667 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1668 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1669 '', # 10 | Ancillary Billing Information CHAR 30
1670 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1671 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1674 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1677 $duedate, # 14 | Bill Due Date CHAR 10
1679 $previous_balance, # 15 | Previous Balance NUM* 9
1680 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1681 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1682 $totaldue, # 18 | Total Amt Due NUM* 9
1683 $totaldue, # 19 | Total Amt Due NUM* 9
1684 '', # 20 | 30 Day Aging NUM* 9
1685 '', # 21 | 60 Day Aging NUM* 9
1686 '', # 22 | 90 Day Aging NUM* 9
1687 'N', # 23 | Y/N CHAR 1
1688 '', # 24 | Remittance automation CHAR 100
1689 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1690 $self->custnum, # 26 | Customer Reference Number CHAR 15
1691 '0', # 27 | Federal Tax*** NUM* 9
1692 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1693 '0', # 29 | Other Taxes & Fees*** NUM* 9
1696 } elsif ( $format eq 'oneline' ) { #name
1698 my ($previous_balance) = $self->previous;
1699 $previous_balance = sprintf('%.2f', $previous_balance);
1700 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1706 $self->_items_pkg, #_items_nontax? no sections or anything
1711 $cust_main->agentnum,
1712 $cust_main->agent->agent,
1716 $cust_main->company,
1717 $cust_main->address1,
1718 $cust_main->address2,
1724 time2str("%x", $self->_date),
1729 $self->due_date2str("%x"),
1734 } elsif ( $format eq 'bridgestone' ) {
1736 # bypass the CSV stuff and just return this
1737 my $longdate = time2str('%B %d, %Y', $time); #current time, right?
1738 my $zip = $cust_main->zip;
1740 my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
1744 "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
1746 $cust_main->display_custnum,
1748 uc(substr($cust_main->contact_firstlast,0,30)),
1749 uc(substr($cust_main->company ,0,30)),
1750 uc(substr($cust_main->address1 ,0,30)),
1751 uc(substr($cust_main->address2 ,0,30)),
1752 uc(substr($cust_main->city ,0,20)),
1753 uc($cust_main->state),
1759 } elsif ( $format eq 'ics' ) {
1761 my $bill = $cust_main->bill_location;
1762 my $zip = $bill->zip;
1766 if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
1771 # minor false laziness with print_generic
1772 my ($previous_balance) = $self->previous;
1773 my $balance_due = $self->owed + $previous_balance;
1774 my $payment_total = sum(0, map { $_->{'amount'} } $self->_items_payments);
1775 my $credit_total = sum(0, map { $_->{'amount'} } $self->_items_credits);
1778 if ( $self->due_date and $time >= $self->due_date ) {
1779 $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
1783 my $header = sprintf(
1784 '%-10s%-30s%-48s%-2s%-50s%-30s%-30s%-25s%-2s%-5s%-4s%-8s%-8s%-10s%-10s%-10s%-10s%-10s%-10s%-480s%-35s',
1785 $cust_main->display_custnum, #BID
1786 uc($cust_main->first), #FNAME
1787 uc($cust_main->last), #LNAME
1788 '00', #BATCH, should this ever be anything else?
1789 uc($cust_main->company), #COMP
1790 uc($bill->address1), #STREET1
1791 uc($bill->address2), #STREET2
1792 uc($bill->city), #CITY
1793 uc($bill->state), #STATE
1796 time2str('%Y%m%d', $self->_date), #BILL_DATE
1797 $self->due_date2str('%Y%m%d'), #DUE_DATE,
1798 ( map {sprintf('%0.2f', $_)}
1799 $balance_due, #AMNT_DUE
1800 $previous_balance, #PREV_BAL
1801 $payment_total, #PYMT_RCVD
1802 $credit_total, #CREDITS
1803 $previous_balance, #BEG_BAL--is this correct?
1804 $self->charged, #NEW_CHRG
1807 $past_due, #PAST_MSG
1811 my %svc_class = ('' => ''); # maybe cache this more persistently?
1813 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1815 my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
1816 my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
1820 my @dates = ( $self->_date, undef );
1821 if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
1822 $dates[1] = $prev->sdate; #questionable
1825 # generate an 01 detail for each service
1826 my @svcs = $cust_pkg->h_cust_svc(@dates, 'I');
1827 foreach my $cust_svc ( @svcs ) {
1828 $show_pkgnum = ''; # hide it if we're showing svcnums
1830 my $svcpart = $cust_svc->svcpart;
1831 if (!exists($svc_class{$svcpart})) {
1832 my $classnum = $cust_svc->part_svc->classnum;
1833 my $part_svc_class = FS::part_svc_class->by_key($classnum)
1835 $svc_class{$svcpart} = $part_svc_class ?
1836 $part_svc_class->classname :
1840 my @h_label = $cust_svc->label(@dates, 'I');
1841 push @details, sprintf('01%-9s%-20s%-47s',
1843 $svc_class{$svcpart},
1846 } #foreach $cust_svc
1849 my $desc = $cust_bill_pkg->desc; # itemdesc or part_pkg.pkg
1850 if ($cust_bill_pkg->recur > 0) {
1851 $desc .= ' '.time2str('%d-%b-%Y', $cust_bill_pkg->sdate).' to '.
1852 time2str('%d-%b-%Y', $cust_bill_pkg->edate - 86400);
1854 push @details, sprintf('02%-6s%-60s%-10s',
1857 sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
1859 } #foreach $cust_bill_pkg
1861 # Tag this row so that we know whether this is one page (1), two pages
1862 # (2), # or "big" (B). The tag will be stripped off before uploading.
1863 if ( scalar(@details) < 12 ) {
1865 } elsif ( scalar(@details) < 58 ) {
1871 return join('', $header, @details, "\n");
1879 time2str("%x", $self->_date),
1880 sprintf("%.2f", $self->charged),
1881 ( map { $cust_main->getfield($_) }
1882 qw( first last company address1 address2 city state zip country ) ),
1884 ) or die "can't create csv";
1887 my $header = $csv->string. "\n";
1890 if ( lc($opt{'format'}) eq 'billco' ) {
1893 my %items_opt = ( format => 'template',
1894 escape_function => sub { shift } );
1895 # I don't know what characters billco actually tolerates in spool entries.
1896 # Text::CSV will take care of delimiters, though.
1898 my @items = ( $self->_items_pkg(%items_opt),
1899 $self->_items_fee(%items_opt) );
1900 foreach my $item (@items) {
1902 my $description = $item->{'description'};
1903 if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
1904 $description .= ': ' . $item->{ext_description}[0];
1908 '', # 1 | N/A-Leave Empty CHAR 2
1909 '', # 2 | N/A-Leave Empty CHAR 15
1910 $tracctnum, # 3 | Account Number CHAR 15
1911 $self->invnum, # 4 | Invoice Number CHAR 15
1912 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1913 $description, # 6 | Transaction Detail CHAR 100
1914 $item->{'amount'}, # 7 | Amount NUM* 9
1915 '', # 8 | Line Format Control** CHAR 2
1916 '', # 9 | Grouping Code CHAR 2
1917 '', # 10 | User Defined CHAR 15
1920 $detail .= $csv->string. "\n";
1924 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
1930 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1932 my($pkg, $setup, $recur, $sdate, $edate);
1933 if ( $cust_bill_pkg->pkgnum ) {
1935 ($pkg, $setup, $recur, $sdate, $edate) = (
1936 $cust_bill_pkg->part_pkg->pkg,
1937 ( $cust_bill_pkg->setup != 0
1938 ? sprintf("%.2f", $cust_bill_pkg->setup )
1940 ( $cust_bill_pkg->recur != 0
1941 ? sprintf("%.2f", $cust_bill_pkg->recur )
1943 ( $cust_bill_pkg->sdate
1944 ? time2str("%x", $cust_bill_pkg->sdate)
1946 ($cust_bill_pkg->edate
1947 ? time2str("%x", $cust_bill_pkg->edate)
1951 } else { #pkgnum tax
1952 next unless $cust_bill_pkg->setup != 0;
1953 $pkg = $cust_bill_pkg->desc;
1954 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1955 ( $sdate, $edate ) = ( '', '' );
1961 ( map { '' } (1..11) ),
1962 ($pkg, $setup, $recur, $sdate, $edate)
1963 ) or die "can't create csv";
1965 $detail .= $csv->string. "\n";
1971 ( $header, $detail );
1976 croak 'cust_bill->comp is deprecated (COMP payments are deprecated)';
1981 Attempts to pay this invoice with a credit card payment via a
1982 Business::OnlinePayment realtime gateway. See
1983 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1984 for supported processors.
1990 $self->realtime_bop( 'CC', @_ );
1995 Attempts to pay this invoice with an electronic check (ACH) payment via a
1996 Business::OnlinePayment realtime gateway. See
1997 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1998 for supported processors.
2004 $self->realtime_bop( 'ECHECK', @_ );
2009 Attempts to pay this invoice with phone bill (LEC) payment via a
2010 Business::OnlinePayment realtime gateway. See
2011 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2012 for supported processors.
2018 $self->realtime_bop( 'LEC', @_ );
2022 my( $self, $method ) = (shift,shift);
2023 my $conf = $self->conf;
2026 my $cust_main = $self->cust_main;
2027 my $balance = $cust_main->balance;
2028 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2029 $amount = sprintf("%.2f", $amount);
2030 return "not run (balance $balance)" unless $amount > 0;
2032 my $description = 'Internet Services';
2033 if ( $conf->exists('business-onlinepayment-description') ) {
2034 my $dtempl = $conf->config('business-onlinepayment-description');
2036 my $agent_obj = $cust_main->agent
2037 or die "can't retreive agent for $cust_main (agentnum ".
2038 $cust_main->agentnum. ")";
2039 my $agent = $agent_obj->agent;
2040 my $pkgs = join(', ',
2041 map { $_->part_pkg->pkg }
2042 grep { $_->pkgnum } $self->cust_bill_pkg
2044 $description = eval qq("$dtempl");
2047 $cust_main->realtime_bop($method, $amount,
2048 'description' => $description,
2049 'invnum' => $self->invnum,
2050 #this didn't do what we want, it just calls apply_payments_and_credits
2052 'apply_to_invoice' => 1,
2055 #this changes application behavior: auto payments
2056 #triggered against a specific invoice are now applied
2057 #to that invoice instead of oldest open.
2063 =item batch_card OPTION => VALUE...
2065 Adds a payment for this invoice to the pending credit card batch (see
2066 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2067 runs the payment using a realtime gateway.
2072 my ($self, %options) = @_;
2073 my $cust_main = $self->cust_main;
2075 $options{invnum} = $self->invnum;
2077 $cust_main->batch_card(%options);
2080 sub _agent_template {
2082 $self->cust_main->agent_template;
2085 sub _agent_invoice_from {
2087 $self->cust_main->agent_invoice_from;
2090 =item invoice_barcode DIR_OR_FALSE
2092 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2093 it is taken as the temp directory where the PNG file will be generated and the
2094 PNG file name is returned. Otherwise, the PNG image itself is returned.
2098 sub invoice_barcode {
2099 my ($self, $dir) = (shift,shift);
2101 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2102 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2103 my $gd = $gdbar->plot(Height => 30);
2106 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2110 ) or die "can't open temp file: $!\n";
2111 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2112 my $png_file = $bh->filename;
2119 =item invnum_date_pretty
2121 Returns a string with the invoice number and date, for example:
2122 "Invoice #54 (3/20/2008)".
2124 Intended for back-end context, with regard to translation and date formatting.
2128 #note: this uses _date_pretty_unlocalized because _date_pretty is too expensive
2129 # for backend use (and also does the wrong thing, localizing for end customer
2130 # instead of backoffice configured date format)
2131 sub invnum_date_pretty {
2133 #$self->mt('Invoice #').
2134 'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
2135 $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
2138 #sub _items_extra_usage_sections {
2140 # my $escape = shift;
2142 # my %sections = ();
2144 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
2145 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2147 # next unless $cust_bill_pkg->pkgnum > 0;
2149 # foreach my $section ( keys %usage_class ) {
2151 # my $usage = $cust_bill_pkg->usage($section);
2153 # next unless $usage && $usage > 0;
2155 # $sections{$section} ||= 0;
2156 # $sections{$section} += $usage;
2162 # map { { 'description' => &{$escape}($_),
2163 # 'subtotal' => $sections{$_},
2164 # 'summarized' => '',
2165 # 'tax_section' => '',
2168 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
2172 sub _items_extra_usage_sections {
2174 my $conf = $self->conf;
2182 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2184 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2185 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2186 next unless $cust_bill_pkg->pkgnum > 0;
2188 foreach my $classnum ( keys %usage_class ) {
2189 my $section = $usage_class{$classnum}->classname;
2190 $classnums{$section} = $classnum;
2192 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
2193 my $amount = $detail->amount;
2194 next unless $amount && $amount > 0;
2196 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
2197 $sections{$section}{amount} += $amount; #subtotal
2198 $sections{$section}{calls}++;
2199 $sections{$section}{duration} += $detail->duration;
2201 my $desc = $detail->regionname;
2202 my $description = $desc;
2203 $description = substr($desc, 0, $maxlength). '...'
2204 if $format eq 'latex' && length($desc) > $maxlength;
2206 $lines{$section}{$desc} ||= {
2207 description => &{$escape}($description),
2208 #pkgpart => $part_pkg->pkgpart,
2209 pkgnum => $cust_bill_pkg->pkgnum,
2214 #unit_amount => $cust_bill_pkg->unitrecur,
2215 quantity => $cust_bill_pkg->quantity,
2216 product_code => 'N/A',
2217 ext_description => [],
2220 $lines{$section}{$desc}{amount} += $amount;
2221 $lines{$section}{$desc}{calls}++;
2222 $lines{$section}{$desc}{duration} += $detail->duration;
2228 my %sectionmap = ();
2229 foreach (keys %sections) {
2230 my $usage_class = $usage_class{$classnums{$_}};
2231 $sectionmap{$_} = { 'description' => &{$escape}($_),
2232 'amount' => $sections{$_}{amount}, #subtotal
2233 'calls' => $sections{$_}{calls},
2234 'duration' => $sections{$_}{duration},
2236 'tax_section' => '',
2237 'sort_weight' => $usage_class->weight,
2238 ( $usage_class->format
2239 ? ( map { $_ => $usage_class->$_($format) }
2240 qw( description_generator header_generator total_generator total_line_generator )
2247 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
2251 foreach my $section ( keys %lines ) {
2252 foreach my $line ( keys %{$lines{$section}} ) {
2253 my $l = $lines{$section}{$line};
2254 $l->{section} = $sectionmap{$section};
2255 $l->{amount} = sprintf( "%.2f", $l->{amount} );
2256 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2261 return(\@sections, \@lines);
2267 my $end = $self->_date;
2269 # start at date of previous invoice + 1 second or 0 if no previous invoice
2270 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
2271 $start = 0 if !$start;
2274 my $cust_main = $self->cust_main;
2275 my @pkgs = $cust_main->all_pkgs;
2276 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
2279 foreach my $pkg ( @pkgs ) {
2280 my @h_cust_svc = $pkg->h_cust_svc($end);
2281 foreach my $h_cust_svc ( @h_cust_svc ) {
2282 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
2283 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
2285 my $inserted = $h_cust_svc->date_inserted;
2286 my $deleted = $h_cust_svc->date_deleted;
2287 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
2289 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
2291 # DID either activated or ported in; cannot be both for same DID simultaneously
2292 if ($inserted >= $start && $inserted <= $end && $phone_inserted
2293 && (!$phone_inserted->lnp_status
2294 || $phone_inserted->lnp_status eq ''
2295 || $phone_inserted->lnp_status eq 'native')) {
2298 else { # this one not so clean, should probably move to (h_)svc_phone
2299 local($FS::Record::qsearch_qualify_columns) = 0;
2300 my $phone_portedin = qsearchs( 'h_svc_phone',
2301 { 'svcnum' => $h_cust_svc->svcnum,
2302 'lnp_status' => 'portedin' },
2303 FS::h_svc_phone->sql_h_searchs($end),
2305 $num_portedin++ if $phone_portedin;
2308 # DID either deactivated or ported out; cannot be both for same DID simultaneously
2309 if($deleted >= $start && $deleted <= $end && $phone_deleted
2310 && (!$phone_deleted->lnp_status
2311 || $phone_deleted->lnp_status ne 'portingout')) {
2314 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
2315 && $phone_deleted->lnp_status
2316 && $phone_deleted->lnp_status eq 'portingout') {
2320 # increment usage minutes
2321 if ( $phone_inserted ) {
2322 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
2323 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
2326 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
2329 # don't look at this service again
2330 push @seen, $h_cust_svc->svcnum;
2334 $minutes = sprintf("%d", $minutes);
2335 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
2336 . "$num_deactivated Ported-Out: $num_portedout ",
2337 "Total Minutes: $minutes");
2340 sub _items_accountcode_cdr {
2345 my $section = { 'amount' => 0,
2348 'sort_weight' => '',
2350 'description' => 'Usage by Account Code',
2356 my %accountcodes = ();
2358 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2359 next unless $cust_bill_pkg->pkgnum > 0;
2361 my @header = $cust_bill_pkg->details_header;
2362 next unless scalar(@header);
2363 $section->{'header'} = join(',',@header);
2365 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2367 $section->{'header'} = $detail->formatted('format' => $format)
2368 if($detail->detail eq $section->{'header'});
2370 my $accountcode = $detail->accountcode;
2371 next unless $accountcode;
2373 my $amount = $detail->amount;
2374 next unless $amount && $amount > 0;
2376 $accountcodes{$accountcode} ||= {
2377 description => $accountcode,
2384 product_code => 'N/A',
2385 section => $section,
2386 ext_description => [ $section->{'header'} ],
2390 $section->{'amount'} += $amount;
2391 $accountcodes{$accountcode}{'amount'} += $amount;
2392 $accountcodes{$accountcode}{calls}++;
2393 $accountcodes{$accountcode}{duration} += $detail->duration;
2394 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
2398 foreach my $l ( values %accountcodes ) {
2399 $l->{amount} = sprintf( "%.2f", $l->{amount} );
2400 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
2401 foreach my $sorted_detail ( @sorted_detail ) {
2402 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
2404 delete $l->{detail_temp};
2408 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
2410 return ($section,\@sorted_lines);
2413 sub _items_svc_phone_sections {
2415 my $conf = $self->conf;
2423 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2425 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2426 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
2428 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2429 next unless $cust_bill_pkg->pkgnum > 0;
2431 my @header = $cust_bill_pkg->details_header;
2432 next unless scalar(@header);
2434 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2436 my $phonenum = $detail->phonenum;
2437 next unless $phonenum;
2439 my $amount = $detail->amount;
2440 next unless $amount && $amount > 0;
2442 $sections{$phonenum} ||= { 'amount' => 0,
2445 'sort_weight' => -1,
2446 'phonenum' => $phonenum,
2448 $sections{$phonenum}{amount} += $amount; #subtotal
2449 $sections{$phonenum}{calls}++;
2450 $sections{$phonenum}{duration} += $detail->duration;
2452 my $desc = $detail->regionname;
2453 my $description = $desc;
2454 $description = substr($desc, 0, $maxlength). '...'
2455 if $format eq 'latex' && length($desc) > $maxlength;
2457 $lines{$phonenum}{$desc} ||= {
2458 description => &{$escape}($description),
2459 #pkgpart => $part_pkg->pkgpart,
2467 product_code => 'N/A',
2468 ext_description => [],
2471 $lines{$phonenum}{$desc}{amount} += $amount;
2472 $lines{$phonenum}{$desc}{calls}++;
2473 $lines{$phonenum}{$desc}{duration} += $detail->duration;
2475 my $line = $usage_class{$detail->classnum}->classname;
2476 $sections{"$phonenum $line"} ||=
2480 'sort_weight' => $usage_class{$detail->classnum}->weight,
2481 'phonenum' => $phonenum,
2482 'header' => [ @header ],
2484 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
2485 $sections{"$phonenum $line"}{calls}++;
2486 $sections{"$phonenum $line"}{duration} += $detail->duration;
2488 $lines{"$phonenum $line"}{$desc} ||= {
2489 description => &{$escape}($description),
2490 #pkgpart => $part_pkg->pkgpart,
2498 product_code => 'N/A',
2499 ext_description => [],
2502 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
2503 $lines{"$phonenum $line"}{$desc}{calls}++;
2504 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
2505 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
2506 $detail->formatted('format' => $format);
2511 my %sectionmap = ();
2512 my $simple = new FS::usage_class { format => 'simple' }; #bleh
2513 foreach ( keys %sections ) {
2514 my @header = @{ $sections{$_}{header} || [] };
2516 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
2517 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
2518 my $usage_class = $summary ? $simple : $usage_simple;
2519 my $ending = $summary ? ' usage charges' : '';
2522 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
2524 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
2525 'amount' => $sections{$_}{amount}, #subtotal
2526 'calls' => $sections{$_}{calls},
2527 'duration' => $sections{$_}{duration},
2529 'tax_section' => '',
2530 'phonenum' => $sections{$_}{phonenum},
2531 'sort_weight' => $sections{$_}{sort_weight},
2532 'post_total' => $summary, #inspire pagebreak
2534 ( map { $_ => $usage_class->$_($format, %gen_opt) }
2535 qw( description_generator
2538 total_line_generator
2545 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
2546 $a->{sort_weight} <=> $b->{sort_weight}
2551 foreach my $section ( keys %lines ) {
2552 foreach my $line ( keys %{$lines{$section}} ) {
2553 my $l = $lines{$section}{$line};
2554 $l->{section} = $sectionmap{$section};
2555 $l->{amount} = sprintf( "%.2f", $l->{amount} );
2556 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2561 if($conf->exists('phone_usage_class_summary')) {
2562 # this only works with Latex
2566 # after this, we'll have only two sections per DID:
2567 # Calls Summary and Calls Detail
2568 foreach my $section ( @sections ) {
2569 if($section->{'post_total'}) {
2570 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
2571 $section->{'total_line_generator'} = sub { '' };
2572 $section->{'total_generator'} = sub { '' };
2573 $section->{'header_generator'} = sub { '' };
2574 $section->{'description_generator'} = '';
2575 push @newsections, $section;
2576 my %calls_detail = %$section;
2577 $calls_detail{'post_total'} = '';
2578 $calls_detail{'sort_weight'} = '';
2579 $calls_detail{'description_generator'} = sub { '' };
2580 $calls_detail{'header_generator'} = sub {
2581 return ' & Date/Time & Called Number & Duration & Price'
2582 if $format eq 'latex';
2585 $calls_detail{'description'} = 'Calls Detail: '
2586 . $section->{'phonenum'};
2587 push @newsections, \%calls_detail;
2591 # after this, each usage class is collapsed/summarized into a single
2592 # line under the Calls Summary section
2593 foreach my $newsection ( @newsections ) {
2594 if($newsection->{'post_total'}) { # this means Calls Summary
2595 foreach my $section ( @sections ) {
2596 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
2597 && !$section->{'post_total'});
2598 my $newdesc = $section->{'description'};
2599 my $tn = $section->{'phonenum'};
2600 $newdesc =~ s/$tn//g;
2601 my $line = { ext_description => [],
2605 calls => $section->{'calls'},
2606 section => $newsection,
2607 duration => $section->{'duration'},
2608 description => $newdesc,
2609 amount => sprintf("%.2f",$section->{'amount'}),
2610 product_code => 'N/A',
2612 push @newlines, $line;
2617 # after this, Calls Details is populated with all CDRs
2618 foreach my $newsection ( @newsections ) {
2619 if(!$newsection->{'post_total'}) { # this means Calls Details
2620 foreach my $line ( @lines ) {
2621 next unless (scalar(@{$line->{'ext_description'}}) &&
2622 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
2624 my @extdesc = @{$line->{'ext_description'}};
2626 foreach my $extdesc ( @extdesc ) {
2627 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
2628 push @newextdesc, $extdesc;
2630 $line->{'ext_description'} = \@newextdesc;
2631 $line->{'section'} = $newsection;
2632 push @newlines, $line;
2637 return(\@newsections, \@newlines);
2640 return(\@sections, \@lines);
2644 =sub _items_usage_class_summary OPTIONS
2646 Returns a list of detail items summarizing the usage charges on this
2647 invoice. Each one will have 'amount', 'description' (the usage charge name),
2648 and 'usage_classnum'.
2650 OPTIONS can include 'escape' (a function to escape the descriptions).
2654 sub _items_usage_class_summary {
2658 my $escape = $opt{escape} || sub { $_[0] };
2659 my $invnum = $self->invnum;
2660 my @classes = qsearch({
2661 'table' => 'usage_class',
2662 'select' => 'classnum, classname, SUM(amount) AS amount',
2663 'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
2664 ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
2665 'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
2666 ' GROUP BY classnum, classname, weight'.
2667 ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
2668 ' ORDER BY weight ASC',
2672 description => &{$escape}($self->mt('Usage Summary')),
2676 foreach my $class (@classes) {
2678 'description' => &{$escape}($class->classname),
2679 'amount' => sprintf('%.2f', $class->amount),
2680 'usage_classnum' => $class->classnum,
2681 'section' => $section,
2687 sub _items_previous {
2689 my $conf = $self->conf;
2690 my $cust_main = $self->cust_main;
2691 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2693 foreach ( @pr_cust_bill ) {
2694 my $date = $conf->exists('invoice_show_prior_due_date')
2695 ? 'due '. $_->due_date2str('short')
2696 : $self->time2str_local('short', $_->_date);
2698 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
2699 #'pkgpart' => 'N/A',
2701 'amount' => sprintf("%.2f", $_->owed),
2707 # 'description' => 'Previous Balance',
2708 # #'pkgpart' => 'N/A',
2709 # 'pkgnum' => 'N/A',
2710 # 'amount' => sprintf("%10.2f", $pr_total ),
2711 # 'ext_description' => [ map {
2712 # "Invoice ". $_->invnum.
2713 # " (". time2str("%x",$_->_date). ") ".
2714 # sprintf("%10.2f", $_->owed)
2715 # } @pr_cust_bill ],
2720 sub _items_credits {
2721 my( $self, %opt ) = @_;
2722 my $trim_len = $opt{'trim_len'} || 40;
2727 if ( $self->conf->exists('previous_balance-payments_since') ) {
2728 if ( $opt{'template'} eq 'statement' ) {
2729 # then the current bill is a "statement" (i.e. an invoice sent as
2730 # a payment receipt)
2731 # and in that case we want to see payments on or after THIS invoice
2732 @objects = qsearch('cust_credit', {
2733 'custnum' => $self->custnum,
2734 '_date' => {op => '>=', value => $self->_date},
2738 $date = $self->previous_bill->_date if $self->previous_bill;
2739 @objects = qsearch('cust_credit', {
2740 'custnum' => $self->custnum,
2741 '_date' => {op => '>=', value => $date},
2745 @objects = $self->cust_credited;
2748 foreach my $obj ( @objects ) {
2749 my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
2751 my $reason = substr($cust_credit->reason, 0, $trim_len);
2752 $reason .= '...' if length($reason) < length($cust_credit->reason);
2753 $reason = " ($reason) " if $reason;
2756 #'description' => 'Credit ref\#'. $_->crednum.
2757 # " (". time2str("%x",$_->cust_credit->_date) .")".
2759 'description' => $self->mt('Credit applied').' '.
2760 $self->time2str_local('short', $obj->_date). $reason,
2761 'amount' => sprintf("%.2f",$obj->amount),
2769 sub _items_payments {
2774 my $detailed = $self->conf->exists('invoice_payment_details');
2776 if ( $self->conf->exists('previous_balance-payments_since') ) {
2777 # then show payments dated on/after the previous bill...
2778 if ( $opt{'template'} eq 'statement' ) {
2779 # then the current bill is a "statement" (i.e. an invoice sent as
2780 # a payment receipt)
2781 # and in that case we want to see payments on or after THIS invoice
2782 @objects = qsearch('cust_pay', {
2783 'custnum' => $self->custnum,
2784 '_date' => {op => '>=', value => $self->_date},
2787 # the normal case: payments on or after the previous invoice
2789 $date = $self->previous_bill->_date if $self->previous_bill;
2790 @objects = qsearch('cust_pay', {
2791 'custnum' => $self->custnum,
2792 '_date' => {op => '>=', value => $date},
2794 # and before the current bill...
2795 @objects = grep { $_->_date < $self->_date } @objects;
2798 @objects = $self->cust_bill_pay;
2801 foreach my $obj (@objects) {
2802 my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
2803 my $desc = $self->mt('Payment received').' '.
2804 $self->time2str_local('short', $cust_pay->_date );
2805 $desc .= $self->mt(' via ') .
2806 $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
2810 'description' => $desc,
2811 'amount' => sprintf("%.2f", $obj->amount )
2821 my $conf = $self->conf;
2824 my ($pr_total) = $self->previous;
2825 my ($previous_charges_desc, $new_charges_desc, $new_charges_amount);
2827 if ( $conf->exists('previous_balance-exclude_from_total') ) {
2828 # can we do some caching on this stuff? it's going to change infrequently
2830 $previous_charges_desc = $self->mt(
2831 $conf->config('previous_balance-text') || 'Previous Balance'
2834 # then return separate lines for previous balance and total new charges
2837 { total_item => $previous_charges_desc,
2838 total_amount => sprintf('%.2f',$pr_total)
2841 $new_charges_desc = $self->mt(
2842 $conf->config('previous_balance-text-total_new_charges')
2843 || 'Total New Charges'
2846 $new_charges_amount = $self->charged;
2850 $new_charges_desc = $self->mt('Total Charges');
2851 $new_charges_amount = sprintf('%.2f',$self->charged + $pr_total);
2855 if ( $conf->exists('invoice_show_prior_due_date') ) {
2856 # then the due date should be shown with Total New Charges,
2857 # and should NOT be shown with the Balance Due message.
2858 if ( $self->due_date ) {
2859 # localize the "Please pay by" message and the date itself
2860 # (grammar issues with this, yeah)
2861 $new_charges_desc .= ' - ' . $self->mt('Please pay by') . ' ' .
2862 $self->due_date2str('short');
2863 } elsif ( $self->terms ) {
2864 # phrases like "due on receipt" should be localized
2865 $new_charges_desc .= ' - ' . $self->mt($self->terms);
2870 { total_item => $new_charges_desc,
2871 total_amount => $new_charges_amount,
2879 =item call_details [ OPTION => VALUE ... ]
2881 Returns an array of CSV strings representing the call details for this invoice
2882 The only option available is the boolean prepend_billed_number
2887 my ($self, %opt) = @_;
2889 my $format_function = sub { shift };
2891 if ($opt{prepend_billed_number}) {
2892 $format_function = sub {
2896 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
2901 my @details = map { $_->details( 'format_function' => $format_function,
2902 'escape_function' => sub{ return() },
2906 $self->cust_bill_pkg;
2907 my $header = $details[0];
2908 ( $header, grep { $_ ne $header } @details );
2918 =item process_reprint
2922 sub process_reprint {
2923 process_re_X('print', @_);
2926 =item process_reemail
2930 sub process_reemail {
2931 process_re_X('email', @_);
2939 process_re_X('fax', @_);
2947 process_re_X('ftp', @_);
2954 sub process_respool {
2955 process_re_X('spool', @_);
2960 my( $method, $job ) = ( shift, shift );
2961 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2964 warn Dumper($param) if $DEBUG;
2975 # spool_invoice ftp_invoice fax_invoice print_invoice
2976 my($method, $job, %param ) = @_;
2978 warn "re_X $method for job $job with param:\n".
2979 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2982 #some false laziness w/search/cust_bill.html
2984 my $orderby = 'ORDER BY cust_bill._date';
2986 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
2988 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2990 my @cust_bill = qsearch( {
2991 #'select' => "cust_bill.*",
2992 'table' => 'cust_bill',
2993 'addl_from' => $addl_from,
2995 'extra_sql' => $extra_sql,
2996 'order_by' => $orderby,
3000 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3002 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3005 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3006 foreach my $cust_bill ( @cust_bill ) {
3007 $cust_bill->$method();
3009 if ( $job ) { #progressbar foo
3011 if ( time - $min_sec > $last ) {
3012 my $error = $job->update_statustext(
3013 int( 100 * $num / scalar(@cust_bill) )
3015 die $error if $error;
3026 +{ ( map { $_=>$self->$_ } $self->fields ),
3027 'owed' => $self->owed,
3028 #XXX last payment applied date
3034 =head1 CLASS METHODS
3040 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3045 my ($class, $start, $end) = @_;
3047 $class->paid_sql($start, $end). ' - '.
3048 $class->credited_sql($start, $end);
3053 Returns an SQL fragment to retreive the net amount (charged minus credited).
3058 my ($class, $start, $end) = @_;
3059 'charged - '. $class->credited_sql($start, $end);
3064 Returns an SQL fragment to retreive the amount paid against this invoice.
3069 my ($class, $start, $end) = @_;
3070 $start &&= "AND cust_bill_pay._date <= $start";
3071 $end &&= "AND cust_bill_pay._date > $end";
3072 $start = '' unless defined($start);
3073 $end = '' unless defined($end);
3074 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3075 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
3080 Returns an SQL fragment to retreive the amount credited against this invoice.
3085 my ($class, $start, $end) = @_;
3086 $start &&= "AND cust_credit_bill._date <= $start";
3087 $end &&= "AND cust_credit_bill._date > $end";
3088 $start = '' unless defined($start);
3089 $end = '' unless defined($end);
3090 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3091 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
3096 Returns an SQL fragment to retrieve the due date of an invoice.
3097 Currently only supported on PostgreSQL.
3102 die "don't use: doesn't account for agent-specific invoice_default_terms";
3104 #we're passed a $conf but not a specific customer (that's in the query), so
3105 # to make this work we'd need an agentnum-aware "condition_sql_conf" like
3106 # "condition_sql_option" that retreives a conf value with SQL in an agent-
3109 my $conf = new FS::Conf;
3113 cust_bill.invoice_terms,
3114 cust_main.invoice_terms,
3115 \''.($conf->config('invoice_default_terms') || '').'\'
3116 ), E\'Net (\\\\d+)\'
3118 ) * 86400 + cust_bill._date'
3129 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3130 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base