X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=c0b9bc51c99d9c30a2c505cb979d68140d22e9de;hb=072a39e8f246c4964f28d70265355d23def05bd9;hp=d9db2ed16faa0671fce89774832eef296818be9c;hpb=d78cfd10ddb82f27cdafaaeb80479d9bb6723480;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index d9db2ed16..c0b9bc51c 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -2,7 +2,8 @@ package FS::cust_main; require 5.006; use strict; -use vars qw( @ISA @EXPORT_OK $DEBUG $me $conf +use base qw( FS::otaker_Mixin FS::payinfo_Mixin FS::Record ); +use vars qw( @EXPORT_OK $DEBUG $me $conf @encrypted_fields $import $ignore_expired_card $skip_fuzzyfiles @fuzzyfields @@ -25,7 +26,7 @@ use String::Approx qw(amatch); use Business::CreditCard 0.28; use Locale::Country; use FS::UID qw( getotaker dbh driver_name ); -use FS::Record qw( qsearchs qsearch dbdef ); +use FS::Record qw( qsearchs qsearch dbdef regexp_sql ); use FS::Misc qw( generate_email send_email generate_ps do_print ); use FS::Msgcat qw(gettext); use FS::payby; @@ -66,11 +67,8 @@ use FS::type_pkgs; use FS::payment_gateway; use FS::agent_payment_gateway; use FS::banned_pay; -use FS::payinfo_Mixin; use FS::TicketSystem; -@ISA = qw( FS::payinfo_Mixin FS::Record ); - @EXPORT_OK = qw( smart_search ); $realtime_bop_decline_quiet = 0; @@ -304,9 +302,9 @@ IP address from which payment information was received Tax exempt, empty or `Y' -=item otaker +=item usernum -Order taker (assigned automatically, see L) +Order taker (see L) =item comments @@ -1912,6 +1910,25 @@ sub has_ship_address { scalar( grep { $self->getfield("ship_$_") ne '' } $self->addr_fields ); } +=item location_hash + +Returns a list of key/value pairs, with the following keys: address1, adddress2, +city, county, state, zip, country. The shipping address is used if present. + +=cut + +#geocode? dependent on tax-ship_address config, not available in cust_location +#mostly. not yet then. + +sub location_hash { + my $self = shift; + my $prefix = $self->has_ship_address ? 'ship_' : ''; + + map { $_ => $self->get($prefix.$_) } + qw( address1 address2 city county state zip country geocode ); + #fields that cust_location has +} + =item all_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ] Returns all packages (see L) for this customer. @@ -2475,33 +2492,33 @@ sub bill_and_collect { $error = $self->cancel_expired_pkgs( $options{actual_time} ); if ( $error ) { $error = "Error expiring custnum ". $self->custnum. ": $error"; - if ( $options{'fatal'} eq 'return' ) { return $error; } - elsif ( $options{'fatal'} ) { die $error; } - else { warn $error; } + if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } + elsif ( $options{fatal} ) { die $error; } + else { warn $error; } } $error = $self->suspend_adjourned_pkgs( $options{actual_time} ); if ( $error ) { $error = "Error adjourning custnum ". $self->custnum. ": $error"; - if ( $options{'fatal'} eq 'return' ) { return $error; } - elsif ( $options{'fatal'} ) { die $error; } - else { warn $error; } + if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } + elsif ( $options{fatal} ) { die $error; } + else { warn $error; } } $error = $self->bill( %options ); if ( $error ) { $error = "Error billing custnum ". $self->custnum. ": $error"; - if ( $options{'fatal'} eq 'return' ) { return $error; } - elsif ( $options{'fatal'} ) { die $error; } - else { warn $error; } + if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } + elsif ( $options{fatal} ) { die $error; } + else { warn $error; } } $error = $self->apply_payments_and_credits; if ( $error ) { $error = "Error applying custnum ". $self->custnum. ": $error"; - if ( $options{'fatal'} eq 'return' ) { return $error; } - elsif ( $options{'fatal'} ) { die $error; } - else { warn $error; } + if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } + elsif ( $options{fatal} ) { die $error; } + else { warn $error; } } unless ( $conf->exists('cancelled_cust-noevents') @@ -2510,9 +2527,9 @@ sub bill_and_collect { $error = $self->collect( %options ); if ( $error ) { $error = "Error collecting custnum ". $self->custnum. ": $error"; - if ( $options{'fatal'} eq 'return' ) { return $error; } - elsif ( $options{'fatal'} ) { die $error; } - else { warn $error; } + if ($options{fatal} && $options{fatal} eq 'return') { return $error; } + elsif ($options{fatal} ) { die $error; } + else { warn $error; } } } @@ -2677,15 +2694,21 @@ sub bill { return $error; } - my @cust_bill_pkg = (); + #keep auto-charge and non-auto-charge line items separate + my @passes = ( '', 'no_auto' ); + + my %cust_bill_pkg = map { $_ => [] } @passes; ### # find the packages which are due for billing, find out how much they are # & generate invoice database. ### - my( $total_setup, $total_recur, $postal_charge ) = ( 0, 0, 0 ); - my %taxlisthash; + my %total_setup = map { my $z = 0; $_ => \$z; } @passes; + my %total_recur = map { my $z = 0; $_ => \$z; } @passes; + + my %taxlisthash = map { $_ => {} } @passes; + my @precommit_hooks = (); $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ]; #param checks? @@ -2708,14 +2731,16 @@ sub bill { $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill ); + my $pass = ($cust_pkg->no_auto || $part_pkg->no_auto) ? 'no_auto' : ''; + my $error = $self->_make_lines( 'part_pkg' => $part_pkg, 'cust_pkg' => $cust_pkg, 'precommit_hooks' => \@precommit_hooks, - 'line_items' => \@cust_bill_pkg, - 'setup' => \$total_setup, - 'recur' => \$total_recur, - 'tax_matrix' => \%taxlisthash, + 'line_items' => $cust_bill_pkg{$pass}, + 'setup' => $total_setup{$pass}, + 'recur' => $total_recur{$pass}, + 'tax_matrix' => $taxlisthash{$pass}, 'time' => $time, 'real_pkgpart' => $real_pkgpart, 'options' => \%options, @@ -2729,130 +2754,138 @@ sub bill { } #foreach my $cust_pkg - unless ( @cust_bill_pkg ) { #don't create an invoice w/o line items - #but do commit any package date cycling that happened - $dbh->commit or die $dbh->errstr if $oldAutoCommit; - return ''; - } + #if the customer isn't on an automatic payby, everything can go on a single + #invoice anyway? + #if ( $cust_main->payby !~ /^(CARD|CHEK)$/ ) { + #merge everything into one list + #} - if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) || - !$conf->exists('postal_invoice-recurring_only') - ) - { + foreach my $pass (@passes) { # keys %cust_bill_pkg ) { - my $postal_pkg = $self->charge_postal_fee(); - if ( $postal_pkg && !ref( $postal_pkg ) ) { + my @cust_bill_pkg = @{ $cust_bill_pkg{$pass} }; - $dbh->rollback if $oldAutoCommit; - return "can't charge postal invoice fee for customer ". - $self->custnum. ": $postal_pkg"; - - } elsif ( $postal_pkg ) { - - my $real_pkgpart = $postal_pkg->pkgpart; - foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) { - my %postal_options = %options; - delete $postal_options{cancel}; - my $error = - $self->_make_lines( 'part_pkg' => $part_pkg, - 'cust_pkg' => $postal_pkg, - 'precommit_hooks' => \@precommit_hooks, - 'line_items' => \@cust_bill_pkg, - 'setup' => \$total_setup, - 'recur' => \$total_recur, - 'tax_matrix' => \%taxlisthash, - 'time' => $time, - 'real_pkgpart' => $real_pkgpart, - 'options' => \%postal_options, - ); - if ($error) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - } + next unless @cust_bill_pkg; #don't create an invoice w/o line items - } + if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) || + !$conf->exists('postal_invoice-recurring_only') + ) + { - } + my $postal_pkg = $self->charge_postal_fee(); + if ( $postal_pkg && !ref( $postal_pkg ) ) { - my $listref_or_error = - $self->calculate_taxes( \@cust_bill_pkg, \%taxlisthash, $invoice_time); + $dbh->rollback if $oldAutoCommit; + return "can't charge postal invoice fee for customer ". + $self->custnum. ": $postal_pkg"; + + } elsif ( $postal_pkg ) { + + my $real_pkgpart = $postal_pkg->pkgpart; + foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) { + my %postal_options = %options; + delete $postal_options{cancel}; + my $error = + $self->_make_lines( 'part_pkg' => $part_pkg, + 'cust_pkg' => $postal_pkg, + 'precommit_hooks' => \@precommit_hooks, + 'line_items' => \@cust_bill_pkg, + 'setup' => $total_setup{$pass}, + 'recur' => $total_recur{$pass}, + 'tax_matrix' => $taxlisthash{$pass}, + 'time' => $time, + 'real_pkgpart' => $real_pkgpart, + 'options' => \%postal_options, + ); + if ($error) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } - unless ( ref( $listref_or_error ) ) { - $dbh->rollback if $oldAutoCommit; - return $listref_or_error; - } + } - foreach my $taxline ( @$listref_or_error ) { - $total_setup = sprintf('%.2f', $total_setup+$taxline->setup ); - push @cust_bill_pkg, $taxline; - } + } - #add tax adjustments - warn "adding tax adjustments...\n" if $DEBUG > 2; - foreach my $cust_tax_adjustment ( - qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum, - 'billpkgnum' => '', - } - ) - ) { + my $listref_or_error = + $self->calculate_taxes( \@cust_bill_pkg, $taxlisthash{$pass}, $invoice_time); - my $tax = sprintf('%.2f', $cust_tax_adjustment->amount ); - - my $itemdesc = $cust_tax_adjustment->taxname; - $itemdesc = '' if $itemdesc eq 'Tax'; - - push @cust_bill_pkg, new FS::cust_bill_pkg { - 'pkgnum' => 0, - 'setup' => $tax, - 'recur' => 0, - 'sdate' => '', - 'edate' => '', - 'itemdesc' => $itemdesc, - 'itemcomment' => $cust_tax_adjustment->comment, - 'cust_tax_adjustment' => $cust_tax_adjustment, - #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location, - }; + unless ( ref( $listref_or_error ) ) { + $dbh->rollback if $oldAutoCommit; + return $listref_or_error; + } - } + foreach my $taxline ( @$listref_or_error ) { + ${ $total_setup{$pass} } = + sprintf('%.2f', ${ $total_setup{$pass} } + $taxline->setup ); + push @cust_bill_pkg, $taxline; + } - my $charged = sprintf('%.2f', $total_setup + $total_recur ); + #add tax adjustments + warn "adding tax adjustments...\n" if $DEBUG > 2; + foreach my $cust_tax_adjustment ( + qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum, + 'billpkgnum' => '', + } + ) + ) { - my @cust_bill = $self->cust_bill; - my $balance = $self->balance; - my $previous_balance = scalar(@cust_bill) - ? ( $cust_bill[$#cust_bill]->billing_balance || 0 ) - : 0; + my $tax = sprintf('%.2f', $cust_tax_adjustment->amount ); + + my $itemdesc = $cust_tax_adjustment->taxname; + $itemdesc = '' if $itemdesc eq 'Tax'; + + push @cust_bill_pkg, new FS::cust_bill_pkg { + 'pkgnum' => 0, + 'setup' => $tax, + 'recur' => 0, + 'sdate' => '', + 'edate' => '', + 'itemdesc' => $itemdesc, + 'itemcomment' => $cust_tax_adjustment->comment, + 'cust_tax_adjustment' => $cust_tax_adjustment, + #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location, + }; - $previous_balance += $cust_bill[$#cust_bill]->charged - if scalar(@cust_bill); - #my $balance_adjustments = - # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged); + } - #create the new invoice - my $cust_bill = new FS::cust_bill ( { - 'custnum' => $self->custnum, - '_date' => ( $invoice_time ), - 'charged' => $charged, - 'billing_balance' => $balance, - 'previous_balance' => $previous_balance, - 'invoice_terms' => $options{'invoice_terms'}, - } ); - $error = $cust_bill->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "can't create invoice for customer #". $self->custnum. ": $error"; - } + my $charged = sprintf('%.2f', ${ $total_setup{$pass} } + ${ $total_recur{$pass} } ); - foreach my $cust_bill_pkg ( @cust_bill_pkg ) { - $cust_bill_pkg->invnum($cust_bill->invnum); - my $error = $cust_bill_pkg->insert; + my @cust_bill = $self->cust_bill; + my $balance = $self->balance; + my $previous_balance = scalar(@cust_bill) + ? ( $cust_bill[$#cust_bill]->billing_balance || 0 ) + : 0; + + $previous_balance += $cust_bill[$#cust_bill]->charged + if scalar(@cust_bill); + #my $balance_adjustments = + # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged); + + #create the new invoice + my $cust_bill = new FS::cust_bill ( { + 'custnum' => $self->custnum, + '_date' => ( $invoice_time ), + 'charged' => $charged, + 'billing_balance' => $balance, + 'previous_balance' => $previous_balance, + 'invoice_terms' => $options{'invoice_terms'}, + } ); + $error = $cust_bill->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; - return "can't create invoice line item: $error"; + return "can't create invoice for customer #". $self->custnum. ": $error"; } - } - + + foreach my $cust_bill_pkg ( @cust_bill_pkg ) { + $cust_bill_pkg->invnum($cust_bill->invnum); + my $error = $cust_bill_pkg->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "can't create invoice line item: $error"; + } + } + + } #foreach my $pass ( keys %cust_bill_pkg ) foreach my $hook ( @precommit_hooks ) { eval { @@ -3065,7 +3098,7 @@ sub _make_lines { my $old_cust_pkg = new FS::cust_pkg \%hash; my @details = (); - + my @discounts = (); my $lineitems = 0; $cust_pkg->pkgpart($part_pkg->pkgpart); @@ -3150,6 +3183,7 @@ sub _make_lines { ); my %param = ( 'precommit_hooks' => $precommit_hooks, 'increment_next_bill' => $increment_next_bill, + 'discounts' => \@discounts, ); my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur'; @@ -3229,6 +3263,7 @@ sub _make_lines { 'unitrecur' => $unitrecur, 'quantity' => $cust_pkg->quantity, 'details' => \@details, + 'discounts' => \@discounts, 'hidden' => $part_pkg->hidden, }; @@ -3926,7 +3961,7 @@ sub due_cust_event { warn " invalid conditions not eliminated with condition_sql:\n". join('', map " $_: ".$unsat{$_}."\n", keys %unsat ) - if $DEBUG; # > 1; + if keys %unsat && $DEBUG; # > 1; ## # insert @@ -4107,6 +4142,8 @@ sub realtime_bop { warn " $_ => $options{$_}\n" foreach keys %options; } + return "Amount must be greater than 0" unless $amount > 0; + unless ( $options{'description'} ) { if ( $conf->exists('business-onlinepayment-description') ) { my $dtempl = $conf->config('business-onlinepayment-description'); @@ -4877,9 +4914,19 @@ sub realtime_refund_bop { ) { warn " attempting void\n" if $DEBUG > 1; my $void = new Business::OnlinePayment( $processor, @bop_options ); - $content{'card_number'} = $cust_pay->payinfo - if $cust_pay->payby eq 'CARD' - && $void->can('info') && $void->info('CC_void_requires_card'); + if ( $void->can('info') ) { + if ( $cust_pay->payby eq 'CARD' + && $void->info('CC_void_requires_card') ) + { + $content{'card_number'} = $cust_pay->payinfo + } elsif ( $cust_pay->payby eq 'CHEK' + && $void->info('ECHECK_void_requires_account') ) + { + ( $content{'account_number'}, $content{'routing_code'} ) = + split('@', $cust_pay->payinfo); + $content{'name'} = $self->get('first'). ' '. $self->get('last'); + } + } $void->content( 'action' => 'void', %content ); $void->submit(); if ( $void->is_success ) { @@ -5043,10 +5090,12 @@ sub _new_bop_required { my $botpp = 'Business::OnlineThirdPartyPayment'; return 1 - if ( $conf->config('business-onlinepayment-namespace') eq $botpp || - scalar( grep { $_->gateway_namespace eq $botpp } - qsearch( 'payment_gateway', { 'disabled' => '' } ) - ) + if ( ( $conf->exists('business-onlinepayment-namespace') + && $conf->config('business-onlinepayment-namespace') eq $botpp + ) + or scalar( grep { $_->gateway_namespace eq $botpp } + qsearch( 'payment_gateway', { 'disabled' => '' } ) + ) ) ; @@ -6218,9 +6267,19 @@ sub _new_realtime_refund_bop { ) { warn " attempting void\n" if $DEBUG > 1; my $void = new Business::OnlinePayment( $processor, @bop_options ); - $content{'card_number'} = $cust_pay->payinfo - if $cust_pay->payby eq 'CARD' - && $void->can('info') && $void->info('CC_void_requires_card'); + if ( $void->can('info') ) { + if ( $cust_pay->payby eq 'CARD' + && $void->info('CC_void_requires_card') ) + { + $content{'card_number'} = $cust_pay->payinfo; + } elsif ( $cust_pay->payby eq 'CHEK' + && $void->info('ECHECK_void_requires_account') ) + { + ( $content{'account_number'}, $content{'routing_code'} ) = + split('@', $cust_pay->payinfo); + $content{'name'} = $self->get('first'). ' '. $self->get('last'); + } + } $void->content( 'action' => 'void', %content ); $void->submit(); if ( $void->is_success ) { @@ -7388,7 +7447,7 @@ sub referral_cust_main_ncancelled { Like referral_cust_main, except returns a flat list of all unsuspended (and uncancelled) packages for each customer. The number of items in this list may -be useful for comission calculations (perhaps after a Cpkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ). +be useful for commission calculations (perhaps after a Cpkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ). =cut @@ -7450,8 +7509,10 @@ sub credit { $cust_credit->set('reason', $reason) } - $cust_credit->addlinfo( delete $options{'addlinfo'} ) - if exists($options{'addlinfo'}); + for (qw( addlinfo eventnum )) { + $cust_credit->$_( delete $options{$_} ) + if exists($options{$_}); + } $cust_credit->insert(%options); @@ -7504,12 +7565,14 @@ sub charge { my ( $pkg, $comment, $additional ); my ( $setuptax, $taxclass ); #internal taxes my ( $taxproduct, $override ); #vendor (CCH) taxes + my $no_auto = ''; my $cust_pkg_ref = ''; my ( $bill_now, $invoice_terms ) = ( 0, '' ); if ( ref( $_[0] ) ) { $amount = $_[0]->{amount}; $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1; $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : ''; + $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : ''; $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge'; $comment = exists($_[0]->{comment}) ? $_[0]->{comment} : '$'. sprintf("%.2f",$amount); @@ -7587,6 +7650,7 @@ sub charge { 'pkgpart' => $pkgpart, 'quantity' => $quantity, 'start_date' => $start_date, + 'no_auto' => $no_auto, } ); $error = $cust_pkg->insert; @@ -8460,6 +8524,15 @@ sub search { } ## + # do the same for user + ## + + if ( $params->{'usernum'} =~ /^(\d+)$/ and $1 ) { + push @where, + "cust_main.usernum = $1"; + } + + ## # parse status ## @@ -8494,13 +8567,23 @@ sub search { next unless exists($params->{$field}); - my($beginning, $ending) = @{$params->{$field}}; + my($beginning, $ending, $hour) = @{$params->{$field}}; push @where, "cust_main.$field IS NOT NULL", "cust_main.$field >= $beginning", "cust_main.$field <= $ending"; + # XXX: do this for mysql and/or pull it out of here + if(defined $hour) { + if ($dbh->{Driver}->{Name} eq 'Pg') { + push @where, "extract(hour from to_timestamp(cust_main.$field)) = $hour"; + } + else { + warn "search by time of day not supported on ".$dbh->{Driver}->{Name}." databases"; + } + } + $orderby ||= "ORDER BY cust_main.$field"; } @@ -9646,14 +9729,7 @@ sub _agent_plandata { my $agentnum = $self->agentnum; - my $regexp = ''; - if ( driver_name =~ /^Pg/i ) { - $regexp = '~'; - } elsif ( driver_name =~ /^mysql/i ) { - $regexp = 'REGEXP'; - } else { - die "don't know how to use regular expressions in ". driver_name. " databases"; - } + my $regexp = regexp_sql(); my $part_event_option = qsearchs({ @@ -9719,6 +9795,9 @@ sub _upgrade_data { #class method my $sth = dbh->prepare($sql) or die dbh->errstr; $sth->execute or die $sth->errstr; + local($ignore_expired_card) = 1 + $class->_upgrade_otaker(%opts); + } =back