4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
8 use FS::UID qw( datasrc );
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
12 use FS::cust_bill_pkg;
16 use FS::cust_credit_bill;
17 use FS::cust_pay_batch;
18 use FS::cust_bill_event;
20 @ISA = qw( FS::Record );
22 #ask FS::UID to run this stuff for us later
23 FS::UID->install_callback( sub {
25 $money_char = $conf->config('money_char') || '$';
30 FS::cust_bill - Object methods for cust_bill records
36 $record = new FS::cust_bill \%hash;
37 $record = new FS::cust_bill { 'column' => 'value' };
39 $error = $record->insert;
41 $error = $new_record->replace($old_record);
43 $error = $record->delete;
45 $error = $record->check;
47 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
49 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
51 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
53 @cust_pay_objects = $cust_bill->cust_pay;
55 $tax_amount = $record->tax;
57 @lines = $cust_bill->print_text;
58 @lines = $cust_bill->print_text $time;
62 An FS::cust_bill object represents an invoice; a declaration that a customer
63 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
64 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
65 following fields are currently supported:
69 =item invnum - primary key (assigned automatically for new invoices)
71 =item custnum - customer (see L<FS::cust_main>)
73 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
74 L<Time::Local> and L<Date::Parse> for conversion functions.
76 =item charged - amount of this invoice
78 =item printed - deprecated
80 =item closed - books closed flag, empty or `Y'
90 Creates a new invoice. To add the invoice to the database, see L<"insert">.
91 Invoices are normally created by calling the bill method of a customer object
92 (see L<FS::cust_main>).
96 sub table { 'cust_bill'; }
100 Adds this invoice to the database ("Posts" the invoice). If there is an error,
101 returns the error, otherwise returns false.
105 Currently unimplemented. I don't remove invoices because there would then be
106 no record you ever posted this invoice (which is bad, no?)
112 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
113 $self->SUPER::delete(@_);
116 =item replace OLD_RECORD
118 Replaces the OLD_RECORD with this one in the database. If there is an error,
119 returns the error, otherwise returns false.
121 Only printed may be changed. printed is normally updated by calling the
122 collect method of a customer object (see L<FS::cust_main>).
127 my( $new, $old ) = ( shift, shift );
128 return "Can't change custnum!" unless $old->custnum == $new->custnum;
129 #return "Can't change _date!" unless $old->_date eq $new->_date;
130 return "Can't change _date!" unless $old->_date == $new->_date;
131 return "Can't change charged!" unless $old->charged == $new->charged;
133 $new->SUPER::replace($old);
138 Checks all fields to make sure this is a valid invoice. If there is an error,
139 returns the error, otherwise returns false. Called by the insert and replace
148 $self->ut_numbern('invnum')
149 || $self->ut_number('custnum')
150 || $self->ut_numbern('_date')
151 || $self->ut_money('charged')
152 || $self->ut_numbern('printed')
153 || $self->ut_enum('closed', [ '', 'Y' ])
155 return $error if $error;
157 return "Unknown customer"
158 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
160 $self->_date(time) unless $self->_date;
162 $self->printed(0) if $self->printed eq '';
169 Returns a list consisting of the total previous balance for this customer,
170 followed by the previous outstanding invoices (as FS::cust_bill objects also).
177 my @cust_bill = sort { $a->_date <=> $b->_date }
178 grep { $_->owed != 0 && $_->_date < $self->_date }
179 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
181 foreach ( @cust_bill ) { $total += $_->owed; }
187 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
193 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
196 =item cust_bill_event
198 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
203 sub cust_bill_event {
205 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
211 Returns the customer (see L<FS::cust_main>) for this invoice.
217 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
222 Depreciated. See the cust_credited method.
224 #Returns a list consisting of the total previous credited (see
225 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
226 #outstanding credits (FS::cust_credit objects).
232 croak "FS::cust_bill->cust_credit depreciated; see ".
233 "FS::cust_bill->cust_credit_bill";
236 #my @cust_credit = sort { $a->_date <=> $b->_date }
237 # grep { $_->credited != 0 && $_->_date < $self->_date }
238 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
240 #foreach (@cust_credit) { $total += $_->credited; }
241 #$total, @cust_credit;
246 Depreciated. See the cust_bill_pay method.
248 #Returns all payments (see L<FS::cust_pay>) for this invoice.
254 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
256 #sort { $a->_date <=> $b->_date }
257 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
263 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
269 sort { $a->_date <=> $b->_date }
270 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
275 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
281 sort { $a->_date <=> $b->_date }
282 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
288 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
295 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
297 foreach (@taxlines) { $total += $_->setup; }
303 Returns the amount owed (still outstanding) on this invoice, which is charged
304 minus all payment applications (see L<FS::cust_bill_pay>) and credit
305 applications (see L<FS::cust_credit_bill>).
311 my $balance = $self->charged;
312 $balance -= $_->amount foreach ( $self->cust_bill_pay );
313 $balance -= $_->amount foreach ( $self->cust_credited );
314 $balance = sprintf( "%.2f", $balance);
315 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
321 Sends this invoice to the destinations configured for this customer: send
322 emails or print. See L<FS::cust_main_invoice>.
327 my($self,$template) = @_;
328 my @print_text = $self->print_text('', $template);
329 my @invoicing_list = $self->cust_main->invoicing_list;
331 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
333 #better to notify this person than silence
334 @invoicing_list = ($conf->config('invoice_from')) unless @invoicing_list;
336 my $error = send_email(
337 'from' => $conf->config('invoice_from'),
338 'to' => [ grep { $_ ne 'POST' } @invoicing_list ],
339 'subject' => 'Invoice',
340 'body' => \@print_text,
342 return "can't send invoice: $error" if $error;
346 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
347 my $lpr = $conf->config('lpr');
349 or return "Can't open pipe to $lpr: $!";
350 print LPR @print_text;
352 or return $! ? "Error closing $lpr: $!"
353 : "Exit status $? from $lpr";
360 =item send_csv OPTIONS
362 Sends invoice as a CSV data-file to a remote host with the specified protocol.
366 protocol - currently only "ftp"
372 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
373 and YYMMDDHHMMSS is a timestamp.
375 The fields of the CSV file is as follows:
377 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
381 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
383 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
384 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
385 fields are filled in.
387 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
388 first two fields (B<record_type> and B<invnum>) and the last five fields
389 (B<pkg> through B<edate>) are filled in.
391 =item invnum - invoice number
393 =item custnum - customer number
395 =item _date - invoice date
397 =item charged - total invoice amount
399 =item first - customer first name
401 =item last - customer first name
403 =item company - company name
405 =item address1 - address line 1
407 =item address2 - address line 1
417 =item pkg - line item description
419 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
421 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
423 =item sdate - start date for recurring fee
425 =item edate - end date for recurring fee
432 my($self, %opt) = @_;
434 #part one: create file
436 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
437 mkdir $spooldir, 0700 unless -d $spooldir;
439 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
441 open(CSV, ">$file") or die "can't open $file: $!";
443 eval "use Text::CSV_XS";
446 my $csv = Text::CSV_XS->new({'always_quote'=>1});
448 my $cust_main = $self->cust_main;
454 time2str("%x", $self->_date),
455 sprintf("%.2f", $self->charged),
456 ( map { $cust_main->getfield($_) }
457 qw( first last company address1 address2 city state zip country ) ),
459 ) or die "can't create csv";
460 print CSV $csv->string. "\n";
462 #new charges (false laziness w/print_text)
463 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
465 my($pkg, $setup, $recur, $sdate, $edate);
466 if ( $cust_bill_pkg->pkgnum ) {
468 ($pkg, $setup, $recur, $sdate, $edate) = (
469 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
470 ( $cust_bill_pkg->setup != 0
471 ? sprintf("%.2f", $cust_bill_pkg->setup )
473 ( $cust_bill_pkg->recur != 0
474 ? sprintf("%.2f", $cust_bill_pkg->recur )
476 time2str("%x", $cust_bill_pkg->sdate),
477 time2str("%x", $cust_bill_pkg->edate),
481 next unless $cust_bill_pkg->setup != 0;
482 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
483 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
485 ($pkg, $setup, $recur, $sdate, $edate) =
486 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
492 ( map { '' } (1..11) ),
493 ($pkg, $setup, $recur, $sdate, $edate)
494 ) or die "can't create csv";
495 print CSV $csv->string. "\n";
499 close CSV or die "can't close CSV: $!";
504 if ( $opt{protocol} eq 'ftp' ) {
505 eval "use Net::FTP;";
507 $net = Net::FTP->new($opt{server}) or die @$;
509 die "unknown protocol: $opt{protocol}";
512 $net->login( $opt{username}, $opt{password} )
513 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
515 $net->binary or die "can't set binary mode";
517 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
519 $net->put($file) or die "can't put $file: $!";
529 Pays this invoice with a compliemntary payment. If there is an error,
530 returns the error, otherwise returns false.
536 my $cust_pay = new FS::cust_pay ( {
537 'invnum' => $self->invnum,
538 'paid' => $self->owed,
541 'payinfo' => $self->cust_main->payinfo,
549 Attempts to pay this invoice with a credit card payment via a
550 Business::OnlinePayment realtime gateway. See
551 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
552 for supported processors.
558 $self->realtime_bop( 'CC', @_ );
563 Attempts to pay this invoice with an electronic check (ACH) payment via a
564 Business::OnlinePayment realtime gateway. See
565 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
566 for supported processors.
572 $self->realtime_bop( 'ECHECK', @_ );
577 Attempts to pay this invoice with phone bill (LEC) payment via a
578 Business::OnlinePayment realtime gateway. See
579 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
580 for supported processors.
586 $self->realtime_bop( 'LEC', @_ );
590 my( $self, $method ) = @_;
592 my $cust_main = $self->cust_main;
593 my $amount = $self->owed;
595 my $description = 'Internet Services';
596 if ( $conf->exists('business-onlinepayment-description') ) {
597 my $dtempl = $conf->config('business-onlinepayment-description');
599 my $agent_obj = $cust_main->agent
600 or die "can't retreive agent for $cust_main (agentnum ".
601 $cust_main->agentnum. ")";
602 my $agent = $agent_obj->agent;
603 my $pkgs = join(', ',
604 map { $_->cust_pkg->part_pkg->pkg }
605 grep { $_->pkgnum } $self->cust_bill_pkg
607 $description = eval qq("$dtempl");
610 $cust_main->realtime_bop($method, $amount,
611 'description' => $description,
612 'invnum' => $self->invnum,
619 Adds a payment for this invoice to the pending credit card batch (see
620 L<FS::cust_pay_batch>).
626 my $cust_main = $self->cust_main;
628 my $cust_pay_batch = new FS::cust_pay_batch ( {
629 'invnum' => $self->getfield('invnum'),
630 'custnum' => $cust_main->getfield('custnum'),
631 'last' => $cust_main->getfield('last'),
632 'first' => $cust_main->getfield('first'),
633 'address1' => $cust_main->getfield('address1'),
634 'address2' => $cust_main->getfield('address2'),
635 'city' => $cust_main->getfield('city'),
636 'state' => $cust_main->getfield('state'),
637 'zip' => $cust_main->getfield('zip'),
638 'country' => $cust_main->getfield('country'),
639 'cardnum' => $cust_main->getfield('payinfo'),
640 'exp' => $cust_main->getfield('paydate'),
641 'payname' => $cust_main->getfield('payname'),
642 'amount' => $self->owed,
644 my $error = $cust_pay_batch->insert;
645 die $error if $error;
650 =item print_text [ TIME [ , TEMPLATE ] ]
652 Returns an text invoice, as a list of lines.
654 TIME an optional value used to control the printing of overdue messages. The
655 default is now. It isn't the date of the invoice; that's the `_date' field.
656 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
657 L<Time::Local> and L<Date::Parse> for conversion functions.
663 my( $self, $today, $template ) = @_;
665 # my $invnum = $self->invnum;
666 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
667 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
668 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
671 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
672 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
673 #my $balance_due = $self->owed + $pr_total - $cr_total;
674 my $balance_due = $self->owed + $pr_total;
677 #my($description,$amount);
681 foreach ( @pr_cust_bill ) {
683 "Previous Balance, Invoice #". $_->invnum.
684 " (". time2str("%x",$_->_date). ")",
685 $money_char. sprintf("%10.2f",$_->owed)
689 push @buf,['','-----------'];
690 push @buf,[ 'Total Previous Balance',
691 $money_char. sprintf("%10.2f",$pr_total ) ];
696 foreach my $cust_bill_pkg (
697 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
698 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
701 if ( $cust_bill_pkg->pkgnum ) {
703 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
704 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
705 my $pkg = $part_pkg->pkg;
707 if ( $cust_bill_pkg->setup != 0 ) {
708 push @buf, [ "$pkg Setup",
709 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
711 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
714 if ( $cust_bill_pkg->recur != 0 ) {
716 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
717 time2str("%x", $cust_bill_pkg->edate) . ")",
718 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
721 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
724 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
726 } else { #pkgnum tax or one-shot line item
727 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
728 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
730 if ( $cust_bill_pkg->setup != 0 ) {
731 push @buf, [ $itemdesc,
732 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
734 if ( $cust_bill_pkg->recur != 0 ) {
735 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
736 . time2str("%x", $cust_bill_pkg->edate). ")",
737 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
743 push @buf,['','-----------'];
744 push @buf,['Total New Charges',
745 $money_char. sprintf("%10.2f",$self->charged) ];
748 push @buf,['','-----------'];
749 push @buf,['Total Charges',
750 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
753 foreach ( $self->cust_bill_pay ) {
755 #something more elaborate if $_->amount ne ->cust_pay->paid ?
758 "Payment received ". time2str("%x",$_->cust_pay->_date ),
759 $money_char. sprintf("%10.2f",$_->amount )
764 push @buf,['','-----------'];
765 push @buf,['Balance Due', $money_char.
766 sprintf("%10.2f", $balance_due ) ];
769 my $templatefile = 'invoice_template';
770 $templatefile .= "_$template" if $template;
771 my @invoice_template = $conf->config($templatefile)
772 or die "cannot load config file $templatefile";
775 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
776 /invoice_lines\((\d*)\)/;
777 $invoice_lines += $1 || scalar(@buf);
780 die "no invoice_lines() functions in template?" unless $wasfunc;
781 my $invoice_template = new Text::Template (
783 SOURCE => [ map "$_\n", @invoice_template ],
784 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
785 $invoice_template->compile()
786 or die "can't compile template: $Text::Template::ERROR";
788 #setup template variables
789 package FS::cust_bill::_template; #!
790 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
792 $invnum = $self->invnum;
793 $date = $self->_date;
795 $agent = $self->cust_main->agent->agent;
797 if ( $FS::cust_bill::invoice_lines ) {
799 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
801 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
806 #format address (variable for the template)
808 @address = ( '', '', '', '', '', '' );
809 package FS::cust_bill; #!
810 $FS::cust_bill::_template::address[$l++] =
812 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
813 ? " (P.O. #". $cust_main->payinfo. ")"
817 $FS::cust_bill::_template::address[$l++] = $cust_main->company
818 if $cust_main->company;
819 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
820 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
821 if $cust_main->address2;
822 $FS::cust_bill::_template::address[$l++] =
823 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
824 $FS::cust_bill::_template::address[$l++] = $cust_main->country
825 unless $cust_main->country eq 'US';
827 # #overdue? (variable for the template)
828 # $FS::cust_bill::_template::overdue = (
830 # && $today > $self->_date
831 ## && $self->printed > 1
832 # && $self->printed > 0
835 #and subroutine for the template
836 sub FS::cust_bill::_template::invoice_lines {
837 my $lines = shift || scalar(@buf);
839 scalar(@buf) ? shift @buf : [ '', '' ];
845 $FS::cust_bill::_template::page = 1;
849 push @collect, split("\n",
850 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
852 $FS::cust_bill::_template::page++;
855 map "$_\n", @collect;
859 =item print_ps [ TIME [ , TEMPLATE ] ]
861 Returns an postscript invoice, as a scalar.
863 TIME an optional value used to control the printing of overdue messages. The
864 default is now. It isn't the date of the invoice; that's the `_date' field.
865 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
866 L<Time::Local> and L<Date::Parse> for conversion functions.
870 #still some false laziness w/print_text
873 my( $self, $today, $template ) = @_;
876 # my $invnum = $self->invnum;
877 my $cust_main = $self->cust_main;
878 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
879 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
881 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
882 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
883 #my $balance_due = $self->owed + $pr_total - $cr_total;
884 my $balance_due = $self->owed + $pr_total;
887 #my($description,$amount);
891 my $templatefile = 'invoice_template_latex';
892 $templatefile .= "_$template" if $template;
893 my @invoice_template = $conf->config($templatefile)
894 or die "cannot load config file $templatefile";
897 'invnum' => $self->invnum,
898 'date' => time2str('%b %o, %Y', $self->_date),
899 'agent' => $cust_main->agent->agent,
900 'payname' => $cust_main->payname,
901 'company' => $cust_main->company,
902 'address1' => $cust_main->address1,
903 'address2' => $cust_main->address2,
904 'city' => $cust_main->city,
905 'state' => $cust_main->state,
906 'zip' => $cust_main->zip,
907 'country' => $cust_main->country,
908 'footer' => <<'END', #should come from config value
911 San Francisco, CA~~94117\\
912 ivan@sisd.com~~~~+1 415 462 1624\\
913 Freeside - open-source billing - http://www.sisd.com/freeside\\
920 #$invoice_data{'footer'} =~ s/\n+$//;
922 my $countrydefault = $conf->config('countrydefault') || 'US';
923 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
925 $invoice_data{'po_line'} =
926 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
927 ? "Purchase Order #". $cust_main->payinfo
933 while ( @invoice_template ) {
934 my $line = shift @invoice_template;
936 if ( $line =~ /^%%Detail\s*$/ ) {
938 while ( ( my $line_item_line = shift @invoice_template )
939 !~ /^%%EndDetail\s*$/ ) {
940 push @line_item, $line_item_line;
942 #foreach my $line_item ( $self->_items ) {
943 foreach my $line_item ( $self->_items_pkg ) {
944 $invoice_data{'ref'} = $line_item->{'pkgnum'};
945 $invoice_data{'description'} = $line_item->{'description'};
946 if ( exists $line_item->{'ext_description'} ) {
947 $invoice_data{'description'} .=
948 "\\tabularnewline\n~~".
949 join("\\tabularnewline\n~~", @{$line_item->{'ext_description'}} );
951 $invoice_data{'amount'} = $line_item->{'amount'};
952 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
954 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
957 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
959 while ( ( my $total_item_line = shift @invoice_template )
960 !~ /^%%EndTotalDetails\s*$/ ) {
961 push @total_item, $total_item_line;
967 foreach my $tax ( $self->_items_tax ) {
968 $invoice_data{'total_item'} = $tax->{'description'};
969 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
971 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
975 $invoice_data{'total_item'} = '\textbf{Total}';
976 $invoice_data{'total_amount'} =
977 '\textbf{\dollar '. sprintf('%.2f', $self->owed ). '}';
979 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
983 $invoice_data{'total_item'} = 'Sub-total';
984 $invoice_data{'total_amount'} =
985 '\dollar '. sprintf('%.2f', $self->owed - $taxtotal );
987 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
991 push @filled_in, @total_fill;
994 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
995 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
996 push @filled_in, $line;
1007 my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
1008 my $unique = int(rand(2**31)); #UGH... use File::Temp or something
1011 my $file = $self->invnum. ".$unique";
1013 open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
1014 print TEX join("\n", @filled_in ), "\n";
1018 system('pslatex', "$file.tex");
1019 system('pslatex', "$file.tex");
1020 #system('dvips', '-t', 'letter', "$file.dvi", "$file.ps");
1021 system('dvips', '-t', 'letter', "$file.dvi" );
1023 open(POSTSCRIPT, "<$file.ps") or die "can't open $file.ps: $!\n";
1025 #rm $file.dvi $file.log $file.aux
1026 #unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps");
1027 unlink("$file.dvi", "$file.log", "$file.aux");
1030 while (<POSTSCRIPT>) {
1040 #utility methods for print_*
1044 my @display = scalar(@_)
1047 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1049 foreach my $display ( @display ) {
1050 push @b, $self->$display(@_);
1055 sub _items_previous {
1057 my $cust_main = $self->cust_main;
1058 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1060 foreach ( @pr_cust_bill ) {
1062 "Previous Balance, Invoice #". $_->invnum.
1063 " (". time2str("%x",$_->_date). ")",
1064 $money_char. sprintf("%10.2f",$_->owed)
1072 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1073 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1078 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1079 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1082 sub _items_cust_bill_pkg {
1084 my $cust_bill_pkg = shift;
1087 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1089 if ( $cust_bill_pkg->pkgnum ) {
1091 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1092 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1093 my $pkg = $part_pkg->pkg;
1095 if ( $cust_bill_pkg->setup != 0 ) {
1097 @d = $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1099 'description' => "$pkg Setup",
1100 'pkgpart' => $part_pkg->pkgpart,
1101 'pkgnum' => $cust_pkg->pkgnum,
1102 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1103 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1104 $cust_pkg->labels ),
1110 if ( $cust_bill_pkg->recur != 0 ) {
1112 'description' => "$pkg (" .
1113 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1114 time2str('%x', $cust_bill_pkg->edate). ')',
1115 'pkgpart' => $part_pkg->pkgpart,
1116 'pkgnum' => $cust_pkg->pkgnum,
1117 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1118 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1119 $cust_pkg->labels ),
1120 $cust_bill_pkg->details,
1125 } else { #pkgnum tax or one-shot line item (??)
1127 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1128 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1130 if ( $cust_bill_pkg->setup != 0 ) {
1132 'description' => $itemdesc,
1133 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1136 if ( $cust_bill_pkg->recur != 0 ) {
1138 'description' => "$itemdesc (".
1139 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1140 time2str("%x", $cust_bill_pkg->edate). ')',
1141 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1153 sub _items_credits {
1158 foreach ( $self->cust_credited ) {
1160 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1162 my $reason = substr($_->cust_credit->reason,0,32);
1163 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1164 $reason = " ($reason) " if $reason;
1166 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1168 $money_char. sprintf("%10.2f",$_->amount)
1171 #foreach ( @cr_cust_credit ) {
1173 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1174 # $money_char. sprintf("%10.2f",$_->credited)
1182 sub _items_payments {
1186 #get & print payments
1187 foreach ( $self->cust_bill_pay ) {
1189 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1192 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1193 $money_char. sprintf("%10.2f",$_->amount )
1207 print_text formatting (and some logic :/) is in source, but needs to be
1208 slurped in from a file. Also number of lines ($=).
1210 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1211 or something similar so the look can be completely customized?)
1215 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1216 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base