use strict;
use vars qw( $DEBUG $me );
-use Carp qw( confess );
+use Carp qw( confess carp cluck );
use FS::UID qw(dbh);
use FS::cust_main;
use FS::Record qw( qsearch qsearchs );
use FS::Misc qw( send_email generate_email );
+use HTML::Entities;
$DEBUG = 0;
$me = '[FS::cust_main_Mixin]';
sub cust_main {
my $self = shift;
+ cluck ref($self). '->cust_main called' if $DEBUG;
$self->cust_linked ? qsearchs('cust_main', {custnum => $self->custnum}) : '';
}
sub country_full {
my $self = shift;
- $self->cust_linked
- ? FS::cust_main::country_full($self)
- : $self->cust_unlinked_msg;
+ if ( $self->locationnum ) { # cust_pkg has this
+ my $location = FS::cust_location->by_key($self->locationnum);
+ $location ? $location->country_full : '';
+ } elsif ( $self->cust_linked ) {
+ $self->cust_main->bill_country_full;
+ }
}
=item invoicing_list_emailonly
=cut
sub ucfirst_cust_status {
+ carp "ucfirst_cust_status deprecated, use cust_status_label";
+ local($FS::cust_main::ucfirst_nowarn) = 1;
my $self = shift;
$self->cust_linked
? ucfirst( $self->cust_status(@_) )
: $self->cust_unlinked_msg;
}
+=item cust_status_label
+
+=cut
+
+sub cust_status_label {
+ my $self = shift;
+
+ $self->cust_linked
+ ? FS::cust_main::cust_status_label($self)
+ : $self->cust_unlinked_msg;
+}
+
=item cust_statuscolor
Given an object that contains fields from cust_main (say, from a JOINed
=item status
-=item payby
-
=back
=cut
push @search, $class->$method();
}
- #payby
- my @payby = ref($param->{'payby'})
- ? @{ $param->{'payby'} }
- : split(',', $param->{'payby'});
- @payby = grep /^([A-Z]{4})$/, @payby;
- if ( @payby ) {
- push @search, 'cust_main.payby IN ('. join(',', map "'$_'", @payby). ')';
- }
-
#here is the agent virtualization
push @search,
$FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' );
use Storable qw(thaw);
use MIME::Base64;
use Data::Dumper qw(Dumper);
+use Digest::SHA qw(sha1); # for duplicate checking
sub email_search_result {
my($class, $param) = @_;
if ( $msgnum ) {
$msg_template = qsearchs('msg_template', { msgnum => $msgnum } )
or die "msgnum $msgnum not found\n";
+ } else {
+ $msg_template = FS::msg_template->new({
+ from_addr => $from,
+ msgname => $subject, # maybe a timestamp also?
+ disabled => 'D', # 'D'raft
+ # msgclass, maybe
+ });
+ $error = $msg_template->insert(
+ subject => $subject,
+ body => $html_body,
+ );
+ return "$error (when creating draft template)" if $error;
}
- $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ]
- unless ref($param->{'payby'});
-
my $sql_query = $class->search($param->{'search'});
+ $sql_query->{'select'} = $sql_query->{'table'} . '.*';
my $count_query = delete($sql_query->{'count_query'});
my $count_sth = dbh->prepare($count_query)
my $success = 0;
my %sent_to = ();
+ if ( !$msg_template ) {
+ die "email_search_result now requires a msg_template";
+ }
+
#eventually order+limit magic to reduce memory use?
foreach my $obj ( qsearch($sql_query) ) {
}
my $cust_main = $obj->cust_main;
- my @message;
if ( !$cust_main ) {
next; # unlinked object; nothing else we can do
}
- if( $sent_to{$cust_main->custnum} ) {
+ my $cust_msg = $msg_template->prepare(
+ 'cust_main' => $cust_main,
+ 'object' => $obj,
+ );
+
+ # For non-cust_main searches, we avoid duplicates based on message
+ # body text.
+ my $unique = $cust_main->custnum;
+ $unique .= sha1($cust_msg->text_body) if $class ne 'FS::cust_main';
+ if( $sent_to{$unique} ) {
# avoid duplicates
$dups++;
next;
}
- $sent_to{$cust_main->custnum} = 1;
+ $sent_to{$unique} = 1;
- if ( $msg_template ) {
- # XXX add support for other context objects?
- # If we do that, handling of "duplicates" will
- # have to be smarter. Currently we limit to
- # one message per custnum because they'd all
- # be identical.
- @message = $msg_template->prepare( 'cust_main' => $cust_main );
- }
- else {
- my $to = $cust_main->invoicing_list_emailonly_scalar;
- next if !$to;
-
- @message = (
- 'from' => $from,
- 'to' => $to,
- 'subject' => $subject,
- 'html_body' => $html_body,
- 'text_body' => $text_body,
- );
- } #if $msg_template
-
- $error = send_email( generate_email( @message ) );
+ $error = $cust_msg->send;
if($error) {
# queue the sending of this message so that the user can see what we
# tried to do, and retry if desired
+ # (note the cust_msg itself also now has a status of 'failed'; that's
+ # fine, as it will get its status reset if we retry the job)
my $queue = new FS::queue {
- 'job' => 'FS::Misc::process_send_email',
+ 'job' => 'FS::cust_msg::process_send',
'custnum' => $cust_main->custnum,
'status' => 'failed',
'statustext' => $error,
};
- $queue->insert(@message);
+ $queue->insert($cust_msg->custmsgnum);
push @retry_jobs, $queue;
}
else {
}
} # foreach $obj
+ # if the message template was created as "draft", change its status to
+ # "completed"
+ if ($msg_template->disabled eq 'D') {
+ $msg_template->set('disabled' => 'C');
+ my $error = $msg_template->replace;
+ warn "$error (setting draft message template status)" if $error;
+ }
+
if(@retry_jobs) {
# fail the job, but with a status message that makes it clear
# something was sent.
my $job = shift;
#warn "$me process_re_X $method for job $job\n" if $DEBUG;
- my $param = thaw(decode_base64(shift));
+ my $param = shift;
warn Dumper($param) if $DEBUG;
$param->{'job'} = $job;
$param->{'search'} = thaw(decode_base64($param->{'search'}))
or die "process_email_search_result requires search params.\n";
-# $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ]
-# unless ref($param->{'payby'});
-
my $table = $param->{'table'}
or die "process_email_search_result requires table.\n";
die "error loading FS::$table: $@\n" if $@;
my $error = "FS::$table"->email_search_result( $param );
+ dbh->commit; # save failed jobs before rethrowing the error
die $error if $error;
}
+=item conf
+
+Returns a configuration handle (L<FS::Conf>) set to the customer's locale,
+if they have one. If not, returns an FS::Conf with no locale.
+
+=cut
+
+sub conf {
+ my $self = shift;
+ return $self->{_conf} if (ref $self and $self->{_conf});
+ my $cust_main = $self->cust_main;
+ my $conf = new FS::Conf {
+ 'locale' => ($cust_main ? $cust_main->locale : '')
+ };
+ $self->{_conf} = $conf if ref $self;
+ return $conf;
+}
+
+=item mt TEXT [, ARGS ]
+
+Localizes a text string (see L<Locale::Maketext>) for the customer's locale,
+if they have one.
+
+=cut
+
+sub mt {
+ my $self = shift;
+ return $self->{_lh}->maketext(@_) if (ref $self and $self->{_lh});
+ my $cust_main = $self->cust_main;
+ my $locale = $cust_main ? $cust_main->locale : '';
+ my $lh = FS::L10N->get_handle($locale);
+ $self->{_lh} = $lh if ref $self;
+ return $lh->maketext(@_);
+}
+
+=item time2str_local FORMAT, TIME[, ESCAPE]
+
+Localizes a date (see L<Date::Language>) for the customer's locale.
+
+FORMAT can be a L<Date::Format> string, or one of these special words:
+
+- "short": the value of the "date_format" config setting for the customer's
+ locale, defaulting to "%x".
+- "rdate": the same as "short" except that the default has a four-digit year.
+- "long": the value of the "date_format_long" config setting for the
+ customer's locale, defaulting to "%b %o, %Y".
+
+ESCAPE, if specified, is one of "latex" or "html", and will escape non-ASCII
+characters and convert spaces to nonbreaking spaces.
+
+=cut
+
+sub time2str_local {
+ # renamed so that we don't have to change every single reference to
+ # time2str everywhere
+ my $self = shift;
+ my ($format, $time, $escape) = @_;
+ return '' unless $time > 0; # work around time2str's traditional stupidity
+
+ $self->{_date_format} ||= {};
+ if (!exists($self->{_dh})) {
+ my $cust_main = $self->cust_main;
+ my $locale = $cust_main->locale if $cust_main;
+ $locale ||= 'en_US';
+ my %info = FS::Locales->locale_info($locale);
+ my $dh = eval { Date::Language->new($info{'name'}) } ||
+ Date::Language->new(); # fall back to English
+ $self->{_dh} = $dh;
+ }
+
+ if ($format eq 'short') {
+ $format = $self->{_date_format}->{short}
+ ||= $self->conf->config('date_format') || '%x';
+ } elsif ($format eq 'rdate') {
+ $format = $self->{_date_format}->{rdate}
+ ||= $self->conf->config('date_format') || '%m/%d/%Y';
+ } elsif ($format eq 'long') {
+ $format = $self->{_date_format}->{long}
+ ||= $self->conf->config('date_format_long') || '%b %o, %Y';
+ }
+
+ # actually render the date
+ my $string = $self->{_dh}->time2str($format, $time);
+
+ if ($escape) {
+ if ($escape eq 'html') {
+ $string = encode_entities($string);
+ $string =~ s/ +/ /g;
+ } elsif ($escape eq 'latex') { # just do nbsp's here
+ $string =~ s/ +/~/g;
+ }
+ }
+
+ $string;
+}
+
+=item unsuspend_balance
+
+If conf I<unsuspend_balance> is set and customer's current balance is
+beneath the set threshold, unsuspends customer packages.
+
+=cut
+
+sub unsuspend_balance {
+ my $self = shift;
+ my $cust_main = $self->cust_main;
+ my $conf = $self->conf;
+ my $setting = $conf->config('unsuspend_balance');
+ my $maxbalance;
+ if ($setting eq 'Zero') {
+ $maxbalance = 0;
+ } elsif ($setting eq 'Latest invoice charges') {
+ my @cust_bill = $cust_main->cust_bill();
+ my $cust_bill = $cust_bill[-1]; #always want the most recent one
+ return unless $cust_bill;
+ $maxbalance = $cust_bill->charged || 0;
+ } elsif (length($setting)) {
+ warn "Unrecognized unsuspend_balance setting $setting";
+ return;
+ } else {
+ return;
+ }
+ my $balance = $cust_main->balance || 0;
+ if ($balance <= $maxbalance) {
+ my @errors = $cust_main->unsuspend;
+ # side-fx with nested transactions? upstack rolls back?
+ warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
+ join(' / ', @errors)
+ if @errors;
+ }
+ return;
+}
+
=back
=head1 BUGS