X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=5554f9f48770b31e5234e9bd21e8a790c7e85b1c;hb=54d73dfad0b27edd10ec7c917a96c88d45ad6789;hp=b348aaa7b49340c846c066dcf8bdea9e2b3db34f;hpb=f411189684b0ca7cc1b4b3e9be1895607245b595;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index b348aaa7b..5554f9f48 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -14,9 +14,7 @@ use Data::Dumper; use Tie::IxHash; use Digest::MD5 qw(md5_base64); use Date::Format; -use Date::Parse; #use Date::Manip; -use File::Slurp qw( slurp ); use File::Temp qw( tempfile ); use String::Approx qw(amatch); use Business::CreditCard 0.28; @@ -343,6 +341,9 @@ sub insert { $self->signupdate(time) unless $self->signupdate; + $self->auto_agent_custid() + if $conf->config('cust_main-auto_agent_custid') && ! $self->agent_custid; + my $error = $self->SUPER::insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -419,6 +420,35 @@ sub insert { } +use File::CounterFile; +sub auto_agent_custid { + my $self = shift; + + my $format = $conf->config('cust_main-auto_agent_custid'); + my $agent_custid; + if ( $format eq '1YMMXXXXXXXX' ) { + + my $counter = new File::CounterFile 'cust_main.agent_custid'; + $counter->lock; + + my $ym = 100000000000 + time2str('%y%m00000000', time); + if ( $ym > $counter->value ) { + $counter->{'value'} = $agent_custid = $ym; + $counter->{'updated'} = 1; + } else { + $agent_custid = $counter->inc; + } + + $counter->unlock; + + } else { + die "Unknown cust_main-auto_agent_custid format: $format"; + } + + $self->agent_custid($agent_custid); + +} + sub start_copy_skel { my $self = shift; @@ -1233,6 +1263,7 @@ sub check { || $self->ut_textn('stateid_state') || $self->ut_textn('invoice_terms') ; + #barf. need message catalogs. i18n. etc. $error .= "Please select an advertising source." if $error =~ /^Illegal or empty \(numeric\) refnum: /; @@ -2147,7 +2178,12 @@ sub bill { return "can't charge postal invoice fee for customer ". $self->custnum. ": $postal_pkg"; } - if ( $postal_pkg ) { + if ( $postal_pkg && + ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) || + !$conf->exists('postal_invoice-recurring_only') + ) + ) + { foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) { my $error = $self->_make_lines( 'part_pkg' => $part_pkg, @@ -2361,9 +2397,13 @@ sub _make_lines { my $recur = 0; my $unitrecur = 0; my $sdate; - if ( $part_pkg->getfield('freq') ne '0' && - ! $cust_pkg->getfield('susp') && - ( $cust_pkg->getfield('bill') || 0 ) <= $time + if ( ! $cust_pkg->getfield('susp') and + ( $part_pkg->getfield('freq') ne '0' && + ( $cust_pkg->getfield('bill') || 0 ) <= $time + ) + || ( $part_pkg->plan eq 'voip_cdr' + && $part_pkg->option('bill_every_call') + ) ) { # XXX should this be a package event? probably. events are called @@ -2380,42 +2420,50 @@ sub _make_lines { $sdate = $cust_pkg->bill || $cust_pkg->setup || $time; #over two params! lets at least switch to a hashref for the rest... - my %param = ( 'precommit_hooks' => $precommit_hooks, ); + my $increment_next_bill = ( $part_pkg->freq ne '0' + && ( $cust_pkg->getfield('bill') || 0 ) <= $time + ); + my %param = ( 'precommit_hooks' => $precommit_hooks, + 'increment_next_bill' => $increment_next_bill, + ); $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details, \%param ) }; return "$@ running calc_recur for $cust_pkg\n" if ( $@ ); + if ( $increment_next_bill ) { - #change this bit to use Date::Manip? CAREFUL with timezones (see - # mailing list archive) - my ($sec,$min,$hour,$mday,$mon,$year) = - (localtime($sdate) )[0,1,2,3,4,5]; - - #pro-rating magic - if $recur_prog fiddles $sdate, want to use that - # only for figuring next bill date, nothing else, so, reset $sdate again - # here - $sdate = $cust_pkg->bill || $cust_pkg->setup || $time; - #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill; - $cust_pkg->last_bill($sdate); - - if ( $part_pkg->freq =~ /^\d+$/ ) { - $mon += $part_pkg->freq; - until ( $mon < 12 ) { $mon -= 12; $year++; } - } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) { - my $weeks = $1; - $mday += $weeks * 7; - } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) { - my $days = $1; - $mday += $days; - } elsif ( $part_pkg->freq =~ /^(\d+)h$/ ) { - my $hours = $1; - $hour += $hours; - } else { - return "unparsable frequency: ". $part_pkg->freq; + #change this bit to use Date::Manip? CAREFUL with timezones (see + # mailing list archive) + my ($sec,$min,$hour,$mday,$mon,$year) = + (localtime($sdate) )[0,1,2,3,4,5]; + + #pro-rating magic - if $recur_prog fiddles $sdate, want to use that + # only for figuring next bill date, nothing else, so, reset $sdate again + # here + $sdate = $cust_pkg->bill || $cust_pkg->setup || $time; + #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill; + $cust_pkg->last_bill($sdate); + + if ( $part_pkg->freq =~ /^\d+$/ ) { + $mon += $part_pkg->freq; + until ( $mon < 12 ) { $mon -= 12; $year++; } + } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) { + my $weeks = $1; + $mday += $weeks * 7; + } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) { + my $days = $1; + $mday += $days; + } elsif ( $part_pkg->freq =~ /^(\d+)h$/ ) { + my $hours = $1; + $hour += $hours; + } else { + return "unparsable frequency: ". $part_pkg->freq; + } + $cust_pkg->setfield('bill', + timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year)); + } - $cust_pkg->setfield('bill', - timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year)); } @@ -2602,7 +2650,7 @@ sub _handle_taxes { my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate; foreach my $key (keys %tax_cust_bill_pkg) { - my @taxes = @{ $taxes{$key} }; + my @taxes = @{ $taxes{$key} || [] }; my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key}; foreach my $tax ( @taxes ) { @@ -5046,6 +5094,22 @@ sub cust_refund { qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) } +=item display_custnum + +Returns the displayed customer number for this customer: agent_custid if +cust_main-default_agent_custid is set and it has a value, custnum otherwise. + +=cut + +sub display_custnum { + my $self = shift; + if ( $conf->exists('cust_main-default_agent_custid') && $self->agent_custid ){ + return $self->agent_custid; + } else { + return $self->custnum; + } +} + =item name Returns a name string for this customer, either "Company (Last, First)" or @@ -5134,15 +5198,16 @@ sub geocode { my $extra_sql = "AND plus4lo <= '$plus4' AND plus4hi >= '$plus4'"; my $geocode = ''; - my $cust_tax_location = - qsearchs( { - 'table' => 'cust_tax_location', - 'hashref' => { 'zip' => $zip, 'data_vendor' => $data_vendor }, - 'extra_sql' => $extra_sql, - } - ); - $geocode = $cust_tax_location->geocode - if $cust_tax_location; + my @cust_tax_location = + qsearch( { + 'table' => 'cust_tax_location', + 'hashref' => { 'zip' => $zip, 'data_vendor' => $data_vendor }, + 'extra_sql' => $extra_sql, + 'order_by' => 'ORDER BY plus4hi',#overlapping with distinct ends + } + ); + $geocode = $cust_tax_location[0]->geocode + if scalar(@cust_tax_location); $geocode; } @@ -5933,22 +5998,28 @@ sub smart_search { # custnum search (also try agent_custid), with some tweaking options if your # legacy cust "numbers" have letters - } elsif ( $search =~ /^\s*(\d+)\s*$/ + } + + if ( $search =~ /^\s*(\d+)\s*$/ || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+' && $search =~ /^\s*(\w\w?\d+)\s*$/ ) ) { - push @cust_main, qsearch( { - 'table' => 'cust_main', - 'hashref' => { 'custnum' => $1, %options }, - 'extra_sql' => " AND $agentnums_sql", #agent virtualization - } ); + my $num = $1; + + if ( $num <= 2147483647 ) { #need a bigint custnum? wow. + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $num, %options }, + 'extra_sql' => " AND $agentnums_sql", #agent virtualization + } ); + } push @cust_main, qsearch( { 'table' => 'cust_main', - 'hashref' => { 'agent_custid' => $1, %options }, + 'hashref' => { 'agent_custid' => $num, %options }, 'extra_sql' => " AND $agentnums_sql", #agent virtualization } ); @@ -6282,322 +6353,6 @@ sub append_fuzzyfiles { 1; } -=item process_batch_import - -Load a batch import as a queued JSRPC job - -=cut - -use Storable qw(thaw); -use Data::Dumper; -use MIME::Base64; -sub process_batch_import { - my $job = shift; - - my $param = thaw(decode_base64(shift)); - warn Dumper($param) if $DEBUG; - - my $files = $param->{'uploaded_files'} - or die "No files provided.\n"; - - my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files; - - my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/'; - my $file = $dir. $files{'file'}; - - my $type; - if ( $file =~ /\.(\w+)$/i ) { - $type = lc($1); - } else { - #or error out??? - warn "can't parse file type from filename $file; defaulting to CSV"; - $type = 'csv'; - } - - my $error = - FS::cust_main::batch_import( { - job => $job, - file => $file, - type => $type, - custbatch => $param->{custbatch}, - agentnum => $param->{'agentnum'}, - refnum => $param->{'refnum'}, - pkgpart => $param->{'pkgpart'}, - #'fields' => [qw( cust_pkg.setup dayphone first last address1 address2 - # city state zip comments )], - 'format' => $param->{'format'}, - } ); - - unlink $file; - - die "$error\n" if $error; - -} - -=item batch_import - -=cut - -#some false laziness w/cdr.pm now -sub batch_import { - my $param = shift; - - my $job = $param->{job}; - - my $filename = $param->{file}; - my $type = $param->{type} || 'csv'; - - my $custbatch = $param->{custbatch}; - - my $agentnum = $param->{agentnum}; - my $refnum = $param->{refnum}; - my $pkgpart = $param->{pkgpart}; - - my $format = $param->{'format'}; - - my @fields; - my $payby; - if ( $format eq 'simple' ) { - @fields = qw( cust_pkg.setup dayphone first last - address1 address2 city state zip comments ); - $payby = 'BILL'; - } elsif ( $format eq 'extended' ) { - @fields = qw( agent_custid refnum - last first address1 address2 city state zip country - daytime night - ship_last ship_first ship_address1 ship_address2 - ship_city ship_state ship_zip ship_country - payinfo paycvv paydate - invoicing_list - cust_pkg.pkgpart - svc_acct.username svc_acct._password - ); - $payby = 'BILL'; - } elsif ( $format eq 'extended-plus_company' ) { - @fields = qw( agent_custid refnum - last first company address1 address2 city state zip country - daytime night - ship_last ship_first ship_company ship_address1 ship_address2 - ship_city ship_state ship_zip ship_country - payinfo paycvv paydate - invoicing_list - cust_pkg.pkgpart - svc_acct.username svc_acct._password - ); - $payby = 'BILL'; - } else { - die "unknown format $format"; - } - - my $count; - my $parser; - my @buffer = (); - if ( $type eq 'csv' ) { - - eval "use Text::CSV_XS;"; - die $@ if $@; - - $parser = new Text::CSV_XS; - - @buffer = split(/\r?\n/, slurp($filename) ); - $count = scalar(@buffer); - - } elsif ( $type eq 'xls' ) { - - eval "use Spreadsheet::ParseExcel;"; - die $@ if $@; - - my $excel = new Spreadsheet::ParseExcel::Workbook->Parse($filename); - $parser = $excel->{Worksheet}[0]; #first sheet - - $count = $parser->{MaxRow} || $parser->{MinRow}; - $count++; - - } else { - die "Unknown file type $type\n"; - } - - #my $columns; - - local $SIG{HUP} = 'IGNORE'; - local $SIG{INT} = 'IGNORE'; - local $SIG{QUIT} = 'IGNORE'; - local $SIG{TERM} = 'IGNORE'; - local $SIG{TSTP} = 'IGNORE'; - local $SIG{PIPE} = 'IGNORE'; - - my $oldAutoCommit = $FS::UID::AutoCommit; - local $FS::UID::AutoCommit = 0; - my $dbh = dbh; - - my $line; - my $row = 0; - my( $last, $min_sec ) = ( time, 5 ); #progressbar foo - while (1) { - - my @columns = (); - if ( $type eq 'csv' ) { - - last unless scalar(@buffer); - $line = shift(@buffer); - - $parser->parse($line) or do { - $dbh->rollback if $oldAutoCommit; - return "can't parse: ". $parser->error_input(); - }; - @columns = $parser->fields(); - - } elsif ( $type eq 'xls' ) { - - last if $row > ($parser->{MaxRow} || $parser->{MinRow}); - - my @row = @{ $parser->{Cells}[$row] }; - @columns = map $_->{Val}, @row; - - #my $z = 'A'; - #warn $z++. ": $_\n" for @columns; - - } else { - die "Unknown file type $type\n"; - } - - #warn join('-',@columns); - - my %cust_main = ( - custbatch => $custbatch, - agentnum => $agentnum, - refnum => $refnum, - country => $conf->config('countrydefault') || 'US', - payby => $payby, #default - paydate => '12/2037', #default - ); - my $billtime = time; - my %cust_pkg = ( pkgpart => $pkgpart ); - my %svc_acct = (); - foreach my $field ( @fields ) { - - if ( $field =~ /^cust_pkg\.(pkgpart|setup|bill|susp|adjourn|expire|cancel)$/ ) { - - #$cust_pkg{$1} = str2time( shift @$columns ); - if ( $1 eq 'pkgpart' ) { - $cust_pkg{$1} = shift @columns; - } elsif ( $1 eq 'setup' ) { - $billtime = str2time(shift @columns); - } else { - $cust_pkg{$1} = str2time( shift @columns ); - } - - } elsif ( $field =~ /^svc_acct\.(username|_password)$/ ) { - - $svc_acct{$1} = shift @columns; - - } else { - - #refnum interception - if ( $field eq 'refnum' && $columns[0] !~ /^\s*(\d+)\s*$/ ) { - - my $referral = $columns[0]; - my %hash = ( 'referral' => $referral, - 'agentnum' => $agentnum, - 'disabled' => '', - ); - - my $part_referral = qsearchs('part_referral', \%hash ) - || new FS::part_referral \%hash; - - unless ( $part_referral->refnum ) { - my $error = $part_referral->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "can't auto-insert advertising source: $referral: $error"; - } - } - - $columns[0] = $part_referral->refnum; - } - - my $value = shift @columns; - $cust_main{$field} = $value if length($value); - } - } - - $cust_main{'payby'} = 'CARD' - if defined $cust_main{'payinfo'} - && length $cust_main{'payinfo'}; - - my $invoicing_list = $cust_main{'invoicing_list'} - ? [ delete $cust_main{'invoicing_list'} ] - : []; - - my $cust_main = new FS::cust_main ( \%cust_main ); - - use Tie::RefHash; - tie my %hash, 'Tie::RefHash'; #this part is important - - if ( $cust_pkg{'pkgpart'} ) { - my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ); - - my @svc_acct = (); - if ( $svc_acct{'username'} ) { - my $part_pkg = $cust_pkg->part_pkg; - unless ( $part_pkg ) { - $dbh->rollback if $oldAutoCommit; - return "unknown pkgpart: ". $cust_pkg{'pkgpart'}; - } - $svc_acct{svcpart} = $part_pkg->svcpart( 'svc_acct' ); - push @svc_acct, new FS::svc_acct ( \%svc_acct ) - } - - $hash{$cust_pkg} = \@svc_acct; - } - - my $error = $cust_main->insert( \%hash, $invoicing_list ); - - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "can't insert customer". ( $line ? " for $line" : '' ). ": $error"; - } - - if ( $format eq 'simple' ) { - - #false laziness w/bill.cgi - $error = $cust_main->bill( 'time' => $billtime ); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "can't bill customer for $line: $error"; - } - - $error = $cust_main->apply_payments_and_credits; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "can't bill customer for $line: $error"; - } - - $error = $cust_main->collect(); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "can't collect customer for $line: $error"; - } - - } - - $row++; - - if ( $job && time - $min_sec > $last ) { #progress bar - $job->update_statustext( int(100 * $row / $count) ); - $last = time; - } - - } - - $dbh->commit or die $dbh->errstr if $oldAutoCommit;; - - return "Empty file!" unless $row; - - ''; #no error - -} - =item batch_charge =cut @@ -6956,7 +6711,7 @@ sub _agent_plandata { " AND action = 'cust_bill_send_agent' ". " AND ( disabled IS NULL OR disabled != 'Y' ) ". " AND peo_agentnum.optionname = 'agentnum' ". - " AND agentnum IS NULL OR agentnum = $agentnum ". + " AND ( agentnum IS NULL OR agentnum = $agentnum ) ". " ORDER BY CASE WHEN peo_cust_bill_age.optionname != 'cust_bill_age' THEN -1