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';
670 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
671 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
672 #my $balance_due = $self->owed + $pr_total - $cr_total;
673 my $balance_due = $self->owed + $pr_total;
676 #my($description,$amount);
680 foreach ( @pr_cust_bill ) {
682 "Previous Balance, Invoice #". $_->invnum.
683 " (". time2str("%x",$_->_date). ")",
684 $money_char. sprintf("%10.2f",$_->owed)
688 push @buf,['','-----------'];
689 push @buf,[ 'Total Previous Balance',
690 $money_char. sprintf("%10.2f",$pr_total ) ];
695 foreach my $cust_bill_pkg (
696 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
697 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
700 if ( $cust_bill_pkg->pkgnum ) {
702 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
703 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
704 my $pkg = $part_pkg->pkg;
706 if ( $cust_bill_pkg->setup != 0 ) {
707 push @buf, [ "$pkg Setup",
708 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
710 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
713 if ( $cust_bill_pkg->recur != 0 ) {
715 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
716 time2str("%x", $cust_bill_pkg->edate) . ")",
717 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
720 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
723 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
725 } else { #pkgnum tax or one-shot line item
726 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
727 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
729 if ( $cust_bill_pkg->setup != 0 ) {
730 push @buf, [ $itemdesc,
731 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
733 if ( $cust_bill_pkg->recur != 0 ) {
734 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
735 . time2str("%x", $cust_bill_pkg->edate). ")",
736 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
742 push @buf,['','-----------'];
743 push @buf,['Total New Charges',
744 $money_char. sprintf("%10.2f",$self->charged) ];
747 push @buf,['','-----------'];
748 push @buf,['Total Charges',
749 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
753 foreach ( $self->cust_credited ) {
755 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
757 my $reason = substr($_->cust_credit->reason,0,32);
758 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
759 $reason = " ($reason) " if $reason;
761 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
763 $money_char. sprintf("%10.2f",$_->amount)
766 #foreach ( @cr_cust_credit ) {
768 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
769 # $money_char. sprintf("%10.2f",$_->credited)
773 #get & print payments
774 foreach ( $self->cust_bill_pay ) {
776 #something more elaborate if $_->amount ne ->cust_pay->paid ?
779 "Payment received ". time2str("%x",$_->cust_pay->_date ),
780 $money_char. sprintf("%10.2f",$_->amount )
785 my $balance_due_msg = 'Balance Due';
787 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
789 ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
790 } elsif ( $conf->config('invoice_default_terms') ) {
791 $balance_due_msg .= ' - '. $conf->config('invoice_default_terms');
794 push @buf,['','-----------'];
795 push @buf,[$balance_due_msg, $money_char.
796 sprintf("%10.2f", $balance_due ) ];
799 my $templatefile = 'invoice_template';
800 $templatefile .= "_$template" if $template;
801 my @invoice_template = $conf->config($templatefile)
802 or die "cannot load config file $templatefile";
805 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
806 /invoice_lines\((\d*)\)/;
807 $invoice_lines += $1 || scalar(@buf);
810 die "no invoice_lines() functions in template?" unless $wasfunc;
811 my $invoice_template = new Text::Template (
813 SOURCE => [ map "$_\n", @invoice_template ],
814 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
815 $invoice_template->compile()
816 or die "can't compile template: $Text::Template::ERROR";
818 #setup template variables
819 package FS::cust_bill::_template; #!
820 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
822 $invnum = $self->invnum;
823 $date = $self->_date;
825 $agent = $self->cust_main->agent->agent;
827 if ( $FS::cust_bill::invoice_lines ) {
829 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
831 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
836 #format address (variable for the template)
838 @address = ( '', '', '', '', '', '' );
839 package FS::cust_bill; #!
840 $FS::cust_bill::_template::address[$l++] =
842 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
843 ? " (P.O. #". $cust_main->payinfo. ")"
847 $FS::cust_bill::_template::address[$l++] = $cust_main->company
848 if $cust_main->company;
849 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
850 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
851 if $cust_main->address2;
852 $FS::cust_bill::_template::address[$l++] =
853 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
854 $FS::cust_bill::_template::address[$l++] = $cust_main->country
855 unless $cust_main->country eq 'US';
857 # #overdue? (variable for the template)
858 # $FS::cust_bill::_template::overdue = (
860 # && $today > $self->_date
861 ## && $self->printed > 1
862 # && $self->printed > 0
865 #and subroutine for the template
866 sub FS::cust_bill::_template::invoice_lines {
867 my $lines = shift || scalar(@buf);
869 scalar(@buf) ? shift @buf : [ '', '' ];
875 $FS::cust_bill::_template::page = 1;
879 push @collect, split("\n",
880 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
882 $FS::cust_bill::_template::page++;
885 map "$_\n", @collect;
889 =item print_ps [ TIME [ , TEMPLATE ] ]
891 Returns an postscript invoice, as a scalar.
893 TIME an optional value used to control the printing of overdue messages. The
894 default is now. It isn't the date of the invoice; that's the `_date' field.
895 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
896 L<Time::Local> and L<Date::Parse> for conversion functions.
900 #still some false laziness w/print_text
903 my( $self, $today, $template ) = @_;
906 # my $invnum = $self->invnum;
907 my $cust_main = $self->cust_main;
908 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
909 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
911 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
912 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
913 #my $balance_due = $self->owed + $pr_total - $cr_total;
914 my $balance_due = $self->owed + $pr_total;
917 #my($description,$amount);
921 my $templatefile = 'invoice_template_latex';
922 $templatefile .= "_$template" if $template;
923 my @invoice_template = $conf->config($templatefile)
924 or die "cannot load config file $templatefile";
927 'invnum' => $self->invnum,
928 'date' => time2str('%b %o, %Y', $self->_date),
929 'agent' => $cust_main->agent->agent,
930 'payname' => $cust_main->payname,
931 'company' => $cust_main->company,
932 'address1' => $cust_main->address1,
933 'address2' => $cust_main->address2,
934 'city' => $cust_main->city,
935 'state' => $cust_main->state,
936 'zip' => $cust_main->zip,
937 'country' => $cust_main->country,
938 'footer' => <<'END', #should come from config value
941 San Francisco, CA~~94117\\
942 ivan@sisd.com~~~~+1 415 462 1624\\
943 Freeside - open-source billing - http://www.sisd.com/freeside\\
950 #$invoice_data{'footer'} =~ s/\n+$//;
952 my $countrydefault = $conf->config('countrydefault') || 'US';
953 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
955 $invoice_data{'po_line'} =
956 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
957 ? "Purchase Order #". $cust_main->payinfo
963 while ( @invoice_template ) {
964 my $line = shift @invoice_template;
966 if ( $line =~ /^%%Detail\s*$/ ) {
968 while ( ( my $line_item_line = shift @invoice_template )
969 !~ /^%%EndDetail\s*$/ ) {
970 push @line_item, $line_item_line;
972 #foreach my $line_item ( $self->_items ) {
973 foreach my $line_item ( $self->_items_pkg ) {
974 $invoice_data{'ref'} = $line_item->{'pkgnum'};
975 $invoice_data{'description'} = $line_item->{'description'};
976 if ( exists $line_item->{'ext_description'} ) {
977 $invoice_data{'description'} .=
978 "\\tabularnewline\n~~".
979 join("\\tabularnewline\n~~", @{$line_item->{'ext_description'}} );
981 $invoice_data{'amount'} = $line_item->{'amount'};
982 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
984 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
987 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
989 while ( ( my $total_item_line = shift @invoice_template )
990 !~ /^%%EndTotalDetails\s*$/ ) {
991 push @total_item, $total_item_line;
997 foreach my $tax ( $self->_items_tax ) {
998 $invoice_data{'total_item'} = $tax->{'description'};
999 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1001 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1005 $invoice_data{'total_item'} = '\textbf{Total}';
1006 $invoice_data{'total_amount'} =
1007 '\textbf{\dollar '. sprintf('%.2f', $self->owed ). '}';
1009 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1013 $invoice_data{'total_item'} = 'Sub-total';
1014 $invoice_data{'total_amount'} =
1015 '\dollar '. sprintf('%.2f', $self->owed - $taxtotal );
1016 unshift @total_fill,
1017 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1021 push @filled_in, @total_fill;
1024 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1025 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1026 push @filled_in, $line;
1037 my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
1038 my $unique = int(rand(2**31)); #UGH... use File::Temp or something
1041 my $file = $self->invnum. ".$unique";
1043 open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
1044 print TEX join("\n", @filled_in ), "\n";
1048 system('pslatex', "$file.tex");
1049 system('pslatex', "$file.tex");
1050 #system('dvips', '-t', 'letter', "$file.dvi", "$file.ps");
1051 system('dvips', '-t', 'letter', "$file.dvi" );
1053 open(POSTSCRIPT, "<$file.ps") or die "can't open $file.ps: $!\n";
1055 #rm $file.dvi $file.log $file.aux
1056 #unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps");
1057 unlink("$file.dvi", "$file.log", "$file.aux");
1060 while (<POSTSCRIPT>) {
1070 #utility methods for print_*
1074 my @display = scalar(@_)
1077 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1079 foreach my $display ( @display ) {
1080 push @b, $self->$display(@_);
1085 sub _items_previous {
1087 my $cust_main = $self->cust_main;
1088 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1090 foreach ( @pr_cust_bill ) {
1092 "Previous Balance, Invoice #". $_->invnum.
1093 " (". time2str("%x",$_->_date). ")",
1094 $money_char. sprintf("%10.2f",$_->owed)
1102 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1103 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1108 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1109 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1112 sub _items_cust_bill_pkg {
1114 my $cust_bill_pkg = shift;
1117 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1119 if ( $cust_bill_pkg->pkgnum ) {
1121 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1122 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1123 my $pkg = $part_pkg->pkg;
1125 if ( $cust_bill_pkg->setup != 0 ) {
1127 @d = $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1129 'description' => "$pkg Setup",
1130 'pkgpart' => $part_pkg->pkgpart,
1131 'pkgnum' => $cust_pkg->pkgnum,
1132 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1133 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1134 $cust_pkg->labels ),
1140 if ( $cust_bill_pkg->recur != 0 ) {
1142 'description' => "$pkg (" .
1143 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1144 time2str('%x', $cust_bill_pkg->edate). ')',
1145 'pkgpart' => $part_pkg->pkgpart,
1146 'pkgnum' => $cust_pkg->pkgnum,
1147 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1148 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1149 $cust_pkg->labels ),
1150 $cust_bill_pkg->details,
1155 } else { #pkgnum tax or one-shot line item (??)
1157 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1158 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1160 if ( $cust_bill_pkg->setup != 0 ) {
1162 'description' => $itemdesc,
1163 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1166 if ( $cust_bill_pkg->recur != 0 ) {
1168 'description' => "$itemdesc (".
1169 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1170 time2str("%x", $cust_bill_pkg->edate). ')',
1171 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1183 sub _items_credits {
1188 foreach ( $self->cust_credited ) {
1190 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1192 my $reason = substr($_->cust_credit->reason,0,32);
1193 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1194 $reason = " ($reason) " if $reason;
1196 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1198 $money_char. sprintf("%10.2f",$_->amount)
1201 #foreach ( @cr_cust_credit ) {
1203 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1204 # $money_char. sprintf("%10.2f",$_->credited)
1212 sub _items_payments {
1216 #get & print payments
1217 foreach ( $self->cust_bill_pay ) {
1219 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1222 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1223 $money_char. sprintf("%10.2f",$_->amount )
1237 print_text formatting (and some logic :/) is in source, but needs to be
1238 slurped in from a file. Also number of lines ($=).
1240 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1241 or something similar so the look can be completely customized?)
1245 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1246 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base