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 cust_status {
my $self = shift;
return $self->cust_unlinked_msg unless $self->cust_linked;
-
- #FS::cust_main::status($self)
- #false laziness w/actual cust_main::status
- # (make sure FS::cust_main methods are called)
- for my $status (qw( prospect active inactive suspended cancelled )) {
- my $method = $status.'_sql';
- my $sql = FS::cust_main->$method();;
- my $numnum = ( $sql =~ s/cust_main\.custnum/?/g );
- my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
- $sth->execute( ($self->custnum) x $numnum )
- or die "Error executing 'SELECT $sql': ". $sth->errstr;
- return $status if $sth->fetchrow_arrayref->[0];
- }
+ my $cust_main = $self->cust_main;
+ return $self->cust_unlinked_msg unless $cust_main;
+ return $cust_main->cust_status;
}
=item ucfirst_cust_status
Text body
+=item to_contact_classnum
+
+The customer contact class (or classes, as a comma-separated list) to send
+the message to. If unspecified, will be sent to any contacts that are marked
+as invoice destinations (the equivalent of specifying 'invoice').
+
=back
Returns an error message, or false for success.
my $subject = delete $param->{subject};
my $html_body = delete $param->{html_body};
my $text_body = delete $param->{text_body};
+ my $to_contact_classnum = delete $param->{to_contact_classnum};
my $error = '';
my $job = delete $param->{'job'}
%message = $msg_template->prepare(
'cust_main' => $cust_main,
'object' => $obj,
+ 'to_contact_classnum' => $to_contact_classnum,
);
- }
- else {
- my @to = $cust_main->invoicing_list_emailonly;
+
+ } else {
+ # 3.x: false laziness with msg_template.pm; on 4.x, all email notices
+ # are generated from templates and this case goes away
+ my @classes;
+ if ( $to_contact_classnum ) {
+ @classes = ref($to_contact_classnum) ? @$to_contact_classnum : split(',', $to_contact_classnum);
+ }
+ if (!@classes) {
+ @classes = ( 'invoice' );
+ }
+ my @to = $cust_main->contact_list_email(@classes);
next if !@to;
%message = (
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;
+
+ # kind of a pain to load/check all cust_bill instead of just open ones,
+ # but if for some reason payment gets applied to later bills before
+ # earlier ones, we still want to consider the later ones as allowable balance
+ } 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
+ if ($cust_bill) {
+ $maxbalance = $cust_bill->charged || 0;
+ } else {
+ $maxbalance = 0;
+ }
+ } elsif ($setting eq 'Charges not past due') {
+ my $now = time;
+ $maxbalance = 0;
+ foreach my $cust_bill ($cust_main->cust_bill()) {
+ next unless $now <= ($cust_bill->due_date || $cust_bill->_date);
+ $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