X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=da1d3e1956d8757d710b5edf34eb3caa0bd3cbc5;hb=ec2059f7847d99e9218d97df988c8d68c7afcf55;hp=8b4181e6f3f8beb3c0cdc31eab2349dcf097ffff;hpb=43301b925a730e808e3c3292da9058a167102f56;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 8b4181e6f..da1d3e195 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -9,14 +9,12 @@ use Safe; use Carp; use Exporter; use Scalar::Util qw( blessed ); -use Time::Local qw(timelocal_nocheck); +use Time::Local qw(timelocal timelocal_nocheck); 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; @@ -29,6 +27,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; @@ -37,7 +36,9 @@ use FS::cust_credit; use FS::cust_refund; use FS::part_referral; use FS::cust_main_county; +use FS::tax_rate; use FS::cust_tax_location; +use FS::part_pkg_taxrate; use FS::agent; use FS::cust_main_invoice; use FS::cust_credit_bill; @@ -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; @@ -1229,7 +1264,9 @@ sub check { || $self->ut_textn('stateid') || $self->ut_textn('stateid_state') || $self->ut_textn('invoice_terms') + || $self->ut_alphan('geocode') ; + #barf. need message catalogs. i18n. etc. $error .= "Please select an advertising source." if $error =~ /^Illegal or empty \(numeric\) refnum: /; @@ -1961,7 +1998,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 +2029,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; @@ -2051,6 +2100,7 @@ sub bill { if $DEBUG; my $time = $options{'time'} || time; + my $invoice_time = $options{'invoice_time'} || $time; #put below somehow? local $SIG{HUP} = 'IGNORE'; @@ -2132,7 +2182,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, @@ -2156,7 +2211,11 @@ sub bill { foreach my $tax ( keys %taxlisthash ) { my $tax_object = shift @{ $taxlisthash{$tax} }; warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2; - my $listref_or_error = $tax_object->taxline( @{ $taxlisthash{$tax} } ); + my $listref_or_error = + $tax_object->taxline( $taxlisthash{$tax}, + 'custnum' => $self->custnum, + 'invoice_time' => $invoice_time + ); unless (ref($listref_or_error)) { $dbh->rollback if $oldAutoCommit; return $listref_or_error; @@ -2166,15 +2225,26 @@ sub bill { warn "adding ". $listref_or_error->[1]. " as ". $listref_or_error->[0]. "\n" if $DEBUG > 2; - $tax{ $tax_object->taxname } += $listref_or_error->[1]; + $tax{ $tax } += $listref_or_error->[1]; if ( $taxname{ $listref_or_error->[0] } ) { - push @{ $taxname{ $listref_or_error->[0] } }, $tax_object->taxname; + push @{ $taxname{ $listref_or_error->[0] } }, $tax; }else{ - $taxname{ $listref_or_error->[0] } = [ $tax_object->taxname ]; + $taxname{ $listref_or_error->[0] } = [ $tax ]; } } + #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit + my %packagemap = map { $_->pkgnum => $_ } @cust_bill_pkg; + foreach my $tax ( keys %taxlisthash ) { + foreach ( @{ $taxlisthash{$tax} }[1 ... scalar(@{ $taxlisthash{$tax} })] ) { + next unless ref($_) eq 'FS::cust_bill_pkg'; # shouldn't happen + + push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg }, + splice( @{ $_->_cust_tax_exempt_pkg } ); + } + } + #some taxes are taxed my %totlisthash; @@ -2194,9 +2264,9 @@ sub bill { # existing taxes warn "adding $totname to taxed taxes\n" if $DEBUG > 2; if ( exists( $totlisthash{ $totname } ) ) { - push @{ $totlisthash{ $totname } }, $tax{ $tax_object->taxname }; + push @{ $totlisthash{ $totname } }, $tax{ $tax }; }else{ - $totlisthash{ $totname } = [ $tot, $tax{ $tax_object->taxname } ]; + $totlisthash{ $totname } = [ $tot, $tax{ $tax } ]; } } } @@ -2206,7 +2276,11 @@ sub bill { my $tax_object = shift @{ $totlisthash{$tax} }; warn "found previously found taxed tax ". $tax_object->taxname. "\n" if $DEBUG > 2; - my $listref_or_error = $tax_object->taxline( @{ $totlisthash{$tax} } ); + my $listref_or_error = + $tax_object->taxline( $totlisthash{$tax}, + 'custnum' => $self->custnum, + 'invoice_time' => $invoice_time + ); unless (ref($listref_or_error)) { $dbh->rollback if $oldAutoCommit; return $listref_or_error; @@ -2215,7 +2289,7 @@ sub bill { warn "adding taxed tax amount ". $listref_or_error->[1]. " as ". $tax_object->taxname. "\n" if $DEBUG; - $tax{ $tax_object->taxname } += $listref_or_error->[1]; + $tax{ $tax } += $listref_or_error->[1]; } #consolidate and create tax line items @@ -2226,6 +2300,7 @@ sub bill { warn "adding $taxname\n" if $DEBUG > 1; foreach my $taxitem ( @{ $taxname{$taxname} } ) { $tax += $tax{$taxitem} unless $seen{$taxitem}; + $seen{$taxitem} = 1; warn "adding $tax{$taxitem}\n" if $DEBUG > 1; } next unless $tax; @@ -2249,7 +2324,7 @@ sub bill { #create the new invoice my $cust_bill = new FS::cust_bill ( { 'custnum' => $self->custnum, - '_date' => ( $options{'invoice_time'} || $time ), + '_date' => ( $invoice_time ), 'charged' => $charged, } ); my $error = $cust_bill->insert; @@ -2346,9 +2421,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 +2444,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 +2546,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 );2#60s*60m*24h-1 - } else { #if ( $part_pkg->option('recur_temporality') eq 'upcoming' ) { + $cust_bill_pkg->edate( $sdate - 86399 ); #60s*60m*24h-1 + } else { #if ( $part_pkg->option('recur_temporality', 1) eq 'upcoming' ) { $cust_bill_pkg->sdate( $sdate ); $cust_bill_pkg->edate( $cust_pkg->bill ); } @@ -2567,89 +2654,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 +3050,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; @@ -4230,7 +4270,9 @@ sub batch_card { die $error; } - my $unapplied = $self->total_credited + $self->total_unapplied_payments + $self->in_transit_payments; + my $unapplied = $self->total_unapplied_credits + + $self->total_unapplied_payments + + $self->in_transit_payments; foreach my $cust_bill ($self->open_cust_bill) { #$dbh->commit or die $dbh->errstr if $oldAutoCommit; my $cust_bill_pay_batch = new FS::cust_bill_pay_batch { @@ -4257,39 +4299,6 @@ sub batch_card { ''; } -=item total_owed - -Returns the total owed for this customer on all invoices -(see L). - -=cut - -sub total_owed { - my $self = shift; - $self->total_owed_date(2145859200); #12/31/2037 -} - -=item total_owed_date TIME - -Returns the total owed for this customer on all invoices with date earlier than -TIME. TIME is specified as a UNIX timestamp; see L). Also -see L and L for conversion functions. - -=cut - -sub total_owed_date { - my $self = shift; - my $time = shift; - my $total_bill = 0; - foreach my $cust_bill ( - grep { $_->_date <= $time } - qsearch('cust_bill', { 'custnum' => $self->custnum, } ) - ) { - $total_bill += $cust_bill->owed; - } - sprintf( "%.2f", $total_bill ); -} - =item apply_payments_and_credits Applies unapplied payments and credits. @@ -4359,7 +4368,7 @@ sub apply_credits { $self->select_for_update; #mutex - unless ( $self->total_credited ) { + unless ( $self->total_unapplied_credits ) { $dbh->commit or die $dbh->errstr if $oldAutoCommit; return 0; } @@ -4400,11 +4409,11 @@ sub apply_credits { } - my $total_credited = $self->total_credited; + my $total_unapplied_credits = $self->total_unapplied_credits; $dbh->commit or die $dbh->errstr if $oldAutoCommit; - return $total_credited; + return $total_unapplied_credits; } =item apply_payments @@ -4436,11 +4445,13 @@ sub apply_payments { #return 0 unless - my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 } - qsearch('cust_pay', { 'custnum' => $self->custnum } ) ); + my @payments = sort { $b->_date <=> $a->_date } + grep { $_->unapplied > 0 } + $self->cust_pay; - my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 } - qsearch('cust_bill', { 'custnum' => $self->custnum } ) ); + my @invoices = sort { $a->_date <=> $b->_date} + grep { $_->owed > 0 } + $self->cust_bill; my $payment; @@ -4479,21 +4490,72 @@ sub apply_payments { return $total_unapplied_payments; } -=item total_credited +=item total_owed + +Returns the total owed for this customer on all invoices +(see L). + +=cut + +sub total_owed { + my $self = shift; + $self->total_owed_date(2145859200); #12/31/2037 +} + +=item total_owed_date TIME + +Returns the total owed for this customer on all invoices with date earlier than +TIME. TIME is specified as a UNIX timestamp; see L). Also +see L and L for conversion functions. + +=cut + +sub total_owed_date { + my $self = shift; + my $time = shift; + my $total_bill = 0; + foreach my $cust_bill ( + grep { $_->_date <= $time } + qsearch('cust_bill', { 'custnum' => $self->custnum, } ) + ) { + $total_bill += $cust_bill->owed; + } + sprintf( "%.2f", $total_bill ); +} + +=item total_paid + +Returns the total amount of all payments. + +=cut + +sub total_paid { + my $self = shift; + my $total = 0; + $total += $_->paid foreach $self->cust_pay; + sprintf( "%.2f", $total ); +} + +=item total_unapplied_credits Returns the total outstanding credit (see L) for this customer. See L. +=item total_credited + +Old name for total_unapplied_credits. Don't use. + =cut sub total_credited { + #carp "total_credited deprecated, use total_unapplied_credits"; + shift->total_unapplied_credits(@_); +} + +sub total_unapplied_credits { my $self = shift; my $total_credit = 0; - foreach my $cust_credit ( qsearch('cust_credit', { - 'custnum' => $self->custnum, - } ) ) { - $total_credit += $cust_credit->credited; - } + $total_credit += $_->credited foreach $self->cust_credit; sprintf( "%.2f", $total_credit ); } @@ -4507,11 +4569,7 @@ See L. sub total_unapplied_payments { my $self = shift; my $total_unapplied = 0; - foreach my $cust_pay ( qsearch('cust_pay', { - 'custnum' => $self->custnum, - } ) ) { - $total_unapplied += $cust_pay->unapplied; - } + $total_unapplied += $_->unapplied foreach $self->cust_pay; sprintf( "%.2f", $total_unapplied ); } @@ -4525,18 +4583,14 @@ customer. See L. sub total_unapplied_refunds { my $self = shift; my $total_unapplied = 0; - foreach my $cust_refund ( qsearch('cust_refund', { - 'custnum' => $self->custnum, - } ) ) { - $total_unapplied += $cust_refund->unapplied; - } + $total_unapplied += $_->unapplied foreach $self->cust_refund; sprintf( "%.2f", $total_unapplied ); } =item balance Returns the balance for this customer (total_owed plus total_unrefunded, minus -total_credited minus total_unapplied_payments). +total_unapplied_credits minus total_unapplied_payments). =cut @@ -4545,7 +4599,7 @@ sub balance { sprintf( "%.2f", $self->total_owed + $self->total_unapplied_refunds - - $self->total_credited + - $self->total_unapplied_credits - $self->total_unapplied_payments ); } @@ -4566,7 +4620,7 @@ sub balance_date { sprintf( "%.2f", $self->total_owed_date($time) + $self->total_unapplied_refunds - - $self->total_credited + - $self->total_unapplied_credits - $self->total_unapplied_payments ); } @@ -4854,21 +4908,47 @@ sub referring_cust_main { qsearchs('cust_main', { 'custnum' => $self->referral_custnum } ); } -=item credit AMOUNT, REASON +=item credit AMOUNT, REASON [ , OPTION => VALUE ... ] Applies a credit to this customer. If there is an error, returns the error, otherwise returns false. +REASON can be a text string, an FS::reason object, or a scalar reference to +a reasonnum. If a text string, it will be automatically inserted as a new +reason, and a 'reason_type' option must be passed to indicate the +FS::reason_type for the new reason. + +An I option may be passed to set the credit's I field. + +Any other options are passed to FS::cust_credit::insert. + =cut sub credit { my( $self, $amount, $reason, %options ) = @_; + my $cust_credit = new FS::cust_credit { 'custnum' => $self->custnum, 'amount' => $amount, - 'reason' => $reason, }; + + if ( ref($reason) ) { + + if ( ref($reason) eq 'SCALAR' ) { + $cust_credit->reasonnum( $$reason ); + } else { + $cust_credit->reasonnum( $reason->reasonnum ); + } + + } else { + $cust_credit->set('reason', $reason) + } + + $cust_credit->addlinfo( delete $options{'addlinfo'} ) + if exists($options{'addlinfo'}); + $cust_credit->insert(%options); + } =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ] @@ -5078,6 +5158,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 @@ -5155,6 +5251,9 @@ Currently this only makes sense for "CCH" as DATA_VENDOR. sub geocode { my ($self, $data_vendor) = (shift, shift); #always cch for now + my $geocode = $self->get('geocode'); #XXX only one data_vendor for geocode + return $geocode if $geocode; + my $prefix = ( $conf->exists('tax-ship_address') && length($self->ship_last) ) ? 'ship_' : ''; @@ -5165,16 +5264,16 @@ sub geocode { #CCH specific location stuff 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; } @@ -5441,7 +5540,7 @@ sub balance_sql { " Returns an SQL fragment to retreive the balance for this customer, only considering invoices with date earlier than START_TIME, and optionally not -later than END_TIME (total_owed_date minus total_credited minus +later than END_TIME (total_owed_date minus total_unapplied_credits minus total_unapplied_payments). Times are specified as SQL fragments or numeric @@ -5965,22 +6064,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 } ); @@ -6314,322 +6419,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 @@ -6888,7 +6677,7 @@ sub generate_letter { $letter_data{company_name} = $conf->config('company_name'); - my $dir = $FS::UID::conf_dir."cache.". $FS::UID::datasrc; + my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc; my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX', DIR => $dir, SUFFIX => '.tex', @@ -6988,7 +6777,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