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
385 =item send [ TEMPLATENAME [ , AGENTNUM ] ]
387 Sends this invoice to the destinations configured for this customer: send
388 emails or print. See L<FS::cust_main_invoice>.
390 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
392 AGENTNUM, if specified, means that this invoice will only be sent for customers
393 of the specified agent.
399 my $template = scalar(@_) ? shift : '';
400 return '' if scalar(@_) && $_[0] && $self->cust_main->agentnum ne shift;
402 my @print_text = $self->print_text('', $template);
403 my @invoicing_list = $self->cust_main->invoicing_list;
405 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
407 #better to notify this person than silence
408 @invoicing_list = ($invoice_from) unless @invoicing_list;
410 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
411 #$ENV{SMTPHOSTS} = $smtpmachine;
412 $ENV{MAILADDRESS} = $invoice_from;
413 my $header = new Mail::Header ( [
414 "From: $invoice_from",
415 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
416 "Sender: $invoice_from",
417 "Reply-To: $invoice_from",
418 "Date: ". time2str("%a, %d %b %Y %X %z", time),
421 my $message = new Mail::Internet (
423 'Body' => [ @print_text ], #( date)
426 $message->smtpsend( Host => $smtpmachine )
427 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
428 or die "(customer # ". $self->custnum. ") can't send invoice email".
429 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
430 " via server $smtpmachine with SMTP: $!\n";
434 if ( $conf->config('invoice_latex') ) {
435 @print_text = $self->print_ps('', $template);
438 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
440 or die "Can't open pipe to $lpr: $!\n";
441 print LPR @print_text;
443 or die $! ? "Error closing $lpr: $!\n"
444 : "Exit status $? from $lpr\n";
451 =item send_csv OPTIONS
453 Sends invoice as a CSV data-file to a remote host with the specified protocol.
457 protocol - currently only "ftp"
463 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
464 and YYMMDDHHMMSS is a timestamp.
466 The fields of the CSV file is as follows:
468 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
472 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
474 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
475 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
476 fields are filled in.
478 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
479 first two fields (B<record_type> and B<invnum>) and the last five fields
480 (B<pkg> through B<edate>) are filled in.
482 =item invnum - invoice number
484 =item custnum - customer number
486 =item _date - invoice date
488 =item charged - total invoice amount
490 =item first - customer first name
492 =item last - customer first name
494 =item company - company name
496 =item address1 - address line 1
498 =item address2 - address line 1
508 =item pkg - line item description
510 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
512 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
514 =item sdate - start date for recurring fee
516 =item edate - end date for recurring fee
523 my($self, %opt) = @_;
525 #part one: create file
527 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
528 mkdir $spooldir, 0700 unless -d $spooldir;
530 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
532 open(CSV, ">$file") or die "can't open $file: $!";
534 eval "use Text::CSV_XS";
537 my $csv = Text::CSV_XS->new({'always_quote'=>1});
539 my $cust_main = $self->cust_main;
545 time2str("%x", $self->_date),
546 sprintf("%.2f", $self->charged),
547 ( map { $cust_main->getfield($_) }
548 qw( first last company address1 address2 city state zip country ) ),
550 ) or die "can't create csv";
551 print CSV $csv->string. "\n";
553 #new charges (false laziness w/print_text)
554 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
556 my($pkg, $setup, $recur, $sdate, $edate);
557 if ( $cust_bill_pkg->pkgnum ) {
559 ($pkg, $setup, $recur, $sdate, $edate) = (
560 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
561 ( $cust_bill_pkg->setup != 0
562 ? sprintf("%.2f", $cust_bill_pkg->setup )
564 ( $cust_bill_pkg->recur != 0
565 ? sprintf("%.2f", $cust_bill_pkg->recur )
567 time2str("%x", $cust_bill_pkg->sdate),
568 time2str("%x", $cust_bill_pkg->edate),
572 next unless $cust_bill_pkg->setup != 0;
573 ($pkg, $setup, $recur, $sdate, $edate) =
574 ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
580 ( map { '' } (1..11) ),
581 ($pkg, $setup, $recur, $sdate, $edate)
582 ) or die "can't create csv";
583 print CSV $csv->string. "\n";
587 close CSV or die "can't close CSV: $!";
592 if ( $opt{protocol} eq 'ftp' ) {
593 eval "use Net::FTP;";
595 $net = Net::FTP->new($opt{server}) or die @$;
597 die "unknown protocol: $opt{protocol}";
600 $net->login( $opt{username}, $opt{password} )
601 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
603 $net->binary or die "can't set binary mode";
605 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
607 $net->put($file) or die "can't put $file: $!";
617 Pays this invoice with a compliemntary payment. If there is an error,
618 returns the error, otherwise returns false.
624 my $cust_pay = new FS::cust_pay ( {
625 'invnum' => $self->invnum,
626 'paid' => $self->owed,
629 'payinfo' => $self->cust_main->payinfo,
637 Attempts to pay this invoice with a credit card payment via a
638 Business::OnlinePayment realtime gateway. See
639 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
640 for supported processors.
659 Attempts to pay this invoice with an electronic check (ACH) payment via a
660 Business::OnlinePayment realtime gateway. See
661 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
662 for supported processors.
681 Attempts to pay this invoice with phone bill (LEC) payment via a
682 Business::OnlinePayment realtime gateway. See
683 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
684 for supported processors.
702 my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
704 #trim an extraneous blank line
705 pop @$options if scalar(@$options) % 2 && $options->[-1] =~ /^\s*$/;
707 my $cust_main = $self->cust_main;
708 my $balance = $cust_main->balance;
709 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
710 $amount = sprintf("%.2f", $amount);
711 return "not run (balance $balance)" unless $amount > 0;
713 my $address = $cust_main->address1;
714 $address .= ", ". $cust_main->address2 if $cust_main->address2;
716 my($payname, $payfirst, $paylast);
717 if ( $cust_main->payname && $method ne 'ECHECK' ) {
718 $payname = $cust_main->payname;
719 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
721 #$dbh->rollback if $oldAutoCommit;
722 return "Illegal payname $payname";
724 ($payfirst, $paylast) = ($1, $2);
726 $payfirst = $cust_main->getfield('first');
727 $paylast = $cust_main->getfield('last');
728 $payname = "$payfirst $paylast";
731 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
732 if ( $conf->exists('emailinvoiceauto')
733 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
734 push @invoicing_list, $cust_main->all_emails;
736 my $email = $invoicing_list[0];
738 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
740 my $description = 'Internet Services';
741 if ( $conf->exists('business-onlinepayment-description') ) {
742 my $dtempl = $conf->config('business-onlinepayment-description');
744 my $agent_obj = $cust_main->agent
745 or die "can't retreive agent for $cust_main (agentnum ".
746 $cust_main->agentnum. ")";
747 my $agent = $agent_obj->agent;
748 my $pkgs = join(', ',
749 map { $_->cust_pkg->part_pkg->pkg }
750 grep { $_->pkgnum } $self->cust_bill_pkg
752 $description = eval qq("$dtempl");
757 if ( $method eq 'CC' ) {
759 $content{card_number} = $cust_main->payinfo;
760 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
761 $content{expiration} = "$2/$1";
763 $content{cvv2} = $cust_main->paycvv
764 if defined $cust_main->dbdef_table->column('paycvv')
765 && length($cust_main->paycvv);
767 $content{recurring_billing} = 'YES'
768 if qsearch('cust_pay', { 'custnum' => $cust_main->custnum,
770 'payinfo' => $cust_main->payinfo, } );
772 } elsif ( $method eq 'ECHECK' ) {
773 my($account_number,$routing_code) = $cust_main->payinfo;
774 ( $content{account_number}, $content{routing_code} ) =
775 split('@', $cust_main->payinfo);
776 $content{bank_name} = $cust_main->payname;
777 $content{account_type} = 'CHECKING';
778 $content{account_name} = $payname;
779 $content{customer_org} = $self->company ? 'B' : 'I';
780 $content{customer_ssn} = $self->ss;
781 } elsif ( $method eq 'LEC' ) {
782 $content{phone} = $cust_main->payinfo;
786 new Business::OnlinePayment( $processor, @$options );
787 $transaction->content(
790 'password' => $password,
791 'action' => $action1,
792 'description' => $description,
794 'invoice_number' => $self->invnum,
795 'customer_id' => $self->custnum,
796 'last_name' => $paylast,
797 'first_name' => $payfirst,
799 'address' => $address,
800 'city' => $cust_main->city,
801 'state' => $cust_main->state,
802 'zip' => $cust_main->zip,
803 'country' => $cust_main->country,
804 'referer' => 'http://cleanwhisker.420.am/',
806 'phone' => $cust_main->daytime || $cust_main->night,
809 $transaction->submit();
811 if ( $transaction->is_success() && $action2 ) {
812 my $auth = $transaction->authorization;
813 my $ordernum = $transaction->can('order_number')
814 ? $transaction->order_number
817 #warn "********* $auth ***********\n";
818 #warn "********* $ordernum ***********\n";
820 new Business::OnlinePayment( $processor, @$options );
827 password => $password,
828 order_number => $ordernum,
830 authorization => $auth,
831 description => $description,
834 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
835 transaction_sequence_num local_transaction_date
836 local_transaction_time AVS_result_code )) {
837 $capture{$field} = $transaction->$field() if $transaction->can($field);
840 $capture->content( %capture );
844 unless ( $capture->is_success ) {
845 my $e = "Authorization sucessful but capture failed, invnum #".
846 $self->invnum. ': '. $capture->result_code.
847 ": ". $capture->error_message;
854 #remove paycvv after initial transaction
855 #make this disable-able via a config option if anyone insists?
856 # (though that probably violates cardholder agreements)
857 use Business::CreditCard;
858 if ( defined $cust_main->dbdef_table->column('paycvv')
859 && length($cust_main->paycvv)
860 && ! grep { $_ eq cardtype($cust_main->payinfo) } $conf->config('cvv-save')
863 my $new = new FS::cust_main { $cust_main->hash };
865 my $error = $new->replace($cust_main);
867 warn "error removing cvv: $error\n";
872 if ( $transaction->is_success() ) {
880 my $cust_pay = new FS::cust_pay ( {
881 'invnum' => $self->invnum,
884 'payby' => $method2payby{$method},
885 'payinfo' => $cust_main->payinfo,
886 'paybatch' => "$processor:". $transaction->authorization,
888 my $error = $cust_pay->insert;
890 $cust_pay->invnum(''); #try again with no specific invnum
891 my $error2 = $cust_pay->insert;
893 # gah, even with transactions.
894 my $e = 'WARNING: Card/ACH debited but database not updated - '.
895 "error inserting payment ($processor): $error2".
896 ' (previously tried insert with invnum #' . $self->invnum.
904 #} elsif ( $options{'report_badcard'} ) {
907 my $perror = "$processor error, invnum #". $self->invnum. ': '.
908 $transaction->result_code. ": ". $transaction->error_message;
910 if ( !$realtime_bop_decline_quiet && $conf->exists('emaildecline')
911 && grep { $_ ne 'POST' } $cust_main->invoicing_list
912 && ! grep { $transaction->error_message =~ /$_/ }
913 $conf->config('emaildecline-exclude')
915 my @templ = $conf->config('declinetemplate');
916 my $template = new Text::Template (
918 SOURCE => [ map "$_\n", @templ ],
919 ) or return "($perror) can't create template: $Text::Template::ERROR";
921 or return "($perror) can't compile template: $Text::Template::ERROR";
923 my $templ_hash = { error => $transaction->error_message };
925 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
926 $ENV{MAILADDRESS} = $invoice_from;
927 my $header = new Mail::Header ( [
928 "From: $invoice_from",
929 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
930 "Sender: $invoice_from",
931 "Reply-To: $invoice_from",
932 "Date: ". time2str("%a, %d %b %Y %X %z", time),
933 "Subject: Your payment could not be processed",
935 my $message = new Mail::Internet (
937 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
940 $message->smtpsend( Host => $smtpmachine )
941 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
942 or return "($perror) (customer # ". $self->custnum.
943 ") can't send card decline email to ".
944 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
945 " via server $smtpmachine with SMTP: $!";
953 =item realtime_card_cybercash
955 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
959 sub realtime_card_cybercash {
961 my $cust_main = $self->cust_main;
962 my $amount = $self->owed;
964 return "CyberCash CashRegister real-time card processing not enabled!"
965 unless $cybercash eq 'cybercash3.2';
967 my $address = $cust_main->address1;
968 $address .= ", ". $cust_main->address2 if $cust_main->address2;
971 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
972 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
977 my $paybatch = $self->invnum.
978 '-' . time2str("%y%m%d%H%M%S", time);
980 my $payname = $cust_main->payname ||
981 $cust_main->getfield('first').' '.$cust_main->getfield('last');
983 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
985 my @full_xaction = ( $xaction,
986 'Order-ID' => $paybatch,
987 'Amount' => "usd $amount",
988 'Card-Number' => $cust_main->getfield('payinfo'),
989 'Card-Name' => $payname,
990 'Card-Address' => $address,
991 'Card-City' => $cust_main->getfield('city'),
992 'Card-State' => $cust_main->getfield('state'),
993 'Card-Zip' => $cust_main->getfield('zip'),
994 'Card-Country' => $country,
999 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
1001 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
1002 my $cust_pay = new FS::cust_pay ( {
1003 'invnum' => $self->invnum,
1007 'payinfo' => $cust_main->payinfo,
1008 'paybatch' => "$cybercash:$paybatch",
1010 my $error = $cust_pay->insert;
1012 # gah, even with transactions.
1013 my $e = 'WARNING: Card debited but database not updated - '.
1014 'error applying payment, invnum #' . $self->invnum.
1015 " (CyberCash Order-ID $paybatch): $error";
1021 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1022 # || $options{'report_badcard'}
1025 return 'Cybercash error, invnum #' .
1026 $self->invnum. ':'. $result{'MErrMsg'};
1033 Adds a payment for this invoice to the pending credit card batch (see
1034 L<FS::cust_pay_batch>).
1040 my $cust_main = $self->cust_main;
1042 my $cust_pay_batch = new FS::cust_pay_batch ( {
1043 'invnum' => $self->getfield('invnum'),
1044 'custnum' => $cust_main->getfield('custnum'),
1045 'last' => $cust_main->getfield('last'),
1046 'first' => $cust_main->getfield('first'),
1047 'address1' => $cust_main->getfield('address1'),
1048 'address2' => $cust_main->getfield('address2'),
1049 'city' => $cust_main->getfield('city'),
1050 'state' => $cust_main->getfield('state'),
1051 'zip' => $cust_main->getfield('zip'),
1052 'country' => $cust_main->getfield('country'),
1053 'cardnum' => $cust_main->getfield('payinfo'),
1054 'exp' => $cust_main->getfield('paydate'),
1055 'payname' => $cust_main->getfield('payname'),
1056 'amount' => $self->owed,
1058 my $error = $cust_pay_batch->insert;
1059 die $error if $error;
1064 sub _agent_template {
1067 my $cust_bill_event = qsearchs( 'part_bill_event',
1069 'payby' => $self->cust_main->payby,
1070 'plan' => 'send_agent',
1071 'eventcode' => { 'op' => 'LIKE',
1072 'value' => '_%, '. $self->cust_main->agentnum. ');' },
1075 'ORDER BY seconds LIMIT 1'
1078 return '' unless $cust_bill_event;
1080 if ( $cust_bill_event->eventcode =~ /\(\s*'(.*)'\s*,\s*(\d+)\s*\)\;$/ ) {
1083 warn "can't parse eventcode for agent-specific invoice template";
1089 =item print_text [ TIME [ , TEMPLATE ] ]
1091 Returns an text invoice, as a list of lines.
1093 TIME an optional value used to control the printing of overdue messages. The
1094 default is now. It isn't the date of the invoice; that's the `_date' field.
1095 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1096 L<Time::Local> and L<Date::Parse> for conversion functions.
1102 my( $self, $today, $template ) = @_;
1104 # my $invnum = $self->invnum;
1105 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
1106 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1107 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1109 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1110 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1111 #my $balance_due = $self->owed + $pr_total - $cr_total;
1112 my $balance_due = $self->owed + $pr_total;
1115 #my($description,$amount);
1119 foreach ( @pr_cust_bill ) {
1121 "Previous Balance, Invoice #". $_->invnum.
1122 " (". time2str("%x",$_->_date). ")",
1123 $money_char. sprintf("%10.2f",$_->owed)
1126 if (@pr_cust_bill) {
1127 push @buf,['','-----------'];
1128 push @buf,[ 'Total Previous Balance',
1129 $money_char. sprintf("%10.2f",$pr_total ) ];
1134 foreach my $cust_bill_pkg (
1135 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1136 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1139 if ( $cust_bill_pkg->pkgnum ) {
1141 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1142 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1143 my $pkg = $part_pkg->pkg;
1145 if ( $cust_bill_pkg->setup != 0 ) {
1146 my $description = $pkg;
1147 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1148 push @buf, [ $description,
1149 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1151 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1154 if ( $cust_bill_pkg->recur != 0 ) {
1156 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1157 time2str("%x", $cust_bill_pkg->edate) . ")",
1158 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1161 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1164 } else { #pkgnum tax or one-shot line item
1165 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1166 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1168 if ( $cust_bill_pkg->setup != 0 ) {
1169 push @buf, [ $itemdesc,
1170 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1172 if ( $cust_bill_pkg->recur != 0 ) {
1173 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1174 . time2str("%x", $cust_bill_pkg->edate). ")",
1175 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1181 push @buf,['','-----------'];
1182 push @buf,['Total New Charges',
1183 $money_char. sprintf("%10.2f",$self->charged) ];
1186 push @buf,['','-----------'];
1187 push @buf,['Total Charges',
1188 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1192 foreach ( $self->cust_credited ) {
1194 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1196 my $reason = substr($_->cust_credit->reason,0,32);
1197 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1198 $reason = " ($reason) " if $reason;
1200 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1202 $money_char. sprintf("%10.2f",$_->amount)
1205 #foreach ( @cr_cust_credit ) {
1207 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1208 # $money_char. sprintf("%10.2f",$_->credited)
1212 #get & print payments
1213 foreach ( $self->cust_bill_pay ) {
1215 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1218 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1219 $money_char. sprintf("%10.2f",$_->amount )
1224 my $balance_due_msg = $self->balance_due_msg;
1226 push @buf,['','-----------'];
1227 push @buf,[$balance_due_msg, $money_char.
1228 sprintf("%10.2f", $balance_due ) ];
1230 #create the template
1231 $template ||= $self->_agent_template;
1232 my $templatefile = 'invoice_template';
1233 $templatefile .= "_$template" if length($template);
1234 my @invoice_template = $conf->config($templatefile)
1235 or die "cannot load config file $templatefile";
1238 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1239 /invoice_lines\((\d*)\)/;
1240 $invoice_lines += $1 || scalar(@buf);
1243 die "no invoice_lines() functions in template?" unless $wasfunc;
1244 my $invoice_template = new Text::Template (
1246 SOURCE => [ map "$_\n", @invoice_template ],
1247 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1248 $invoice_template->compile()
1249 or die "can't compile template: $Text::Template::ERROR";
1251 #setup template variables
1252 package FS::cust_bill::_template; #!
1253 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1255 $invnum = $self->invnum;
1256 $date = $self->_date;
1258 $agent = $self->cust_main->agent->agent;
1260 if ( $FS::cust_bill::invoice_lines ) {
1262 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1264 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1269 #format address (variable for the template)
1271 @address = ( '', '', '', '', '', '' );
1272 package FS::cust_bill; #!
1273 $FS::cust_bill::_template::address[$l++] =
1274 $cust_main->payname.
1275 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1276 ? " (P.O. #". $cust_main->payinfo. ")"
1280 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1281 if $cust_main->company;
1282 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1283 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1284 if $cust_main->address2;
1285 $FS::cust_bill::_template::address[$l++] =
1286 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1287 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1288 unless $cust_main->country eq 'US';
1290 # #overdue? (variable for the template)
1291 # $FS::cust_bill::_template::overdue = (
1293 # && $today > $self->_date
1294 ## && $self->printed > 1
1295 # && $self->printed > 0
1298 #and subroutine for the template
1299 sub FS::cust_bill::_template::invoice_lines {
1300 my $lines = shift || scalar(@buf);
1302 scalar(@buf) ? shift @buf : [ '', '' ];
1308 $FS::cust_bill::_template::page = 1;
1312 push @collect, split("\n",
1313 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1315 $FS::cust_bill::_template::page++;
1318 map "$_\n", @collect;
1322 =item print_latex [ TIME [ , TEMPLATE ] ]
1324 Internal method - returns a filename of a filled-in LaTeX template for this
1325 invoice (Note: add ".tex" to get the actual filename).
1327 See print_ps and print_pdf for methods that return PostScript and PDF output.
1329 TIME an optional value used to control the printing of overdue messages. The
1330 default is now. It isn't the date of the invoice; that's the `_date' field.
1331 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1332 L<Time::Local> and L<Date::Parse> for conversion functions.
1336 #still some false laziness w/print_text
1339 my( $self, $today, $template ) = @_;
1342 # my $invnum = $self->invnum;
1343 my $cust_main = $self->cust_main;
1344 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1345 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1347 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1348 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1349 #my $balance_due = $self->owed + $pr_total - $cr_total;
1350 my $balance_due = $self->owed + $pr_total;
1353 #my($description,$amount);
1356 #create the template
1357 $template ||= $self->_agent_template;
1358 my $templatefile = 'invoice_latex';
1359 my $suffix = length($template) ? "_$template" : '';
1360 $templatefile .= $suffix;
1361 my @invoice_template = $conf->config($templatefile)
1362 or die "cannot load config file $templatefile";
1364 my %invoice_data = (
1365 'invnum' => $self->invnum,
1366 'date' => time2str('%b %o, %Y', $self->_date),
1367 'agent' => _latex_escape($cust_main->agent->agent),
1368 'payname' => _latex_escape($cust_main->payname),
1369 'company' => _latex_escape($cust_main->company),
1370 'address1' => _latex_escape($cust_main->address1),
1371 'address2' => _latex_escape($cust_main->address2),
1372 'city' => _latex_escape($cust_main->city),
1373 'state' => _latex_escape($cust_main->state),
1374 'zip' => _latex_escape($cust_main->zip),
1375 'country' => _latex_escape($cust_main->country),
1376 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1377 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1379 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1380 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1383 my $countrydefault = $conf->config('countrydefault') || 'US';
1384 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1386 #do variable substitutions in notes
1387 $invoice_data{'notes'} =
1389 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1390 $conf->config_orbase('invoice_latexnotes', $suffix)
1393 $invoice_data{'footer'} =~ s/\n+$//;
1394 $invoice_data{'smallfooter'} =~ s/\n+$//;
1395 $invoice_data{'notes'} =~ s/\n+$//;
1397 $invoice_data{'po_line'} =
1398 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1399 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1403 my @total_item = ();
1405 while ( @invoice_template ) {
1406 my $line = shift @invoice_template;
1408 if ( $line =~ /^%%Detail\s*$/ ) {
1410 while ( ( my $line_item_line = shift @invoice_template )
1411 !~ /^%%EndDetail\s*$/ ) {
1412 push @line_item, $line_item_line;
1414 foreach my $line_item ( $self->_items ) {
1415 #foreach my $line_item ( $self->_items_pkg ) {
1416 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1417 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1418 if ( exists $line_item->{'ext_description'} ) {
1419 $invoice_data{'description'} .=
1420 "\\tabularnewline\n~~".
1421 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1423 $invoice_data{'amount'} = $line_item->{'amount'};
1424 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1426 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1429 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1431 while ( ( my $total_item_line = shift @invoice_template )
1432 !~ /^%%EndTotalDetails\s*$/ ) {
1433 push @total_item, $total_item_line;
1436 my @total_fill = ();
1439 foreach my $tax ( $self->_items_tax ) {
1440 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1441 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1443 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1448 $invoice_data{'total_item'} = 'Sub-total';
1449 $invoice_data{'total_amount'} =
1450 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1451 unshift @total_fill,
1452 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1456 $invoice_data{'total_item'} = '\textbf{Total}';
1457 $invoice_data{'total_amount'} =
1458 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1460 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1463 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1466 foreach my $credit ( $self->_items_credits ) {
1467 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1469 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1471 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1476 foreach my $payment ( $self->_items_payments ) {
1477 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1479 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1481 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1485 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1486 $invoice_data{'total_amount'} =
1487 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1489 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1492 push @filled_in, @total_fill;
1495 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1496 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1497 push @filled_in, $line;
1508 my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
1509 my $unique = int(rand(2**31)); #UGH... use File::Temp or something
1512 my $file = $self->invnum. ".$unique";
1514 open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
1515 print TEX join("\n", @filled_in ), "\n";
1522 =item print_ps [ TIME [ , TEMPLATE ] ]
1524 Returns an postscript invoice, as a scalar.
1526 TIME an optional value used to control the printing of overdue messages. The
1527 default is now. It isn't the date of the invoice; that's the `_date' field.
1528 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1529 L<Time::Local> and L<Date::Parse> for conversion functions.
1536 my $file = $self->print_latex(@_);
1538 system("pslatex $file.tex >/dev/null 2>&1") == 0
1539 or die "pslatex failed: $!";
1540 system("pslatex $file.tex >/dev/null 2>&1") == 0
1541 or die "pslatex failed: $!";
1543 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1544 or die "dbips failed: $!";
1546 open(POSTSCRIPT, "<$file.ps")
1547 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1549 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1552 while (<POSTSCRIPT>) {
1562 =item print_pdf [ TIME [ , TEMPLATE ] ]
1564 Returns an PDF invoice, as a scalar.
1566 TIME an optional value used to control the printing of overdue messages. The
1567 default is now. It isn't the date of the invoice; that's the `_date' field.
1568 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1569 L<Time::Local> and L<Date::Parse> for conversion functions.
1576 my $file = $self->print_latex(@_);
1578 #system('pdflatex', "$file.tex");
1579 #system('pdflatex', "$file.tex");
1580 #! LaTeX Error: Unknown graphics extension: .eps.
1582 system("pslatex $file.tex >/dev/null 2>&1") == 0
1583 or die "pslatex failed: $!";
1584 system("pslatex $file.tex >/dev/null 2>&1") == 0
1585 or die "pslatex failed: $!";
1587 #system('dvipdf', "$file.dvi", "$file.pdf" );
1589 "dvips -q -t letter -f $file.dvi ".
1590 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf ".
1593 or die "dvips failed: $!";
1595 open(PDF, "<$file.pdf")
1596 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1598 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1611 # quick subroutine for print_latex
1613 # There are ten characters that LaTeX treats as special characters, which
1614 # means that they do not simply typeset themselves:
1615 # # $ % & ~ _ ^ \ { }
1617 # TeX ignores blanks following an escaped character; if you want a blank (as
1618 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1622 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1626 #utility methods for print_*
1628 sub balance_due_msg {
1630 my $msg = 'Balance Due';
1631 return $msg unless $conf->exists('invoice_default_terms');
1632 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1633 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1634 } elsif ( $conf->config('invoice_default_terms') ) {
1635 $msg .= ' - '. $conf->config('invoice_default_terms');
1642 my @display = scalar(@_)
1644 : qw( _items_previous _items_pkg );
1645 #: qw( _items_pkg );
1646 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1648 foreach my $display ( @display ) {
1649 push @b, $self->$display(@_);
1654 sub _items_previous {
1656 my $cust_main = $self->cust_main;
1657 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1659 foreach ( @pr_cust_bill ) {
1661 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1662 ' ('. time2str('%x',$_->_date). ')',
1663 #'pkgpart' => 'N/A',
1665 'amount' => sprintf("%10.2f", $_->owed),
1671 # 'description' => 'Previous Balance',
1672 # #'pkgpart' => 'N/A',
1673 # 'pkgnum' => 'N/A',
1674 # 'amount' => sprintf("%10.2f", $pr_total ),
1675 # 'ext_description' => [ map {
1676 # "Invoice ". $_->invnum.
1677 # " (". time2str("%x",$_->_date). ") ".
1678 # sprintf("%10.2f", $_->owed)
1679 # } @pr_cust_bill ],
1686 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1687 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1692 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1693 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1696 sub _items_cust_bill_pkg {
1698 my $cust_bill_pkg = shift;
1701 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1703 if ( $cust_bill_pkg->pkgnum ) {
1705 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1706 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1707 my $pkg = $part_pkg->pkg;
1710 #tie %labels, 'Tie::IxHash';
1711 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1712 my @ext_description;
1713 foreach my $label ( keys %labels ) {
1714 my @values = @{ $labels{$label} };
1715 my $num = scalar(@values);
1717 push @ext_description, "$label ($num)";
1719 push @ext_description, map { "$label: $_" } @values;
1723 if ( $cust_bill_pkg->setup != 0 ) {
1724 my $description = $pkg;
1725 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1726 my @d = @ext_description;
1727 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1729 'description' => $description,
1730 #'pkgpart' => $part_pkg->pkgpart,
1731 'pkgnum' => $cust_pkg->pkgnum,
1732 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1733 'ext_description' => \@d,
1737 if ( $cust_bill_pkg->recur != 0 ) {
1739 'description' => "$pkg (" .
1740 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1741 time2str('%x', $cust_bill_pkg->edate). ')',
1742 #'pkgpart' => $part_pkg->pkgpart,
1743 'pkgnum' => $cust_pkg->pkgnum,
1744 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1745 'ext_description' => [ @ext_description,
1746 $cust_bill_pkg->details,
1751 } else { #pkgnum tax or one-shot line item (??)
1753 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1754 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1756 if ( $cust_bill_pkg->setup != 0 ) {
1758 'description' => $itemdesc,
1759 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1762 if ( $cust_bill_pkg->recur != 0 ) {
1764 'description' => "$itemdesc (".
1765 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1766 time2str("%x", $cust_bill_pkg->edate). ')',
1767 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1779 sub _items_credits {
1784 foreach ( $self->cust_credited ) {
1786 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1788 my $reason = $_->cust_credit->reason;
1789 #my $reason = substr($_->cust_credit->reason,0,32);
1790 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1791 $reason = " ($reason) " if $reason;
1793 #'description' => 'Credit ref\#'. $_->crednum.
1794 # " (". time2str("%x",$_->cust_credit->_date) .")".
1796 'description' => 'Credit applied'.
1797 time2str("%x",$_->cust_credit->_date). $reason,
1798 'amount' => sprintf("%10.2f",$_->amount),
1801 #foreach ( @cr_cust_credit ) {
1803 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1804 # $money_char. sprintf("%10.2f",$_->credited)
1812 sub _items_payments {
1816 #get & print payments
1817 foreach ( $self->cust_bill_pay ) {
1819 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1822 'description' => "Payment received ".
1823 time2str("%x",$_->cust_pay->_date ),
1824 'amount' => sprintf("%10.2f", $_->amount )
1838 print_text formatting (and some logic :/) is in source, but needs to be
1839 slurped in from a file. Also number of lines ($=).
1843 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1844 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base