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'),
640 'cardnum' => $cust_main->getfield('payinfo'),
641 'exp' => $cust_main->getfield('paydate'),
642 'payname' => $cust_main->getfield('payname'),
643 'amount' => $self->owed,
645 my $error = $cust_pay_batch->insert;
646 die $error if $error;
651 =item print_text [TIME];
653 Returns an text invoice, as a list of lines.
655 TIME an optional value used to control the printing of overdue messages. The
656 default is now. It isn't the date of the invoice; that's the `_date' field.
657 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
658 L<Time::Local> and L<Date::Parse> for conversion functions.
664 my( $self, $today, $template ) = @_;
666 # my $invnum = $self->invnum;
667 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
668 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
669 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) ];
754 foreach ( $self->cust_credited ) {
756 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
758 my $reason = substr($_->cust_credit->reason,0,32);
759 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
760 $reason = " ($reason) " if $reason;
762 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
764 $money_char. sprintf("%10.2f",$_->amount)
767 #foreach ( @cr_cust_credit ) {
769 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
770 # $money_char. sprintf("%10.2f",$_->credited)
774 #get & print payments
775 foreach ( $self->cust_bill_pay ) {
777 #something more elaborate if $_->amount ne ->cust_pay->paid ?
780 "Payment received ". time2str("%x",$_->cust_pay->_date ),
781 $money_char. sprintf("%10.2f",$_->amount )
786 push @buf,['','-----------'];
787 push @buf,['Balance Due', $money_char.
788 sprintf("%10.2f", $balance_due ) ];
791 my $templatefile = 'invoice_template';
792 $templatefile .= "_$template" if $template;
793 my @invoice_template = $conf->config($templatefile)
794 or die "cannot load config file $templatefile";
797 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
798 /invoice_lines\((\d*)\)/;
799 $invoice_lines += $1 || scalar(@buf);
802 die "no invoice_lines() functions in template?" unless $wasfunc;
803 my $invoice_template = new Text::Template (
805 SOURCE => [ map "$_\n", @invoice_template ],
806 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
807 $invoice_template->compile()
808 or die "can't compile template: $Text::Template::ERROR";
810 #setup template variables
811 package FS::cust_bill::_template; #!
812 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
814 $invnum = $self->invnum;
815 $date = $self->_date;
817 $agent = $self->cust_main->agent->agent;
819 if ( $FS::cust_bill::invoice_lines ) {
821 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
823 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
828 #format address (variable for the template)
830 @address = ( '', '', '', '', '', '' );
831 package FS::cust_bill; #!
832 $FS::cust_bill::_template::address[$l++] =
834 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
835 ? " (P.O. #". $cust_main->payinfo. ")"
839 $FS::cust_bill::_template::address[$l++] = $cust_main->company
840 if $cust_main->company;
841 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
842 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
843 if $cust_main->address2;
844 $FS::cust_bill::_template::address[$l++] =
845 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
846 $FS::cust_bill::_template::address[$l++] = $cust_main->country
847 unless $cust_main->country eq 'US';
849 # #overdue? (variable for the template)
850 # $FS::cust_bill::_template::overdue = (
852 # && $today > $self->_date
853 ## && $self->printed > 1
854 # && $self->printed > 0
857 #and subroutine for the template
858 sub FS::cust_bill::_template::invoice_lines {
859 my $lines = shift || scalar(@buf);
861 scalar(@buf) ? shift @buf : [ '', '' ];
867 $FS::cust_bill::_template::page = 1;
871 push @collect, split("\n",
872 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
874 $FS::cust_bill::_template::page++;
877 map "$_\n", @collect;
887 print_text formatting (and some logic :/) is in source, but needs to be
888 slurped in from a file. Also number of lines ($=).
890 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
891 or something similar so the look can be completely customized?)
895 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
896 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base