diff options
Diffstat (limited to 'FS')
-rw-r--r-- | FS/FS/AccessRight.pm | 5 | ||||
-rw-r--r-- | FS/FS/ClientAPI/MyAccount.pm | 13 | ||||
-rw-r--r-- | FS/FS/ClientAPI_XMLRPC.pm | 19 | ||||
-rw-r--r-- | FS/FS/Conf.pm | 64 | ||||
-rw-r--r-- | FS/FS/GeocodeCache.pm | 209 | ||||
-rw-r--r-- | FS/FS/Report/Table.pm | 21 | ||||
-rw-r--r-- | FS/FS/Report/Table/Monthly.pm | 31 | ||||
-rw-r--r-- | FS/FS/Schema.pm | 13 | ||||
-rw-r--r-- | FS/FS/Upgrade.pm | 1 | ||||
-rw-r--r-- | FS/FS/access_right.pm | 45 | ||||
-rw-r--r-- | FS/FS/cdr/cia.pm | 4 | ||||
-rw-r--r-- | FS/FS/cust_bill.pm | 17 | ||||
-rw-r--r-- | FS/FS/cust_main.pm | 24 | ||||
-rw-r--r-- | FS/FS/cust_main/Search.pm | 36 | ||||
-rw-r--r-- | FS/FS/cust_pay_void.pm | 30 | ||||
-rw-r--r-- | FS/FS/cust_pkg.pm | 2 | ||||
-rw-r--r-- | FS/FS/part_event/Condition.pm | 4 | ||||
-rw-r--r-- | FS/FS/part_event/Condition/cust_bill_has_service.pm | 4 | ||||
-rw-r--r-- | FS/FS/payby.pm | 5 | ||||
-rwxr-xr-x | FS/FS/svc_broadband.pm | 9 | ||||
-rw-r--r-- | FS/FS/upgrade_journal.pm | 151 | ||||
-rw-r--r-- | FS/MANIFEST | 2 | ||||
-rw-r--r-- | FS/t/GeocodeCache.t | 5 | ||||
-rw-r--r-- | FS/t/upgrade_journal.t | 5 |
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"; |