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 String::ShellQuote;
18 use FS::UID qw( datasrc );
19 use FS::Record qw( qsearch qsearchs );
21 use FS::cust_bill_pkg;
25 use FS::cust_credit_bill;
26 use FS::cust_pay_batch;
27 use FS::cust_bill_event;
29 @ISA = qw( FS::Record );
31 $realtime_bop_decline_quiet = 0;
33 #ask FS::UID to run this stuff for us later
34 $FS::UID::callback{'FS::cust_bill'} = sub {
38 $money_char = $conf->config('money_char') || '$';
40 $lpr = $conf->config('lpr');
41 $invoice_from = $conf->config('invoice_from');
42 $smtpmachine = $conf->config('smtpmachine');
44 ( $bop_processor,$bop_login, $bop_password, $bop_action ) = ( '', '', '', '');
46 ( $ach_processor,$ach_login, $ach_password, $ach_action ) = ( '', '', '', '');
49 if ( $conf->exists('cybercash3.2') ) {
51 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
52 require CCMckDirectLib3_2;
54 require CCMckErrno3_2;
55 #qw(MCKGetErrorMessage $E_NoErr);
56 import CCMckErrno3_2 qw($E_NoErr);
59 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
60 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
61 if ( $status != $E_NoErr ) {
62 warn "CCMckLib3_2::InitConfig error:\n";
63 foreach my $key (keys %CCMckLib3_2::Config) {
64 warn " $key => $CCMckLib3_2::Config{$key}\n"
66 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
67 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
69 $cybercash='cybercash3.2';
70 } elsif ( $conf->exists('business-onlinepayment') ) {
76 ) = $conf->config('business-onlinepayment');
77 $bop_action ||= 'normal authorization';
78 ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) =
79 ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options );
80 eval "use Business::OnlinePayment";
83 if ( $conf->exists('business-onlinepayment-ach') ) {
89 ) = $conf->config('business-onlinepayment-ach');
90 $ach_action ||= 'normal authorization';
91 eval "use Business::OnlinePayment";
98 FS::cust_bill - Object methods for cust_bill records
104 $record = new FS::cust_bill \%hash;
105 $record = new FS::cust_bill { 'column' => 'value' };
107 $error = $record->insert;
109 $error = $new_record->replace($old_record);
111 $error = $record->delete;
113 $error = $record->check;
115 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
117 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
119 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
121 @cust_pay_objects = $cust_bill->cust_pay;
123 $tax_amount = $record->tax;
125 @lines = $cust_bill->print_text;
126 @lines = $cust_bill->print_text $time;
130 An FS::cust_bill object represents an invoice; a declaration that a customer
131 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
132 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
133 following fields are currently supported:
137 =item invnum - primary key (assigned automatically for new invoices)
139 =item custnum - customer (see L<FS::cust_main>)
141 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
142 L<Time::Local> and L<Date::Parse> for conversion functions.
144 =item charged - amount of this invoice
146 =item printed - deprecated
148 =item closed - books closed flag, empty or `Y'
158 Creates a new invoice. To add the invoice to the database, see L<"insert">.
159 Invoices are normally created by calling the bill method of a customer object
160 (see L<FS::cust_main>).
164 sub table { 'cust_bill'; }
168 Adds this invoice to the database ("Posts" the invoice). If there is an error,
169 returns the error, otherwise returns false.
173 Currently unimplemented. I don't remove invoices because there would then be
174 no record you ever posted this invoice (which is bad, no?)
180 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
181 $self->SUPER::delete(@_);
184 =item replace OLD_RECORD
186 Replaces the OLD_RECORD with this one in the database. If there is an error,
187 returns the error, otherwise returns false.
189 Only printed may be changed. printed is normally updated by calling the
190 collect method of a customer object (see L<FS::cust_main>).
195 my( $new, $old ) = ( shift, shift );
196 return "Can't change custnum!" unless $old->custnum == $new->custnum;
197 #return "Can't change _date!" unless $old->_date eq $new->_date;
198 return "Can't change _date!" unless $old->_date == $new->_date;
199 return "Can't change charged!" unless $old->charged == $new->charged;
201 $new->SUPER::replace($old);
206 Checks all fields to make sure this is a valid invoice. If there is an error,
207 returns the error, otherwise returns false. Called by the insert and replace
216 $self->ut_numbern('invnum')
217 || $self->ut_number('custnum')
218 || $self->ut_numbern('_date')
219 || $self->ut_money('charged')
220 || $self->ut_numbern('printed')
221 || $self->ut_enum('closed', [ '', 'Y' ])
223 return $error if $error;
225 return "Unknown customer"
226 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
228 $self->_date(time) unless $self->_date;
230 $self->printed(0) if $self->printed eq '';
237 Returns a list consisting of the total previous balance for this customer,
238 followed by the previous outstanding invoices (as FS::cust_bill objects also).
245 my @cust_bill = sort { $a->_date <=> $b->_date }
246 grep { $_->owed != 0 && $_->_date < $self->_date }
247 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
249 foreach ( @cust_bill ) { $total += $_->owed; }
255 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
261 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
264 =item cust_bill_event
266 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
271 sub cust_bill_event {
273 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
279 Returns the customer (see L<FS::cust_main>) for this invoice.
285 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
290 Depreciated. See the cust_credited method.
292 #Returns a list consisting of the total previous credited (see
293 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
294 #outstanding credits (FS::cust_credit objects).
300 croak "FS::cust_bill->cust_credit depreciated; see ".
301 "FS::cust_bill->cust_credit_bill";
304 #my @cust_credit = sort { $a->_date <=> $b->_date }
305 # grep { $_->credited != 0 && $_->_date < $self->_date }
306 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
308 #foreach (@cust_credit) { $total += $_->credited; }
309 #$total, @cust_credit;
314 Depreciated. See the cust_bill_pay method.
316 #Returns all payments (see L<FS::cust_pay>) for this invoice.
322 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
324 #sort { $a->_date <=> $b->_date }
325 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
331 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
337 sort { $a->_date <=> $b->_date }
338 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
343 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
349 sort { $a->_date <=> $b->_date }
350 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
356 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
363 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
365 foreach (@taxlines) { $total += $_->setup; }
371 Returns the amount owed (still outstanding) on this invoice, which is charged
372 minus all payment applications (see L<FS::cust_bill_pay>) and credit
373 applications (see L<FS::cust_credit_bill>).
379 my $balance = $self->charged;
380 $balance -= $_->amount foreach ( $self->cust_bill_pay );
381 $balance -= $_->amount foreach ( $self->cust_credited );
382 $balance = sprintf( "%.2f", $balance);
383 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
387 =item send [ TEMPLATENAME [ , AGENTNUM ] ]
389 Sends this invoice to the destinations configured for this customer: send
390 emails or print. See L<FS::cust_main_invoice>.
392 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
394 AGENTNUM, if specified, means that this invoice will only be sent for customers
395 of the specified agent.
401 my $template = scalar(@_) ? shift : '';
402 return '' if scalar(@_) && $_[0] && $self->cust_main->agentnum ne shift;
404 my @print_text = $self->print_text('', $template);
405 my @invoicing_list = $self->cust_main->invoicing_list;
407 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
409 #better to notify this person than silence
410 @invoicing_list = ($invoice_from) unless @invoicing_list;
412 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
413 #$ENV{SMTPHOSTS} = $smtpmachine;
414 $ENV{MAILADDRESS} = $invoice_from;
415 my $header = new Mail::Header ( [
416 "From: $invoice_from",
417 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
418 "Sender: $invoice_from",
419 "Reply-To: $invoice_from",
420 "Date: ". time2str("%a, %d %b %Y %X %z", time),
423 my $message = new Mail::Internet (
425 'Body' => [ @print_text ], #( date)
428 $message->smtpsend( Host => $smtpmachine )
429 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
430 or die "(customer # ". $self->custnum. ") can't send invoice email".
431 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
432 " via server $smtpmachine with SMTP: $!\n";
436 if ( $conf->config('invoice_latex') ) {
437 @print_text = $self->print_ps('', $template);
440 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
442 or die "Can't open pipe to $lpr: $!\n";
443 print LPR @print_text;
445 or die $! ? "Error closing $lpr: $!\n"
446 : "Exit status $? from $lpr\n";
453 =item send_csv OPTIONS
455 Sends invoice as a CSV data-file to a remote host with the specified protocol.
459 protocol - currently only "ftp"
465 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
466 and YYMMDDHHMMSS is a timestamp.
468 The fields of the CSV file is as follows:
470 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
474 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
476 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
477 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
478 fields are filled in.
480 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
481 first two fields (B<record_type> and B<invnum>) and the last five fields
482 (B<pkg> through B<edate>) are filled in.
484 =item invnum - invoice number
486 =item custnum - customer number
488 =item _date - invoice date
490 =item charged - total invoice amount
492 =item first - customer first name
494 =item last - customer first name
496 =item company - company name
498 =item address1 - address line 1
500 =item address2 - address line 1
510 =item pkg - line item description
512 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
514 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
516 =item sdate - start date for recurring fee
518 =item edate - end date for recurring fee
525 my($self, %opt) = @_;
527 #part one: create file
529 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
530 mkdir $spooldir, 0700 unless -d $spooldir;
532 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
534 open(CSV, ">$file") or die "can't open $file: $!";
536 eval "use Text::CSV_XS";
539 my $csv = Text::CSV_XS->new({'always_quote'=>1});
541 my $cust_main = $self->cust_main;
547 time2str("%x", $self->_date),
548 sprintf("%.2f", $self->charged),
549 ( map { $cust_main->getfield($_) }
550 qw( first last company address1 address2 city state zip country ) ),
552 ) or die "can't create csv";
553 print CSV $csv->string. "\n";
555 #new charges (false laziness w/print_text)
556 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
558 my($pkg, $setup, $recur, $sdate, $edate);
559 if ( $cust_bill_pkg->pkgnum ) {
561 ($pkg, $setup, $recur, $sdate, $edate) = (
562 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
563 ( $cust_bill_pkg->setup != 0
564 ? sprintf("%.2f", $cust_bill_pkg->setup )
566 ( $cust_bill_pkg->recur != 0
567 ? sprintf("%.2f", $cust_bill_pkg->recur )
569 time2str("%x", $cust_bill_pkg->sdate),
570 time2str("%x", $cust_bill_pkg->edate),
574 next unless $cust_bill_pkg->setup != 0;
575 ($pkg, $setup, $recur, $sdate, $edate) =
576 ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
582 ( map { '' } (1..11) ),
583 ($pkg, $setup, $recur, $sdate, $edate)
584 ) or die "can't create csv";
585 print CSV $csv->string. "\n";
589 close CSV or die "can't close CSV: $!";
594 if ( $opt{protocol} eq 'ftp' ) {
595 eval "use Net::FTP;";
597 $net = Net::FTP->new($opt{server}) or die @$;
599 die "unknown protocol: $opt{protocol}";
602 $net->login( $opt{username}, $opt{password} )
603 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
605 $net->binary or die "can't set binary mode";
607 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
609 $net->put($file) or die "can't put $file: $!";
619 Pays this invoice with a compliemntary payment. If there is an error,
620 returns the error, otherwise returns false.
626 my $cust_pay = new FS::cust_pay ( {
627 'invnum' => $self->invnum,
628 'paid' => $self->owed,
631 'payinfo' => $self->cust_main->payinfo,
639 Attempts to pay this invoice with a credit card payment via a
640 Business::OnlinePayment realtime gateway. See
641 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
642 for supported processors.
661 Attempts to pay this invoice with an electronic check (ACH) payment via a
662 Business::OnlinePayment realtime gateway. See
663 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
664 for supported processors.
683 Attempts to pay this invoice with phone bill (LEC) payment via a
684 Business::OnlinePayment realtime gateway. See
685 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
686 for supported processors.
704 my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
706 #trim an extraneous blank line
707 pop @$options if scalar(@$options) % 2 && $options->[-1] =~ /^\s*$/;
709 my $cust_main = $self->cust_main;
710 my $balance = $cust_main->balance;
711 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
712 $amount = sprintf("%.2f", $amount);
713 return "not run (balance $balance)" unless $amount > 0;
715 my $address = $cust_main->address1;
716 $address .= ", ". $cust_main->address2 if $cust_main->address2;
718 my($payname, $payfirst, $paylast);
719 if ( $cust_main->payname && $method ne 'ECHECK' ) {
720 $payname = $cust_main->payname;
721 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
723 #$dbh->rollback if $oldAutoCommit;
724 return "Illegal payname $payname";
726 ($payfirst, $paylast) = ($1, $2);
728 $payfirst = $cust_main->getfield('first');
729 $paylast = $cust_main->getfield('last');
730 $payname = "$payfirst $paylast";
733 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
734 if ( $conf->exists('emailinvoiceauto')
735 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
736 push @invoicing_list, $cust_main->all_emails;
738 my $email = $invoicing_list[0];
740 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
742 my $description = 'Internet Services';
743 if ( $conf->exists('business-onlinepayment-description') ) {
744 my $dtempl = $conf->config('business-onlinepayment-description');
746 my $agent_obj = $cust_main->agent
747 or die "can't retreive agent for $cust_main (agentnum ".
748 $cust_main->agentnum. ")";
749 my $agent = $agent_obj->agent;
750 my $pkgs = join(', ',
751 map { $_->cust_pkg->part_pkg->pkg }
752 grep { $_->pkgnum } $self->cust_bill_pkg
754 $description = eval qq("$dtempl");
759 if ( $method eq 'CC' ) {
761 $content{card_number} = $cust_main->payinfo;
762 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
763 $content{expiration} = "$2/$1";
765 $content{cvv2} = $cust_main->paycvv
766 if defined $cust_main->dbdef_table->column('paycvv')
767 && length($cust_main->paycvv);
769 $content{recurring_billing} = 'YES'
770 if qsearch('cust_pay', { 'custnum' => $cust_main->custnum,
772 'payinfo' => $cust_main->payinfo, } );
774 } elsif ( $method eq 'ECHECK' ) {
775 my($account_number,$routing_code) = $cust_main->payinfo;
776 ( $content{account_number}, $content{routing_code} ) =
777 split('@', $cust_main->payinfo);
778 $content{bank_name} = $cust_main->payname;
779 $content{account_type} = 'CHECKING';
780 $content{account_name} = $payname;
781 $content{customer_org} = $self->company ? 'B' : 'I';
782 $content{customer_ssn} = $self->ss;
783 } elsif ( $method eq 'LEC' ) {
784 $content{phone} = $cust_main->payinfo;
788 new Business::OnlinePayment( $processor, @$options );
789 $transaction->content(
792 'password' => $password,
793 'action' => $action1,
794 'description' => $description,
796 'invoice_number' => $self->invnum,
797 'customer_id' => $self->custnum,
798 'last_name' => $paylast,
799 'first_name' => $payfirst,
801 'address' => $address,
802 'city' => $cust_main->city,
803 'state' => $cust_main->state,
804 'zip' => $cust_main->zip,
805 'country' => $cust_main->country,
806 'referer' => 'http://cleanwhisker.420.am/',
808 'phone' => $cust_main->daytime || $cust_main->night,
811 $transaction->submit();
813 if ( $transaction->is_success() && $action2 ) {
814 my $auth = $transaction->authorization;
815 my $ordernum = $transaction->can('order_number')
816 ? $transaction->order_number
819 #warn "********* $auth ***********\n";
820 #warn "********* $ordernum ***********\n";
822 new Business::OnlinePayment( $processor, @$options );
829 password => $password,
830 order_number => $ordernum,
832 authorization => $auth,
833 description => $description,
836 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
837 transaction_sequence_num local_transaction_date
838 local_transaction_time AVS_result_code )) {
839 $capture{$field} = $transaction->$field() if $transaction->can($field);
842 $capture->content( %capture );
846 unless ( $capture->is_success ) {
847 my $e = "Authorization sucessful but capture failed, invnum #".
848 $self->invnum. ': '. $capture->result_code.
849 ": ". $capture->error_message;
856 #remove paycvv after initial transaction
857 #make this disable-able via a config option if anyone insists?
858 # (though that probably violates cardholder agreements)
859 use Business::CreditCard;
860 if ( defined $cust_main->dbdef_table->column('paycvv')
861 && length($cust_main->paycvv)
862 && ! grep { $_ eq cardtype($cust_main->payinfo) } $conf->config('cvv-save')
865 my $new = new FS::cust_main { $cust_main->hash };
867 my $error = $new->replace($cust_main);
869 warn "error removing cvv: $error\n";
874 if ( $transaction->is_success() ) {
882 my $cust_pay = new FS::cust_pay ( {
883 'invnum' => $self->invnum,
886 'payby' => $method2payby{$method},
887 'payinfo' => $cust_main->payinfo,
888 'paybatch' => "$processor:". $transaction->authorization,
890 my $error = $cust_pay->insert;
892 $cust_pay->invnum(''); #try again with no specific invnum
893 my $error2 = $cust_pay->insert;
895 # gah, even with transactions.
896 my $e = 'WARNING: Card/ACH debited but database not updated - '.
897 "error inserting payment ($processor): $error2".
898 ' (previously tried insert with invnum #' . $self->invnum.
906 #} elsif ( $options{'report_badcard'} ) {
909 my $perror = "$processor error, invnum #". $self->invnum. ': '.
910 $transaction->result_code. ": ". $transaction->error_message;
912 if ( !$realtime_bop_decline_quiet && $conf->exists('emaildecline')
913 && grep { $_ ne 'POST' } $cust_main->invoicing_list
914 && ! grep { $transaction->error_message =~ /$_/ }
915 $conf->config('emaildecline-exclude')
917 my @templ = $conf->config('declinetemplate');
918 my $template = new Text::Template (
920 SOURCE => [ map "$_\n", @templ ],
921 ) or return "($perror) can't create template: $Text::Template::ERROR";
923 or return "($perror) can't compile template: $Text::Template::ERROR";
925 my $templ_hash = { error => $transaction->error_message };
927 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
928 $ENV{MAILADDRESS} = $invoice_from;
929 my $header = new Mail::Header ( [
930 "From: $invoice_from",
931 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
932 "Sender: $invoice_from",
933 "Reply-To: $invoice_from",
934 "Date: ". time2str("%a, %d %b %Y %X %z", time),
935 "Subject: Your payment could not be processed",
937 my $message = new Mail::Internet (
939 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
942 $message->smtpsend( Host => $smtpmachine )
943 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
944 or return "($perror) (customer # ". $self->custnum.
945 ") can't send card decline email to ".
946 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
947 " via server $smtpmachine with SMTP: $!";
955 =item realtime_card_cybercash
957 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
961 sub realtime_card_cybercash {
963 my $cust_main = $self->cust_main;
964 my $amount = $self->owed;
966 return "CyberCash CashRegister real-time card processing not enabled!"
967 unless $cybercash eq 'cybercash3.2';
969 my $address = $cust_main->address1;
970 $address .= ", ". $cust_main->address2 if $cust_main->address2;
973 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
974 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
979 my $paybatch = $self->invnum.
980 '-' . time2str("%y%m%d%H%M%S", time);
982 my $payname = $cust_main->payname ||
983 $cust_main->getfield('first').' '.$cust_main->getfield('last');
985 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
987 my @full_xaction = ( $xaction,
988 'Order-ID' => $paybatch,
989 'Amount' => "usd $amount",
990 'Card-Number' => $cust_main->getfield('payinfo'),
991 'Card-Name' => $payname,
992 'Card-Address' => $address,
993 'Card-City' => $cust_main->getfield('city'),
994 'Card-State' => $cust_main->getfield('state'),
995 'Card-Zip' => $cust_main->getfield('zip'),
996 'Card-Country' => $country,
1001 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
1003 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
1004 my $cust_pay = new FS::cust_pay ( {
1005 'invnum' => $self->invnum,
1009 'payinfo' => $cust_main->payinfo,
1010 'paybatch' => "$cybercash:$paybatch",
1012 my $error = $cust_pay->insert;
1014 # gah, even with transactions.
1015 my $e = 'WARNING: Card debited but database not updated - '.
1016 'error applying payment, invnum #' . $self->invnum.
1017 " (CyberCash Order-ID $paybatch): $error";
1023 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1024 # || $options{'report_badcard'}
1027 return 'Cybercash error, invnum #' .
1028 $self->invnum. ':'. $result{'MErrMsg'};
1035 Adds a payment for this invoice to the pending credit card batch (see
1036 L<FS::cust_pay_batch>).
1042 my $cust_main = $self->cust_main;
1044 my $cust_pay_batch = new FS::cust_pay_batch ( {
1045 'invnum' => $self->getfield('invnum'),
1046 'custnum' => $cust_main->getfield('custnum'),
1047 'last' => $cust_main->getfield('last'),
1048 'first' => $cust_main->getfield('first'),
1049 'address1' => $cust_main->getfield('address1'),
1050 'address2' => $cust_main->getfield('address2'),
1051 'city' => $cust_main->getfield('city'),
1052 'state' => $cust_main->getfield('state'),
1053 'zip' => $cust_main->getfield('zip'),
1054 'country' => $cust_main->getfield('country'),
1055 'cardnum' => $cust_main->getfield('payinfo'),
1056 'exp' => $cust_main->getfield('paydate'),
1057 'payname' => $cust_main->getfield('payname'),
1058 'amount' => $self->owed,
1060 my $error = $cust_pay_batch->insert;
1061 die $error if $error;
1066 sub _agent_template {
1069 my $cust_bill_event = qsearchs( 'part_bill_event',
1071 'payby' => $self->cust_main->payby,
1072 'plan' => 'send_agent',
1073 'eventcode' => { 'op' => 'LIKE',
1074 'value' => '_%, '. $self->cust_main->agentnum. ');' },
1077 'ORDER BY seconds LIMIT 1'
1080 return '' unless $cust_bill_event;
1082 if ( $cust_bill_event->eventcode =~ /\(\s*'(.*)'\s*,\s*(\d+)\s*\)\;$/ ) {
1085 warn "can't parse eventcode for agent-specific invoice template";
1091 =item print_text [ TIME [ , TEMPLATE ] ]
1093 Returns an text invoice, as a list of lines.
1095 TIME an optional value used to control the printing of overdue messages. The
1096 default is now. It isn't the date of the invoice; that's the `_date' field.
1097 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1098 L<Time::Local> and L<Date::Parse> for conversion functions.
1104 my( $self, $today, $template ) = @_;
1106 # my $invnum = $self->invnum;
1107 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
1108 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1109 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1111 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1112 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1113 #my $balance_due = $self->owed + $pr_total - $cr_total;
1114 my $balance_due = $self->owed + $pr_total;
1117 #my($description,$amount);
1121 foreach ( @pr_cust_bill ) {
1123 "Previous Balance, Invoice #". $_->invnum.
1124 " (". time2str("%x",$_->_date). ")",
1125 $money_char. sprintf("%10.2f",$_->owed)
1128 if (@pr_cust_bill) {
1129 push @buf,['','-----------'];
1130 push @buf,[ 'Total Previous Balance',
1131 $money_char. sprintf("%10.2f",$pr_total ) ];
1136 foreach my $cust_bill_pkg (
1137 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1138 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1141 if ( $cust_bill_pkg->pkgnum ) {
1143 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1144 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1145 my $pkg = $part_pkg->pkg;
1147 if ( $cust_bill_pkg->setup != 0 ) {
1148 my $description = $pkg;
1149 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1150 push @buf, [ $description,
1151 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1153 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1156 if ( $cust_bill_pkg->recur != 0 ) {
1158 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1159 time2str("%x", $cust_bill_pkg->edate) . ")",
1160 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1163 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1166 } else { #pkgnum tax or one-shot line item
1167 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1168 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1170 if ( $cust_bill_pkg->setup != 0 ) {
1171 push @buf, [ $itemdesc,
1172 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1174 if ( $cust_bill_pkg->recur != 0 ) {
1175 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1176 . time2str("%x", $cust_bill_pkg->edate). ")",
1177 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1183 push @buf,['','-----------'];
1184 push @buf,['Total New Charges',
1185 $money_char. sprintf("%10.2f",$self->charged) ];
1188 push @buf,['','-----------'];
1189 push @buf,['Total Charges',
1190 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1194 foreach ( $self->cust_credited ) {
1196 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1198 my $reason = substr($_->cust_credit->reason,0,32);
1199 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1200 $reason = " ($reason) " if $reason;
1202 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1204 $money_char. sprintf("%10.2f",$_->amount)
1207 #foreach ( @cr_cust_credit ) {
1209 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1210 # $money_char. sprintf("%10.2f",$_->credited)
1214 #get & print payments
1215 foreach ( $self->cust_bill_pay ) {
1217 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1220 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1221 $money_char. sprintf("%10.2f",$_->amount )
1226 my $balance_due_msg = $self->balance_due_msg;
1228 push @buf,['','-----------'];
1229 push @buf,[$balance_due_msg, $money_char.
1230 sprintf("%10.2f", $balance_due ) ];
1232 #create the template
1233 $template ||= $self->_agent_template;
1234 my $templatefile = 'invoice_template';
1235 $templatefile .= "_$template" if length($template);
1236 my @invoice_template = $conf->config($templatefile)
1237 or die "cannot load config file $templatefile";
1240 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1241 /invoice_lines\((\d*)\)/;
1242 $invoice_lines += $1 || scalar(@buf);
1245 die "no invoice_lines() functions in template?" unless $wasfunc;
1246 my $invoice_template = new Text::Template (
1248 SOURCE => [ map "$_\n", @invoice_template ],
1249 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1250 $invoice_template->compile()
1251 or die "can't compile template: $Text::Template::ERROR";
1253 #setup template variables
1254 package FS::cust_bill::_template; #!
1255 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1257 $invnum = $self->invnum;
1258 $date = $self->_date;
1260 $agent = $self->cust_main->agent->agent;
1262 if ( $FS::cust_bill::invoice_lines ) {
1264 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1266 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1271 #format address (variable for the template)
1273 @address = ( '', '', '', '', '', '' );
1274 package FS::cust_bill; #!
1275 $FS::cust_bill::_template::address[$l++] =
1276 $cust_main->payname.
1277 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1278 ? " (P.O. #". $cust_main->payinfo. ")"
1282 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1283 if $cust_main->company;
1284 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1285 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1286 if $cust_main->address2;
1287 $FS::cust_bill::_template::address[$l++] =
1288 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1289 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1290 unless $cust_main->country eq 'US';
1292 # #overdue? (variable for the template)
1293 # $FS::cust_bill::_template::overdue = (
1295 # && $today > $self->_date
1296 ## && $self->printed > 1
1297 # && $self->printed > 0
1300 #and subroutine for the template
1301 sub FS::cust_bill::_template::invoice_lines {
1302 my $lines = shift || scalar(@buf);
1304 scalar(@buf) ? shift @buf : [ '', '' ];
1310 $FS::cust_bill::_template::page = 1;
1314 push @collect, split("\n",
1315 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1317 $FS::cust_bill::_template::page++;
1320 map "$_\n", @collect;
1324 =item print_latex [ TIME [ , TEMPLATE ] ]
1326 Internal method - returns a filename of a filled-in LaTeX template for this
1327 invoice (Note: add ".tex" to get the actual filename).
1329 See print_ps and print_pdf for methods that return PostScript and PDF output.
1331 TIME an optional value used to control the printing of overdue messages. The
1332 default is now. It isn't the date of the invoice; that's the `_date' field.
1333 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1334 L<Time::Local> and L<Date::Parse> for conversion functions.
1338 #still some false laziness w/print_text
1341 my( $self, $today, $template ) = @_;
1344 # my $invnum = $self->invnum;
1345 my $cust_main = $self->cust_main;
1346 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1347 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1349 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1350 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1351 #my $balance_due = $self->owed + $pr_total - $cr_total;
1352 my $balance_due = $self->owed + $pr_total;
1355 #my($description,$amount);
1358 #create the template
1359 $template ||= $self->_agent_template;
1360 my $templatefile = 'invoice_latex';
1361 my $suffix = length($template) ? "_$template" : '';
1362 $templatefile .= $suffix;
1363 my @invoice_template = $conf->config($templatefile)
1364 or die "cannot load config file $templatefile";
1366 my %invoice_data = (
1367 'invnum' => $self->invnum,
1368 'date' => time2str('%b %o, %Y', $self->_date),
1369 'agent' => _latex_escape($cust_main->agent->agent),
1370 'payname' => _latex_escape($cust_main->payname),
1371 'company' => _latex_escape($cust_main->company),
1372 'address1' => _latex_escape($cust_main->address1),
1373 'address2' => _latex_escape($cust_main->address2),
1374 'city' => _latex_escape($cust_main->city),
1375 'state' => _latex_escape($cust_main->state),
1376 'zip' => _latex_escape($cust_main->zip),
1377 'country' => _latex_escape($cust_main->country),
1378 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1379 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1381 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1382 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1385 my $countrydefault = $conf->config('countrydefault') || 'US';
1386 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1388 #do variable substitutions in notes
1389 $invoice_data{'notes'} =
1391 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1392 $conf->config_orbase('invoice_latexnotes', $suffix)
1395 $invoice_data{'footer'} =~ s/\n+$//;
1396 $invoice_data{'smallfooter'} =~ s/\n+$//;
1397 $invoice_data{'notes'} =~ s/\n+$//;
1399 $invoice_data{'po_line'} =
1400 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1401 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1405 my @total_item = ();
1407 while ( @invoice_template ) {
1408 my $line = shift @invoice_template;
1410 if ( $line =~ /^%%Detail\s*$/ ) {
1412 while ( ( my $line_item_line = shift @invoice_template )
1413 !~ /^%%EndDetail\s*$/ ) {
1414 push @line_item, $line_item_line;
1416 foreach my $line_item ( $self->_items ) {
1417 #foreach my $line_item ( $self->_items_pkg ) {
1418 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1419 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1420 if ( exists $line_item->{'ext_description'} ) {
1421 $invoice_data{'description'} .=
1422 "\\tabularnewline\n~~".
1423 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1425 $invoice_data{'amount'} = $line_item->{'amount'};
1426 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1428 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1431 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1433 while ( ( my $total_item_line = shift @invoice_template )
1434 !~ /^%%EndTotalDetails\s*$/ ) {
1435 push @total_item, $total_item_line;
1438 my @total_fill = ();
1441 foreach my $tax ( $self->_items_tax ) {
1442 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1443 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1445 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1450 $invoice_data{'total_item'} = 'Sub-total';
1451 $invoice_data{'total_amount'} =
1452 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1453 unshift @total_fill,
1454 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1458 $invoice_data{'total_item'} = '\textbf{Total}';
1459 $invoice_data{'total_amount'} =
1460 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1462 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1465 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1468 foreach my $credit ( $self->_items_credits ) {
1469 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1471 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1473 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1478 foreach my $payment ( $self->_items_payments ) {
1479 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1481 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1483 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1487 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1488 $invoice_data{'total_amount'} =
1489 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1491 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1494 push @filled_in, @total_fill;
1497 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1498 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1499 push @filled_in, $line;
1510 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1511 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1515 ) or die "can't open temp file: $!\n";
1516 print $fh join("\n", @filled_in ), "\n";
1519 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1524 =item print_ps [ TIME [ , TEMPLATE ] ]
1526 Returns an postscript invoice, as a scalar.
1528 TIME an optional value used to control the printing of overdue messages. The
1529 default is now. It isn't the date of the invoice; that's the `_date' field.
1530 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1531 L<Time::Local> and L<Date::Parse> for conversion functions.
1538 my $file = $self->print_latex(@_);
1540 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1543 my $sfile = shell_quote $file;
1545 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1546 or die "pslatex $file.tex failed: $!";
1547 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1548 or die "pslatex $file.tex failed: $!";
1550 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1551 or die "dvips failed: $!";
1553 open(POSTSCRIPT, "<$file.ps")
1554 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1556 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1559 while (<POSTSCRIPT>) {
1569 =item print_pdf [ TIME [ , TEMPLATE ] ]
1571 Returns an PDF invoice, as a scalar.
1573 TIME an optional value used to control the printing of overdue messages. The
1574 default is now. It isn't the date of the invoice; that's the `_date' field.
1575 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1576 L<Time::Local> and L<Date::Parse> for conversion functions.
1583 my $file = $self->print_latex(@_);
1585 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1588 #system('pdflatex', "$file.tex");
1589 #system('pdflatex', "$file.tex");
1590 #! LaTeX Error: Unknown graphics extension: .eps.
1592 my $sfile = shell_quote $file;
1594 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1595 or die "pslatex $file.tex failed: $!";
1596 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1597 or die "pslatex $file.tex failed: $!";
1599 #system('dvipdf', "$file.dvi", "$file.pdf" );
1601 "dvips -q -t letter -f $sfile.dvi ".
1602 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1605 or die "dvips | gs failed: $!";
1607 open(PDF, "<$file.pdf")
1608 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1610 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1623 # quick subroutine for print_latex
1625 # There are ten characters that LaTeX treats as special characters, which
1626 # means that they do not simply typeset themselves:
1627 # # $ % & ~ _ ^ \ { }
1629 # TeX ignores blanks following an escaped character; if you want a blank (as
1630 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1634 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1638 #utility methods for print_*
1640 sub balance_due_msg {
1642 my $msg = 'Balance Due';
1643 return $msg unless $conf->exists('invoice_default_terms');
1644 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1645 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1646 } elsif ( $conf->config('invoice_default_terms') ) {
1647 $msg .= ' - '. $conf->config('invoice_default_terms');
1654 my @display = scalar(@_)
1656 : qw( _items_previous _items_pkg );
1657 #: qw( _items_pkg );
1658 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1660 foreach my $display ( @display ) {
1661 push @b, $self->$display(@_);
1666 sub _items_previous {
1668 my $cust_main = $self->cust_main;
1669 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1671 foreach ( @pr_cust_bill ) {
1673 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1674 ' ('. time2str('%x',$_->_date). ')',
1675 #'pkgpart' => 'N/A',
1677 'amount' => sprintf("%10.2f", $_->owed),
1683 # 'description' => 'Previous Balance',
1684 # #'pkgpart' => 'N/A',
1685 # 'pkgnum' => 'N/A',
1686 # 'amount' => sprintf("%10.2f", $pr_total ),
1687 # 'ext_description' => [ map {
1688 # "Invoice ". $_->invnum.
1689 # " (". time2str("%x",$_->_date). ") ".
1690 # sprintf("%10.2f", $_->owed)
1691 # } @pr_cust_bill ],
1698 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1699 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1704 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1705 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1708 sub _items_cust_bill_pkg {
1710 my $cust_bill_pkg = shift;
1713 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1715 if ( $cust_bill_pkg->pkgnum ) {
1717 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1718 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1719 my $pkg = $part_pkg->pkg;
1722 #tie %labels, 'Tie::IxHash';
1723 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1724 my @ext_description;
1725 foreach my $label ( keys %labels ) {
1726 my @values = @{ $labels{$label} };
1727 my $num = scalar(@values);
1729 push @ext_description, "$label ($num)";
1731 push @ext_description, map { "$label: $_" } @values;
1735 if ( $cust_bill_pkg->setup != 0 ) {
1736 my $description = $pkg;
1737 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1738 my @d = @ext_description;
1739 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1741 'description' => $description,
1742 #'pkgpart' => $part_pkg->pkgpart,
1743 'pkgnum' => $cust_pkg->pkgnum,
1744 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1745 'ext_description' => \@d,
1749 if ( $cust_bill_pkg->recur != 0 ) {
1751 'description' => "$pkg (" .
1752 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1753 time2str('%x', $cust_bill_pkg->edate). ')',
1754 #'pkgpart' => $part_pkg->pkgpart,
1755 'pkgnum' => $cust_pkg->pkgnum,
1756 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1757 'ext_description' => [ @ext_description,
1758 $cust_bill_pkg->details,
1763 } else { #pkgnum tax or one-shot line item (??)
1765 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1766 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1768 if ( $cust_bill_pkg->setup != 0 ) {
1770 'description' => $itemdesc,
1771 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1774 if ( $cust_bill_pkg->recur != 0 ) {
1776 'description' => "$itemdesc (".
1777 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1778 time2str("%x", $cust_bill_pkg->edate). ')',
1779 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1791 sub _items_credits {
1796 foreach ( $self->cust_credited ) {
1798 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1800 my $reason = $_->cust_credit->reason;
1801 #my $reason = substr($_->cust_credit->reason,0,32);
1802 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1803 $reason = " ($reason) " if $reason;
1805 #'description' => 'Credit ref\#'. $_->crednum.
1806 # " (". time2str("%x",$_->cust_credit->_date) .")".
1808 'description' => 'Credit applied '.
1809 time2str("%x",$_->cust_credit->_date). $reason,
1810 'amount' => sprintf("%10.2f",$_->amount),
1813 #foreach ( @cr_cust_credit ) {
1815 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1816 # $money_char. sprintf("%10.2f",$_->credited)
1824 sub _items_payments {
1828 #get & print payments
1829 foreach ( $self->cust_bill_pay ) {
1831 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1834 'description' => "Payment received ".
1835 time2str("%x",$_->cust_pay->_date ),
1836 'amount' => sprintf("%10.2f", $_->amount )
1850 print_text formatting (and some logic :/) is in source, but needs to be
1851 slurped in from a file. Also number of lines ($=).
1855 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1856 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base