X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=8647c829a1436d0ac9828cbd30864a27c0370235;hp=640eee359826cc993a6b484d9019903c107d4f42;hb=1fe87434632f2627de487ca2aed6cfadea2c6061;hpb=c145d0efaf3c9d43ca6cad0ec36342f92a6dd646 diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 640eee359..8647c829a 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -79,6 +79,7 @@ use FS::sales; use FS::cust_payby; use FS::contact; use FS::reason; +use FS::Misc::Savepoint; # 1 is mostly method/subroutine entry and options # 2 traces progress of some operations @@ -377,6 +378,10 @@ sub insert { join(', ', map { "$_: $options{$_}" } keys %options ). "\n" if $DEBUG; + return "You are not permitted to change customer invoicing terms." + if $self->invoice_terms #i.e. not the default + && ! $FS::CurrentUser::CurrentUser->access_right('Edit customer invoice terms'); + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -738,20 +743,6 @@ sub insert { } } - # FS::geocode_Mixin::after_insert or something? - if ( $conf->config('tax_district_method') and !$import ) { - # if anything non-empty, try to look it up - my $queue = new FS::queue { - 'job' => 'FS::geocode_Mixin::process_district_update', - 'custnum' => $self->custnum, - }; - my $error = $queue->insert( ref($self), $self->custnum ); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "queueing tax district update: $error"; - } - } - # cust_main exports! warn " exporting\n" if $DEBUG > 1; @@ -1385,6 +1376,10 @@ sub replace { && ! $self->locale && $conf->exists('cust_main-require_locale'); + return "You are not permitted to change customer invoicing terms." + if $old->invoice_terms ne $self->invoice_terms + && ! $curuser->access_right('Edit customer invoice terms'); + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -1515,8 +1510,10 @@ sub replace { custnum => $self->custnum, } ); - $implicit_contact->set($_, $i_cust_contact->$_) - foreach qw( classnum selfservice_access comment ); + if ( $i_cust_contact ) { + $implicit_contact->set($_, $i_cust_contact->$_) + foreach qw( classnum selfservice_access comment ); + } my $error; if ( $implicit_contact->contactnum ) { @@ -2053,7 +2050,7 @@ Returns a list: an empty list on success or a list of errors. sub unsuspend { my $self = shift; - grep { ($_->get('setup')) && $_->unsuspend } $self->suspended_pkgs; + grep { ($_->get('setup')) && $_->unsuspend } $self->suspended_pkgs(@_); } =item release_hold @@ -2202,11 +2199,15 @@ sub cancel_pkgs { my( $self, %opt ) = @_; # we're going to cancel services, which is not reversible + # unless exports are suppressed die "cancel_pkgs cannot be run inside a transaction" - if $FS::UID::AutoCommit == 0; + if !$FS::UID::AutoCommit && !$FS::svc_Common::noexport_hack; + my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; + savepoint_create('cancel_pkgs'); + return ( 'access denied' ) unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer'); @@ -2223,7 +2224,8 @@ sub cancel_pkgs { my $ban = new FS::banned_pay $cust_payby->_new_banned_pay_hashref; my $error = $ban->insert; if ($error) { - dbh->rollback; + savepoint_rollback_and_release('cancel_pkgs'); + dbh->rollback if $oldAutoCommit; return ( $error ); } @@ -2243,11 +2245,13 @@ sub cancel_pkgs { 'time' => $cancel_time ); if ($error) { warn "Error billing during cancel, custnum ". $self->custnum. ": $error"; - dbh->rollback; + savepoint_rollback_and_release('cancel_pkgs'); + dbh->rollback if $oldAutoCommit; return ( "Error billing during cancellation: $error" ); } } - dbh->commit; + savepoint_release('cancel_pkgs'); + dbh->commit if $oldAutoCommit; my @errors; # try to cancel each service, the same way we would for individual packages, @@ -2261,17 +2265,22 @@ sub cancel_pkgs { warn "$me removing ".scalar(@sorted_cust_svc)." service(s) for customer ". $self->custnum."\n" if $DEBUG; + my $i = 0; foreach my $cust_svc (@sorted_cust_svc) { + my $savepoint = 'cancel_pkgs_'.$i++; + savepoint_create( $savepoint ); my $part_svc = $cust_svc->part_svc; next if ( defined($part_svc) and $part_svc->preserve ); # immediate cancel, no date option # transactionize individually my $error = try { $cust_svc->cancel } catch { $_ }; if ( $error ) { - dbh->rollback; + savepoint_rollback_and_release( $savepoint ); + dbh->rollback if $oldAutoCommit; push @errors, $error; } else { - dbh->commit; + savepoint_release( $savepoint ); + dbh->commit if $oldAutoCommit; } } if (@errors) { @@ -2287,8 +2296,11 @@ sub cancel_pkgs { @cprs = @{ delete $opt{'cust_pkg_reason'} }; } my $null_reason; + $i = 0; foreach (@pkgs) { my %lopt = %opt; + my $savepoint = 'cancel_pkgs_'.$i++; + savepoint_create( $savepoint ); if (@cprs) { my $cpr = shift @cprs; if ( $cpr ) { @@ -2309,10 +2321,12 @@ sub cancel_pkgs { } my $error = $_->cancel(%lopt); if ( $error ) { - dbh->rollback; + savepoint_rollback_and_release( $savepoint ); + dbh->rollback if $oldAutoCommit; push @errors, 'pkgnum '.$_->pkgnum.': '.$error; } else { - dbh->commit; + savepoint_release( $savepoint ); + dbh->commit if $oldAutoCommit; } } @@ -3012,48 +3026,104 @@ sub invoicing_list_emailonly_scalar { join(', ', $self->invoicing_list_emailonly); } -=item contact_list [ CLASSNUM, ... ] +=item contact_list [ CLASSNUM, DEST_FLAG... ] -Returns a list of contacts (L objects) for the customer. If -a list of contact classnums is given, returns only contacts in those -classes. If the pseudo-classnum 'invoice' is given, returns contacts that -are marked as invoice destinations. If '0' is given, also returns contacts -with no class. +Returns a list of contacts (L objects) for the customer. If no arguments are given, returns all contacts for the customer. +Arguments may contain classnums. When classnums are specified, only +contacts with a matching cust_contact.classnum are returned. When a +classnum of 0 is given, contacts with a null classnum are also included. + +Arguments may also contain the dest flag names 'invoice' or 'message'. +If given, contacts who's invoice_dest and/or message_dest flags are +not set to 'Y' will be excluded. + =cut sub contact_list { my $self = shift; my $search = { table => 'contact', - select => 'contact.*, cust_contact.invoice_dest', + select => join(', ',( + 'contact.*', + 'cust_contact.invoice_dest', + 'cust_contact.message_dest', + )), addl_from => ' JOIN cust_contact USING (contactnum)', extra_sql => ' WHERE cust_contact.custnum = '.$self->custnum, }; - my @orwhere; + # Bugfix notes: + # Calling methods were relying on this method to use invoice_dest to + # block e-mail messages. Depending on parameters, this may or may not + # have actually happened. + # + # The bug could cause this SQL to be used to filter e-mail addresses: + # + # AND ( + # cust_contact.classnums IN (1,2,3) + # OR cust_contact.invoice_dest = 'Y' + # ) + # + # improperly including everybody with the opt-in flag AND everybody + # in the contact classes + # + # Possibility to introduce new bugs: + # If callers of this method called it incorrectly, and didn't notice + # because it seemed to send the e-mails they wanted. + + # WHERE ... + # AND ( + # ( + # cust_contact.classnum IN (1,2,3) + # OR + # cust_contact.classnum IS NULL + # ) + # AND ( + # cust_contact.invoice_dest = 'Y' + # OR + # cust_contact.message_dest = 'Y' + # ) + # ) + + my @and_dest; + my @or_classnum; my @classnums; - foreach (@_) { - if ( $_ eq 'invoice' ) { - push @orwhere, 'cust_contact.invoice_dest = \'Y\''; - } elsif ( $_ eq '0' ) { - push @orwhere, 'cust_contact.classnum is null'; + for (@_) { + if ($_ eq 'invoice' || $_ eq 'message') { + push @and_dest, " cust_contact.${_}_dest = 'Y' "; + } elsif ($_ eq '0') { + push @or_classnum, ' cust_contact.classnum IS NULL '; } elsif ( /^\d+$/ ) { push @classnums, $_; } else { - die "bad classnum argument '$_'"; + croak "bad classnum argument '$_'"; } } - if (@classnums) { - push @orwhere, 'cust_contact.classnum IN ('.join(',', @classnums).')'; - } - if (@orwhere) { - $search->{extra_sql} .= ' AND (' . - join(' OR ', map "( $_ )", @orwhere) . - ')'; + push @or_classnum, 'cust_contact.classnum IN ('.join(',',@classnums).')' + if @classnums; + + if (@or_classnum || @and_dest) { # catch, no arguments given + $search->{extra_sql} .= ' AND ( '; + + if (@or_classnum) { + $search->{extra_sql} .= ' ( '; + $search->{extra_sql} .= join ' OR ', map {" $_ "} @or_classnum; + $search->{extra_sql} .= ' ) '; + $search->{extra_sql} .= ' AND ( ' if @and_dest; + } + + if (@and_dest) { + $search->{extra_sql} .= join ' OR ', map {" $_ "} @and_dest; + $search->{extra_sql} .= ' ) ' if @or_classnum; + } + + $search->{extra_sql} .= ' ) '; + + warn "\$extra_sql: $search->{extra_sql} \n" if $DEBUG; } qsearch($search); @@ -3080,6 +3150,101 @@ sub contact_list_email { @emails; } +=item contact_list_email_destinations + +Returns a list of emails and whether they receive invoices or messages destinations. +{ emailaddress => 'email.com', invoice => 'Y', message => '', } + +=cut + +sub contact_list_email_destinations { + my $self = shift; + warn "$me contact_list_email_destinations" + if $DEBUG; + return () if !$self->custnum; # not yet inserted + return map { $_ } + qsearch({ + table => 'cust_contact', + select => 'emailaddress, cust_contact.invoice_dest as invoice, cust_contact.message_dest as message', + addl_from => ' JOIN contact USING (contactnum) '. + ' JOIN contact_email USING (contactnum)', + hashref => { 'custnum' => $self->custnum, }, + order_by => 'ORDER BY custcontactnum DESC', + extra_sql => '', + }); +} + +=item contact_list_emailonly + +Returns an array of hashes containing the emails. Used for displaying contact email field in advanced customer reports. +[ { data => 'email.com', }, ] + +=cut + +sub contact_list_emailonly { + my $self = shift; + warn "$me contact_list_emailonly called" + if $DEBUG; + my @emails; + foreach ($self->contact_list_email_destinations) { + my $data = [ + { + 'data' => $_->emailaddress, + }, + ]; + push @emails, $data; + } + return \@emails; +} + +=item contact_list_cust_invoice_only + +Returns an array of hashes containing cust_contact.invoice_dest. Does this email receive invoices. Used for displaying email Invoice field in advanced customer reports. +[ { data => 'Yes', }, ] + +=cut + +sub contact_list_cust_invoice_only { + my $self = shift; + warn "$me contact_list_cust_invoice_only called" + if $DEBUG; + my @emails; + foreach ($self->contact_list_email_destinations) { + my $invoice = $_->invoice ? 'Yes' : 'No'; + my $data = [ + { + 'data' => $invoice, + }, + ]; + push @emails, $data; + } + return \@emails; +} + +=item contact_list_cust_message_only + +Returns an array of hashes containing cust_contact.message_dest. Does this email receive message notifications. Used for displaying email Message field in advanced customer reports. +[ { data => 'Yes', }, ] + +=cut + +sub contact_list_cust_message_only { + my $self = shift; + warn "$me contact_list_cust_message_only called" + if $DEBUG; + my @emails; + foreach ($self->contact_list_email_destinations) { + my $message = $_->message ? 'Yes' : 'No'; + my $data = [ + { + 'data' => $message, + }, + ]; + push @emails, $data; + } + return \@emails; +} + =item referral_custnum_cust_main Returns the customer who referred this customer (or the empty string, if @@ -3856,6 +4021,27 @@ sub name { $name; } +=item batch_payment_payname + +Returns a name string for this customer, either "cust_batch_payment->payname" or "First Last" or "Company, +based on if a company name exists and is the account being used a business account. + +=cut + +sub batch_payment_payname { + my $self = shift; + my $cust_pay_batch = shift; + my $name; + + if ($cust_pay_batch->{Hash}->{payby} eq "CARD") { $name = $cust_pay_batch->payname; } + else { $name = $self->first .' '. $self->last; } + + $name = $self->company + if (($cust_pay_batch->{Hash}->{paytype} eq "Business checking" || $cust_pay_batch->{Hash}->{paytype} eq "Business savings") && $self->company); + + $name; +} + =item service_contact Returns the L object for this customer that has the 'Service' @@ -4607,6 +4793,8 @@ PAYBYLOOP: next if grep(/^$field$/, qw( custpaybynum payby weight ) ); next if grep(/^$field$/, @preserve ); next PAYBYLOOP unless $new->get($field) eq $cust_payby->get($field); + # check if paymask exists, if so stop and don't save, no need for a duplicate. + return '' if $new->get('paymask') eq $cust_payby->get('paymask'); } # now check fields that can replace if one value is blank my $replace = 0; @@ -5322,7 +5510,68 @@ sub process_bill_and_collect { $param->{'fatal'} = 1; # runs from job queue, will be caught $param->{'retry'} = 1; - $cust_main->bill_and_collect( %$param ); + local $@; + eval { $cust_main->bill_and_collect( %$param) }; + if ( $@ ) { + die $@ =~ /cancel_pkgs cannot be run inside a transaction/ + ? "Bill Now unavailable for customer with pending package expiration\n" + : $@; + } +} + +=item pending_invoice_count + +Return number of cust_bill with pending=Y for this customer + +=cut + +sub pending_invoice_count { + FS::cust_bill->count( 'custnum = '.shift->custnum."AND pending = 'Y'" ); +} + +=item cust_locations_missing_district + +Always returns empty list, unless tax_district_method eq 'wa_sales' + +Return cust_location rows for this customer, associated with active +customer packages, where tax district column is empty. Presense of +these rows should block billing, because invoice would be generated +with incorrect taxes + +=cut + +sub cust_locations_missing_district { + my ( $self ) = @_; + + my $tax_district_method = FS::Conf->new->config('tax_district_method'); + + return () + unless $tax_district_method + && $tax_district_method eq 'wa_sales'; + + qsearch({ + table => 'cust_location', + select => 'cust_location.*', + addl_from => ' + LEFT JOIN cust_main USING (custnum) + LEFT JOIN cust_pkg ON cust_location.locationnum = cust_pkg.locationnum + ', + extra_sql => sprintf(q{ + WHERE cust_location.state = 'WA' + AND cust_location.custnum = %s + AND ( + cust_location.district IS NULL + or cust_location.district = '' + ) + AND cust_pkg.pkgnum IS NOT NULL + AND ( + cust_pkg.cancel > %s + OR cust_pkg.cancel IS NULL + ) + }, + $self->custnum, time() + ), + }); } #starting to take quite a while for big dbs @@ -5528,4 +5777,3 @@ L, L, schema.html from the base documentation. =cut 1; -