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;
16 use FS::UID qw( datasrc );
17 use FS::Record qw( qsearch qsearchs );
19 use FS::cust_bill_pkg;
23 use FS::cust_credit_bill;
24 use FS::cust_pay_batch;
25 use FS::cust_bill_event;
27 @ISA = qw( FS::Record );
29 $realtime_bop_decline_quiet = 0;
31 #ask FS::UID to run this stuff for us later
32 $FS::UID::callback{'FS::cust_bill'} = sub {
36 $money_char = $conf->config('money_char') || '$';
38 $lpr = $conf->config('lpr');
39 $invoice_from = $conf->config('invoice_from');
40 $smtpmachine = $conf->config('smtpmachine');
42 ( $bop_processor,$bop_login, $bop_password, $bop_action ) = ( '', '', '', '');
44 ( $ach_processor,$ach_login, $ach_password, $ach_action ) = ( '', '', '', '');
47 if ( $conf->exists('cybercash3.2') ) {
49 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
50 require CCMckDirectLib3_2;
52 require CCMckErrno3_2;
53 #qw(MCKGetErrorMessage $E_NoErr);
54 import CCMckErrno3_2 qw($E_NoErr);
57 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
58 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
59 if ( $status != $E_NoErr ) {
60 warn "CCMckLib3_2::InitConfig error:\n";
61 foreach my $key (keys %CCMckLib3_2::Config) {
62 warn " $key => $CCMckLib3_2::Config{$key}\n"
64 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
65 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
67 $cybercash='cybercash3.2';
68 } elsif ( $conf->exists('business-onlinepayment') ) {
74 ) = $conf->config('business-onlinepayment');
75 $bop_action ||= 'normal authorization';
76 ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) =
77 ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options );
78 eval "use Business::OnlinePayment";
81 if ( $conf->exists('business-onlinepayment-ach') ) {
87 ) = $conf->config('business-onlinepayment-ach');
88 $ach_action ||= 'normal authorization';
89 eval "use Business::OnlinePayment";
96 FS::cust_bill - Object methods for cust_bill records
102 $record = new FS::cust_bill \%hash;
103 $record = new FS::cust_bill { 'column' => 'value' };
105 $error = $record->insert;
107 $error = $new_record->replace($old_record);
109 $error = $record->delete;
111 $error = $record->check;
113 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
115 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
117 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
119 @cust_pay_objects = $cust_bill->cust_pay;
121 $tax_amount = $record->tax;
123 @lines = $cust_bill->print_text;
124 @lines = $cust_bill->print_text $time;
128 An FS::cust_bill object represents an invoice; a declaration that a customer
129 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
130 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
131 following fields are currently supported:
135 =item invnum - primary key (assigned automatically for new invoices)
137 =item custnum - customer (see L<FS::cust_main>)
139 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
140 L<Time::Local> and L<Date::Parse> for conversion functions.
142 =item charged - amount of this invoice
144 =item printed - deprecated
146 =item closed - books closed flag, empty or `Y'
156 Creates a new invoice. To add the invoice to the database, see L<"insert">.
157 Invoices are normally created by calling the bill method of a customer object
158 (see L<FS::cust_main>).
162 sub table { 'cust_bill'; }
166 Adds this invoice to the database ("Posts" the invoice). If there is an error,
167 returns the error, otherwise returns false.
171 Currently unimplemented. I don't remove invoices because there would then be
172 no record you ever posted this invoice (which is bad, no?)
178 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
179 $self->SUPER::delete(@_);
182 =item replace OLD_RECORD
184 Replaces the OLD_RECORD with this one in the database. If there is an error,
185 returns the error, otherwise returns false.
187 Only printed may be changed. printed is normally updated by calling the
188 collect method of a customer object (see L<FS::cust_main>).
193 my( $new, $old ) = ( shift, shift );
194 return "Can't change custnum!" unless $old->custnum == $new->custnum;
195 #return "Can't change _date!" unless $old->_date eq $new->_date;
196 return "Can't change _date!" unless $old->_date == $new->_date;
197 return "Can't change charged!" unless $old->charged == $new->charged;
199 $new->SUPER::replace($old);
204 Checks all fields to make sure this is a valid invoice. If there is an error,
205 returns the error, otherwise returns false. Called by the insert and replace
214 $self->ut_numbern('invnum')
215 || $self->ut_number('custnum')
216 || $self->ut_numbern('_date')
217 || $self->ut_money('charged')
218 || $self->ut_numbern('printed')
219 || $self->ut_enum('closed', [ '', 'Y' ])
221 return $error if $error;
223 return "Unknown customer"
224 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
226 $self->_date(time) unless $self->_date;
228 $self->printed(0) if $self->printed eq '';
235 Returns a list consisting of the total previous balance for this customer,
236 followed by the previous outstanding invoices (as FS::cust_bill objects also).
243 my @cust_bill = sort { $a->_date <=> $b->_date }
244 grep { $_->owed != 0 && $_->_date < $self->_date }
245 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
247 foreach ( @cust_bill ) { $total += $_->owed; }
253 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
259 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
262 =item cust_bill_event
264 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
269 sub cust_bill_event {
271 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
277 Returns the customer (see L<FS::cust_main>) for this invoice.
283 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
288 Depreciated. See the cust_credited method.
290 #Returns a list consisting of the total previous credited (see
291 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
292 #outstanding credits (FS::cust_credit objects).
298 croak "FS::cust_bill->cust_credit depreciated; see ".
299 "FS::cust_bill->cust_credit_bill";
302 #my @cust_credit = sort { $a->_date <=> $b->_date }
303 # grep { $_->credited != 0 && $_->_date < $self->_date }
304 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
306 #foreach (@cust_credit) { $total += $_->credited; }
307 #$total, @cust_credit;
312 Depreciated. See the cust_bill_pay method.
314 #Returns all payments (see L<FS::cust_pay>) for this invoice.
320 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
322 #sort { $a->_date <=> $b->_date }
323 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
329 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
335 sort { $a->_date <=> $b->_date }
336 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
341 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
347 sort { $a->_date <=> $b->_date }
348 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
354 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
361 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
363 foreach (@taxlines) { $total += $_->setup; }
369 Returns the amount owed (still outstanding) on this invoice, which is charged
370 minus all payment applications (see L<FS::cust_bill_pay>) and credit
371 applications (see L<FS::cust_credit_bill>).
377 my $balance = $self->charged;
378 $balance -= $_->amount foreach ( $self->cust_bill_pay );
379 $balance -= $_->amount foreach ( $self->cust_credited );
380 $balance = sprintf( "%.2f", $balance);
381 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
387 Sends this invoice to the destinations configured for this customer: send
388 emails or print. See L<FS::cust_main_invoice>.
393 my($self,$template) = @_;
394 my @print_text = $self->print_text('', $template);
395 my @invoicing_list = $self->cust_main->invoicing_list;
397 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
399 #better to notify this person than silence
400 @invoicing_list = ($invoice_from) unless @invoicing_list;
402 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
403 #$ENV{SMTPHOSTS} = $smtpmachine;
404 $ENV{MAILADDRESS} = $invoice_from;
405 my $header = new Mail::Header ( [
406 "From: $invoice_from",
407 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
408 "Sender: $invoice_from",
409 "Reply-To: $invoice_from",
410 "Date: ". time2str("%a, %d %b %Y %X %z", time),
413 my $message = new Mail::Internet (
415 'Body' => [ @print_text ], #( date)
418 $message->smtpsend( Host => $smtpmachine )
419 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
420 or die "(customer # ". $self->custnum. ") can't send invoice email".
421 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
422 " via server $smtpmachine with SMTP: $!\n";
426 if ( $conf->config('invoice_latex') ) {
427 @print_text = $self->print_ps('', $template);
430 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
432 or die "Can't open pipe to $lpr: $!\n";
433 print LPR @print_text;
435 or die $! ? "Error closing $lpr: $!\n"
436 : "Exit status $? from $lpr\n";
443 =item send_csv OPTIONS
445 Sends invoice as a CSV data-file to a remote host with the specified protocol.
449 protocol - currently only "ftp"
455 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
456 and YYMMDDHHMMSS is a timestamp.
458 The fields of the CSV file is as follows:
460 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
464 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
466 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
467 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
468 fields are filled in.
470 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
471 first two fields (B<record_type> and B<invnum>) and the last five fields
472 (B<pkg> through B<edate>) are filled in.
474 =item invnum - invoice number
476 =item custnum - customer number
478 =item _date - invoice date
480 =item charged - total invoice amount
482 =item first - customer first name
484 =item last - customer first name
486 =item company - company name
488 =item address1 - address line 1
490 =item address2 - address line 1
500 =item pkg - line item description
502 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
504 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
506 =item sdate - start date for recurring fee
508 =item edate - end date for recurring fee
515 my($self, %opt) = @_;
517 #part one: create file
519 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
520 mkdir $spooldir, 0700 unless -d $spooldir;
522 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
524 open(CSV, ">$file") or die "can't open $file: $!";
526 eval "use Text::CSV_XS";
529 my $csv = Text::CSV_XS->new({'always_quote'=>1});
531 my $cust_main = $self->cust_main;
537 time2str("%x", $self->_date),
538 sprintf("%.2f", $self->charged),
539 ( map { $cust_main->getfield($_) }
540 qw( first last company address1 address2 city state zip country ) ),
542 ) or die "can't create csv";
543 print CSV $csv->string. "\n";
545 #new charges (false laziness w/print_text)
546 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
548 my($pkg, $setup, $recur, $sdate, $edate);
549 if ( $cust_bill_pkg->pkgnum ) {
551 ($pkg, $setup, $recur, $sdate, $edate) = (
552 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
553 ( $cust_bill_pkg->setup != 0
554 ? sprintf("%.2f", $cust_bill_pkg->setup )
556 ( $cust_bill_pkg->recur != 0
557 ? sprintf("%.2f", $cust_bill_pkg->recur )
559 time2str("%x", $cust_bill_pkg->sdate),
560 time2str("%x", $cust_bill_pkg->edate),
564 next unless $cust_bill_pkg->setup != 0;
565 ($pkg, $setup, $recur, $sdate, $edate) =
566 ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
572 ( map { '' } (1..11) ),
573 ($pkg, $setup, $recur, $sdate, $edate)
574 ) or die "can't create csv";
575 print CSV $csv->string. "\n";
579 close CSV or die "can't close CSV: $!";
584 if ( $opt{protocol} eq 'ftp' ) {
585 eval "use Net::FTP;";
587 $net = Net::FTP->new($opt{server}) or die @$;
589 die "unknown protocol: $opt{protocol}";
592 $net->login( $opt{username}, $opt{password} )
593 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
595 $net->binary or die "can't set binary mode";
597 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
599 $net->put($file) or die "can't put $file: $!";
609 Pays this invoice with a compliemntary payment. If there is an error,
610 returns the error, otherwise returns false.
616 my $cust_pay = new FS::cust_pay ( {
617 'invnum' => $self->invnum,
618 'paid' => $self->owed,
621 'payinfo' => $self->cust_main->payinfo,
629 Attempts to pay this invoice with a credit card payment via a
630 Business::OnlinePayment realtime gateway. See
631 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
632 for supported processors.
651 Attempts to pay this invoice with an electronic check (ACH) payment via a
652 Business::OnlinePayment realtime gateway. See
653 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
654 for supported processors.
673 Attempts to pay this invoice with phone bill (LEC) payment via a
674 Business::OnlinePayment realtime gateway. See
675 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
676 for supported processors.
694 my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
696 #trim an extraneous blank line
697 pop @$options if scalar(@$options) % 2 && $options->[-1] =~ /^\s*$/;
699 my $cust_main = $self->cust_main;
700 my $balance = $cust_main->balance;
701 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
702 $amount = sprintf("%.2f", $amount);
703 return "not run (balance $balance)" unless $amount > 0;
705 my $address = $cust_main->address1;
706 $address .= ", ". $cust_main->address2 if $cust_main->address2;
708 my($payname, $payfirst, $paylast);
709 if ( $cust_main->payname && $method ne 'ECHECK' ) {
710 $payname = $cust_main->payname;
711 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
713 #$dbh->rollback if $oldAutoCommit;
714 return "Illegal payname $payname";
716 ($payfirst, $paylast) = ($1, $2);
718 $payfirst = $cust_main->getfield('first');
719 $paylast = $cust_main->getfield('last');
720 $payname = "$payfirst $paylast";
723 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
724 if ( $conf->exists('emailinvoiceauto')
725 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
726 push @invoicing_list, $cust_main->all_emails;
728 my $email = $invoicing_list[0];
730 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
732 my $description = 'Internet Services';
733 if ( $conf->exists('business-onlinepayment-description') ) {
734 my $dtempl = $conf->config('business-onlinepayment-description');
736 my $agent_obj = $cust_main->agent
737 or die "can't retreive agent for $cust_main (agentnum ".
738 $cust_main->agentnum. ")";
739 my $agent = $agent_obj->agent;
740 my $pkgs = join(', ',
741 map { $_->cust_pkg->part_pkg->pkg }
742 grep { $_->pkgnum } $self->cust_bill_pkg
744 $description = eval qq("$dtempl");
749 if ( $method eq 'CC' ) {
751 $content{card_number} = $cust_main->payinfo;
752 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
753 $content{expiration} = "$2/$1";
755 $content{cvv2} = $cust_main->paycvv
756 if defined $cust_main->dbdef_table->column('paycvv')
757 && length($cust_main->paycvv);
759 $content{recurring_billing} = 'YES'
760 if qsearch('cust_pay', { 'custnum' => $cust_main->custnum,
762 'payinfo' => $cust_main->payinfo, } );
764 } elsif ( $method eq 'ECHECK' ) {
765 my($account_number,$routing_code) = $cust_main->payinfo;
766 ( $content{account_number}, $content{routing_code} ) =
767 split('@', $cust_main->payinfo);
768 $content{bank_name} = $cust_main->payname;
769 $content{account_type} = 'CHECKING';
770 $content{account_name} = $payname;
771 $content{customer_org} = $self->company ? 'B' : 'I';
772 $content{customer_ssn} = $self->ss;
773 } elsif ( $method eq 'LEC' ) {
774 $content{phone} = $cust_main->payinfo;
778 new Business::OnlinePayment( $processor, @$options );
779 $transaction->content(
782 'password' => $password,
783 'action' => $action1,
784 'description' => $description,
786 'invoice_number' => $self->invnum,
787 'customer_id' => $self->custnum,
788 'last_name' => $paylast,
789 'first_name' => $payfirst,
791 'address' => $address,
792 'city' => $cust_main->city,
793 'state' => $cust_main->state,
794 'zip' => $cust_main->zip,
795 'country' => $cust_main->country,
796 'referer' => 'http://cleanwhisker.420.am/',
798 'phone' => $cust_main->daytime || $cust_main->night,
801 $transaction->submit();
803 if ( $transaction->is_success() && $action2 ) {
804 my $auth = $transaction->authorization;
805 my $ordernum = $transaction->can('order_number')
806 ? $transaction->order_number
809 #warn "********* $auth ***********\n";
810 #warn "********* $ordernum ***********\n";
812 new Business::OnlinePayment( $processor, @$options );
819 password => $password,
820 order_number => $ordernum,
822 authorization => $auth,
823 description => $description,
826 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
827 transaction_sequence_num local_transaction_date
828 local_transaction_time AVS_result_code )) {
829 $capture{$field} = $transaction->$field() if $transaction->can($field);
832 $capture->content( %capture );
836 unless ( $capture->is_success ) {
837 my $e = "Authorization sucessful but capture failed, invnum #".
838 $self->invnum. ': '. $capture->result_code.
839 ": ". $capture->error_message;
846 #remove paycvv after initial transaction
847 #make this disable-able via a config option if anyone insists?
848 # (though that probably violates cardholder agreements)
849 use Business::CreditCard;
850 if ( defined $cust_main->dbdef_table->column('paycvv')
851 && length($cust_main->paycvv)
852 && ! grep { $_ eq cardtype($cust_main->payinfo) } $conf->config('cvv-save')
855 my $new = new FS::cust_main { $cust_main->hash };
857 my $error = $new->replace($cust_main);
859 warn "error removing cvv: $error\n";
864 if ( $transaction->is_success() ) {
872 my $cust_pay = new FS::cust_pay ( {
873 'invnum' => $self->invnum,
876 'payby' => $method2payby{$method},
877 'payinfo' => $cust_main->payinfo,
878 'paybatch' => "$processor:". $transaction->authorization,
880 my $error = $cust_pay->insert;
882 $cust_pay->invnum(''); #try again with no specific invnum
883 my $error2 = $cust_pay->insert;
885 # gah, even with transactions.
886 my $e = 'WARNING: Card/ACH debited but database not updated - '.
887 "error inserting payment ($processor): $error2".
888 ' (previously tried insert with invnum #' . $self->invnum.
896 #} elsif ( $options{'report_badcard'} ) {
899 my $perror = "$processor error, invnum #". $self->invnum. ': '.
900 $transaction->result_code. ": ". $transaction->error_message;
902 if ( !$realtime_bop_decline_quiet && $conf->exists('emaildecline')
903 && grep { $_ ne 'POST' } $cust_main->invoicing_list
904 && ! grep { $transaction->error_message =~ /$_/ }
905 $conf->config('emaildecline-exclude')
907 my @templ = $conf->config('declinetemplate');
908 my $template = new Text::Template (
910 SOURCE => [ map "$_\n", @templ ],
911 ) or return "($perror) can't create template: $Text::Template::ERROR";
913 or return "($perror) can't compile template: $Text::Template::ERROR";
915 my $templ_hash = { error => $transaction->error_message };
917 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
918 $ENV{MAILADDRESS} = $invoice_from;
919 my $header = new Mail::Header ( [
920 "From: $invoice_from",
921 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
922 "Sender: $invoice_from",
923 "Reply-To: $invoice_from",
924 "Date: ". time2str("%a, %d %b %Y %X %z", time),
925 "Subject: Your payment could not be processed",
927 my $message = new Mail::Internet (
929 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
932 $message->smtpsend( Host => $smtpmachine )
933 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
934 or return "($perror) (customer # ". $self->custnum.
935 ") can't send card decline email to ".
936 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
937 " via server $smtpmachine with SMTP: $!";
945 =item realtime_card_cybercash
947 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
951 sub realtime_card_cybercash {
953 my $cust_main = $self->cust_main;
954 my $amount = $self->owed;
956 return "CyberCash CashRegister real-time card processing not enabled!"
957 unless $cybercash eq 'cybercash3.2';
959 my $address = $cust_main->address1;
960 $address .= ", ". $cust_main->address2 if $cust_main->address2;
963 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
964 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
969 my $paybatch = $self->invnum.
970 '-' . time2str("%y%m%d%H%M%S", time);
972 my $payname = $cust_main->payname ||
973 $cust_main->getfield('first').' '.$cust_main->getfield('last');
975 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
977 my @full_xaction = ( $xaction,
978 'Order-ID' => $paybatch,
979 'Amount' => "usd $amount",
980 'Card-Number' => $cust_main->getfield('payinfo'),
981 'Card-Name' => $payname,
982 'Card-Address' => $address,
983 'Card-City' => $cust_main->getfield('city'),
984 'Card-State' => $cust_main->getfield('state'),
985 'Card-Zip' => $cust_main->getfield('zip'),
986 'Card-Country' => $country,
991 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
993 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
994 my $cust_pay = new FS::cust_pay ( {
995 'invnum' => $self->invnum,
999 'payinfo' => $cust_main->payinfo,
1000 'paybatch' => "$cybercash:$paybatch",
1002 my $error = $cust_pay->insert;
1004 # gah, even with transactions.
1005 my $e = 'WARNING: Card debited but database not updated - '.
1006 'error applying payment, invnum #' . $self->invnum.
1007 " (CyberCash Order-ID $paybatch): $error";
1013 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1014 # || $options{'report_badcard'}
1017 return 'Cybercash error, invnum #' .
1018 $self->invnum. ':'. $result{'MErrMsg'};
1025 Adds a payment for this invoice to the pending credit card batch (see
1026 L<FS::cust_pay_batch>).
1032 my $cust_main = $self->cust_main;
1034 my $cust_pay_batch = new FS::cust_pay_batch ( {
1035 'invnum' => $self->getfield('invnum'),
1036 'custnum' => $cust_main->getfield('custnum'),
1037 'last' => $cust_main->getfield('last'),
1038 'first' => $cust_main->getfield('first'),
1039 'address1' => $cust_main->getfield('address1'),
1040 'address2' => $cust_main->getfield('address2'),
1041 'city' => $cust_main->getfield('city'),
1042 'state' => $cust_main->getfield('state'),
1043 'zip' => $cust_main->getfield('zip'),
1044 'country' => $cust_main->getfield('country'),
1045 'cardnum' => $cust_main->getfield('payinfo'),
1046 'exp' => $cust_main->getfield('paydate'),
1047 'payname' => $cust_main->getfield('payname'),
1048 'amount' => $self->owed,
1050 my $error = $cust_pay_batch->insert;
1051 die $error if $error;
1056 =item print_text [ TIME [ , TEMPLATE ] ]
1058 Returns an text invoice, as a list of lines.
1060 TIME an optional value used to control the printing of overdue messages. The
1061 default is now. It isn't the date of the invoice; that's the `_date' field.
1062 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1063 L<Time::Local> and L<Date::Parse> for conversion functions.
1069 my( $self, $today, $template ) = @_;
1071 # my $invnum = $self->invnum;
1072 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
1073 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1074 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1076 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1077 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1078 #my $balance_due = $self->owed + $pr_total - $cr_total;
1079 my $balance_due = $self->owed + $pr_total;
1082 #my($description,$amount);
1086 foreach ( @pr_cust_bill ) {
1088 "Previous Balance, Invoice #". $_->invnum.
1089 " (". time2str("%x",$_->_date). ")",
1090 $money_char. sprintf("%10.2f",$_->owed)
1093 if (@pr_cust_bill) {
1094 push @buf,['','-----------'];
1095 push @buf,[ 'Total Previous Balance',
1096 $money_char. sprintf("%10.2f",$pr_total ) ];
1101 foreach my $cust_bill_pkg (
1102 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1103 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1106 if ( $cust_bill_pkg->pkgnum ) {
1108 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1109 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1110 my $pkg = $part_pkg->pkg;
1112 if ( $cust_bill_pkg->setup != 0 ) {
1113 my $description = $pkg;
1114 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1115 push @buf, [ $description,
1116 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1118 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1121 if ( $cust_bill_pkg->recur != 0 ) {
1123 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1124 time2str("%x", $cust_bill_pkg->edate) . ")",
1125 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1128 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1131 } else { #pkgnum tax or one-shot line item
1132 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1133 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1135 if ( $cust_bill_pkg->setup != 0 ) {
1136 push @buf, [ $itemdesc,
1137 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1139 if ( $cust_bill_pkg->recur != 0 ) {
1140 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1141 . time2str("%x", $cust_bill_pkg->edate). ")",
1142 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1148 push @buf,['','-----------'];
1149 push @buf,['Total New Charges',
1150 $money_char. sprintf("%10.2f",$self->charged) ];
1153 push @buf,['','-----------'];
1154 push @buf,['Total Charges',
1155 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1159 foreach ( $self->cust_credited ) {
1161 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1163 my $reason = substr($_->cust_credit->reason,0,32);
1164 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1165 $reason = " ($reason) " if $reason;
1167 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1169 $money_char. sprintf("%10.2f",$_->amount)
1172 #foreach ( @cr_cust_credit ) {
1174 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1175 # $money_char. sprintf("%10.2f",$_->credited)
1179 #get & print payments
1180 foreach ( $self->cust_bill_pay ) {
1182 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1185 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1186 $money_char. sprintf("%10.2f",$_->amount )
1191 my $balance_due_msg = $self->balance_due_msg;
1193 push @buf,['','-----------'];
1194 push @buf,[$balance_due_msg, $money_char.
1195 sprintf("%10.2f", $balance_due ) ];
1197 #create the template
1198 my $templatefile = 'invoice_template';
1199 $templatefile .= "_$template" if $template;
1200 my @invoice_template = $conf->config($templatefile)
1201 or die "cannot load config file $templatefile";
1204 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1205 /invoice_lines\((\d*)\)/;
1206 $invoice_lines += $1 || scalar(@buf);
1209 die "no invoice_lines() functions in template?" unless $wasfunc;
1210 my $invoice_template = new Text::Template (
1212 SOURCE => [ map "$_\n", @invoice_template ],
1213 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1214 $invoice_template->compile()
1215 or die "can't compile template: $Text::Template::ERROR";
1217 #setup template variables
1218 package FS::cust_bill::_template; #!
1219 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1221 $invnum = $self->invnum;
1222 $date = $self->_date;
1224 $agent = $self->cust_main->agent->agent;
1226 if ( $FS::cust_bill::invoice_lines ) {
1228 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1230 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1235 #format address (variable for the template)
1237 @address = ( '', '', '', '', '', '' );
1238 package FS::cust_bill; #!
1239 $FS::cust_bill::_template::address[$l++] =
1240 $cust_main->payname.
1241 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1242 ? " (P.O. #". $cust_main->payinfo. ")"
1246 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1247 if $cust_main->company;
1248 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1249 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1250 if $cust_main->address2;
1251 $FS::cust_bill::_template::address[$l++] =
1252 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1253 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1254 unless $cust_main->country eq 'US';
1256 # #overdue? (variable for the template)
1257 # $FS::cust_bill::_template::overdue = (
1259 # && $today > $self->_date
1260 ## && $self->printed > 1
1261 # && $self->printed > 0
1264 #and subroutine for the template
1265 sub FS::cust_bill::_template::invoice_lines {
1266 my $lines = shift || scalar(@buf);
1268 scalar(@buf) ? shift @buf : [ '', '' ];
1274 $FS::cust_bill::_template::page = 1;
1278 push @collect, split("\n",
1279 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1281 $FS::cust_bill::_template::page++;
1284 map "$_\n", @collect;
1288 =item print_latex [ TIME [ , TEMPLATE ] ]
1290 Internal method - returns a filename of a filled-in LaTeX template for this
1291 invoice (Note: add ".tex" to get the actual filename).
1293 See print_ps and print_pdf for methods that return PostScript and PDF output.
1295 TIME an optional value used to control the printing of overdue messages. The
1296 default is now. It isn't the date of the invoice; that's the `_date' field.
1297 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1298 L<Time::Local> and L<Date::Parse> for conversion functions.
1302 #still some false laziness w/print_text
1305 my( $self, $today, $template ) = @_;
1308 # my $invnum = $self->invnum;
1309 my $cust_main = $self->cust_main;
1310 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1311 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1313 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1314 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1315 #my $balance_due = $self->owed + $pr_total - $cr_total;
1316 my $balance_due = $self->owed + $pr_total;
1319 #my($description,$amount);
1322 #create the template
1323 my $templatefile = 'invoice_latex';
1324 $templatefile .= "_$template" if $template;
1325 my @invoice_template = $conf->config($templatefile)
1326 or die "cannot load config file $templatefile";
1328 my %invoice_data = (
1329 'invnum' => $self->invnum,
1330 'date' => time2str('%b %o, %Y', $self->_date),
1331 'agent' => _latex_escape($cust_main->agent->agent),
1332 'payname' => _latex_escape($cust_main->payname),
1333 'company' => _latex_escape($cust_main->company),
1334 'address1' => _latex_escape($cust_main->address1),
1335 'address2' => _latex_escape($cust_main->address2),
1336 'city' => _latex_escape($cust_main->city),
1337 'state' => _latex_escape($cust_main->state),
1338 'zip' => _latex_escape($cust_main->zip),
1339 'country' => _latex_escape($cust_main->country),
1340 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1341 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1343 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1344 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1347 my $countrydefault = $conf->config('countrydefault') || 'US';
1348 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1350 #do variable substitutions in notes
1351 $invoice_data{'notes'} =
1353 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1354 $conf->config('invoice_latexnotes')
1357 $invoice_data{'footer'} =~ s/\n+$//;
1358 $invoice_data{'smallfooter'} =~ s/\n+$//;
1359 $invoice_data{'notes'} =~ s/\n+$//;
1361 $invoice_data{'po_line'} =
1362 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1363 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1367 my @total_item = ();
1369 while ( @invoice_template ) {
1370 my $line = shift @invoice_template;
1372 if ( $line =~ /^%%Detail\s*$/ ) {
1374 while ( ( my $line_item_line = shift @invoice_template )
1375 !~ /^%%EndDetail\s*$/ ) {
1376 push @line_item, $line_item_line;
1378 foreach my $line_item ( $self->_items ) {
1379 #foreach my $line_item ( $self->_items_pkg ) {
1380 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1381 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1382 if ( exists $line_item->{'ext_description'} ) {
1383 $invoice_data{'description'} .=
1384 "\\tabularnewline\n~~".
1385 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1387 $invoice_data{'amount'} = $line_item->{'amount'};
1388 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1390 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1393 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1395 while ( ( my $total_item_line = shift @invoice_template )
1396 !~ /^%%EndTotalDetails\s*$/ ) {
1397 push @total_item, $total_item_line;
1400 my @total_fill = ();
1403 foreach my $tax ( $self->_items_tax ) {
1404 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1405 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1407 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1412 $invoice_data{'total_item'} = 'Sub-total';
1413 $invoice_data{'total_amount'} =
1414 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1415 unshift @total_fill,
1416 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1420 $invoice_data{'total_item'} = '\textbf{Total}';
1421 $invoice_data{'total_amount'} =
1422 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1424 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1427 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1430 foreach my $credit ( $self->_items_credits ) {
1431 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1433 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1435 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1440 foreach my $payment ( $self->_items_payments ) {
1441 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1443 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1445 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1449 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1450 $invoice_data{'total_amount'} =
1451 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1453 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1456 push @filled_in, @total_fill;
1459 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1460 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1461 push @filled_in, $line;
1472 my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
1473 my $unique = int(rand(2**31)); #UGH... use File::Temp or something
1476 my $file = $self->invnum. ".$unique";
1478 open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
1479 print TEX join("\n", @filled_in ), "\n";
1486 =item print_ps [ TIME [ , TEMPLATE ] ]
1488 Returns an postscript invoice, as a scalar.
1490 TIME an optional value used to control the printing of overdue messages. The
1491 default is now. It isn't the date of the invoice; that's the `_date' field.
1492 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1493 L<Time::Local> and L<Date::Parse> for conversion functions.
1500 my $file = $self->print_latex(@_);
1503 system('pslatex', "$file.tex");
1504 system('pslatex', "$file.tex");
1505 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" );
1507 open(POSTSCRIPT, "<$file.ps")
1508 or die "can't open $file.ps (probable error in LaTeX template): $!\n";
1510 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1513 while (<POSTSCRIPT>) {
1523 =item print_pdf [ TIME [ , TEMPLATE ] ]
1525 Returns an PDF 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 #system('pdflatex', "$file.tex");
1540 #system('pdflatex', "$file.tex");
1541 #! LaTeX Error: Unknown graphics extension: .eps.
1544 system('pslatex', "$file.tex");
1545 system('pslatex', "$file.tex");
1547 #system('dvipdf', "$file.dvi", "$file.pdf" );
1548 system("dvips -q -t letter -f $file.dvi | gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf -c save pop -");
1550 open(PDF, "<$file.pdf")
1551 or die "can't open $file.pdf (probably error in LaTeX tempalte: $!\n";
1553 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1566 # quick subroutine for print_latex
1568 # There are ten characters that LaTeX treats as special characters, which
1569 # means that they do not simply typeset themselves:
1570 # # $ % & ~ _ ^ \ { }
1572 # TeX ignores blanks following an escaped character; if you want a blank (as
1573 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1577 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1581 #utility methods for print_*
1583 sub balance_due_msg {
1585 my $msg = 'Balance Due';
1586 return $msg unless $conf->exists('invoice_default_terms');
1587 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1588 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1589 } elsif ( $conf->config('invoice_default_terms') ) {
1590 $msg .= ' - '. $conf->config('invoice_default_terms');
1597 my @display = scalar(@_)
1599 : qw( _items_previous _items_pkg );
1600 #: qw( _items_pkg );
1601 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1603 foreach my $display ( @display ) {
1604 push @b, $self->$display(@_);
1609 sub _items_previous {
1611 my $cust_main = $self->cust_main;
1612 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1614 foreach ( @pr_cust_bill ) {
1616 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1617 ' ('. time2str('%x',$_->_date). ')',
1618 #'pkgpart' => 'N/A',
1620 'amount' => sprintf("%10.2f", $_->owed),
1626 # 'description' => 'Previous Balance',
1627 # #'pkgpart' => 'N/A',
1628 # 'pkgnum' => 'N/A',
1629 # 'amount' => sprintf("%10.2f", $pr_total ),
1630 # 'ext_description' => [ map {
1631 # "Invoice ". $_->invnum.
1632 # " (". time2str("%x",$_->_date). ") ".
1633 # sprintf("%10.2f", $_->owed)
1634 # } @pr_cust_bill ],
1641 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1642 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1647 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1648 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1651 sub _items_cust_bill_pkg {
1653 my $cust_bill_pkg = shift;
1656 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1658 if ( $cust_bill_pkg->pkgnum ) {
1660 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1661 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1662 my $pkg = $part_pkg->pkg;
1665 #tie %labels, 'Tie::IxHash';
1666 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1667 my @ext_description;
1668 foreach my $label ( keys %labels ) {
1669 my @values = @{ $labels{$label} };
1670 my $num = scalar(@values);
1672 push @ext_description, "$label ($num)";
1674 push @ext_description, map { "$label: $_" } @values;
1678 if ( $cust_bill_pkg->setup != 0 ) {
1679 my $description = $pkg;
1680 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1681 my @d = @ext_description;
1682 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1684 'description' => $description,
1685 #'pkgpart' => $part_pkg->pkgpart,
1686 'pkgnum' => $cust_pkg->pkgnum,
1687 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1688 'ext_description' => \@d,
1692 if ( $cust_bill_pkg->recur != 0 ) {
1694 'description' => "$pkg (" .
1695 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1696 time2str('%x', $cust_bill_pkg->edate). ')',
1697 #'pkgpart' => $part_pkg->pkgpart,
1698 'pkgnum' => $cust_pkg->pkgnum,
1699 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1700 'ext_description' => [ @ext_description,
1701 $cust_bill_pkg->details,
1706 } else { #pkgnum tax or one-shot line item (??)
1708 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1709 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1711 if ( $cust_bill_pkg->setup != 0 ) {
1713 'description' => $itemdesc,
1714 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1717 if ( $cust_bill_pkg->recur != 0 ) {
1719 'description' => "$itemdesc (".
1720 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1721 time2str("%x", $cust_bill_pkg->edate). ')',
1722 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1734 sub _items_credits {
1739 foreach ( $self->cust_credited ) {
1741 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1743 my $reason = $_->cust_credit->reason;
1744 #my $reason = substr($_->cust_credit->reason,0,32);
1745 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1746 $reason = " ($reason) " if $reason;
1748 #'description' => 'Credit ref\#'. $_->crednum.
1749 # " (". time2str("%x",$_->cust_credit->_date) .")".
1751 'description' => 'Credit applied'.
1752 time2str("%x",$_->cust_credit->_date). $reason,
1753 'amount' => sprintf("%10.2f",$_->amount),
1756 #foreach ( @cr_cust_credit ) {
1758 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1759 # $money_char. sprintf("%10.2f",$_->credited)
1767 sub _items_payments {
1771 #get & print payments
1772 foreach ( $self->cust_bill_pay ) {
1774 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1777 'description' => "Payment received ".
1778 time2str("%x",$_->cust_pay->_date ),
1779 'amount' => sprintf("%10.2f", $_->amount )
1793 print_text formatting (and some logic :/) is in source, but needs to be
1794 slurped in from a file. Also number of lines ($=).
1796 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1797 or something similar so the look can be completely customized?)
1801 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1802 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base