summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/AccessRight.pm5
-rw-r--r--FS/FS/ClientAPI/MyAccount.pm13
-rw-r--r--FS/FS/ClientAPI_XMLRPC.pm19
-rw-r--r--FS/FS/Conf.pm64
-rw-r--r--FS/FS/GeocodeCache.pm209
-rw-r--r--FS/FS/Report/Table.pm21
-rw-r--r--FS/FS/Report/Table/Monthly.pm31
-rw-r--r--FS/FS/Schema.pm13
-rw-r--r--FS/FS/Upgrade.pm1
-rw-r--r--FS/FS/access_right.pm45
-rw-r--r--FS/FS/cdr/cia.pm4
-rw-r--r--FS/FS/cust_bill.pm17
-rw-r--r--FS/FS/cust_main.pm24
-rw-r--r--FS/FS/cust_main/Search.pm36
-rw-r--r--FS/FS/cust_pay_void.pm30
-rw-r--r--FS/FS/cust_pkg.pm2
-rw-r--r--FS/FS/part_event/Condition.pm4
-rw-r--r--FS/FS/part_event/Condition/cust_bill_has_service.pm4
-rw-r--r--FS/FS/payby.pm5
-rwxr-xr-xFS/FS/svc_broadband.pm9
-rw-r--r--FS/FS/upgrade_journal.pm151
-rw-r--r--FS/MANIFEST2
-rw-r--r--FS/t/GeocodeCache.t5
-rw-r--r--FS/t/upgrade_journal.t5
24 files changed, 625 insertions, 94 deletions
diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm
index 1bfae03ad..d2417f069 100644
--- a/FS/FS/AccessRight.pm
+++ b/FS/FS/AccessRight.pm
@@ -253,9 +253,11 @@ tie my %rights, 'Tie::IxHash',
###
'Reporting/listing rights' => [
'List customers',
+ 'List all customers',
'List zip codes', #NEW
'List invoices',
'List packages',
+ 'Summarize packages',
'List services',
'List service passwords',
@@ -266,6 +268,8 @@ tie my %rights, 'Tie::IxHash',
{ rightname=> 'List inventory', global=>1 },
{ rightname=>'View email logs', global=>1 },
+ 'Download report data',
+
#{ rightname => 'List customers of all agents', global=>1 },
],
@@ -361,6 +365,7 @@ sub default_superuser_rights {
'Delete payment',
'Delete credit', #?
'Delete refund', #?
+ 'Edit customer package dates',
'Time queue',
'Redownload resolved batches',
'Raw SQL',
diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index acd0c6e85..7bc3011d2 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -151,12 +151,25 @@ sub login_info {
%{ skin_info($p) },
'phone_login' => $conf->exists('selfservice_server-phone_login'),
'single_domain'=> scalar($conf->config('selfservice_server-single_domain')),
+ 'banner_url' => scalar($conf->config('selfservice-login_banner_url')),
+ 'banner_image_md5' =>
+ md5_hex($conf->config_binary('selfservice-login_banner_image')),
);
return \%info;
}
+sub login_banner_image {
+ my $p = shift;
+ my $conf = new FS::Conf;
+ my $image = $conf->config_binary('selfservice-login_banner_image');
+ return {
+ 'md5' => md5_hex($image),
+ 'image' => $image,
+ };
+}
+
#false laziness w/FS::ClientAPI::passwd::passwd
sub login {
my $p = shift;
diff --git a/FS/FS/ClientAPI_XMLRPC.pm b/FS/FS/ClientAPI_XMLRPC.pm
index 1e068f428..98e1910c3 100644
--- a/FS/FS/ClientAPI_XMLRPC.pm
+++ b/FS/FS/ClientAPI_XMLRPC.pm
@@ -37,17 +37,21 @@ $DEBUG = 0;
$FS::ClientAPI::DEBUG = $DEBUG;
#false laziness w/FS::SelfService/XMLRPC.pm, same problem as below but worse
+our %typefix_skin_info = (
+ 'logo' => 'base64',
+ 'title_left_image' => 'base64',
+ 'title_right_image' => 'base64',
+ 'menu_top_image' => 'base64',
+ 'menu_body_image' => 'base64',
+ 'menu_bottom_image' => 'base64',
+);
our %typefix = (
'invoice_pdf' => { 'invoice_pdf' => 'base64', },
'legacy_invoice_pdf' => { 'invoice_pdf' => 'base64', },
- 'skin_info' => { 'logo' => 'base64',
- 'title_left_image' => 'base64',
- 'title_right_image' => 'base64',
- 'menu_top_image' => 'base64',
- 'menu_body_image' => 'base64',
- 'menu_bottom_image' => 'base64',
- },
+ 'skin_info' => \%typefix_skin_info,
+ 'login_info' => \%typefix_skin_info,
'invoice_logo' => { 'logo' => 'base64', },
+ 'login_banner_image' => { 'image' => 'base64', },
);
sub AUTOLOAD {
@@ -94,6 +98,7 @@ sub ss2clientapi {
'chfn' => 'passwd/passwd',
'chsh' => 'passwd/passwd',
'login_info' => 'MyAccount/login_info',
+ 'login_banner_image' => 'MyAccount/login_banner_image',
'login' => 'MyAccount/login',
'logout' => 'MyAccount/logout',
'switch_acct' => 'MyAccount/switch_acct',
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 04ca35ac2..63fc8869c 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -1594,6 +1594,13 @@ and customer address. Include units.',
},
{
+ 'key' => 'disable_maxselect',
+ 'section' => 'UI',
+ 'description' => 'Prevent changing the number of records per page.',
+ 'type' => 'checkbox',
+ },
+
+ {
'key' => 'session-start',
'section' => 'session',
'description' => 'If defined, the command which is executed on the Freeside machine when a session begins. The contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$ip</code>, <code>$nasip</code> and <code>$nasfqdn</code>, which are the IP address of the starting session, and the IP address and fully-qualified domain name of the NAS this session is on.',
@@ -2850,6 +2857,14 @@ and customer address. Include units.',
},
{
+ 'key' => 'company_url',
+ 'section' => 'UI',
+ 'description' => 'Your company URL',
+ 'type' => 'text',
+ 'per_agent' => 1,
+ },
+
+ {
'key' => 'company_address',
'section' => 'required',
'description' => 'Your company address',
@@ -3587,9 +3602,19 @@ and customer address. Include units.',
'section' => 'billing',
'description' => 'Display format for line item date ranges on invoice line items.',
'type' => 'select',
- 'select_hash' => [ '' => 'STARTDATE-ENDDATE',
- 'month_of' => 'Month of MONTHNAME',
+ 'select_hash' => [ '' => 'STARTDATE-ENDDATE',
+ 'month_of' => 'Month of MONTHNAME',
+ 'X_month' => 'DATE_DESC MONTHNAME',
],
+ 'per_agent' => 1,
+ },
+
+ {
+ 'key' => 'cust_bill-line_item-date_description',
+ 'section' => 'billing',
+ 'description' => 'Text to display for "DATE_DESC" when using cust_bill-line_item-date_style DATE_DESC MONTHNAME.',
+ 'type' => 'text',
+ 'per_agent' => 1,
},
{
@@ -3896,7 +3921,17 @@ and customer address. Include units.',
'section' => 'UI',
'description' => 'Prefix the customer number with this string for display purposes.',
'type' => 'text',
- #and then probably agent-virt this to merge these instances
+ 'per_agent' => 1,
+ },
+
+ {
+ 'key' => 'cust_main-custnum-display_special',
+ 'section' => 'UI',
+ 'description' => 'Use this customer number prefix format',
+ 'type' => 'select',
+ 'select_hash' => [ '' => '',
+ 'CoStAg' => 'CoStAg (country, state, agent name or display_prefix)',
+ 'CoStCl' => 'CoStCl (country, state, class name)' ],
},
{
@@ -4164,6 +4199,20 @@ and customer address. Include units.',
},
{
+ 'key' => 'selfservice-login_banner_image',
+ 'section' => 'self-service',
+ 'description' => 'Banner image shown on the login page, in PNG format.',
+ 'type' => 'image',
+ },
+
+ {
+ 'key' => 'selfservice-login_banner_url',
+ 'section' => 'self-service',
+ 'description' => 'Link for the login banner.',
+ 'type' => 'text',
+ },
+
+ {
'key' => 'selfservice-bulk_format',
'section' => 'deprecated',
'description' => 'Parameter arrangement for selfservice bulk features',
@@ -4885,7 +4934,14 @@ and customer address. Include units.',
},
},
-
+ {
+ 'key' => 'brand-agent',
+ 'section' => 'UI',
+ 'description' => 'Brand the backoffice interface (currently Help->About) using the company_name, company_url and logo.png configuration settings of the selected agent. Typically used when selling or bundling hosted access to the backoffice interface. NOTE: The AGPL software license has specific requirements for source code availability in this situation.',
+ 'type' => 'select-agent',
+ },
+
+
{ key => "apacheroot", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
{ key => "apachemachine", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
{ key => "apachemachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
diff --git a/FS/FS/GeocodeCache.pm b/FS/FS/GeocodeCache.pm
new file mode 100644
index 000000000..7829c4df2
--- /dev/null
+++ b/FS/FS/GeocodeCache.pm
@@ -0,0 +1,209 @@
+package FS::GeocodeCache;
+
+use strict;
+use vars qw($conf $DEBUG);
+use base qw( FS::geocode_Mixin );
+use FS::Record qw( qsearch qsearchs );
+use FS::Conf;
+use FS::Misc::Geo;
+
+use Data::Dumper;
+
+FS::UID->install_callback( sub { $conf = new FS::Conf; } );
+
+$DEBUG = 0;
+
+=head1 NAME
+
+FS::GeocodeCache - An address undergoing the geocode process.
+
+=head1 SYNOPSIS
+
+ use FS::GeocodeCache;
+
+ $record = FS::GeocodeCache->standardize(%location_hash);
+
+=head1 DESCRIPTION
+
+An FS::GeocodeCache object represents a street address in the process of
+being geocoded. FS::GeocodeCache inherits from FS::geocode_Mixin.
+
+Most methods on this object throw an exception on error.
+
+FS::GeocodeCache has the following fields, with the same meaning as in
+L<FS::cust_location>:
+
+=over 4
+
+=item address1
+
+=item address2
+
+=item city
+
+=item county
+
+=item state
+
+=item zip
+
+=item latitude
+
+=item longitude
+
+=item addr_clean
+
+=item country
+
+=item censustract
+
+=item geocode
+
+=item district
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new cache object. For internal use. See C<standardize>.
+
+=cut
+
+# minimalist constructor
+sub new {
+ my $class = shift;
+ my $self = {
+ company => '',
+ address1 => '',
+ address2 => '',
+ city => '',
+ state => '',
+ zip => '',
+ country => '',
+ latitude => '',
+ longitude => '',
+ addr_clean => '',
+ censustract => '',
+ @_
+ };
+ bless $self, $class;
+}
+
+# minimalist accessor, for compatibility with geocode_Mixin
+sub get {
+ $_[0]->{$_[1]}
+}
+
+sub set {
+ $_[0]->{$_[1]} = $_[2];
+}
+
+sub location_hash { %{$_[0]} };
+
+=item set_censustract
+
+Look up the censustract, if it's not already filled in, and return it.
+On error, sets 'error' and returns nothing.
+
+This uses the "get_censustract_*" methods in L<FS::Misc::Geo>; currently
+the only one is 'ffiec'.
+
+=cut
+
+sub set_censustract {
+ my $self = shift;
+
+ if ( $self->get('censustract') =~ /^\d{9}\.\d{2}$/ ) {
+ return $self->get('censustract');
+ }
+ my $censusyear = $conf->config('census_year');
+ return if !$censusyear;
+
+ my $method = 'ffiec';
+ # configurable censustract-only lookup goes here if it's ever needed.
+ $method = "get_censustract_$method";
+ my $censustract = eval { FS::Misc::Geo->$method($self, $censusyear) };
+ $self->set("censustract_error", $@);
+ $self->set("censustract", $censustract);
+}
+
+=item set_coord
+
+Set the latitude and longitude fields if they're not already set. Returns
+those values, in order.
+
+=cut
+
+sub set_coord { # the one in geocode_Mixin will suffice
+ my $self = shift;
+ if ( !$self->get('latitude') || !$self->get('longitude') ) {
+ $self->SUPER::set_coord;
+ $self->set('coord_error', $@);
+ }
+ return $self->get('latitude'), $self->get('longitude');
+}
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item standardize LOCATION
+
+Given a location hash or L<FS::geocode_Mixin> object, standardize the
+address using the configured method and return an L<FS::GeocodeCache>
+object.
+
+The methods are the "standardize_*" functions in L<FS::Geo::Misc>.
+
+=cut
+
+sub standardize {
+ my $class = shift;
+ my $location = shift;
+ $location = { $location->location_hash }
+ if UNIVERSAL::can($location, 'location_hash');
+
+ local $Data::Dumper::Terse = 1;
+ warn "standardizing location:\n".Dumper($location) if $DEBUG;
+
+ my $method = $conf->config('address_standardize_method');
+
+ if ( $method ) {
+ $method = "standardize_$method";
+ my $new_location = eval { FS::Misc::Geo->$method( $location ) };
+ if ( $new_location ) {
+ $location = {
+ addr_clean => 'Y',
+ %$new_location
+ # standardize_* can return an address with addr_clean => '' if
+ # the address is somehow questionable
+ }
+ }
+ else {
+ # XXX need an option to decide what to do on error
+ $location->{'addr_clean'} = '';
+ $location->{'error'} = $@;
+ }
+ warn "result:\n".Dumper($location) if $DEBUG;
+ }
+ # else $location = $location
+ my $cache = $class->new(%$location);
+ return $cache;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/Report/Table.pm b/FS/FS/Report/Table.pm
index 3942543b5..b0e911f84 100644
--- a/FS/FS/Report/Table.pm
+++ b/FS/FS/Report/Table.pm
@@ -32,21 +32,32 @@ options in %opt.
=over 4
-=item signups: The number of customers signed up.
+=item signups: The number of customers signed up. Options are "refnum"
+(limit by advertising source) and "indirect" (boolean, tells us to limit
+to customers that have a referral_custnum that matches the advertising source).
=cut
sub signups {
my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
- my @where = (
- $self->in_time_period_and_agent($speriod, $eperiod, $agentnum, 'signupdate')
+ my @where = ( $self->in_time_period_and_agent($speriod, $eperiod, $agentnum,
+ 'cust_main.signupdate')
);
- if ( $opt{'refnum'} ) {
+ my $join = '';
+ if ( $opt{'indirect'} ) {
+ $join = " JOIN cust_main AS referring_cust_main".
+ " ON (cust_main.referral_custnum = referring_cust_main.custnum)";
+
+ if ( $opt{'refnum'} ) {
+ push @where, "referring_cust_main.refnum = ".$opt{'refnum'};
+ }
+ }
+ elsif ( $opt{'refnum'} ) {
push @where, "refnum = ".$opt{'refnum'};
}
$self->scalar_sql(
- "SELECT COUNT(*) FROM cust_main WHERE ".join(' AND ', @where)
+ "SELECT COUNT(*) FROM cust_main $join WHERE ".join(' AND ', @where)
);
}
diff --git a/FS/FS/Report/Table/Monthly.pm b/FS/FS/Report/Table/Monthly.pm
index 41216992f..87c13a8ca 100644
--- a/FS/FS/Report/Table/Monthly.pm
+++ b/FS/FS/Report/Table/Monthly.pm
@@ -49,9 +49,8 @@ sub data {
my $syear = $self->{'start_year'};
my $emonth = $self->{'end_month'};
my $eyear = $self->{'end_year'};
- # how far to extrapolate into the future
- my $pmonth = $self->{'project_month'};
- my $pyear = $self->{'project_year'};
+ # whether to extrapolate into the future
+ my $projecting = $self->{'projection'};
# sanity checks
if ( $eyear < $syear or
@@ -61,17 +60,14 @@ sub data {
my $agentnum = $self->{'agentnum'};
- if ( $pyear > $eyear or
- ($pyear == $eyear and $pmonth > $emonth) ) {
+ if ( $projecting ) {
- # create the entire projection set first to avoid timing problems
+ $self->init_projection;
- $self->init_projection if $pmonth;
-
- my $thisyear = $eyear;
- my $thismonth = $emonth;
- while ( $thisyear < $pyear ||
- ( $thisyear == $pyear and $thismonth <= $pmonth )
+ my $thismonth = $smonth;
+ my $thisyear = $syear;
+ while ( $thisyear < $eyear ||
+ ( $thisyear == $eyear and $thismonth <= $emonth )
) {
my $speriod = timelocal(0,0,0,1,$thismonth-1,$thisyear);
$thismonth++;
@@ -84,10 +80,8 @@ sub data {
my %data;
- my $max_year = $pyear || $eyear;
- my $max_month = $pmonth || $emonth;
-
- my $projecting = 0; # are we currently projecting?
+ my $max_year = $eyear;
+ my $max_month = $emonth;
while ( $syear < $max_year
|| ( $syear == $max_year && $smonth < $max_month+1 ) ) {
@@ -101,11 +95,6 @@ sub data {
push @{$data{label}}, "$smonth/$syear";
}
- if ( $syear > $eyear || ( $syear == $eyear && $smonth >= $emonth + 1 ) ) {
- # start getting data from the projection
- $projecting = 1;
- }
-
my $speriod = timelocal(0,0,0,1,$smonth-1,$syear);
push @{$data{speriod}}, $speriod;
if ( ++$smonth == 13 ) { $syear++; $smonth=1; }
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 2deb88441..e69b0bc2c 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -3629,6 +3629,19 @@ sub tables_hashref {
'index' => [],
},
+ 'upgrade_journal' => {
+ 'columns' => [
+ 'upgradenum', 'serial', '', '', '', '',
+ '_date', 'int', '', '', '', '',
+ 'upgrade', 'varchar', '', $char_d, '', '',
+ 'status', 'varchar', '', $char_d, '', '',
+ 'statustext', 'varchar', 'NULL', $char_d, '', '',
+ ],
+ 'primary_key' => 'upgradenum',
+ 'unique' => [ [ 'upgradenum' ] ],
+ 'index' => [ [ 'upgrade' ] ],
+ },
+
%{ tables_hashref_torrus() },
# tables of ours for doing torrus virtual port combining
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index 8f66c66b5..aabc4e72f 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -7,6 +7,7 @@ use Tie::IxHash;
use FS::UID qw( dbh driver_name );
use FS::Conf;
use FS::Record qw(qsearchs qsearch str2time_sql);
+use FS::upgrade_journal;
use FS::svc_domain;
$FS::svc_domain::whois_hack = 1;
diff --git a/FS/FS/access_right.pm b/FS/FS/access_right.pm
index ef8cc6cd8..341055bfc 100644
--- a/FS/FS/access_right.pm
+++ b/FS/FS/access_right.pm
@@ -180,6 +180,51 @@ sub _upgrade_data { # class method
}
+ my @all_groups = qsearch('access_group', {});
+
+ my %onetime = (
+ 'List customers' => 'List all customers',
+ 'List packages' => 'Summarize packages',
+ );
+
+ foreach my $old_acl ( keys %onetime ) {
+ my $new_acl = $onetime{$old_acl}; #support arrayref too?
+ ( my $journal = 'ACL_'.lc($new_acl) ) =~ s/ /_/g;
+ next if FS::upgrade_journal->is_done($journal);
+
+ # grant $new_acl to all groups who have $old_acl
+ for my $group (@all_groups) {
+ if ( $group->access_right($old_acl) ) {
+ my $access_right = FS::access_right->new( {
+ 'righttype' => 'FS::access_group',
+ 'rightobjnum' => $group->groupnum,
+ 'rightname' => $new_acl,
+ } );
+ my $error = $access_right->insert;
+ die $error if $error;
+ }
+ }
+
+ FS::upgrade_journal->set_done($journal);
+ }
+
+ ### ACL_download_report_data
+ if ( !FS::upgrade_journal->is_done('ACL_download_report_data') ) {
+
+ # grant to everyone
+ for my $group (@all_groups) {
+ my $access_right = FS::access_right->new( {
+ 'righttype' => 'FS::access_group',
+ 'rightobjnum' => $group->groupnum,
+ 'rightname' => 'Download report data',
+ } );
+ my $error = $access_right->insert;
+ die $error if $error;
+ }
+
+ FS::upgrade_journal->set_done('ACL_download_report_data');
+ }
+
'';
}
diff --git a/FS/FS/cdr/cia.pm b/FS/FS/cdr/cia.pm
index 61343338a..070f3fb0d 100644
--- a/FS/FS/cdr/cia.pm
+++ b/FS/FS/cdr/cia.pm
@@ -28,8 +28,8 @@ use FS::cdr qw(_cdr_date_parser_maker);
'dst', # DNIS
'src', # ANI
skip(2), # Call Type, Toll Free,
- skip(1), # Chair Conference Entry Code
- 'accountcode', # Participant Conference Entry Code,
+ 'accountcode', # Chair Conference Entry Code
+ skip(1), # Participant Conference Entry Code,
],
);
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index 945771e0d..a76170a9b 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -2006,7 +2006,7 @@ sub print_csv {
);
} elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
-
+
my ($previous_balance) = $self->previous;
my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
my @items = map {
@@ -2017,6 +2017,7 @@ sub print_csv {
$csv->combine(
$cust_main->agentnum,
+ $cust_main->agent->agent,
$self->custnum,
$cust_main->first,
$cust_main->last,
@@ -4886,6 +4887,8 @@ sub _items_cust_bill_pkg {
my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+ my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
+
my @b = ();
my ($s, $r, $u) = ( undef, undef, undef );
foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
@@ -5021,14 +5024,24 @@ sub _items_cust_bill_pkg {
my $description = ($is_summary && $type && $type eq 'U')
? "Usage charges" : $desc;
+ #pry be a bit more efficient to look some of this conf stuff up
+ # outside the loop
unless (
$conf->exists('disable_line_item_date_ranges')
|| $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
) {
my $time_period;
- my $date_style = $conf->config('cust_bill-line_item-date_style');
+ my $date_style = $conf->config( 'cust_bill-line_item-date_style',
+ $cust_main->agentnum
+ );
if ( defined($date_style) && $date_style eq 'month_of' ) {
$time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
+ } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
+ my $desc = $conf->config( 'cust_bill-line_item-date_description',
+ $cust_main->agentnum
+ );
+ $desc .= ' ' unless $desc =~ /\s$/;
+ $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
} else {
$time_period = time2str($date_format, $cust_bill_pkg->sdate).
" - ". time2str($date_format, $cust_bill_pkg->edate);
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index ebc049182..af65ca7de 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -3963,12 +3963,32 @@ cust_main-default_agent_custid is set and it has a value, custnum otherwise.
sub display_custnum {
my $self = shift;
+
+ my $prefix = $conf->config('cust_main-custnum-display_prefix', $self->agentnum) || '';
+ if ( my $special = $conf->config('cust_main-custnum-display_special') ) {
+ if ( $special eq 'CoStAg' ) {
+ $prefix = uc( join('',
+ $self->country,
+ ($self->state =~ /^(..)/),
+ $prefix || ($self->agent->agent =~ /^(..)/)
+ ) );
+ }
+ elsif ( $special eq 'CoStCl' ) {
+ $prefix = uc( join('',
+ $self->country,
+ ($self->state =~ /^(..)/),
+ ($self->classnum ? $self->cust_class->classname =~ /^(..)/ : '__')
+ ) );
+ }
+ # add any others here if needed
+ }
+
my $length = $conf->config('cust_main-custnum-display_length');
if ( $conf->exists('cust_main-default_agent_custid') && $self->agent_custid ){
return $self->agent_custid;
- } elsif ( $conf->config('cust_main-custnum-display_prefix') ) {
+ } elsif ( $prefix ) {
$length = 8 if !defined($length);
- return $conf->config('cust_main-custnum-display_prefix').
+ return $prefix .
sprintf('%0'.$length.'d', $self->custnum)
} elsif ( $length ) {
return sprintf('%0'.$length.'d', $self->custnum);
diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm
index 62464e4aa..1e9eee79d 100644
--- a/FS/FS/cust_main/Search.pm
+++ b/FS/FS/cust_main/Search.pm
@@ -127,6 +127,12 @@ sub smart_search {
|| ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+'
&& $search =~ /^\s*(\w\w?\d+)\s*$/
)
+ || ( $conf->config('cust_main-custnum-display_special')
+ # it's not currently possible for special prefixes to contain
+ # digits, so just strip off any alphabetic prefix and match
+ # the rest to custnum
+ && $search =~ /^\s*[[:alpha:]]*(\d+)\s*$/
+ )
|| ( $conf->exists('address1-search' )
&& $search =~ /^\s*(\d+\-?\w*)\s*$/ #i.e. 1234A or 9432-D
)
@@ -143,25 +149,23 @@ sub smart_search {
} );
}
- #if this becomes agent-virt need to get a list of all prefixes the current
- #user can see (via their agents)
- my $prefix = $conf->config('cust_main-custnum-display_prefix');
- if ( $prefix && $prefix eq substr($num, 0, length($prefix)) ) {
- push @cust_main, qsearch( {
- 'table' => 'cust_main',
- 'hashref' => { 'custnum' => 0 + substr($num, length($prefix)),
- %options,
+ # for all agents this user can see, if any of them have custnum prefixes
+ # that match the search string, include customers that match the rest
+ # of the custnum and belong to that agent
+ foreach my $agentnum ( $FS::CurrentUser::CurrentUser->agentnums ) {
+ my $p = $conf->config('cust_main-custnum-display_prefix', $agentnum);
+ next if !$p;
+ if ( $p eq substr($num, 0, length($p)) ) {
+ push @cust_main, qsearch( {
+ 'table' => 'cust_main',
+ 'hashref' => { 'custnum' => 0 + substr($num, length($p)),
+ 'agentnum' => $agentnum,
+ %options,
},
- 'extra_sql' => " AND $agentnums_sql", #agent virtualization
- } );
+ } );
+ }
}
- push @cust_main, qsearch( {
- 'table' => 'cust_main',
- 'hashref' => { 'agent_custid' => $num, %options },
- 'extra_sql' => " AND $agentnums_sql", #agent virtualization
- } );
-
if ( $conf->exists('address1-search') ) {
my $len = length($num);
$num = lc($num);
diff --git a/FS/FS/cust_pay_void.pm b/FS/FS/cust_pay_void.pm
index f1193cd24..bebcfd4cc 100644
--- a/FS/FS/cust_pay_void.pm
+++ b/FS/FS/cust_pay_void.pm
@@ -68,9 +68,7 @@ order taker (see L<FS::access_user>)
=item payby
-`CARD' (credit cards), `CHEK' (electronic check/ACH),
-`LECB' (phone bill billing), `BILL' (billing), `CASH' (cash),
-`WEST' (Western Union), `MCRD' (Manual credit card), or `COMP' (free)
+Payment Type (See L<FS::payinfo_Mixin> for valid values)
=item payinfo
@@ -186,6 +184,7 @@ sub check {
|| $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
|| $self->ut_numbern('void_date')
|| $self->ut_textn('reason')
+ || $self->payinfo_check
;
return $error if $error;
@@ -197,31 +196,6 @@ sub check {
$self->void_date(time) unless $self->void_date;
- $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP|PREP|CASH|WEST|MCRD)$/
- or return "Illegal payby";
- $self->payby($1);
-
- #false laziness with cust_refund::check
- if ( $self->payby eq 'CARD' ) {
- my $payinfo = $self->payinfo;
- $payinfo =~ s/\D//g;
- $self->payinfo($payinfo);
- if ( $self->payinfo ) {
- $self->payinfo =~ /^(\d{13,16}|\d{8,9})$/
- or return "Illegal (mistyped?) credit card number (payinfo)";
- $self->payinfo($1);
- validate($self->payinfo) or return "Illegal credit card number";
- return "Unknown card type" if $self->payinfo !~ /^99\d{14}$/ #token
- && cardtype($self->payinfo) eq "Unknown";
- } else {
- $self->payinfo('N/A');
- }
-
- } else {
- $error = $self->ut_textn('payinfo');
- return $error if $error;
- }
-
$self->void_usernum($FS::CurrentUser::CurrentUser->usernum)
unless $self->void_usernum;
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 855accc0c..bee1b82fb 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -3300,7 +3300,7 @@ sub search {
"NOT (".FS::cust_pkg->onetime_sql . ")";
}
else {
- foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end cancel )) {
+ foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel )) {
next unless exists($params->{$field});
diff --git a/FS/FS/part_event/Condition.pm b/FS/FS/part_event/Condition.pm
index b3948153e..fc69f1d0c 100644
--- a/FS/FS/part_event/Condition.pm
+++ b/FS/FS/part_event/Condition.pm
@@ -363,7 +363,7 @@ sub condition_sql_option_option {
#used for part_event/Condition/cust_bill_has_service.pm and has_cust_tag.pm
#a little false laziness w/above and condition_sql_option_integer
sub condition_sql_option_option_integer {
- my( $class, $option, $driver_name ) = @_;
+ my( $class, $option ) = @_;
( my $condname = $class ) =~ s/^.*:://;
@@ -375,7 +375,7 @@ sub condition_sql_option_option_integer {
AND part_event_condition_option.optionvalue = 'HASH'
)";
- my $integer = ($driver_name =~ /^mysql/) ? 'UNSIGNED INTEGER' : 'INTEGER';
+ my $integer = (driver_name =~ /^mysql/) ? 'UNSIGNED INTEGER' : 'INTEGER';
my $optionname = "CAST(optionname AS $integer)";
diff --git a/FS/FS/part_event/Condition/cust_bill_has_service.pm b/FS/FS/part_event/Condition/cust_bill_has_service.pm
index 65c996437..6e981ee03 100644
--- a/FS/FS/part_event/Condition/cust_bill_has_service.pm
+++ b/FS/FS/part_event/Condition/cust_bill_has_service.pm
@@ -42,9 +42,7 @@ sub condition_sql {
my( $class, $table, %opt ) = @_;
my $servicenums =
- $class->condition_sql_option_option_integer( 'has_service',
- $opt{'driver_name'},
- );
+ $class->condition_sql_option_option_integer('has_service');
my $sql = qq| 0 < ( SELECT COUNT(cs.svcpart)
FROM cust_bill_pkg cbp, cust_svc cs
diff --git a/FS/FS/payby.pm b/FS/FS/payby.pm
index 33ed42507..d1961a58d 100644
--- a/FS/FS/payby.pm
+++ b/FS/FS/payby.pm
@@ -176,6 +176,11 @@ sub realtime { # can use realtime payment facilities
return $hash{$payby}->{realtime};
}
+sub payby2shortname {
+ my $self = shift;
+ map { $_ => $hash{$_}->{shortname} } $self->payby;
+}
+
sub payby2longname {
my $self = shift;
map { $_ => $hash{$_}->{longname} } $self->payby;
diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm
index 2b6be2c6c..64cc3770e 100755
--- a/FS/FS/svc_broadband.pm
+++ b/FS/FS/svc_broadband.pm
@@ -428,7 +428,8 @@ sub check {
}
else {
my $addr_block = $self->addr_block;
- unless ( $addr_block and $addr_block->manual_flag ) {
+ if ( $self->ip_addr eq ''
+ and not ( $addr_block and $addr_block->manual_flag ) ) {
my $error = $self->assign_ip_addr;
return $error if $error;
}
@@ -525,6 +526,12 @@ sub _check_ip_addr {
else {
return 'Cannot parse address: '.$self->ip_addr unless $self->NetAddr;
}
+
+ if ( $self->addr_block
+ and not $self->addr_block->NetAddr->contains($self->NetAddr) ) {
+ return 'Address '.$self->ip_addr.' not in block '.$self->addr_block->cidr;
+ }
+
# if (my $dup = qsearchs('svc_broadband', {
# ip_addr => $self->ip_addr,
# svcnum => {op=>'!=', value => $self->svcnum}
diff --git a/FS/FS/upgrade_journal.pm b/FS/FS/upgrade_journal.pm
new file mode 100644
index 000000000..8f6d121a3
--- /dev/null
+++ b/FS/FS/upgrade_journal.pm
@@ -0,0 +1,151 @@
+package FS::upgrade_journal;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::upgrade_journal - Object methods for upgrade_journal records
+
+=head1 SYNOPSIS
+
+ use FS::upgrade_journal;
+
+ $record = new FS::upgrade_journal \%hash;
+ $record = new FS::upgrade_journal { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ # Typical use case
+ my $upgrade = 'rename_all_customers_to_Bob';
+ if ( ! FS::upgrade_journal->is_done($upgrade) ) {
+ ... # do the upgrade, then, if it succeeds
+ FS::upgrade_journal->set_done($upgrade);
+ }
+
+=head1 DESCRIPTION
+
+An FS::upgrade_journal object records an upgrade procedure that was run
+on the database. FS::upgrade_journal inherits from FS::Record. The
+following fields are currently supported:
+
+=over 4
+
+=item upgradenum - primary key
+
+=item _date - unix timestamp when the upgrade was run
+
+=item upgrade - string identifier for the upgrade procedure; must match /^\w+$/
+
+=item status - either 'done' or 'failed'
+
+=item statustext - any other message that needs to be recorded
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new upgrade record. To add it to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'upgrade_journal'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+sub delete { die "upgrade_journal records can't be deleted" }
+sub replace { die "upgrade_journal records can't be modified" }
+
+=item check
+
+Checks all fields to make sure this is a valid example. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+ my $self = shift;
+
+ if ( !$self->_date ) {
+ $self->_date(time);
+ }
+
+ my $error =
+ $self->ut_numbern('upgradenum')
+ || $self->ut_number('_date')
+ || $self->ut_alpha('upgrade')
+ || $self->ut_text('status')
+ || $self->ut_textn('statustext')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item is_done UPGRADE
+
+Returns the upgrade entry with identifier UPGRADE and status 'done', if
+there is one. This is an easy way to check whether an upgrade has been done.
+
+=cut
+
+sub is_done {
+ my ($class, $upgrade) = @_;
+ qsearch('upgrade_journal', { 'status' => 'done', 'upgrade' => $upgrade })
+}
+
+=item set_done UPGRADE
+
+Creates and inserts an upgrade entry with the current time, status 'done',
+and identifier UPGRADE. Dies on error.
+
+=cut
+
+sub set_done {
+ my ($class, $upgrade) = @_;
+ my $new = $class->new({ 'status' => 'done', 'upgrade' => $upgrade });
+ my $error = $new->insert;
+ die $error if $error;
+ $new;
+}
+
+
+=head1 BUGS
+
+Despite how it looks, this is not currently suitable for use as a mutex.
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/MANIFEST b/FS/MANIFEST
index e97f8702a..9cff85651 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -634,3 +634,5 @@ FS/contact_class.pm
t/contact_class.t
FS/GeocodeCache.pm
t/GeocodeCache.t
+FS/upgrade_journal.pm
+t/upgrade_journal.t
diff --git a/FS/t/GeocodeCache.t b/FS/t/GeocodeCache.t
new file mode 100644
index 000000000..eae6f0d01
--- /dev/null
+++ b/FS/t/GeocodeCache.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::GeocodeCache;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/upgrade_journal.t b/FS/t/upgrade_journal.t
new file mode 100644
index 000000000..0822effc5
--- /dev/null
+++ b/FS/t/upgrade_journal.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::upgrade_journal;
+$loaded=1;
+print "ok 1\n";