X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=83748be1bb14d8f7b813e29e5308e492b59930c2;hb=81978af92ecdaaefeff5156d9ab3b4f99586df1c;hp=25f9c495a210c3d49588fff1cdb9d54d7d8989b5;hpb=06a1de4f0969dbbe1a689c1185393e664cdb907f;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 25f9c495a..83748be1b 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1,24 +1,20 @@ package FS::cust_bill; +use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::Record ); use strict; -use vars qw( @ISA $DEBUG $me $conf - $money_char $date_format $rdate_format $date_format_long ); -use vars qw( $invoice_lines @buf ); #yuck +use vars qw( $DEBUG $me $date_format ); + # but NOT $conf use Fcntl qw(:flock); #for spool_csv use Cwd; use List::Util qw(min max); use Date::Format; -use Text::Template 1.20; use File::Temp 0.14; -use String::ShellQuote; use HTML::Entities; -use Locale::Country; use Storable qw( freeze thaw ); use GD::Barcode; use FS::UID qw( datasrc ); -use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print ); +use FS::Misc qw( send_email send_fax do_print ); use FS::Record qw( qsearch qsearchs dbh ); -use FS::cust_main_Mixin; use FS::cust_main; use FS::cust_statement; use FS::cust_bill_pkg; @@ -41,19 +37,16 @@ use FS::bill_batch; use FS::cust_bill_batch; use FS::cust_bill_pay_pkg; use FS::cust_credit_bill_pkg; - -@ISA = qw( FS::cust_main_Mixin FS::Record ); +use FS::discount_plan; +use FS::L10N; $DEBUG = 0; $me = '[FS::cust_bill]'; #ask FS::UID to run this stuff for us later FS::UID->install_callback( sub { - $conf = new FS::Conf; - $money_char = $conf->config('money_char') || '$'; + my $conf = new FS::Conf; #global $date_format = $conf->config('date_format') || '%x'; #/YY - $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY - $date_format_long = $conf->config('date_format_long') || '%b %o, %Y'; } ); =head1 NAME @@ -140,6 +133,8 @@ Specific use cases =item agent_invid - legacy invoice number +=item promised_date - customer promised payment date, for collection + =back =head1 METHODS @@ -155,6 +150,7 @@ Invoices are normally created by calling the bill method of a customer object =cut sub table { 'cust_bill'; } +sub notice_name { 'Invoice'; } sub cust_linked { $_[0]->cust_main_custnum; } sub cust_unlinked_msg { @@ -240,7 +236,6 @@ sub delete { cust_event cust_credit_bill cust_bill_pay - cust_bill_pay cust_credit_bill cust_pay_batch cust_bill_pay_batch @@ -364,6 +359,7 @@ cust_bill-default_agent_invid is set and it has a value, invnum otherwise. sub display_invnum { my $self = shift; + my $conf = $self->conf; if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){ return $self->agent_invid; } else { @@ -382,8 +378,10 @@ sub previous { my $self = shift; my $total = 0; my @cust_bill = sort { $a->_date <=> $b->_date } - grep { $_->owed != 0 && $_->_date < $self->_date } - qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) + grep { $_->owed != 0 } + qsearch( 'cust_bill', { 'custnum' => $self->custnum, + '_date' => { op=>'<', value=>$self->_date }, + } ) ; foreach ( @cust_bill ) { $total += $_->owed; } $total, @cust_bill; @@ -745,6 +743,18 @@ sub cust_bill_batch { qsearch('cust_bill_batch', { 'invnum' => $self->invnum }); } +=item discount_plans + +Returns all discount plans (L) for this invoice, as a +hash keyed by term length. + +=cut + +sub discount_plans { + my $self = shift; + FS::discount_plan->all($self); +} + =item tax Returns the tax amount (see L) for this invoice. @@ -793,6 +803,23 @@ sub owed_pkgnum { $balance; } +=item hide + +Returns true if this invoice should be hidden. See the +selfservice-hide_invoices-taxclass configuraiton setting. + +=cut + +sub hide { + my $self = shift; + my $conf = $self->conf; + my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass') + or return ''; + my @cust_bill_pkg = $self->cust_bill_pkg; + my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg; + ! grep { $_->taxclass ne $hide_taxclass } @part_pkg; +} + =item apply_payments_and_credits [ OPTION => VALUE ... ] Applies unapplied payments and credits to this invoice. @@ -807,6 +834,7 @@ If there is an error, returns the error, otherwise returns false. sub apply_payments_and_credits { my( $self, %options ) = @_; + my $conf = $self->conf; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -955,6 +983,7 @@ sub generate_email { my $self = shift; my %args = @_; + my $conf = $self->conf; my $me = '[FS::cust_bill::generate_email]'; @@ -989,7 +1018,7 @@ sub generate_email { my $alternative = build MIME::Entity 'Type' => 'multipart/alternative', - 'Encoding' => '7bit', + #'Encoding' => '7bit', 'Disposition' => 'inline' ; @@ -1017,47 +1046,60 @@ sub generate_email { $alternative->attach( 'Type' => 'text/plain', - #'Encoding' => 'quoted-printable', - 'Encoding' => '7bit', + 'Encoding' => 'quoted-printable', + #'Encoding' => '7bit', 'Data' => $data, 'Disposition' => 'inline', ); - $args{'from'} =~ /\@([\w\.\-]+)/; - my $from = $1 || 'example.com'; - my $content_id = join('.', rand()*(2**32), $$, time). "\@$from"; - - my $logo; - my $agentnum = $cust_main->agentnum; - if ( defined($args{'template'}) && length($args{'template'}) - && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum ) - ) - { - $logo = 'logo_'. $args{'template'}. '.png'; + + my $htmldata; + my $image = ''; + my $barcode = ''; + if ( $conf->exists('invoice_email_pdf') + and scalar($conf->config('invoice_email_pdf_note')) ) { + + $htmldata = join('
', $conf->config('invoice_email_pdf_note') ); + } else { - $logo = "logo.png"; - } - my $image_data = $conf->config_binary( $logo, $agentnum); - - my $image = build MIME::Entity - 'Type' => 'image/png', - 'Encoding' => 'base64', - 'Data' => $image_data, - 'Filename' => 'logo.png', - 'Content-ID' => "<$content_id>", - ; + + $args{'from'} =~ /\@([\w\.\-]+)/; + my $from = $1 || 'example.com'; + my $content_id = join('.', rand()*(2**32), $$, time). "\@$from"; + + my $logo; + my $agentnum = $cust_main->agentnum; + if ( defined($args{'template'}) && length($args{'template'}) + && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum ) + ) + { + $logo = 'logo_'. $args{'template'}. '.png'; + } else { + $logo = "logo.png"; + } + my $image_data = $conf->config_binary( $logo, $agentnum); + + $image = build MIME::Entity + 'Type' => 'image/png', + 'Encoding' => 'base64', + 'Data' => $image_data, + 'Filename' => 'logo.png', + 'Content-ID' => "<$content_id>", + ; - my $barcode; - if($conf->exists('invoice-barcode')){ - my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from"; - $barcode = build MIME::Entity - 'Type' => 'image/png', - 'Encoding' => 'base64', - 'Data' => $self->invoice_barcode(0), - 'Filename' => 'barcode.png', - 'Content-ID' => "<$barcode_content_id>", - ; - $opt{'barcode_cid'} = $barcode_content_id; + if ($conf->exists('invoice-barcode')) { + my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from"; + $barcode = build MIME::Entity + 'Type' => 'image/png', + 'Encoding' => 'base64', + 'Data' => $self->invoice_barcode(0), + 'Filename' => 'barcode.png', + 'Content-ID' => "<$barcode_content_id>", + ; + $opt{'barcode_cid'} = $barcode_content_id; + } + + $htmldata = $self->print_html({ 'cid'=>$content_id, %opt }); } $alternative->attach( @@ -1070,7 +1112,7 @@ sub generate_email { ' ', ' ', ' ', - $self->print_html({ 'cid'=>$content_id, %opt }), + $htmldata, ' ', '', ], @@ -1078,6 +1120,7 @@ sub generate_email { #'Filename' => 'invoice.pdf', ); + my @otherparts = (); if ( $cust_main->email_csv_cdr ) { @@ -1116,7 +1159,7 @@ sub generate_email { $related->add_part($alternative); - $related->add_part($image); + $related->add_part($image) if $image; my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt); @@ -1132,11 +1175,10 @@ sub generate_email { # image/png $return{'content-type'} = 'multipart/related'; - if($conf->exists('invoice-barcode')){ - $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ]; - } - else { - $return{'mimeparts'} = [ $alternative, $image, @otherparts ]; + if ($conf->exists('invoice-barcode') && $barcode) { + $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ]; + } else { + $return{'mimeparts'} = [ $alternative, $image, @otherparts ]; } $return{'type'} = 'multipart/alternative'; #Content-Type of first part... #$return{'disposition'} = 'inline'; @@ -1240,6 +1282,7 @@ sub queueable_send { sub send { my $self = shift; + my $conf = $self->conf; my( $template, $invoice_from, $notice_name ); my $agentnums = ''; @@ -1263,14 +1306,16 @@ sub send { $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/; } + my $cust_main = $self->cust_main; + return 'N/A' unless ! $agentnums - or grep { $_ == $self->cust_main->agentnum } @$agentnums; + or grep { $_ == $cust_main->agentnum } @$agentnums; return '' - unless $self->cust_main->total_owed_date($self->_date) > $balance_over; + unless $cust_main->total_owed_date($self->_date) > $balance_over; $invoice_from ||= $self->_agent_invoice_from || #XXX should go away - $conf->config('invoice_from', $self->cust_main->agentnum ); + $conf->config('invoice_from', $cust_main->agentnum ); my %opt = ( 'template' => $template, @@ -1278,11 +1323,12 @@ sub send { 'notice_name' => ( $notice_name || 'Invoice' ), ); - my @invoicing_list = $self->cust_main->invoicing_list; + my @invoicing_list = $cust_main->invoicing_list; #$self->email_invoice(\%opt) $self->email(\%opt) - if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list; + if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list ) + && ! $self->invoice_noemail; #$self->print_invoice(\%opt) $self->print(\%opt) @@ -1329,6 +1375,8 @@ sub queueable_email { #sub email_invoice { sub email { my $self = shift; + return if $self->hide; + my $conf = $self->conf; my( $template, $invoice_from, $notice_name, $no_coupon ); if ( ref($_[0]) ) { @@ -1378,6 +1426,7 @@ sub email { sub email_subject { my $self = shift; + my $conf = $self->conf; #my $template = scalar(@_) ? shift : ''; #per-template? @@ -1409,6 +1458,7 @@ I, if specified, overrides "Invoice" as the name of the sent docume sub lpr_data { my $self = shift; + my $conf = $self->conf; my( $template, $notice_name ); if ( ref($_[0]) ) { my $opt = shift; @@ -1444,6 +1494,9 @@ I, if specified, overrides "Invoice" as the name of the sent docume #sub print_invoice { sub print { my $self = shift; + return if $self->hide; + my $conf = $self->conf; + my( $template, $notice_name ); if ( ref($_[0]) ) { my $opt = shift; @@ -1483,6 +1536,9 @@ I, if specified, overrides "Invoice" as the name of the sent docume sub fax_invoice { my $self = shift; + return if $self->hide; + my $conf = $self->conf; + my( $template, $notice_name ); if ( ref($_[0]) ) { my $opt = shift; @@ -1520,14 +1576,37 @@ isn't an open batch, one will be created. sub batch_invoice { my ($self, $opt) = @_; - my $batch = FS::bill_batch->get_open_batch; + my $bill_batch = $self->get_open_bill_batch; my $cust_bill_batch = FS::cust_bill_batch->new({ - batchnum => $batch->batchnum, + batchnum => $bill_batch->batchnum, invnum => $self->invnum, }); return $cust_bill_batch->insert($opt); } +=item get_open_batch + +Returns the currently open batch as an FS::bill_batch object, creating a new +one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is +enabled) + +=cut + +sub get_open_bill_batch { + my $self = shift; + my $conf = $self->conf; + my $hashref = { status => 'O' }; + $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent') + ? $self->cust_main->agentnum + : ''; + my $batch = qsearchs('bill_batch', $hashref); + return $batch if $batch; + $batch = FS::bill_batch->new($hashref); + my $error = $batch->insert; + die $error if $error; + return $batch; +} + =item ftp_invoice [ TEMPLATENAME ] Sends this invoice data via FTP. @@ -1538,6 +1617,7 @@ TEMPLATENAME is unused? sub ftp_invoice { my $self = shift; + my $conf = $self->conf; my $template = scalar(@_) ? shift : ''; $self->send_csv( @@ -1560,6 +1640,7 @@ TEMPLATENAME is unused? sub spool_invoice { my $self = shift; + my $conf = $self->conf; my $template = scalar(@_) ? shift : ''; $self->spool_csv( @@ -1662,13 +1743,21 @@ Options are: =over 4 -=item format - 'default' or 'billco' +=item format - any of FS::Misc::::Invoicing::spool_formats -=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L). +=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the +customer has the corresponding invoice destinations set (see +L). -=item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file +=item agent_spools - if set to a true value, will spool to per-agent files +rather than a single global file -=item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount. +=item ftp_targetnum - if set to an FTP target (see L), will +append to that spool. L will then send the spool file to +that destination. + +=item balanceover - if set, only spools the invoice if the total amount owed on +this invoice and all older invoices is greater than the specified amount. =back @@ -1696,11 +1785,23 @@ sub spool_csv { my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time); - my $file = - "$spooldir/". - ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ). - ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) . - '.csv'; + my $file; + if ( $opt{'agent_spools'} ) { + $file = 'agentnum'.$cust_main->agentnum; + } else { + $file = 'spool'; + } + + if ( $opt{'ftp_targetnum'} ) { + $spooldir .= '/target'.$opt{'ftp_targetnum'}; + mkdir $spooldir, 0700 unless -d $spooldir; + } # otherwise it just goes into export.xxx/cust_bill + + if ( lc($opt{'format'}) eq 'billco' ) { + $file .= '-header'; + } + + $file = "$spooldir/$file.csv"; my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum ); @@ -1715,10 +1816,7 @@ sub spool_csv { flock(CSV, LOCK_UN); close CSV; - $file = - "$spooldir/". - ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ). - '-detail.csv'; + $file =~ s/-header.csv$/-detail.csv/; open(CSV,">>$file") or die "can't open $file: $!"; flock(CSV, LOCK_EX); @@ -1740,7 +1838,7 @@ Returns CSV data for this invoice. Options are: -format - 'default' or 'billco' +format - 'default', 'billco', 'oneline', 'bridgestone' Returns a list consisting of two scalars. The first is a single line of CSV header information for this invoice. The second is one or more lines of CSV @@ -1749,7 +1847,8 @@ detail information for this invoice. If I is not specified or "default", the fields of the CSV file are as follows: -record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate +record_type, invnum, custnum, _date, charged, first, last, company, address1, +address2, city, state, zip, country, pkg, setup, recur, sdate, edate =over 4 @@ -1854,6 +1953,26 @@ If I is "billco", the fields of the detail CSV file are as follows: 9 | Grouping Code | GROUP | CHAR | 2 10 | User Defined | ACCT CODE | CHAR | 15 +If format is 'oneline', there is no detail file. Each invoice has a +header line only, with the fields: + +Agent number, agent name, customer number, first name, last name, address +line 1, address line 2, city, state, zip, invoice date, invoice number, +amount charged, amount due, + +and then, for each line item, three columns containing the package number, +description, and amount. + +If format is 'bridgestone', there is no detail file. Each invoice has a +header line with the following fields in a fixed-width format: + +Customer number (in display format), date, name (first last), company, +address 1, address 2, city, state, zip. + +This is a mailing list format, and has no per-invoice fields. To avoid +sending redundant notices, the spooling event should have a "once" or +"once_percust_every" condition. + =cut sub print_csv { @@ -1919,6 +2038,62 @@ sub print_csv { '0', # 29 | Other Taxes & Fees*** NUM* 9 ); + } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name? + + my ($previous_balance) = $self->previous; + my $totaldue = sprintf('%.2f', $self->owed + $previous_balance); + my @items = map { + ($_->{pkgnum} || ''), + $_->{description}, + $_->{amount} + } $self->_items_pkg; + + $csv->combine( + $cust_main->agentnum, + $cust_main->agent->agent, + $self->custnum, + $cust_main->first, + $cust_main->last, + $cust_main->address1, + $cust_main->address2, + $cust_main->city, + $cust_main->state, + $cust_main->zip, + + # invoice fields + time2str("%x", $self->_date), + $self->invnum, + $self->charged, + $totaldue, + + @items, + ); + + } elsif ( lc($opt{'format'}) eq 'bridgestone' ) { + + # bypass the CSV stuff and just return this + my $longdate = time2str('%B %d, %Y', time); #current time, right? + my $zip = $cust_main->zip; + $zip =~ s/\D//; + my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum) + || ''; + return ( + sprintf( + "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n", + $prefix, + $cust_main->display_custnum, + $longdate, + uc(substr($cust_main->contact_firstlast,0,30)), + uc(substr($cust_main->company ,0,30)), + uc(substr($cust_main->address1 ,0,30)), + uc(substr($cust_main->address2 ,0,30)), + uc(substr($cust_main->city ,0,20)), + uc($cust_main->state), + $zip + ), + '' #detail + ); + } else { $csv->combine( @@ -1958,6 +2133,10 @@ sub print_csv { } + } elsif ( lc($opt{'format'}) eq 'oneline' ) { + + #do nothing + } else { foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { @@ -2069,6 +2248,7 @@ sub realtime_lec { sub realtime_bop { my( $self, $method ) = (shift,shift); + my $conf = $self->conf; my %opt = @_; my $cust_main = $self->cust_main; @@ -2135,141 +2315,6 @@ sub _agent_invoice_from { $self->cust_main->agent_invoice_from; } -=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ] - -Returns an text invoice, as a list of lines. - -Options can be passed as a hashref (recommended) or as a list of time, template -and then any key/value pairs for any other options. - -I