4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $lpr $invoice_from $smtpmachine );
6 use vars qw( $cybercash );
7 use vars qw( $xaction $E_NoErr );
8 use vars qw( $bop_processor $bop_login $bop_password $bop_action @bop_options );
9 use vars qw( $ach_processor $ach_login $ach_password $ach_action @ach_options );
10 use vars qw( $invoice_lines @buf ); #yuck
11 use vars qw( $realtime_bop_decline_quiet );
13 use Mail::Internet 1.44;
17 use FS::UID qw( datasrc );
18 use FS::Record qw( qsearch qsearchs );
20 use FS::cust_bill_pkg;
24 use FS::cust_credit_bill;
25 use FS::cust_pay_batch;
26 use FS::cust_bill_event;
28 @ISA = qw( FS::Record );
30 $realtime_bop_decline_quiet = 0;
32 #ask FS::UID to run this stuff for us later
33 $FS::UID::callback{'FS::cust_bill'} = sub {
37 $money_char = $conf->config('money_char') || '$';
39 $lpr = $conf->config('lpr');
40 $invoice_from = $conf->config('invoice_from');
41 $smtpmachine = $conf->config('smtpmachine');
43 ( $bop_processor,$bop_login, $bop_password, $bop_action ) = ( '', '', '', '');
45 ( $ach_processor,$ach_login, $ach_password, $ach_action ) = ( '', '', '', '');
48 if ( $conf->exists('cybercash3.2') ) {
50 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
51 require CCMckDirectLib3_2;
53 require CCMckErrno3_2;
54 #qw(MCKGetErrorMessage $E_NoErr);
55 import CCMckErrno3_2 qw($E_NoErr);
58 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
59 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
60 if ( $status != $E_NoErr ) {
61 warn "CCMckLib3_2::InitConfig error:\n";
62 foreach my $key (keys %CCMckLib3_2::Config) {
63 warn " $key => $CCMckLib3_2::Config{$key}\n"
65 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
66 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
68 $cybercash='cybercash3.2';
69 } elsif ( $conf->exists('business-onlinepayment') ) {
75 ) = $conf->config('business-onlinepayment');
76 $bop_action ||= 'normal authorization';
77 ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) =
78 ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options );
79 eval "use Business::OnlinePayment";
82 if ( $conf->exists('business-onlinepayment-ach') ) {
88 ) = $conf->config('business-onlinepayment-ach');
89 $ach_action ||= 'normal authorization';
90 eval "use Business::OnlinePayment";
97 FS::cust_bill - Object methods for cust_bill records
103 $record = new FS::cust_bill \%hash;
104 $record = new FS::cust_bill { 'column' => 'value' };
106 $error = $record->insert;
108 $error = $new_record->replace($old_record);
110 $error = $record->delete;
112 $error = $record->check;
114 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
116 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
118 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
120 @cust_pay_objects = $cust_bill->cust_pay;
122 $tax_amount = $record->tax;
124 @lines = $cust_bill->print_text;
125 @lines = $cust_bill->print_text $time;
129 An FS::cust_bill object represents an invoice; a declaration that a customer
130 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
131 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
132 following fields are currently supported:
136 =item invnum - primary key (assigned automatically for new invoices)
138 =item custnum - customer (see L<FS::cust_main>)
140 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
141 L<Time::Local> and L<Date::Parse> for conversion functions.
143 =item charged - amount of this invoice
145 =item printed - deprecated
147 =item closed - books closed flag, empty or `Y'
157 Creates a new invoice. To add the invoice to the database, see L<"insert">.
158 Invoices are normally created by calling the bill method of a customer object
159 (see L<FS::cust_main>).
163 sub table { 'cust_bill'; }
167 Adds this invoice to the database ("Posts" the invoice). If there is an error,
168 returns the error, otherwise returns false.
172 Currently unimplemented. I don't remove invoices because there would then be
173 no record you ever posted this invoice (which is bad, no?)
179 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
180 $self->SUPER::delete(@_);
183 =item replace OLD_RECORD
185 Replaces the OLD_RECORD with this one in the database. If there is an error,
186 returns the error, otherwise returns false.
188 Only printed may be changed. printed is normally updated by calling the
189 collect method of a customer object (see L<FS::cust_main>).
194 my( $new, $old ) = ( shift, shift );
195 return "Can't change custnum!" unless $old->custnum == $new->custnum;
196 #return "Can't change _date!" unless $old->_date eq $new->_date;
197 return "Can't change _date!" unless $old->_date == $new->_date;
198 return "Can't change charged!" unless $old->charged == $new->charged;
200 $new->SUPER::replace($old);
205 Checks all fields to make sure this is a valid invoice. If there is an error,
206 returns the error, otherwise returns false. Called by the insert and replace
215 $self->ut_numbern('invnum')
216 || $self->ut_number('custnum')
217 || $self->ut_numbern('_date')
218 || $self->ut_money('charged')
219 || $self->ut_numbern('printed')
220 || $self->ut_enum('closed', [ '', 'Y' ])
222 return $error if $error;
224 return "Unknown customer"
225 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
227 $self->_date(time) unless $self->_date;
229 $self->printed(0) if $self->printed eq '';
236 Returns a list consisting of the total previous balance for this customer,
237 followed by the previous outstanding invoices (as FS::cust_bill objects also).
244 my @cust_bill = sort { $a->_date <=> $b->_date }
245 grep { $_->owed != 0 && $_->_date < $self->_date }
246 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
248 foreach ( @cust_bill ) { $total += $_->owed; }
254 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
260 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
263 =item cust_bill_event
265 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
270 sub cust_bill_event {
272 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
278 Returns the customer (see L<FS::cust_main>) for this invoice.
284 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
289 Depreciated. See the cust_credited method.
291 #Returns a list consisting of the total previous credited (see
292 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
293 #outstanding credits (FS::cust_credit objects).
299 croak "FS::cust_bill->cust_credit depreciated; see ".
300 "FS::cust_bill->cust_credit_bill";
303 #my @cust_credit = sort { $a->_date <=> $b->_date }
304 # grep { $_->credited != 0 && $_->_date < $self->_date }
305 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
307 #foreach (@cust_credit) { $total += $_->credited; }
308 #$total, @cust_credit;
313 Depreciated. See the cust_bill_pay method.
315 #Returns all payments (see L<FS::cust_pay>) for this invoice.
321 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
323 #sort { $a->_date <=> $b->_date }
324 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
330 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
336 sort { $a->_date <=> $b->_date }
337 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
342 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
348 sort { $a->_date <=> $b->_date }
349 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
355 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
362 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
364 foreach (@taxlines) { $total += $_->setup; }
370 Returns the amount owed (still outstanding) on this invoice, which is charged
371 minus all payment applications (see L<FS::cust_bill_pay>) and credit
372 applications (see L<FS::cust_credit_bill>).
378 my $balance = $self->charged;
379 $balance -= $_->amount foreach ( $self->cust_bill_pay );
380 $balance -= $_->amount foreach ( $self->cust_credited );
381 $balance = sprintf( "%.2f", $balance);
382 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
386 =item send [ TEMPLATENAME [ , AGENTNUM ] ]
388 Sends this invoice to the destinations configured for this customer: send
389 emails or print. See L<FS::cust_main_invoice>.
391 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
393 AGENTNUM, if specified, means that this invoice will only be sent for customers
394 of the specified agent.
400 my $template = scalar(@_) ? shift : '';
401 return '' if scalar(@_) && $_[0] && $self->cust_main->agentnum ne shift;
403 my @print_text = $self->print_text('', $template);
404 my @invoicing_list = $self->cust_main->invoicing_list;
406 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
408 #better to notify this person than silence
409 @invoicing_list = ($invoice_from) unless @invoicing_list;
411 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
412 #$ENV{SMTPHOSTS} = $smtpmachine;
413 $ENV{MAILADDRESS} = $invoice_from;
414 my $header = new Mail::Header ( [
415 "From: $invoice_from",
416 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
417 "Sender: $invoice_from",
418 "Reply-To: $invoice_from",
419 "Date: ". time2str("%a, %d %b %Y %X %z", time),
422 my $message = new Mail::Internet (
424 'Body' => [ @print_text ], #( date)
427 $message->smtpsend( Host => $smtpmachine )
428 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
429 or die "(customer # ". $self->custnum. ") can't send invoice email".
430 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
431 " via server $smtpmachine with SMTP: $!\n";
435 if ( $conf->config('invoice_latex') ) {
436 @print_text = $self->print_ps('', $template);
439 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
441 or die "Can't open pipe to $lpr: $!\n";
442 print LPR @print_text;
444 or die $! ? "Error closing $lpr: $!\n"
445 : "Exit status $? from $lpr\n";
452 =item send_csv OPTIONS
454 Sends invoice as a CSV data-file to a remote host with the specified protocol.
458 protocol - currently only "ftp"
464 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
465 and YYMMDDHHMMSS is a timestamp.
467 The fields of the CSV file is as follows:
469 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
473 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
475 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
476 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
477 fields are filled in.
479 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
480 first two fields (B<record_type> and B<invnum>) and the last five fields
481 (B<pkg> through B<edate>) are filled in.
483 =item invnum - invoice number
485 =item custnum - customer number
487 =item _date - invoice date
489 =item charged - total invoice amount
491 =item first - customer first name
493 =item last - customer first name
495 =item company - company name
497 =item address1 - address line 1
499 =item address2 - address line 1
509 =item pkg - line item description
511 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
513 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
515 =item sdate - start date for recurring fee
517 =item edate - end date for recurring fee
524 my($self, %opt) = @_;
526 #part one: create file
528 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
529 mkdir $spooldir, 0700 unless -d $spooldir;
531 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
533 open(CSV, ">$file") or die "can't open $file: $!";
535 eval "use Text::CSV_XS";
538 my $csv = Text::CSV_XS->new({'always_quote'=>1});
540 my $cust_main = $self->cust_main;
546 time2str("%x", $self->_date),
547 sprintf("%.2f", $self->charged),
548 ( map { $cust_main->getfield($_) }
549 qw( first last company address1 address2 city state zip country ) ),
551 ) or die "can't create csv";
552 print CSV $csv->string. "\n";
554 #new charges (false laziness w/print_text)
555 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
557 my($pkg, $setup, $recur, $sdate, $edate);
558 if ( $cust_bill_pkg->pkgnum ) {
560 ($pkg, $setup, $recur, $sdate, $edate) = (
561 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
562 ( $cust_bill_pkg->setup != 0
563 ? sprintf("%.2f", $cust_bill_pkg->setup )
565 ( $cust_bill_pkg->recur != 0
566 ? sprintf("%.2f", $cust_bill_pkg->recur )
568 time2str("%x", $cust_bill_pkg->sdate),
569 time2str("%x", $cust_bill_pkg->edate),
573 next unless $cust_bill_pkg->setup != 0;
574 ($pkg, $setup, $recur, $sdate, $edate) =
575 ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
581 ( map { '' } (1..11) ),
582 ($pkg, $setup, $recur, $sdate, $edate)
583 ) or die "can't create csv";
584 print CSV $csv->string. "\n";
588 close CSV or die "can't close CSV: $!";
593 if ( $opt{protocol} eq 'ftp' ) {
594 eval "use Net::FTP;";
596 $net = Net::FTP->new($opt{server}) or die @$;
598 die "unknown protocol: $opt{protocol}";
601 $net->login( $opt{username}, $opt{password} )
602 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
604 $net->binary or die "can't set binary mode";
606 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
608 $net->put($file) or die "can't put $file: $!";
618 Pays this invoice with a compliemntary payment. If there is an error,
619 returns the error, otherwise returns false.
625 my $cust_pay = new FS::cust_pay ( {
626 'invnum' => $self->invnum,
627 'paid' => $self->owed,
630 'payinfo' => $self->cust_main->payinfo,
638 Attempts to pay this invoice with a credit card payment via a
639 Business::OnlinePayment realtime gateway. See
640 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
641 for supported processors.
660 Attempts to pay this invoice with an electronic check (ACH) payment via a
661 Business::OnlinePayment realtime gateway. See
662 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
663 for supported processors.
682 Attempts to pay this invoice with phone bill (LEC) payment via a
683 Business::OnlinePayment realtime gateway. See
684 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
685 for supported processors.
703 my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
705 #trim an extraneous blank line
706 pop @$options if scalar(@$options) % 2 && $options->[-1] =~ /^\s*$/;
708 my $cust_main = $self->cust_main;
709 my $balance = $cust_main->balance;
710 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
711 $amount = sprintf("%.2f", $amount);
712 return "not run (balance $balance)" unless $amount > 0;
714 my $address = $cust_main->address1;
715 $address .= ", ". $cust_main->address2 if $cust_main->address2;
717 my($payname, $payfirst, $paylast);
718 if ( $cust_main->payname && $method ne 'ECHECK' ) {
719 $payname = $cust_main->payname;
720 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
722 #$dbh->rollback if $oldAutoCommit;
723 return "Illegal payname $payname";
725 ($payfirst, $paylast) = ($1, $2);
727 $payfirst = $cust_main->getfield('first');
728 $paylast = $cust_main->getfield('last');
729 $payname = "$payfirst $paylast";
732 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
733 if ( $conf->exists('emailinvoiceauto')
734 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
735 push @invoicing_list, $cust_main->all_emails;
737 my $email = $invoicing_list[0];
739 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
741 my $description = 'Internet Services';
742 if ( $conf->exists('business-onlinepayment-description') ) {
743 my $dtempl = $conf->config('business-onlinepayment-description');
745 my $agent_obj = $cust_main->agent
746 or die "can't retreive agent for $cust_main (agentnum ".
747 $cust_main->agentnum. ")";
748 my $agent = $agent_obj->agent;
749 my $pkgs = join(', ',
750 map { $_->cust_pkg->part_pkg->pkg }
751 grep { $_->pkgnum } $self->cust_bill_pkg
753 $description = eval qq("$dtempl");
758 if ( $method eq 'CC' ) {
760 $content{card_number} = $cust_main->payinfo;
761 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
762 $content{expiration} = "$2/$1";
764 $content{cvv2} = $cust_main->paycvv
765 if defined $cust_main->dbdef_table->column('paycvv')
766 && length($cust_main->paycvv);
768 $content{recurring_billing} = 'YES'
769 if qsearch('cust_pay', { 'custnum' => $cust_main->custnum,
771 'payinfo' => $cust_main->payinfo, } );
773 } elsif ( $method eq 'ECHECK' ) {
774 my($account_number,$routing_code) = $cust_main->payinfo;
775 ( $content{account_number}, $content{routing_code} ) =
776 split('@', $cust_main->payinfo);
777 $content{bank_name} = $cust_main->payname;
778 $content{account_type} = 'CHECKING';
779 $content{account_name} = $payname;
780 $content{customer_org} = $self->company ? 'B' : 'I';
781 $content{customer_ssn} = $self->ss;
782 } elsif ( $method eq 'LEC' ) {
783 $content{phone} = $cust_main->payinfo;
787 new Business::OnlinePayment( $processor, @$options );
788 $transaction->content(
791 'password' => $password,
792 'action' => $action1,
793 'description' => $description,
795 'invoice_number' => $self->invnum,
796 'customer_id' => $self->custnum,
797 'last_name' => $paylast,
798 'first_name' => $payfirst,
800 'address' => $address,
801 'city' => $cust_main->city,
802 'state' => $cust_main->state,
803 'zip' => $cust_main->zip,
804 'country' => $cust_main->country,
805 'referer' => 'http://cleanwhisker.420.am/',
807 'phone' => $cust_main->daytime || $cust_main->night,
810 $transaction->submit();
812 if ( $transaction->is_success() && $action2 ) {
813 my $auth = $transaction->authorization;
814 my $ordernum = $transaction->can('order_number')
815 ? $transaction->order_number
818 #warn "********* $auth ***********\n";
819 #warn "********* $ordernum ***********\n";
821 new Business::OnlinePayment( $processor, @$options );
828 password => $password,
829 order_number => $ordernum,
831 authorization => $auth,
832 description => $description,
835 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
836 transaction_sequence_num local_transaction_date
837 local_transaction_time AVS_result_code )) {
838 $capture{$field} = $transaction->$field() if $transaction->can($field);
841 $capture->content( %capture );
845 unless ( $capture->is_success ) {
846 my $e = "Authorization sucessful but capture failed, invnum #".
847 $self->invnum. ': '. $capture->result_code.
848 ": ". $capture->error_message;
855 #remove paycvv after initial transaction
856 #make this disable-able via a config option if anyone insists?
857 # (though that probably violates cardholder agreements)
858 use Business::CreditCard;
859 if ( defined $cust_main->dbdef_table->column('paycvv')
860 && length($cust_main->paycvv)
861 && ! grep { $_ eq cardtype($cust_main->payinfo) } $conf->config('cvv-save')
864 my $new = new FS::cust_main { $cust_main->hash };
866 my $error = $new->replace($cust_main);
868 warn "error removing cvv: $error\n";
873 if ( $transaction->is_success() ) {
881 my $cust_pay = new FS::cust_pay ( {
882 'invnum' => $self->invnum,
885 'payby' => $method2payby{$method},
886 'payinfo' => $cust_main->payinfo,
887 'paybatch' => "$processor:". $transaction->authorization,
889 my $error = $cust_pay->insert;
891 $cust_pay->invnum(''); #try again with no specific invnum
892 my $error2 = $cust_pay->insert;
894 # gah, even with transactions.
895 my $e = 'WARNING: Card/ACH debited but database not updated - '.
896 "error inserting payment ($processor): $error2".
897 ' (previously tried insert with invnum #' . $self->invnum.
905 #} elsif ( $options{'report_badcard'} ) {
908 my $perror = "$processor error, invnum #". $self->invnum. ': '.
909 $transaction->result_code. ": ". $transaction->error_message;
911 if ( !$realtime_bop_decline_quiet && $conf->exists('emaildecline')
912 && grep { $_ ne 'POST' } $cust_main->invoicing_list
913 && ! grep { $transaction->error_message =~ /$_/ }
914 $conf->config('emaildecline-exclude')
916 my @templ = $conf->config('declinetemplate');
917 my $template = new Text::Template (
919 SOURCE => [ map "$_\n", @templ ],
920 ) or return "($perror) can't create template: $Text::Template::ERROR";
922 or return "($perror) can't compile template: $Text::Template::ERROR";
924 my $templ_hash = { error => $transaction->error_message };
926 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
927 $ENV{MAILADDRESS} = $invoice_from;
928 my $header = new Mail::Header ( [
929 "From: $invoice_from",
930 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
931 "Sender: $invoice_from",
932 "Reply-To: $invoice_from",
933 "Date: ". time2str("%a, %d %b %Y %X %z", time),
934 "Subject: Your payment could not be processed",
936 my $message = new Mail::Internet (
938 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
941 $message->smtpsend( Host => $smtpmachine )
942 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
943 or return "($perror) (customer # ". $self->custnum.
944 ") can't send card decline email to ".
945 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
946 " via server $smtpmachine with SMTP: $!";
954 =item realtime_card_cybercash
956 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
960 sub realtime_card_cybercash {
962 my $cust_main = $self->cust_main;
963 my $amount = $self->owed;
965 return "CyberCash CashRegister real-time card processing not enabled!"
966 unless $cybercash eq 'cybercash3.2';
968 my $address = $cust_main->address1;
969 $address .= ", ". $cust_main->address2 if $cust_main->address2;
972 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
973 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
978 my $paybatch = $self->invnum.
979 '-' . time2str("%y%m%d%H%M%S", time);
981 my $payname = $cust_main->payname ||
982 $cust_main->getfield('first').' '.$cust_main->getfield('last');
984 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
986 my @full_xaction = ( $xaction,
987 'Order-ID' => $paybatch,
988 'Amount' => "usd $amount",
989 'Card-Number' => $cust_main->getfield('payinfo'),
990 'Card-Name' => $payname,
991 'Card-Address' => $address,
992 'Card-City' => $cust_main->getfield('city'),
993 'Card-State' => $cust_main->getfield('state'),
994 'Card-Zip' => $cust_main->getfield('zip'),
995 'Card-Country' => $country,
1000 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
1002 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
1003 my $cust_pay = new FS::cust_pay ( {
1004 'invnum' => $self->invnum,
1008 'payinfo' => $cust_main->payinfo,
1009 'paybatch' => "$cybercash:$paybatch",
1011 my $error = $cust_pay->insert;
1013 # gah, even with transactions.
1014 my $e = 'WARNING: Card debited but database not updated - '.
1015 'error applying payment, invnum #' . $self->invnum.
1016 " (CyberCash Order-ID $paybatch): $error";
1022 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1023 # || $options{'report_badcard'}
1026 return 'Cybercash error, invnum #' .
1027 $self->invnum. ':'. $result{'MErrMsg'};
1034 Adds a payment for this invoice to the pending credit card batch (see
1035 L<FS::cust_pay_batch>).
1041 my $cust_main = $self->cust_main;
1043 my $cust_pay_batch = new FS::cust_pay_batch ( {
1044 'invnum' => $self->getfield('invnum'),
1045 'custnum' => $cust_main->getfield('custnum'),
1046 'last' => $cust_main->getfield('last'),
1047 'first' => $cust_main->getfield('first'),
1048 'address1' => $cust_main->getfield('address1'),
1049 'address2' => $cust_main->getfield('address2'),
1050 'city' => $cust_main->getfield('city'),
1051 'state' => $cust_main->getfield('state'),
1052 'zip' => $cust_main->getfield('zip'),
1053 'country' => $cust_main->getfield('country'),
1054 'cardnum' => $cust_main->getfield('payinfo'),
1055 'exp' => $cust_main->getfield('paydate'),
1056 'payname' => $cust_main->getfield('payname'),
1057 'amount' => $self->owed,
1059 my $error = $cust_pay_batch->insert;
1060 die $error if $error;
1065 sub _agent_template {
1068 my $cust_bill_event = qsearchs( 'part_bill_event',
1070 'payby' => $self->cust_main->payby,
1071 'plan' => 'send_agent',
1072 'eventcode' => { 'op' => 'LIKE',
1073 'value' => '_%, '. $self->cust_main->agentnum. ');' },
1076 'ORDER BY seconds LIMIT 1'
1079 return '' unless $cust_bill_event;
1081 if ( $cust_bill_event->eventcode =~ /\(\s*'(.*)'\s*,\s*(\d+)\s*\)\;$/ ) {
1084 warn "can't parse eventcode for agent-specific invoice template";
1090 =item print_text [ TIME [ , TEMPLATE ] ]
1092 Returns an text invoice, as a list of lines.
1094 TIME an optional value used to control the printing of overdue messages. The
1095 default is now. It isn't the date of the invoice; that's the `_date' field.
1096 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1097 L<Time::Local> and L<Date::Parse> for conversion functions.
1103 my( $self, $today, $template ) = @_;
1105 # my $invnum = $self->invnum;
1106 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
1107 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1108 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1110 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1111 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1112 #my $balance_due = $self->owed + $pr_total - $cr_total;
1113 my $balance_due = $self->owed + $pr_total;
1116 #my($description,$amount);
1120 foreach ( @pr_cust_bill ) {
1122 "Previous Balance, Invoice #". $_->invnum.
1123 " (". time2str("%x",$_->_date). ")",
1124 $money_char. sprintf("%10.2f",$_->owed)
1127 if (@pr_cust_bill) {
1128 push @buf,['','-----------'];
1129 push @buf,[ 'Total Previous Balance',
1130 $money_char. sprintf("%10.2f",$pr_total ) ];
1135 foreach my $cust_bill_pkg (
1136 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1137 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1140 if ( $cust_bill_pkg->pkgnum ) {
1142 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1143 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1144 my $pkg = $part_pkg->pkg;
1146 if ( $cust_bill_pkg->setup != 0 ) {
1147 my $description = $pkg;
1148 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1149 push @buf, [ $description,
1150 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1152 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1155 if ( $cust_bill_pkg->recur != 0 ) {
1157 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1158 time2str("%x", $cust_bill_pkg->edate) . ")",
1159 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1162 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1165 } else { #pkgnum tax or one-shot line item
1166 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1167 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1169 if ( $cust_bill_pkg->setup != 0 ) {
1170 push @buf, [ $itemdesc,
1171 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1173 if ( $cust_bill_pkg->recur != 0 ) {
1174 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1175 . time2str("%x", $cust_bill_pkg->edate). ")",
1176 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1182 push @buf,['','-----------'];
1183 push @buf,['Total New Charges',
1184 $money_char. sprintf("%10.2f",$self->charged) ];
1187 push @buf,['','-----------'];
1188 push @buf,['Total Charges',
1189 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1193 foreach ( $self->cust_credited ) {
1195 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1197 my $reason = substr($_->cust_credit->reason,0,32);
1198 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1199 $reason = " ($reason) " if $reason;
1201 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1203 $money_char. sprintf("%10.2f",$_->amount)
1206 #foreach ( @cr_cust_credit ) {
1208 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1209 # $money_char. sprintf("%10.2f",$_->credited)
1213 #get & print payments
1214 foreach ( $self->cust_bill_pay ) {
1216 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1219 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1220 $money_char. sprintf("%10.2f",$_->amount )
1225 my $balance_due_msg = $self->balance_due_msg;
1227 push @buf,['','-----------'];
1228 push @buf,[$balance_due_msg, $money_char.
1229 sprintf("%10.2f", $balance_due ) ];
1231 #create the template
1232 $template ||= $self->_agent_template;
1233 my $templatefile = 'invoice_template';
1234 $templatefile .= "_$template" if length($template);
1235 my @invoice_template = $conf->config($templatefile)
1236 or die "cannot load config file $templatefile";
1239 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1240 /invoice_lines\((\d*)\)/;
1241 $invoice_lines += $1 || scalar(@buf);
1244 die "no invoice_lines() functions in template?" unless $wasfunc;
1245 my $invoice_template = new Text::Template (
1247 SOURCE => [ map "$_\n", @invoice_template ],
1248 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1249 $invoice_template->compile()
1250 or die "can't compile template: $Text::Template::ERROR";
1252 #setup template variables
1253 package FS::cust_bill::_template; #!
1254 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1256 $invnum = $self->invnum;
1257 $date = $self->_date;
1259 $agent = $self->cust_main->agent->agent;
1261 if ( $FS::cust_bill::invoice_lines ) {
1263 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1265 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1270 #format address (variable for the template)
1272 @address = ( '', '', '', '', '', '' );
1273 package FS::cust_bill; #!
1274 $FS::cust_bill::_template::address[$l++] =
1275 $cust_main->payname.
1276 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1277 ? " (P.O. #". $cust_main->payinfo. ")"
1281 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1282 if $cust_main->company;
1283 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1284 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1285 if $cust_main->address2;
1286 $FS::cust_bill::_template::address[$l++] =
1287 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1288 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1289 unless $cust_main->country eq 'US';
1291 # #overdue? (variable for the template)
1292 # $FS::cust_bill::_template::overdue = (
1294 # && $today > $self->_date
1295 ## && $self->printed > 1
1296 # && $self->printed > 0
1299 #and subroutine for the template
1300 sub FS::cust_bill::_template::invoice_lines {
1301 my $lines = shift || scalar(@buf);
1303 scalar(@buf) ? shift @buf : [ '', '' ];
1309 $FS::cust_bill::_template::page = 1;
1313 push @collect, split("\n",
1314 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1316 $FS::cust_bill::_template::page++;
1319 map "$_\n", @collect;
1323 =item print_latex [ TIME [ , TEMPLATE ] ]
1325 Internal method - returns a filename of a filled-in LaTeX template for this
1326 invoice (Note: add ".tex" to get the actual filename).
1328 See print_ps and print_pdf for methods that return PostScript and PDF output.
1330 TIME an optional value used to control the printing of overdue messages. The
1331 default is now. It isn't the date of the invoice; that's the `_date' field.
1332 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1333 L<Time::Local> and L<Date::Parse> for conversion functions.
1337 #still some false laziness w/print_text
1340 my( $self, $today, $template ) = @_;
1343 # my $invnum = $self->invnum;
1344 my $cust_main = $self->cust_main;
1345 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1346 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1348 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1349 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1350 #my $balance_due = $self->owed + $pr_total - $cr_total;
1351 my $balance_due = $self->owed + $pr_total;
1354 #my($description,$amount);
1357 #create the template
1358 $template ||= $self->_agent_template;
1359 my $templatefile = 'invoice_latex';
1360 my $suffix = length($template) ? "_$template" : '';
1361 $templatefile .= $suffix;
1362 my @invoice_template = $conf->config($templatefile)
1363 or die "cannot load config file $templatefile";
1365 my %invoice_data = (
1366 'invnum' => $self->invnum,
1367 'date' => time2str('%b %o, %Y', $self->_date),
1368 'agent' => _latex_escape($cust_main->agent->agent),
1369 'payname' => _latex_escape($cust_main->payname),
1370 'company' => _latex_escape($cust_main->company),
1371 'address1' => _latex_escape($cust_main->address1),
1372 'address2' => _latex_escape($cust_main->address2),
1373 'city' => _latex_escape($cust_main->city),
1374 'state' => _latex_escape($cust_main->state),
1375 'zip' => _latex_escape($cust_main->zip),
1376 'country' => _latex_escape($cust_main->country),
1377 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1378 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1380 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1381 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1384 my $countrydefault = $conf->config('countrydefault') || 'US';
1385 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1387 #do variable substitutions in notes
1388 $invoice_data{'notes'} =
1390 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1391 $conf->config_orbase('invoice_latexnotes', $suffix)
1394 $invoice_data{'footer'} =~ s/\n+$//;
1395 $invoice_data{'smallfooter'} =~ s/\n+$//;
1396 $invoice_data{'notes'} =~ s/\n+$//;
1398 $invoice_data{'po_line'} =
1399 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1400 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1404 my @total_item = ();
1406 while ( @invoice_template ) {
1407 my $line = shift @invoice_template;
1409 if ( $line =~ /^%%Detail\s*$/ ) {
1411 while ( ( my $line_item_line = shift @invoice_template )
1412 !~ /^%%EndDetail\s*$/ ) {
1413 push @line_item, $line_item_line;
1415 foreach my $line_item ( $self->_items ) {
1416 #foreach my $line_item ( $self->_items_pkg ) {
1417 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1418 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1419 if ( exists $line_item->{'ext_description'} ) {
1420 $invoice_data{'description'} .=
1421 "\\tabularnewline\n~~".
1422 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1424 $invoice_data{'amount'} = $line_item->{'amount'};
1425 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1427 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1430 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1432 while ( ( my $total_item_line = shift @invoice_template )
1433 !~ /^%%EndTotalDetails\s*$/ ) {
1434 push @total_item, $total_item_line;
1437 my @total_fill = ();
1440 foreach my $tax ( $self->_items_tax ) {
1441 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1442 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1444 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1449 $invoice_data{'total_item'} = 'Sub-total';
1450 $invoice_data{'total_amount'} =
1451 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1452 unshift @total_fill,
1453 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1457 $invoice_data{'total_item'} = '\textbf{Total}';
1458 $invoice_data{'total_amount'} =
1459 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1461 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1464 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1467 foreach my $credit ( $self->_items_credits ) {
1468 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1470 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1472 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1477 foreach my $payment ( $self->_items_payments ) {
1478 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1480 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1482 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1486 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1487 $invoice_data{'total_amount'} =
1488 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1490 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1493 push @filled_in, @total_fill;
1496 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1497 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1498 push @filled_in, $line;
1509 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1510 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1514 ) or die "can't open temp file: $!\n";
1515 print $fh join("\n", @filled_in ), "\n";
1518 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1523 =item print_ps [ TIME [ , TEMPLATE ] ]
1525 Returns an postscript invoice, as a scalar.
1527 TIME an optional value used to control the printing of overdue messages. The
1528 default is now. It isn't the date of the invoice; that's the `_date' field.
1529 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1530 L<Time::Local> and L<Date::Parse> for conversion functions.
1537 my $file = $self->print_latex(@_);
1539 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1542 system("pslatex $file.tex >/dev/null 2>&1") == 0
1543 or die "pslatex $file.tex failed: $!";
1544 system("pslatex $file.tex >/dev/null 2>&1") == 0
1545 or die "pslatex $file.tex failed: $!";
1547 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1548 or die "dbips failed: $!";
1550 open(POSTSCRIPT, "<$file.ps")
1551 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1553 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1556 while (<POSTSCRIPT>) {
1566 =item print_pdf [ TIME [ , TEMPLATE ] ]
1568 Returns an PDF invoice, as a scalar.
1570 TIME an optional value used to control the printing of overdue messages. The
1571 default is now. It isn't the date of the invoice; that's the `_date' field.
1572 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1573 L<Time::Local> and L<Date::Parse> for conversion functions.
1580 my $file = $self->print_latex(@_);
1582 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1585 #system('pdflatex', "$file.tex");
1586 #system('pdflatex', "$file.tex");
1587 #! LaTeX Error: Unknown graphics extension: .eps.
1589 system("pslatex $file.tex >/dev/null 2>&1") == 0
1590 or die "pslatex $file.tex failed: $!";
1591 system("pslatex $file.tex >/dev/null 2>&1") == 0
1592 or die "pslatex $file.tex failed: $!";
1594 #system('dvipdf', "$file.dvi", "$file.pdf" );
1596 "dvips -q -t letter -f $file.dvi ".
1597 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf ".
1600 or die "dvips | gs failed: $!";
1602 open(PDF, "<$file.pdf")
1603 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1605 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1618 # quick subroutine for print_latex
1620 # There are ten characters that LaTeX treats as special characters, which
1621 # means that they do not simply typeset themselves:
1622 # # $ % & ~ _ ^ \ { }
1624 # TeX ignores blanks following an escaped character; if you want a blank (as
1625 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1629 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1633 #utility methods for print_*
1635 sub balance_due_msg {
1637 my $msg = 'Balance Due';
1638 return $msg unless $conf->exists('invoice_default_terms');
1639 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1640 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1641 } elsif ( $conf->config('invoice_default_terms') ) {
1642 $msg .= ' - '. $conf->config('invoice_default_terms');
1649 my @display = scalar(@_)
1651 : qw( _items_previous _items_pkg );
1652 #: qw( _items_pkg );
1653 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1655 foreach my $display ( @display ) {
1656 push @b, $self->$display(@_);
1661 sub _items_previous {
1663 my $cust_main = $self->cust_main;
1664 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1666 foreach ( @pr_cust_bill ) {
1668 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1669 ' ('. time2str('%x',$_->_date). ')',
1670 #'pkgpart' => 'N/A',
1672 'amount' => sprintf("%10.2f", $_->owed),
1678 # 'description' => 'Previous Balance',
1679 # #'pkgpart' => 'N/A',
1680 # 'pkgnum' => 'N/A',
1681 # 'amount' => sprintf("%10.2f", $pr_total ),
1682 # 'ext_description' => [ map {
1683 # "Invoice ". $_->invnum.
1684 # " (". time2str("%x",$_->_date). ") ".
1685 # sprintf("%10.2f", $_->owed)
1686 # } @pr_cust_bill ],
1693 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1694 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1699 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1700 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1703 sub _items_cust_bill_pkg {
1705 my $cust_bill_pkg = shift;
1708 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1710 if ( $cust_bill_pkg->pkgnum ) {
1712 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1713 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1714 my $pkg = $part_pkg->pkg;
1717 #tie %labels, 'Tie::IxHash';
1718 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1719 my @ext_description;
1720 foreach my $label ( keys %labels ) {
1721 my @values = @{ $labels{$label} };
1722 my $num = scalar(@values);
1724 push @ext_description, "$label ($num)";
1726 push @ext_description, map { "$label: $_" } @values;
1730 if ( $cust_bill_pkg->setup != 0 ) {
1731 my $description = $pkg;
1732 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1733 my @d = @ext_description;
1734 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1736 'description' => $description,
1737 #'pkgpart' => $part_pkg->pkgpart,
1738 'pkgnum' => $cust_pkg->pkgnum,
1739 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1740 'ext_description' => \@d,
1744 if ( $cust_bill_pkg->recur != 0 ) {
1746 'description' => "$pkg (" .
1747 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1748 time2str('%x', $cust_bill_pkg->edate). ')',
1749 #'pkgpart' => $part_pkg->pkgpart,
1750 'pkgnum' => $cust_pkg->pkgnum,
1751 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1752 'ext_description' => [ @ext_description,
1753 $cust_bill_pkg->details,
1758 } else { #pkgnum tax or one-shot line item (??)
1760 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1761 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1763 if ( $cust_bill_pkg->setup != 0 ) {
1765 'description' => $itemdesc,
1766 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1769 if ( $cust_bill_pkg->recur != 0 ) {
1771 'description' => "$itemdesc (".
1772 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1773 time2str("%x", $cust_bill_pkg->edate). ')',
1774 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1786 sub _items_credits {
1791 foreach ( $self->cust_credited ) {
1793 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1795 my $reason = $_->cust_credit->reason;
1796 #my $reason = substr($_->cust_credit->reason,0,32);
1797 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1798 $reason = " ($reason) " if $reason;
1800 #'description' => 'Credit ref\#'. $_->crednum.
1801 # " (". time2str("%x",$_->cust_credit->_date) .")".
1803 'description' => 'Credit applied'.
1804 time2str("%x",$_->cust_credit->_date). $reason,
1805 'amount' => sprintf("%10.2f",$_->amount),
1808 #foreach ( @cr_cust_credit ) {
1810 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1811 # $money_char. sprintf("%10.2f",$_->credited)
1819 sub _items_payments {
1823 #get & print payments
1824 foreach ( $self->cust_bill_pay ) {
1826 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1829 'description' => "Payment received ".
1830 time2str("%x",$_->cust_pay->_date ),
1831 'amount' => sprintf("%10.2f", $_->amount )
1845 print_text formatting (and some logic :/) is in source, but needs to be
1846 slurped in from a file. Also number of lines ($=).
1850 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1851 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base