X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=038577a9b0844c12eb37cf49b1bf16eef3451435;hb=c8436a358075a901c8d7b3d47919de8c4d6f6f46;hp=8b57b934cc6bda2c6f60397a7c4066ed19b0fdc8;hpb=58f99accce35aa76abe9ff852f6c6ee84e8ce712;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 8b57b934c..038577a9b 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -9,7 +9,7 @@ use Safe; use Carp; use Exporter; use Scalar::Util qw( blessed ); -use Time::Local qw(timelocal_nocheck); +use Time::Local qw(timelocal); use Data::Dumper; use Tie::IxHash; use Digest::MD5 qw(md5_base64); @@ -28,6 +28,7 @@ use FS::cust_svc; use FS::cust_bill; use FS::cust_bill_pkg; use FS::cust_bill_pkg_display; +use FS::cust_bill_pkg_tax_location; use FS::cust_pay; use FS::cust_pay_pending; use FS::cust_pay_void; @@ -36,7 +37,10 @@ use FS::cust_credit; use FS::cust_refund; use FS::part_referral; use FS::cust_main_county; +use FS::cust_location; +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; @@ -133,101 +137,181 @@ FS::Record. The following fields are currently supported: =over 4 -=item custnum - primary key (assigned automatically for new customers) +=item custnum -=item agentnum - agent (see L) +Primary key (assigned automatically for new customers) -=item refnum - Advertising source (see L) +=item agentnum + +Agent (see L) + +=item refnum + +Advertising source (see L) + +=item first + +First name + +=item last -=item first - name +Last name -=item last - name +=item ss -=item ss - social security number (optional) +Cocial security number (optional) -=item company - (optional) +=item company + +(optional) =item address1 -=item address2 - (optional) +=item address2 + +(optional) =item city -=item county - (optional, see L) +=item county + +(optional, see L) + +=item state -=item state - (see L) +(see L) =item zip -=item country - (see L) +=item country + +(see L) + +=item daytime + +phone (optional) -=item daytime - phone (optional) +=item night -=item night - phone (optional) +phone (optional) -=item fax - phone (optional) +=item fax -=item ship_first - name +phone (optional) -=item ship_last - name +=item ship_first -=item ship_company - (optional) +Shipping first name + +=item ship_last + +Shipping last name + +=item ship_company + +(optional) =item ship_address1 -=item ship_address2 - (optional) +=item ship_address2 + +(optional) =item ship_city -=item ship_county - (optional, see L) +=item ship_county + +(optional, see L) + +=item ship_state -=item ship_state - (see L) +(see L) =item ship_zip -=item ship_country - (see L) +=item ship_country -=item ship_daytime - phone (optional) +(see L) -=item ship_night - phone (optional) +=item ship_daytime + +phone (optional) + +=item ship_night + +phone (optional) + +=item ship_fax + +phone (optional) + +=item payby -=item ship_fax - phone (optional) +Payment Type (See L for valid payby values) -=item payby - Payment Type (See L for valid payby values) +=item payinfo -=item payinfo - Payment Information (See L for data format) +Payment Information (See L for data format) -=item paymask - Masked payinfo (See L for how this works) +=item paymask + +Masked payinfo (See L for how this works) =item paycvv Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card -=item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy +=item paydate + +Expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy + +=item paystart_month + +Start date month (maestro/solo cards only) + +=item paystart_year + +Start date year (maestro/solo cards only) + +=item payissue + +Issue number (maestro/solo cards only) + +=item payname + +Name on card or billing name + +=item payip + +IP address from which payment information was received + +=item tax + +Tax exempt, empty or `Y' -=item paystart_month - start date month (maestro/solo cards only) +=item otaker -=item paystart_year - start date year (maestro/solo cards only) +Order taker (assigned automatically, see L) -=item payissue - issue number (maestro/solo cards only) +=item comments -=item payname - name on card or billing name +Comments (optional) -=item payip - IP address from which payment information was received +=item referral_custnum -=item tax - tax exempt, empty or `Y' +Referring customer number -=item otaker - order taker (assigned automatically, see L) +=item spool_cdr -=item comments - comments (optional) +Enable individual CDR spooling, empty or `Y' -=item referral_custnum - referring customer number +=item dundate -=item spool_cdr - Enable individual CDR spooling, empty or `Y' +A suggestion to events (see L) to delay until this unix timestamp -=item dundate - a suggestion to events (see L) to delay until this unix timestamp +=item squelch_cdr -=item squelch_cdr - Discourage individual CDR printing, empty or `Y' +Discourage individual CDR printing, empty or `Y' =back @@ -576,12 +660,115 @@ sub _copy_skel { } +=item order_pkg HASHREF | OPTION => VALUE ... + +Orders a single package. + +Options may be passed as a list of key/value pairs or as a hash reference. +Options are: + +=over 4 + +=item cust_pkg + +FS::cust_pkg object + +=item cust_location + +Optional FS::cust_location object + +=item svcs + +Optional arryaref of FS::svc_* service objects. + +=item depend_jobnum + +If this option is set to a job queue jobnum (see L), all provisioning +jobs will have a dependancy on the supplied job (they will not run until the +specific job completes). This can be used to defer provisioning until some +action completes (such as running the customer's credit card successfully). + +=back + +=cut + +sub order_pkg { + my $self = shift; + my $opt = ref($_[0]) ? shift : { @_ }; + + warn "$me order_pkg called with options ". + join(', ', map { "$_: $opt->{$_}" } keys %$opt ). "\n" + if $DEBUG; + + my $cust_pkg = $opt->{'cust_pkg'}; + my $seconds = $opt->{'seconds'}; + my $svcs = $opt->{'svcs'} || []; + + my %svc_options = (); + $svc_options{'depend_jobnum'} = $opt->{'depend_jobnum'} + if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'}; + + 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; + + if ( $opt->{'cust_location'} && + ( ! $cust_pkg->locationnum || $cust_pkg->locationnum == -1 ) ) { + my $error = $opt->{'cust_location'}->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "inserting cust_location (transaction rolled back): $error"; + } + $cust_pkg->locationnum($opt->{'cust_location'}->locationnum); + } + + $cust_pkg->custnum( $self->custnum ); + + my $error = $cust_pkg->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "inserting cust_pkg (transaction rolled back): $error"; + } + + foreach my $svc_something ( @{ $opt->{'svcs'} } ) { + if ( $svc_something->svcnum ) { + my $old_cust_svc = $svc_something->cust_svc; + my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash }; + $new_cust_svc->pkgnum( $cust_pkg->pkgnum); + $error = $new_cust_svc->replace($old_cust_svc); + } else { + $svc_something->pkgnum( $cust_pkg->pkgnum ); + if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) { + $svc_something->seconds( $svc_something->seconds + $$seconds ); + $$seconds = 0; + } + $error = $svc_something->insert(%svc_options); + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "inserting svc_ (transaction rolled back): $error"; + } + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; #no error + +} + =item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ] -Like the insert method on an existing record, this method orders a package -and included services atomicaly. Pass a Tie::RefHash data structure to this -method containing FS::cust_pkg and FS::svc_I objects. There should -be a better explanation of this, but until then, here's an example: +Like the insert method on an existing record, this method orders multiple +packages and included services atomicaly. Pass a Tie::RefHash data structure +to this method containing FS::cust_pkg and FS::svc_I objects. +There should be a better explanation of this, but until then, here's an +example: use Tie::RefHash; tie %hash, 'Tie::RefHash'; #this part is important @@ -614,9 +801,7 @@ sub order_pkgs { my $cust_pkgs = shift; my $seconds = shift; my %options = @_; - my %svc_options = (); - $svc_options{'depend_jobnum'} = $options{'depend_jobnum'} - if exists $options{'depend_jobnum'}; + warn "$me order_pkgs called with options ". join(', ', map { "$_: $options{$_}" } keys %options ). "\n" if $DEBUG; @@ -635,32 +820,17 @@ sub order_pkgs { local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'}; foreach my $cust_pkg ( keys %$cust_pkgs ) { - $cust_pkg->custnum( $self->custnum ); - my $error = $cust_pkg->insert; + + my $error = $self->order_pkg( 'cust_pkg' => $cust_pkg, + 'svcs' => $cust_pkgs->{$cust_pkg}, + 'seconds' => $seconds, + 'depend_jobnum' => $options{'depend_jobnum'}, + ); if ( $error ) { $dbh->rollback if $oldAutoCommit; - return "inserting cust_pkg (transaction rolled back): $error"; - } - foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) { - if ( $svc_something->svcnum ) { - my $old_cust_svc = $svc_something->cust_svc; - my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash }; - $new_cust_svc->pkgnum( $cust_pkg->pkgnum); - $error = $new_cust_svc->replace($old_cust_svc); - } else { - $svc_something->pkgnum( $cust_pkg->pkgnum ); - if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) { - $svc_something->seconds( $svc_something->seconds + $$seconds ); - $$seconds = 0; - } - $error = $svc_something->insert(%svc_options); - } - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - #return "inserting svc_ (transaction rolled back): $error"; - return $error; - } + return $error; } + } $dbh->commit or die $dbh->errstr if $oldAutoCommit; @@ -1262,6 +1432,7 @@ 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. @@ -1638,6 +1809,17 @@ sub cust_pkg { shift->all_pkgs(@_); } +=item cust_location + +Returns all locations (see L) for this customer. + +=cut + +sub cust_location { + my $self = shift; + qsearch('cust_location', { 'custnum' => $self->custnum } ); +} + =item ncancelled_pkgs Returns all non-cancelled packages (see L) for this customer. @@ -2097,6 +2279,7 @@ sub bill { if $DEBUG; my $time = $options{'time'} || time; + my $invoice_time = $options{'invoice_time'} || $time; #put below somehow? local $SIG{HUP} = 'IGNORE'; @@ -2120,9 +2303,7 @@ sub bill { ### my( $total_setup, $total_recur, $postal_charge ) = ( 0, 0, 0 ); - my %tax; my %taxlisthash; - my %taxname; my @precommit_hooks = (); my @cust_pkgs = qsearch('cust_pkg', { 'custnum' => $self->custnum } ); @@ -2204,26 +2385,65 @@ sub bill { } warn "having a look at the taxes we found...\n" if $DEBUG > 2; + + # keys are tax names (as printed on invoices / itemdesc ) + # values are listrefs of taxlisthash keys (internal identifiers) + my %taxname = (); + + # keys are taxlisthash keys (internal identifiers) + # values are (cumulative) amounts + my %tax = (); + + # keys are taxlisthash keys (internal identifiers) + # values are listrefs of cust_bill_pkg_tax_location hashrefs + my %tax_location = (); + 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} } ); - unless (ref($listref_or_error)) { + my $hashref_or_error = + $tax_object->taxline( $taxlisthash{$tax}, + 'custnum' => $self->custnum, + 'invoice_time' => $invoice_time + ); + unless ( ref($hashref_or_error) ) { $dbh->rollback if $oldAutoCommit; - return $listref_or_error; + return $hashref_or_error; } unshift @{ $taxlisthash{$tax} }, $tax_object; - warn "adding ". $listref_or_error->[1]. - " as ". $listref_or_error->[0]. "\n" - if $DEBUG > 2; - $tax{ $tax_object->taxname } += $listref_or_error->[1]; - if ( $taxname{ $listref_or_error->[0] } ) { - push @{ $taxname{ $listref_or_error->[0] } }, $tax_object->taxname; - }else{ - $taxname{ $listref_or_error->[0] } = [ $tax_object->taxname ]; + my $name = $hashref_or_error->{'name'}; + my $amount = $hashref_or_error->{'amount'}; + + #warn "adding $amount as $name\n"; + $taxname{ $name } ||= []; + push @{ $taxname{ $name } }, $tax; + + $tax{ $tax } += $amount; + + $tax_location{ $tax } ||= []; + if ( $tax_object->get('pkgnum') || $tax_object->get('locationnum') ) { + push @{ $tax_location{ $tax } }, + { + 'taxnum' => $tax_object->taxnum, + 'taxtype' => ref($tax_object), + 'pkgnum' => $tax_object->get('pkgnum'), + 'locationnum' => $tax_object->get('locationnum'), + 'amount' => sprintf('%.2f', $amount ), + }; + } + + } + + #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 @@ -2245,9 +2465,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 } ]; } } } @@ -2257,7 +2477,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; @@ -2266,7 +2490,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 @@ -2274,10 +2498,15 @@ sub bill { foreach my $taxname ( keys %taxname ) { my $tax = 0; my %seen = (); + my @cust_bill_pkg_tax_location = (); warn "adding $taxname\n" if $DEBUG > 1; foreach my $taxitem ( @{ $taxname{$taxname} } ) { - $tax += $tax{$taxitem} unless $seen{$taxitem}; + next if $seen{$taxitem}++; warn "adding $tax{$taxitem}\n" if $DEBUG > 1; + $tax += $tax{$taxitem}; + push @cust_bill_pkg_tax_location, + map { new FS::cust_bill_pkg_tax_location $_ } + @{ $tax_location{ $taxitem } }; } next unless $tax; @@ -2291,6 +2520,7 @@ sub bill { 'sdate' => '', 'edate' => '', 'itemdesc' => $taxname, + 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location, }; } @@ -2300,7 +2530,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; @@ -2432,36 +2662,19 @@ sub _make_lines { 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 + my $next_bill = $part_pkg->add_freq($sdate); + return "unparsable frequency: ". $part_pkg->freq + if $next_bill == -1; + + #pro-rating magic - if $recur_prog fiddled $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', $next_bill ); } @@ -2564,71 +2777,98 @@ sub _handle_taxes { my %cust_bill_pkg = (); my %taxes = (); - my $prefix = - ( $conf->exists('tax-ship_address') && length($self->ship_last) ) - ? 'ship_' - : ''; - my @classes; #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U'; push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage; push @classes, 'setup' if $cust_bill_pkg->setup; push @classes, 'recur' if $cust_bill_pkg->recur; - if ( $conf->exists('enable_taxproducts') - && (scalar($part_pkg->part_pkg_taxoverride) || $part_pkg->has_taxproduct) - && ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) - ) - { + if ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) { - foreach my $class (@classes) { - my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $prefix ); - return $err_or_ref unless ref($err_or_ref); - $taxes{$class} = $err_or_ref; - } + if ( $conf->exists('enable_taxproducts') + && ( scalar($part_pkg->part_pkg_taxoverride) + || $part_pkg->has_taxproduct + ) + ) + { - unless (exists $taxes{''}) { - my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $prefix ); - return $err_or_ref unless ref($err_or_ref); - $taxes{''} = $err_or_ref; - } + if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) { + return "fatal: Can't (yet) use tax-pkg_address with taxproducts"; + } - } elsif ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) { + foreach my $class (@classes) { + my $err_or_ref = $self->_gather_taxes( $part_pkg, $class ); + return $err_or_ref unless ref($err_or_ref); + $taxes{$class} = $err_or_ref; + } - my %taxhash = map { $_ => $self->get("$prefix$_") } - qw( state county country ); + unless (exists $taxes{''}) { + my $err_or_ref = $self->_gather_taxes( $part_pkg, '' ); + return $err_or_ref unless ref($err_or_ref); + $taxes{''} = $err_or_ref; + } - $taxhash{'taxclass'} = $part_pkg->taxclass; + } else { - my @taxes = qsearch( 'cust_main_county', \%taxhash ); + my @loc_keys = qw( state county country ); + my %taxhash; + if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) { + my $cust_location = $cust_pkg->cust_location; + %taxhash = map { $_ => $cust_location->$_() } @loc_keys; + } else { + my $prefix = + ( $conf->exists('tax-ship_address') && length($self->ship_last) ) + ? 'ship_' + : ''; + %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys; + } - unless ( @taxes ) { - $taxhash{'taxclass'} = ''; - @taxes = qsearch( 'cust_main_county', \%taxhash ); - } + $taxhash{'taxclass'} = $part_pkg->taxclass; - #one more try at a whole-country tax rate - unless ( @taxes ) { - $taxhash{$_} = '' foreach qw( state county ); - @taxes = qsearch( 'cust_main_county', \%taxhash ); - } + my @taxes = qsearch( 'cust_main_county', \%taxhash ); - $taxes{''} = [ @taxes ]; - $taxes{'setup'} = [ @taxes ]; - $taxes{'recur'} = [ @taxes ]; - $taxes{$_} = [ @taxes ] foreach (@classes); - - # maybe eliminate this entirely, along with all the 0% records - unless ( @taxes ) { - return - "fatal: can't find tax rate for state/county/country/taxclass ". - join('/', ( map $self->get("$prefix$_"), - qw(state county country) - ), - $part_pkg->taxclass ). "\n"; - } + my %taxhash_elim = %taxhash; + + # no, unexpected change in behavior. + #my @elim = qw( taxclass county state ); + #while ( !scalar(@taxes) && scalar(@elim) ) { + # $taxhash_elim{ shift(@elim) } = ''; + # @taxes = qsearch( 'cust_main_county', \%taxhash_elim ); + #} + + #just try taxclass first, then state+county, not county in the middle + unless ( @taxes ) { + $taxhash_elim{'taxclass'} = ''; + @taxes = qsearch( 'cust_main_county', \%taxhash_elim ); + } + #one more try at a whole-country tax rate + unless ( @taxes ) { + $taxhash_elim{$_} = '' foreach qw( state county ); + @taxes = qsearch( 'cust_main_county', \%taxhash_elim ); + } - } #if $conf->exists('enable_taxproducts') ... + if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) { + foreach (@taxes) { + $_->set('pkgnum', $cust_pkg->pkgnum ); + $_->set('locationnum', $cust_pkg->locationnum ); + } + } + + $taxes{''} = [ @taxes ]; + $taxes{'setup'} = [ @taxes ]; + $taxes{'recur'} = [ @taxes ]; + $taxes{$_} = [ @taxes ] foreach (@classes); + + # maybe eliminate this entirely, along with all the 0% records + unless ( @taxes ) { + return + "fatal: can't find tax rate for state/county/country/taxclass ". + join('/', map $taxhash{$_}, qw(state county country taxclass) ); + } + + } #if $conf->exists('enable_taxproducts') ... + + } my @display = (); if ( $conf->exists('separate_usage') ) { @@ -2654,7 +2894,12 @@ sub _handle_taxes { my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key}; foreach my $tax ( @taxes ) { - my $taxname = ref( $tax ). ' '. $tax->taxnum; + + my $taxname = ref( $tax ). ' taxnum'. $tax->taxnum; +# $taxname .= ' pkgnum'. $cust_pkg->pkgnum. +# ' locationnum'. $cust_pkg->locationnum +# if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum; + if ( exists( $taxlisthash->{ $taxname } ) ) { push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg; }else{ @@ -2670,7 +2915,6 @@ sub _gather_taxes { my $self = shift; my $part_pkg = shift; my $class = shift; - my $prefix = shift; my @taxes = (); my $geocode = $self->geocode('cch'); @@ -2698,12 +2942,11 @@ sub _gather_taxes { # maybe eliminate this entirely, along with all the 0% records unless ( @taxes ) { return - "fatal: can't find tax rate for zip/taxproduct/pkgpart ". - join('/', ( map $self->get("$prefix$_"), - qw(zip) - ), + "fatal: can't find tax rate for geocode/taxproduct/pkgpart ". + join('/', $geocode, $part_pkg->taxproduct_description, - $part_pkg->pkgpart ). "\n"; + $part_pkg->pkgpart + ); } warn "Found taxes ". @@ -2881,6 +3124,11 @@ Only return events for the specified eventtable (by default, events of all event Explicitly pass the objects to be tested (typically used with eventtable). +=item testonly + +Set to true to return the objects, but not actually insert them into the +database. + =back =cut @@ -2911,7 +3159,8 @@ sub due_cust_event { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - $self->select_for_update; #mutex + $self->select_for_update #mutex + unless $opt{testonly}; ### # 1: find possible events (initial search) @@ -3687,7 +3936,7 @@ sub realtime_bop { my $templ_hash = { error => $transaction->error_message }; my $error = send_email( - 'from' => $conf->config('invoice_from'), + 'from' => $conf->config('invoice_from', $self->agentnum ), 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ], 'subject' => 'Your payment could not be processed', 'body' => [ $template->fill_in(HASH => $templ_hash) ], @@ -5182,6 +5431,35 @@ sub ship_name { } } +=item name_short + +Returns a name string for this customer, either "Company" or "First Last". + +=cut + +sub name_short { + my $self = shift; + $self->company !~ /^\s*$/ ? $self->company : $self->contact_firstlast; +} + +=item ship_name_short + +Returns a name string for this (service/shipping) contact, either "Company" +or "First Last". + +=cut + +sub ship_name_short { + my $self = shift; + if ( $self->get('ship_last') ) { + $self->ship_company !~ /^\s*$/ + ? $self->ship_company + : $self->ship_contact_firstlast; + } else { + $self->name_company_or_firstlast; + } +} + =item contact Returns this customer's full (billing) contact name only, "Last, First" @@ -5206,6 +5484,30 @@ sub ship_contact { : $self->contact; } +=item contact_firstlast + +Returns this customers full (billing) contact name only, "First Last". + +=cut + +sub contact_firstlast { + my $self = shift; + $self->first. ' '. $self->get('last'); +} + +=item ship_contact_firstlast + +Returns this customer's full (shipping) contact name only, "First Last". + +=cut + +sub ship_contact_firstlast { + my $self = shift; + $self->get('ship_last') + ? $self->first. ' '. $self->get('ship_last') + : $self->contact_firstlast; +} + =item country_full Returns this customer's full country name @@ -5227,6 +5529,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_' : ''; @@ -5237,7 +5542,6 @@ sub geocode { #CCH specific location stuff my $extra_sql = "AND plus4lo <= '$plus4' AND plus4hi >= '$plus4'"; - my $geocode = ''; my @cust_tax_location = qsearch( { 'table' => 'cust_tax_location', @@ -5337,22 +5641,24 @@ sub tickets { my $num = $conf->config('cust_main-max_tickets') || 10; my @tickets = (); - unless ( $conf->config('ticket_system-custom_priority_field') ) { + if ( $conf->config('ticket_system') ) { + unless ( $conf->config('ticket_system-custom_priority_field') ) { - @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) }; + @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) }; - } else { + } else { - foreach my $priority ( - $conf->config('ticket_system-custom_priority_field-values'), '' - ) { - last if scalar(@tickets) >= $num; - push @tickets, - @{ FS::TicketSystem->customer_tickets( $self->custnum, - $num - scalar(@tickets), - $priority, - ) - }; + foreach my $priority ( + $conf->config('ticket_system-custom_priority_field-values'), '' + ) { + last if scalar(@tickets) >= $num; + push @tickets, + @{ FS::TicketSystem->customer_tickets( $self->custnum, + $num - scalar(@tickets), + $priority, + ) + }; + } } } (@tickets); @@ -6506,18 +6812,19 @@ I<$expdate> - the expiration of the customer payment in seconds from epoch =cut sub notify { - my ($customer, $template, %options) = @_; + my ($self, $template, %options) = @_; return unless $conf->exists($template); - my $from = $conf->config('invoice_from') if $conf->exists('invoice_from'); + my $from = $conf->config('invoice_from', $self->agentnum) + if $conf->exists('invoice_from', $self->agentnum); $from = $options{from} if exists($options{from}); - my $to = join(',', $customer->invoicing_list_emailonly); + my $to = join(',', $self->invoicing_list_emailonly); $to = $options{to} if exists($options{to}); - my $subject = "Notice from " . $conf->config('company_name') - if $conf->exists('company_name'); + my $subject = "Notice from " . $conf->config('company_name', $self->agentnum) + if $conf->exists('company_name', $self->agentnum); $subject = $options{subject} if exists($options{subject}); my $notify_template = new Text::Template (TYPE => 'ARRAY', @@ -6528,16 +6835,17 @@ sub notify { $notify_template->compile() or die "can't compile template: Text::Template::ERROR"; - $FS::notify_template::_template::company_name = $conf->config('company_name'); + $FS::notify_template::_template::company_name = + $conf->config('company_name', $self->agentnum); $FS::notify_template::_template::company_address = - join("\n", $conf->config('company_address') ). "\n"; - - my $paydate = $customer->paydate || '2037-12-31'; - $FS::notify_template::_template::first = $customer->first; - $FS::notify_template::_template::last = $customer->last; - $FS::notify_template::_template::company = $customer->company; - $FS::notify_template::_template::payinfo = $customer->mask_payinfo; - my $payby = $customer->payby; + join("\n", $conf->config('company_address', $self->agentnum) ). "\n"; + + my $paydate = $self->paydate || '2037-12-31'; + $FS::notify_template::_template::first = $self->first; + $FS::notify_template::_template::last = $self->last; + $FS::notify_template::_template::company = $self->company; + $FS::notify_template::_template::payinfo = $self->mask_payinfo; + my $payby = $self->payby; my ($payyear,$paymonth,$payday) = split (/-/,$paydate); my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear); @@ -6637,10 +6945,10 @@ sub generate_letter { ); if ( length($retadd) ) { $letter_data{returnaddress} = $retadd; - } elsif ( grep /\S/, $conf->config('company_address') ) { + } elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) { $letter_data{returnaddress} = join( '\\*'."\n", map s/( {2,})/'~' x length($1)/eg, - $conf->config('company_address') + $conf->config('company_address', $self->agentnum) ); } else { $letter_data{returnaddress} = '~'; @@ -6649,9 +6957,9 @@ sub generate_letter { $letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc"; - $letter_data{company_name} = $conf->config('company_name'); + $letter_data{company_name} = $conf->config('company_name', $self->agentnum); - 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', @@ -6699,6 +7007,8 @@ sub print { do_print [ $self->print_ps($template) ]; } +#these three subs should just go away once agent stuff is all config overrides + sub agent_template { my $self = shift; $self->_agent_plandata('agent_templatename'); @@ -6739,9 +7049,13 @@ sub _agent_plandata { AND peo_agentnum.optionname = 'agentnum' AND peo_agentnum.optionvalue }. $regexp. q{ '(^|,)}. $agentnum. q{(,|$)' ) - LEFT JOIN part_event_option AS peo_cust_bill_age - ON ( part_event.eventpart = peo_cust_bill_age.eventpart - AND peo_cust_bill_age.optionname = 'cust_bill_age' + LEFT JOIN part_event_condition + ON ( part_event.eventpart = part_event_condition.eventpart + AND part_event_condition.conditionname = 'cust_bill_age' + ) + LEFT JOIN part_event_condition_option + ON ( part_event_condition.eventconditionnum = part_event_condition_option.eventconditionnum + AND part_event_condition_option.optionname = 'age' ) }, #'hashref' => { 'optionname' => $option }, @@ -6753,9 +7067,9 @@ sub _agent_plandata { " AND peo_agentnum.optionname = 'agentnum' ". " AND ( agentnum IS NULL OR agentnum = $agentnum ) ". " ORDER BY - CASE WHEN peo_cust_bill_age.optionname != 'cust_bill_age' + CASE WHEN part_event_condition_option.optionname IS NULL THEN -1 - ELSE ". FS::part_event::Condition->age2seconds_sql('peo_cust_bill_age.optionvalue'). + ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue'). " END , part_event.weight". " LIMIT 1"