X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=3e767d9f0277f9cb844d8294c2d79215127799f5;hb=a20987ec5ea35450afbf311829e48e507099dad4;hp=e64d666bcbf989ee4af1dd12716dabd9d9bb2970;hpb=bbc49345e8101dae7311cb3f4043bc6d0775d0ca;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index e64d666bc..3e767d9f0 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -29,6 +29,7 @@ use FS::cust_pkg; use FS::cust_svc; use FS::cust_bill; use FS::cust_bill_pkg; +use FS::cust_bill_pkg_display; use FS::cust_pay; use FS::cust_pay_pending; use FS::cust_pay_void; @@ -226,6 +227,8 @@ Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit nu =item spool_cdr - Enable individual CDR spooling, empty or `Y' +=item dundate - a suggestion to events (see L) to delay until this unix timestamp + =item squelch_cdr - Discourage individual CDR printing, empty or `Y' =back @@ -340,6 +343,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; @@ -416,6 +422,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; @@ -1230,6 +1265,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: /; @@ -1961,7 +1997,12 @@ sub bill_and_collect { $self->ncancelled_pkgs; foreach my $cust_pkg ( @cancel_pkgs ) { - my $error = $cust_pkg->cancel; + my $cpr = $cust_pkg->last_cust_pkg_reason('expire'); + my $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum, + 'reason_otaker' => $cpr->otaker + ) + : () + ); warn "Error cancelling expired pkg ". $cust_pkg->pkgnum. " for custnum ". $self->custnum. ": $error" if $error; @@ -1987,7 +2028,14 @@ sub bill_and_collect { $self->ncancelled_pkgs; foreach my $cust_pkg ( @susp_pkgs ) { - my $error = $cust_pkg->suspend; + my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn') + if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T); + my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum, + 'reason_otaker' => $cpr->otaker + ) + : () + ); + warn "Error suspending package ". $cust_pkg->pkgnum. " for custnum ". $self->custnum. ": $error" if $error; @@ -2132,7 +2180,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, @@ -2346,9 +2399,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 @@ -2365,42 +2422,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)); } @@ -2459,10 +2524,10 @@ sub _make_lines { 'details' => \@details, }; - if ( $part_pkg->option('recur_temporality') eq 'preceding' ) { + if ( $part_pkg->option('recur_temporality', 1) eq 'preceding' ) { $cust_bill_pkg->sdate( $hash{last_bill} ); $cust_bill_pkg->edate( $sdate - 86399 ); #60s*60m*24h-1 - } else { #if ( $part_pkg->option('recur_temporality') eq 'upcoming' ) { + } else { #if ( $part_pkg->option('recur_temporality', 1) eq 'upcoming' ) { $cust_bill_pkg->sdate( $sdate ); $cust_bill_pkg->edate( $cust_pkg->bill ); } @@ -2567,89 +2632,40 @@ sub _handle_taxes { } #if $conf->exists('enable_taxproducts') ... - my $section = $cust_pkg->part_pkg->option('usage_section', 'Hush!') - if $cust_pkg->part_pkg->option('separate_usage', 'Hush!' ); - my $want_duplicate = - $cust_pkg->part_pkg->option('summarize_usage', 'Hush!') && - $cust_pkg->part_pkg->option('usage_section', 'Hush!'); + my @display = (); + if ( $conf->exists('separate_usage') ) { + my $section = $cust_pkg->part_pkg->option('usage_section', 'Hush!'); + my $summary = $cust_pkg->part_pkg->option('summarize_usage', 'Hush!'); + push @display, new FS::cust_bill_pkg_display { type => 'S' }; + push @display, new FS::cust_bill_pkg_display { type => 'R' }; + push @display, new FS::cust_bill_pkg_display { type => 'U', + section => $section + }; + if ($section && $summary) { + $display[2]->post_total('Y'); + push @display, new FS::cust_bill_pkg_display { type => 'U', + summary => 'Y', + } + } + } + $cust_bill_pkg->set('display', \@display); -#BUNK. DO NOT CREATE DUPLICATE cust_bill_pkg!!!!!!!!!!!! -# -# # XXX this mostly goes away with cust_bill_pkg refactor -# -# $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup; -# $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur; -# -# -# #split setup and recur -# if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) { -# my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash }; -# $cust_bill_pkg_recur->details($cust_bill_pkg-> -# $cust_bill_pkg_recur->setup(0); -# $cust_bill_pkg_recur->unitsetup(0); -# $cust_bill_pkg{recur} = $cust_bill_pkg_recur; -# -# $cust_bill_pkg->set('details', []); -# $cust_bill_pkg->recur(0); -# $cust_bill_pkg->unitrecur(0); -# $cust_bill_pkg->type(''); -# } -# -# #split usage from recur -# my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage ); -# warn "usage is $usage\n" if $DEBUG; -# if ($usage) { -# my $cust_bill_pkg_usage = -# new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash }; -# $cust_bill_pkg_usage->recur( $usage ); -# $cust_bill_pkg_usage->type( 'U' ); -# $cust_bill_pkg_usage->duplicate( $want_duplicate ? 'Y' : '' ); -# $cust_bill_pkg_usage->section( $section ); -# $cust_bill_pkg_usage->post_total( $want_duplicate ? 'Y' : '' ); -# my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage ); -# $cust_bill_pkg{recur}->recur( $recur ); -# $cust_bill_pkg{recur}->type( '' ); -# $cust_bill_pkg{recur}->set('details', []); -# $cust_bill_pkg{''} = $cust_bill_pkg_usage; -# } -# -# #subdivide usage by usage_class -# if (exists($cust_bill_pkg{''})) { -# foreach my $class (grep {$_ && $_ ne 'setup' && $_ ne 'recur' } @classes) { -# my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) ); -# my $cust_bill_pkg_usage = -# new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash }; -# $cust_bill_pkg_usage->recur( $usage ); -# $cust_bill_pkg_usage->set('details', []); -# my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage ); -# $cust_bill_pkg{''}->recur( $classless ); -# $cust_bill_pkg{$class} = $cust_bill_pkg_usage; -# } -# delete $cust_bill_pkg{''} unless $cust_bill_pkg{''}->recur; -# } -# -# foreach my $key (keys %cust_bill_pkg) { -# my @taxes = @{ $taxes{$key} }; -# my $cust_bill_pkg = $cust_bill_pkg{$key}; -# -# foreach my $tax ( @taxes ) { -# my $taxname = ref( $tax ). ' '. $tax->taxnum; -# if ( exists( $taxlisthash->{ $taxname } ) ) { -# push @{ $taxlisthash->{ $taxname } }, $cust_bill_pkg; -# }else{ -# $taxlisthash->{ $taxname } = [ $tax, $cust_bill_pkg ]; -# } -# } -# } -# -# # sort setup,recur,'', and the rest numeric && return -# my @result = map { $cust_bill_pkg{$_} } -# sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/); -# ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a ) -# } -# keys %cust_bill_pkg; -# -# \@result; + my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate; + foreach my $key (keys %tax_cust_bill_pkg) { + my @taxes = @{ $taxes{$key} }; + my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key}; + + foreach my $tax ( @taxes ) { + my $taxname = ref( $tax ). ' '. $tax->taxnum; + if ( exists( $taxlisthash->{ $taxname } ) ) { + push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg; + }else{ + $taxlisthash->{ $taxname } = [ $tax, $tax_cust_bill_pkg ]; + } + } + } + + ''; } sub _gather_taxes { @@ -3012,14 +3028,16 @@ sub due_cust_event { # 3: insert ## - foreach my $cust_event ( @cust_event ) { + unless( $opt{testonly} ) { + foreach my $cust_event ( @cust_event ) { - my $error = $cust_event->insert(); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } + my $error = $cust_event->insert(); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } } $dbh->commit or die $dbh->errstr if $oldAutoCommit; @@ -5078,6 +5096,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 @@ -5166,15 +5200,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; } @@ -5965,22 +6000,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 } ); @@ -6370,6 +6411,9 @@ sub process_batch_import { =cut +use FS::svc_acct; +use FS::svc_external; + #some false laziness w/cdr.pm now sub batch_import { my $param = shift; @@ -6417,6 +6461,18 @@ sub batch_import { svc_acct.username svc_acct._password ); $payby = 'BILL'; + } elsif ( $format eq 'svc_external' ) { + @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 cust_pkg.bill + svc_external.id svc_external.title + ); + $payby = 'BILL'; } else { die "unknown format $format"; } @@ -6505,7 +6561,7 @@ sub batch_import { ); my $billtime = time; my %cust_pkg = ( pkgpart => $pkgpart ); - my %svc_acct = (); + my %svc_x = (); foreach my $field ( @fields ) { if ( $field =~ /^cust_pkg\.(pkgpart|setup|bill|susp|adjourn|expire|cancel)$/ ) { @@ -6521,7 +6577,11 @@ sub batch_import { } elsif ( $field =~ /^svc_acct\.(username|_password)$/ ) { - $svc_acct{$1} = shift @columns; + $svc_x{$1} = shift @columns; + + } elsif ( $field =~ /^svc_external\.(id|title)$/ ) { + + $svc_x{$1} = shift @columns; } else { @@ -6569,18 +6629,25 @@ sub batch_import { if ( $cust_pkg{'pkgpart'} ) { my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ); - my @svc_acct = (); - if ( $svc_acct{'username'} ) { + my @svc_x = (); + my $svcdb = ''; + if ( $svc_x{'username'} ) { + $svcdb = 'svc_acct'; + } elsif ( $svc_x{'id'} || $svc_x{'title'} ) { + $svcdb = 'svc_external'; + } + if ( $svcdb ) { 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 ) + $svc_x{svcpart} = $part_pkg->svcpart( $svcdb ); + my $class = "FS::$svcdb"; + push @svc_x, $class->new( \%svc_x ); } - $hash{$cust_pkg} = \@svc_acct; + $hash{$cust_pkg} = \@svc_x; } my $error = $cust_main->insert( \%hash, $invoicing_list ); @@ -6988,7 +7055,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