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];
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 push @buf,['','-----------'];
786 push @buf,['Balance Due', $money_char.
787 sprintf("%10.2f", $balance_due ) ];
790 my $templatefile = 'invoice_template';
791 $templatefile .= "_$template" if $template;
792 my @invoice_template = $conf->config($templatefile)
793 or die "cannot load config file $templatefile";
796 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
797 /invoice_lines\((\d*)\)/;
798 $invoice_lines += $1 || scalar(@buf);
801 die "no invoice_lines() functions in template?" unless $wasfunc;
802 my $invoice_template = new Text::Template (
804 SOURCE => [ map "$_\n", @invoice_template ],
805 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
806 $invoice_template->compile()
807 or die "can't compile template: $Text::Template::ERROR";
809 #setup template variables
810 package FS::cust_bill::_template; #!
811 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
813 $invnum = $self->invnum;
814 $date = $self->_date;
816 $agent = $self->cust_main->agent->agent;
818 if ( $FS::cust_bill::invoice_lines ) {
820 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
822 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
827 #format address (variable for the template)
829 @address = ( '', '', '', '', '', '' );
830 package FS::cust_bill; #!
831 $FS::cust_bill::_template::address[$l++] =
833 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
834 ? " (P.O. #". $cust_main->payinfo. ")"
838 $FS::cust_bill::_template::address[$l++] = $cust_main->company
839 if $cust_main->company;
840 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
841 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
842 if $cust_main->address2;
843 $FS::cust_bill::_template::address[$l++] =
844 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
845 $FS::cust_bill::_template::address[$l++] = $cust_main->country
846 unless $cust_main->country eq 'US';
848 # #overdue? (variable for the template)
849 # $FS::cust_bill::_template::overdue = (
851 # && $today > $self->_date
852 ## && $self->printed > 1
853 # && $self->printed > 0
856 #and subroutine for the template
857 sub FS::cust_bill::_template::invoice_lines {
858 my $lines = shift || scalar(@buf);
860 scalar(@buf) ? shift @buf : [ '', '' ];
866 $FS::cust_bill::_template::page = 1;
870 push @collect, split("\n",
871 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
873 $FS::cust_bill::_template::page++;
876 map "$_\n", @collect;
886 print_text formatting (and some logic :/) is in source, but needs to be
887 slurped in from a file. Also number of lines ($=).
889 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
890 or something similar so the look can be completely customized?)
894 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
895 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base