From: Ivan Kohler Date: Tue, 18 Jul 2023 23:28:58 +0000 (-0700) Subject: default to a session cookie instead of setting an explicit timeout, weird timezone... X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=HEAD;hp=44d19ccf1d63e25db7cc3dea59ad9ecfa3c81fbf default to a session cookie instead of setting an explicit timeout, weird timezone/clock skew effects on server can cause firefox and other browsers to reject the session cookie, leading to silent login failures --- diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 701f7724a..601337ef0 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -210,6 +210,7 @@ tie my %rights, 'Tie::IxHash', { rightname=>'Backdate payment', desc=>'Enable payments to be posted for days other than today.' }, 'Post check payment', 'Post cash payment', + 'Post Paypal payment', 'Post payment batch', 'Apply payment', #NEWNEW { rightname=>'Unapply payment', desc=>'Enable "unapplication" of unclosed payments from specific invoices.' }, #aka. unapplypayments @@ -416,6 +417,8 @@ tie my %rights, 'Tie::IxHash', { rightname=>'Alarm configuration' }, { rightname=>'Alarm global configuration', global=>1 }, + { rightname=>'Edit hardware classes and types' }, + { rightname=> 'Configure network monitoring', global=>1 }, #{ rightname=>'Edit employees', global=>1, }, diff --git a/FS/FS/Auth/internal.pm b/FS/FS/Auth/internal.pm index dfc5f301d..92dff0318 100644 --- a/FS/FS/Auth/internal.pm +++ b/FS/FS/Auth/internal.pm @@ -7,7 +7,7 @@ use FS::Record qw( qsearchs ); use FS::access_user; sub authenticate { - my($self, $username, $check_password ) = @_; + my($self, $username, $check_password, $totp_code ) = @_; my $access_user = ref($username) ? $username @@ -17,6 +17,7 @@ sub authenticate { ) or return 0; + my $pw_check; if ( $access_user->_password_encoding eq 'bcrypt' ) { my( $cost, $salt, $hash ) = split(',', $access_user->_password); @@ -29,17 +30,21 @@ sub authenticate { ) ); - $hash eq $check_hash; + $pw_check = $hash eq $check_hash; - } else { + } else { return 0 if $access_user->_password eq 'notyet' || $access_user->_password eq ''; - $access_user->_password eq $check_password; + $pw_check = $access_user->_password eq $check_password; } + return $pw_check if ! $pw_check || ! length($access_user->totp_secret32); + + #2fa + $access_user->google_auth->verify( $totp_code, 1 ); } sub autocreate { 0; } diff --git a/FS/FS/AuthCookieHandler.pm b/FS/FS/AuthCookieHandler.pm index 93d8ea6a5..b7d0dbf5b 100644 --- a/FS/FS/AuthCookieHandler.pm +++ b/FS/FS/AuthCookieHandler.pm @@ -13,13 +13,13 @@ sub useragent_ip { } sub authen_cred { - my( $self, $r, $username, $password ) = @_; + my( $self, $r, $username, $password, $totp_code ) = @_; preuser_setup(); my $info = {}; - unless ( FS::Auth->authenticate($username, $password, $info) ) { + unless ( FS::Auth->authenticate($username, $password, $totp_code, $info) ) { warn "failed auth $username from ". $self->useragent_ip($r). "\n"; return undef; } diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index cbccffb94..57a886770 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2452,8 +2452,8 @@ and customer address. Include units.', { 'key' => 'selfservice-timeout', - 'section' => 'self-service', - 'description' => 'Timeout for the self-service login cookie, in seconds. Defaults to 1 hour.', + 'section' => 'deprecated', + 'description' => 'Deprecated. Was the timeout for the self-service login cookie, in seconds. Defaulted to 1 hour.', 'type' => 'text', }, @@ -2672,7 +2672,7 @@ and customer address. Include units.', 'section' => 'payments', 'description' => 'Available payment types.', 'type' => 'selectmultiple', - 'select_enum' => [ qw(CARD DCRD CHEK DCHK) ], #BILL CASH WEST MCRD MCHK PPAL) ], + 'select_enum' => [ qw(CARD DCRD CHEK DCHK PPAL) ], #BILL CASH WEST MCRD MCHK PPAL) ], }, { @@ -4203,6 +4203,13 @@ and customer address. Include units.', }, { + 'key' => 'dashboard-topnotes', + 'section' => 'UI', + 'description' => 'Note to display on the top of the front page', + 'type' => 'textarea', + }, + + { 'key' => 'dashboard-toplist', 'section' => 'UI', 'description' => 'List of items to display on the top of the front page', @@ -4581,13 +4588,25 @@ and customer address. Include units.', { 'key' => 'census_year', - 'section' => 'addresses', - 'description' => 'The year to use in census tract lookups. NOTE: you need to select 2012 or 2013 for Year 2010 Census tract codes. A selection of 2011 provides Year 2000 Census tract codes. Use the freeside-censustract-update tool if exisitng customers need to be changed.', + 'section' => 'deprecated', + 'description' => 'Deprecated. Used to control the year used for census lookups. 2020 census data is now the default. Use the freeside-censustract-update tool if exisitng customers need to be changed. See the census_legacy configuration option if you need old census data to re-file pre-2022 FCC 477 reports.', 'type' => 'select', 'select_enum' => [ qw( 2017 2016 2015 ) ], }, { + 'key' => 'census_legacy', + 'section' => 'addresses', + 'description' => 'Use old census data (and source). Should only be needed if re-filing pre-2022 FCC 477 reports.', + 'type' => 'select', + 'select_hash' => [ '' => 'Disabled (2020)', + '2015' => '2015', + '2016' => '2016', + '2017' => '2017', + ], + }, + + { 'key' => 'tax_district_method', 'section' => 'taxation', 'description' => 'The method to use to look up tax district codes.', @@ -5188,6 +5207,14 @@ and customer address. Include units.', }, { + 'key' => 'cdr-skip_duplicate_rewrite-sipcallid', + 'section' => 'telephony', + 'description' => 'Use the freeside-cdrrewrited daemon to prevent billing CDRs with a sipcallid identical to an existing CDR', + 'type' => 'checkbox', + }, + + + { 'key' => 'cdr-charged_party_rewrite', 'section' => 'telephony', 'description' => 'Do charged party rewriting in the freeside-cdrrewrited daemon; useful if CDRs are being dropped off directly in the database and require special charged_party processing such as cdr-charged_party-accountcode or cdr-charged_party-truncate*.', diff --git a/FS/FS/Cron/backup.pm b/FS/FS/Cron/backup.pm index 8264ae1d6..65d7acb0b 100644 --- a/FS/FS/Cron/backup.pm +++ b/FS/FS/Cron/backup.pm @@ -25,7 +25,11 @@ sub backup { my $ext; if ( driver_name eq 'Pg' ) { - system("pg_dump -Fc -T h_cdr -T h_queue -T h_queue_arg -T sessions $database >/var/tmp/$database.Pg"); + system('pg_dump -Fc '. join(' ', map { "--exclude-table-data $_" } + qw( h_cdr h_queue h_queue_arg sessions ) + ). + " $database >/var/tmp/$database.Pg" + ); $ext = 'Pg'; } elsif ( driver_name eq 'mysql' ) { system("mysqldump $database >/var/tmp/$database.sql"); diff --git a/FS/FS/GeocodeCache.pm b/FS/FS/GeocodeCache.pm index 7829c4df2..9a30f0050 100644 --- a/FS/FS/GeocodeCache.pm +++ b/FS/FS/GeocodeCache.pm @@ -110,23 +110,23 @@ 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; currently -the only one is 'ffiec'. +available are 'uscensus' (default) or 'ffiec' (legacy, used if the +census_legacy configuration option is set). =cut sub set_censustract { my $self = shift; - if ( $self->get('censustract') =~ /^\d{9}\.\d{2}$/ ) { + if ( $self->get('censustract') =~ /^\d{9}(\.\d{2}|\d{6})$/ ) { 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. + my $year = $conf->config('census_legacy') || 2020; + my $method = ($year==2020) ? 'uscensus' : 'ffiec'; + $method = "get_censustract_$method"; - my $censustract = eval { FS::Misc::Geo->$method($self, $censusyear) }; + my $censustract = eval { FS::Misc::Geo->$method($self, $year) }; $self->set("censustract_error", $@); $self->set("censustract", $censustract); } diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 8dd72ac79..ebd40adda 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -85,6 +85,7 @@ if ( -e $addl_handler_use_file ) { die $@ if $@; } use Text::CSV_XS; + use Archive::Zip; use Spreadsheet::WriteExcel; use Spreadsheet::WriteExcel::Utility; use OLE::Storage_Lite; @@ -120,7 +121,10 @@ if ( -e $addl_handler_use_file ) { use Locale::Currency::Format; use Number::Phone::Country qw( noexport ); use Business::US::USPS::WebTools::AddressStandardization; - use Geo::GoogleEarth::Pluggable; + use Geo::GoogleEarth::Pluggable 0.16; + use Geo::Shapelib; + use Geo::JSON; + use Geo::JSON::FeatureCollection; use LWP::UserAgent; use Storable qw( nfreeze thaw ); use FS; diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm index 5eba874ff..139f05ddf 100644 --- a/FS/FS/Misc.pm +++ b/FS/FS/Misc.pm @@ -15,7 +15,7 @@ use Encode; #instead @ISA = qw( Exporter ); -@EXPORT_OK = qw( send_email generate_email send_fax +@EXPORT_OK = qw( send_email generate_email send_fax _sendmail states_hash counties cities state_label card_types pkg_freqs @@ -272,28 +272,6 @@ sub send_email { #send the email - my %smtp_opt = ( 'host' => $conf->config('smtpmachine'), - 'helo' => $domain, - ); - - my($port, $enc) = split('-', ($conf->config('smtp-encryption') || '25') ); - $smtp_opt{'port'} = $port; - - my $error = ''; - if ( $conf->exists('smtp-username') && $conf->exists('smtp-password') ) { - $smtp_opt{"sasl_$_"} = $conf->config("smtp-$_") for qw(username password); - } elsif ( defined($enc) && $enc eq 'starttls') { - $error = "SMTP settings misconfiguration: STARTTLS enabled in ". - "smtp-encryption but smtp-username or smtp-password missing"; - } - - if ( defined($enc) ) { - $smtp_opt{'ssl'} = 'starttls' if $enc eq 'starttls'; - $smtp_opt{'ssl'} = 1 if $enc eq 'tls'; - } - - my $transport = Email::Sender::Transport::SMTP->new( %smtp_opt ); - push @to, $options{bcc} if defined($options{bcc}); # fully unpack all addresses found in @to (including Bcc) to make the # envelope list @@ -302,22 +280,11 @@ sub send_email { push @env_to, map { $_->address } Email::Address->parse($dest); } - unless ( length($error) ) { - - local $SIG{__DIE__}; # don't want Mason __DIE__ handler active - local $@; # just in case - eval { sendmail($message, { transport => $transport, - from => $from, - to => \@env_to }) }; - - if (ref($@) and $@->isa('Email::Sender::Failure')) { - $error = $@->code.' ' if $@->code; - $error .= $@->message; - } else { - $error = $@; - } - - } + my $error = _sendmail( $message, { 'from' => $from, + 'to' => \@env_to, + 'domain' => $domain, + } + ); # Logging if ( $conf->exists('log_sent_mail') ) { @@ -341,6 +308,48 @@ sub send_email { } +sub _sendmail { + my($message, $options) = @_; + my $domain = delete $options->{'domain'}; + + my %smtp_opt = ( 'host' => $conf->config('smtpmachine'), + 'helo' => $domain, + ); + + my($port, $enc) = split('-', ($conf->config('smtp-encryption') || '25') ); + $smtp_opt{'port'} = $port; + + if ( $conf->exists('smtp-username') && $conf->exists('smtp-password') ) { + $smtp_opt{"sasl_$_"} = $conf->config("smtp-$_") for qw(username password); + } elsif ( defined($enc) && $enc eq 'starttls') { + return "SMTP settings misconfiguration: STARTTLS enabled in ". + "smtp-encryption but smtp-username or smtp-password missing"; + } + + if ( defined($enc) ) { + $smtp_opt{'ssl'} = 'starttls' if $enc eq 'starttls'; + $smtp_opt{'ssl'} = 1 if $enc eq 'tls'; + } + + $options->{'transport'} = Email::Sender::Transport::SMTP->new( %smtp_opt ); + + my $error = ''; + + local $SIG{__DIE__}; # don't want Mason __DIE__ handler active + local $@; # just in case + eval { sendmail($message, $options) }; + + if (ref($@) and $@->isa('Email::Sender::Failure')) { + $error = $@->code.' ' if $@->code; + $error .= $@->message; + } else { + $error = $@; + } + + $error; + +} + =item generate_email OPTION => VALUE ... Options: diff --git a/FS/FS/Misc/Geo.pm b/FS/FS/Misc/Geo.pm index bc020a22d..6f923f595 100644 --- a/FS/FS/Misc/Geo.pm +++ b/FS/FS/Misc/Geo.pm @@ -38,6 +38,10 @@ Given a location hash (see L) and a census map year, returns a census tract code (consisting of state, county, and tract codes) or an error message. +Data source: Federal Financial Institutions Examination Council + +Note: This is the old method for pre-2022 (census year 2020) reporting. + =cut sub get_censustract_ffiec { @@ -105,6 +109,84 @@ sub get_censustract_ffiec { } } +=item get_censustract_uscensus LOCATION YEAR + +Given a location hash (see L) and a census map year, +returns a census tract code (consisting of state, county, tract, and block +codes) or an error message. + +Data source: US Census Bureau + +This is the new method for 2022+ (census year 2020) reporting. + +=cut + +sub get_censustract_uscensus { + my $class = shift; + my $location = shift; + my $year = shift || 2020; + + if ( length($location->{country}) and uc($location->{country}) ne 'US' ) { + return ''; + } + + warn Dumper($location, $year) if $DEBUG; + + my $url = 'https://geocoding.geo.census.gov/geocoder/geographies/address?'; + + my $address1 = $location->{address1}; + $address1 =~ s/(apt|ste|suite|unit)[\s\d]\w*\s*$//i; + + my $query_hash = { + street => $address1, + city => $location->{city}, + state => $location->{state}, + benchmark => 'Public_AR_Current', + vintage => 'Census'.$year.'_Current', + format => 'json', + }; + + my $full_url = URI->new($url); + $full_url->query_form($query_hash); + + warn "Full Request URL: \n".$full_url if $DEBUG; + + my $ua = new LWP::UserAgent; + my $res = $ua->get( $full_url ); + + warn $res->as_string if $DEBUG > 2; + + if (!$res->is_success) { + die 'Census tract lookup error: '.$res->message; + } + + local $@; + my $content = eval { decode_json($res->content) }; + die "Census tract JSON error: $@\n" if $@; + + warn Dumper($content) if $DEBUG; + + my $addressMatches_ref = $content->{result}->{addressMatches}; + + if ( $addressMatches_ref && scalar @{$addressMatches_ref} ) { + + my $tract = $addressMatches_ref->[0]->{geographies}->{'Census Blocks'}[0]->{GEOID}; + return $tract; + + } else { + + my $error = 'Lookup failed, but with no status message.'; + + if ( $content->{errors} ) { + $error = join("\n", $content->{errors}); + } + + die "$error\n"; + + } +} + + #sub get_district_methods { # '' => '', # 'wa_sales' => 'Washington sales tax', @@ -660,6 +742,7 @@ sub subloc_address2 { ($subloc, $addr2); } +#is anyone still using this? sub standardize_melissa { my $class = shift; my $location = shift; diff --git a/FS/FS/Report/FCC_477.pm b/FS/FS/Report/FCC_477.pm index 75ddee0d7..3749bfebe 100644 --- a/FS/FS/Report/FCC_477.pm +++ b/FS/FS/Report/FCC_477.pm @@ -365,10 +365,12 @@ sub fbd_sql { 'adv_speed_down', 'adv_speed_up', 'CASE WHEN is_business IS NOT NULL THEN 1 ELSE 0 END', - 'cir_speed_down', - 'cir_speed_up', ); - push @select, 'blocknum' if $opt{detail}; + push @select, 'cir_speed_down', 'cir_speed_up' + if $opt{date} < 1569826800; #9/30/2019, halfway between the two filing + # "as of" dates when it changed + push @select, 'blocknum' + if $opt{detail}; my $from = 'deploy_zone_block JOIN deploy_zone USING (zonenum) @@ -380,9 +382,10 @@ sub fbd_sql { ); push @where, "agentnum = $agentnum" if $agentnum; - my $order_by = 'censusblock, agentnum, technology, is_consumer, is_business'; + #my $order_by = 'censusblock, agentnum, technology, is_consumer, is_business'; + my $order_by = 'censusblock, technology'; - "SELECT ".join(', ', @select) . " + "SELECT DISTINCT ".join(', ', @select) . " FROM $from WHERE ".join(' AND ', @where)." ORDER BY $order_by @@ -396,7 +399,7 @@ sub fbs_sql { my $agentnum = $opt{agentnum}; my $q = $opt{ignore_quantity} ? '1' : 'COALESCE(cust_pkg.quantity, 1)'; - my $censustract = "replace(cust_location.censustract, '.', '')"; + my $censustract = "substr( replace(cust_location.censustract, '.', ''), 1, 11)"; my @select = ( "$censustract AS censustract", @@ -471,7 +474,7 @@ sub fvs_sql { my $date = $opt{date} || time; my $agentnum = $opt{agentnum}; my $q = $opt{ignore_quantity} ? '1' : 'COALESCE(cust_pkg.quantity, 1)'; - my $censustract = "replace(cust_location.censustract, '.', '')"; + my $censustract = "substr( replace(cust_location.censustract, '.', ''), 1, 11)"; my @select = ( "$censustract AS censustract", diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 569401b1d..61b793bb4 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -5736,6 +5736,9 @@ sub tables_hashref { # FK to cust_bill_pkg_detail; having a value here absolutely means # that the CDR appears on an invoice 'detailnum', 'bigint', 'NULL', '', '', '', + + #for mediation/deduplication + 'sipcallid', 'varchar', 'NULL', 255, '', '', ], 'primary_key' => 'acctid', 'unique' => [], @@ -5749,7 +5752,7 @@ sub tables_hashref { [ 'freesidestatus' ], [ 'freesiderewritestatus' ], [ 'cdrbatchnum' ], [ 'src_ip_addr' ], [ 'dst_ip_addr' ], [ 'dst_term' ], - [ 'detailnum' ], + [ 'detailnum' ], [ 'sipcallid' ], ], #no FKs on cdr table... choosing not to throw errors no matter what's # thrown in here. better to have the data. @@ -5928,6 +5931,7 @@ sub tables_hashref { 'username', 'varchar', '', $char_d, '', '', '_password', 'varchar', 'NULL', $char_d, '', '', '_password_encoding', 'varchar', 'NULL', $char_d, '', '', + 'totp_secret32', 'char', 'NULL', 32, '', '', 'last', 'varchar', 'NULL', $char_d, '', '', 'first', 'varchar', 'NULL', $char_d, '', '', 'user_custnum', 'int', 'NULL', '', '', '', @@ -7450,6 +7454,7 @@ sub tables_hashref { 'is_business', 'char', 'NULL', 1, '', '', 'active_date', @date_type, '', '', 'expire_date', @date_type, '', '', + 'disabled', 'char', 'NULL', 1, '', '', ], 'primary_key' => 'zonenum', 'unique' => [], diff --git a/FS/FS/access_right.pm b/FS/FS/access_right.pm index 155da739e..226c81efb 100644 --- a/FS/FS/access_right.pm +++ b/FS/FS/access_right.pm @@ -261,6 +261,7 @@ sub _upgrade_data { # class method 'Edit customer note' => 'Delete customer note', 'Edit customer' => 'Edit customer invoice terms', 'Financial reports' => 'Basic payment and refund reports', + 'Configuration' => 'Edit hardware clases and types', ); # foreach my $old_acl ( keys %onetime ) { diff --git a/FS/FS/access_user.pm b/FS/FS/access_user.pm index f23aa77f9..270f8bb27 100644 --- a/FS/FS/access_user.pm +++ b/FS/FS/access_user.pm @@ -13,6 +13,7 @@ use FS::agent; use FS::cust_main; use FS::sales; use Carp qw( croak ); +use Auth::GoogleAuth; $DEBUG = 0; $me = '[FS::access_user]'; @@ -239,6 +240,7 @@ sub check { $self->ut_numbern('usernum') || $self->ut_alpha_lower('username') || $self->ut_textn('_password') + || $self->ut_alphan('totp_secret32') || $self->ut_textn('last') || $self->ut_textn('first') || $self->ut_foreign_keyn('user_custnum', 'cust_main', 'custnum') @@ -733,6 +735,44 @@ sub change_password_fields { FS::Auth->auth_class->change_password_fields( @_ ); } +=item google_auth + +=cut + +sub google_auth { + my( $self ) = @_; + my $issuer = FS::Conf->new->config('company_name'). ' Freeside'; + my $label = $issuer. ':'. $self->username; + + Auth::GoogleAuth->new({ + secret => $self->totp_secret32, + issuer => $issuer, + key_id => $label, + }); + +} + +=item set_totp_secret32 + +=cut + +sub set_totp_secret32 { + my( $self ) = @_; + + $self->totp_secret32( $self->google_auth->generate_secret32 ); + $self->replace; +} + +=item totp_qr_code_url + +=cut + +sub totp_qr_code_url { + my( $self ) = @_; + + $self->google_auth->qr_code; +} + =item locale =cut diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index 85fccac69..e0c4bd4a4 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -185,6 +185,8 @@ following fields are currently supported: =item detailnum - Link to invoice detail (L) +=item sipcallid - SIP Call-ID + =back =head1 METHODS @@ -1870,10 +1872,11 @@ sub batch_import { my $iopt = _import_options; $opt->{$_} = $iopt->{$_} foreach keys %$iopt; - if ( defined $opt->{'cdrtypenum'} ) { - $opt->{'preinsert_callback'} = sub { - my($record,$param) = (shift,shift); - $record->cdrtypenum($opt->{'cdrtypenum'}); + if ( grep defined $opt->{$_}, qw(cdrtypenum carrierid) ) { + $opt->{preinsert_callback} = sub { + my($record, $param) = @_; + $record->$_($opt->{$_}) + foreach grep defined $opt->{$_}, qw(cdrtypenum carrierid); ''; }; } diff --git a/FS/FS/cdr/broadsoft.pm b/FS/FS/cdr/broadsoft.pm index a6f4d01c0..ab4815095 100644 --- a/FS/FS/cdr/broadsoft.pm +++ b/FS/FS/cdr/broadsoft.pm @@ -1,49 +1,149 @@ package FS::cdr::broadsoft; +=head1 NAME + +FS::cdr::broadsoft - CDR parse module for Broadsoft + +=head1 DESCRIPTION + +Ref: BW-AccountingCDRInterfaceSpec-R22.pdf + +=cut + use strict; use base qw( FS::cdr ); use vars qw( %info ); use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker ); %info = ( - 'name' => 'Broadsoft', - 'weight' => 500, - 'header' => 1, #0 default, set to 1 to ignore the first line, or - # to higher numbers to ignore that number of lines - 'type' => 'csv', - 'sep_char' => ',', #for csv, defaults to , - 'disabled' => 0, #0 default, set to 1 to disable - - #listref of what to do with each field from the CDR, in order - 'import_fields' => [ - + name => 'Broadsoft', + weight => 500, + header => 1, + type => 'csv', + sep_char => ',', + disabled => 0, + + #deal with broadsoft's awful non-standard CSV escaping :/ + row_callback => sub { + my $line = shift; + $line = qq("$line"); # put " at the beginning and end + $line =~ s/(? [ + + # 1: recordId + # 2: serviceProvider skip(2), - sub { my($cdr, $data, $conf, $param) = @_; - $param->{skiprow} = 1 if lc($data) ne 'normal'; - '' }, # 3: type - + + # 3: type + sub { + my ( $cdr, $data, $conf, $param ) = @_; + $param->{skiprow} = 1 + if lc($data) ne 'normal'; + ''; + }, + + # 4: userNumber + # 5: groupNumber skip(2), - 'dcontext', # 6: direction - trim('src'), # 7: callingNumber + + # 6: direction + 'dcontext', + + # 7: callingNumber + trim('src'), + + # 8: callingPresentationINdicator skip(1), - trim('dst'), # 9: calledNumber - _cdr_date_parser_maker('startdate'), # 10: startTime + # 9: calledNumber + trim('dst'), + + # 10: startTime + _cdr_date_parser_maker('startdate'), + + # 11: userTimeZone skip(1), - sub { my($cdr, $data) = @_; - $cdr->disposition( - lc($data) eq 'yes' ? - 'ANSWERED' : 'NO ANSWER') }, # 12: answerIndicator - _cdr_date_parser_maker('answerdate'), # 13: answerTime - _cdr_date_parser_maker('enddate'), # 14: releaseTime - skip(17), - sub { my($cdr, $accountcode) = @_; - if ($cdr->is_tollfree){ - my $dst = substr($cdr->dst,0,32); - $cdr->set('accountcode', $dst); - } else { - $cdr->set('accountcode', $accountcode); - }}, + + # 12: answerIndicator + sub { + my( $cdr, $data ) = @_; + $cdr->disposition( $data =~ /^yes/i ? 'ANSWERED' : 'NO ANSWER'); + }, + + # 13: answerTime + _cdr_date_parser_maker('answerdate'), + + # 14: releaseTime + _cdr_date_parser_maker('enddate'), + + # 15: terminationCause + # 16: networkType + # 17: carrierIdentificationCode + # 18: dialedDigits + # 19: callCategory + # 20: networkCallType + # 21: networkTranslatedNumber + # 22: networkTranslatedGroup + # 23: releasingParty + # 24: route + skip(10), + + # 25: networkCallID + 'sipcallid', + + # 26: codedc + # 27: accessDeviceAddress + # 28: accessCallID + # 29: spare + # 30: failoverCorrelationId + # 31: spare + # 32: group + # 33: department + skip(8), + + # 34: accountCode + 'accountcode', + + # 35: authorizationCode + # 36: originalCalledNumber + # 37: originalCalledPresentationIndicator + # 38: originalCalledReason + # 39: redirectingNumber + # 40: redirectingPresentationIndicator + # 41: redirectingReason + # 42: chargeIndicator + # 43: typeOfNetwork + # 44: voicePortalCalling.invocationTime + # 45: localCallId + # 46: remoteCallId + # 47: callingPartyCategory + # + # Also... cols 48 - 448 see Broadsoft documentation + skip(87), #35-121 inclusive + + #122: otherPartyName + 'clid', + + #123: otherPartyNamePresentationIndicator + sub { + my( $cdr, $data ) = @_; + $cdr->clid( $data ) unless $data =~ /^Public$/i; + }, + + skip(22), #124-145 inclusive + + # 146: chargedNumber + 'charged_party', + ], ); diff --git a/FS/FS/cdr/broadsoft22.pm b/FS/FS/cdr/broadsoft22.pm index 437d31e91..a90e592ef 100644 --- a/FS/FS/cdr/broadsoft22.pm +++ b/FS/FS/cdr/broadsoft22.pm @@ -1,188 +1,12 @@ package FS::cdr::broadsoft22; -=head1 NAME - -FS::cdr::broadsoft22 - CDR parse module for Broadsoft R22.0 - -=head1 DESCRIPTION - -Ref: BW-AccountingCDRInterfaceSpec-R22.pdf - -=cut - use strict; use base qw( FS::cdr ); use vars qw( %info ); -use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker ); %info = ( - name => 'Broadsoft R22', - weight => 500, - header => 1, - type => 'csv', - sep_char => ',', - disabled => 0, - - import_fields => [ - - # 1: recordId - # 2: serviceProvider - skip(2), - - # 3: type - sub { - my ( $cdr, $data, $conf, $param ) = @_; - $param->{skiprow} = 1 - if lc($data) ne 'normal'; - ''; - }, - - # 4: userNumber - # 5: groupNumber - skip(2), - - # 6: direction - 'dcontext', - - # 7: callingNumber - trim('src'), - - # 8: callingPresentationINdicator - skip(1), - - # 9: calledNumber - trim('dst'), - - # 10: startTime - _cdr_date_parser_maker('startdate'), - - # 11: userTimeZone - skip(1), - - # 12: answerIndicator - sub { - my( $cdr, $data ) = @_; - $cdr->disposition( lc($data) eq 'yes' ? 'ANSWERED' : 'NO ANSWER'); - }, - - # 13: answerTime - _cdr_date_parser_maker('answerdate'), - - # 14: releaseTime - _cdr_date_parser_maker('enddate'), - - # 15: terminationCause - # 16: networkType - # 17: carrierIdentificationCode - # 18: dialedDigits - # 19: callCategory - # 20: networkCallType - # 21: networkTranslatedNumber - # 22: networkTranslatedGroup - # 23: releasingParty - # 24: route - # 25: networkCallID - # 26: codedc - # 27: accessDeviceAddress - # 28: accessCallID - # 29: spare - # 30: failoverCorrelationId - # 31: spare - # 32: group - # 33: department - skip(19), - - # 34: accountCode - sub { - my( $cdr, $data ) = @_; - $cdr->set( - 'accountcode', - $cdr->is_tollfree ? substr( $cdr->dst, 0, 32 ) : $data - ); - }, - - # 35: authorizationCode - # 36: originalCalledNumber - # 37: originalCalledPresentationIndicator - # 38: originalCalledReason - # 39: redirectingNumber - # 40: redirectingPresentationIndicator - # 41: redirectingReason - # 42: chargeIndicator - # 43: typeOfNetwork - # 44: voicePortalCalling.invocationTime - # 45: localCallId - # 46: remoteCallId - # 47: callingPartyCategory - # - # Also... cols 48 - 448 see Broadsoft documentation - - ], - + name => 'Broadsoft R22 (deprecated)', + disabled => 1, ); -sub trim { - my $fieldname = shift; - return sub { - my($cdr, $data) = @_; - $data =~ s/^\+1//; - $cdr->$fieldname($data); - '' - } -} - -sub skip { - map { undef } (1..$_[0]); -} - 1; - -__END__ - -list of freeside CDR fields, useful ones marked with * - - acctid - primary key - *[1] calldate - Call timestamp (SQL timestamp) - clid - Caller*ID with text -7 * src - Caller*ID number / Source number -9 * dst - Destination extension - dcontext - Destination context - channel - Channel used - dstchannel - Destination channel if appropriate - lastapp - Last application if appropriate - lastdata - Last application data -10 * startdate - Start of call (UNIX-style integer timestamp) -13 answerdate - Answer time of call (UNIX-style integer timestamp) -14 * enddate - End time of call (UNIX-style integer timestamp) - * duration - Total time in system, in seconds - * billsec - Total time call is up, in seconds -12 *[2] disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY - amaflags - What flags to use: BILL, IGNORE etc, specified on a per - channel basis like accountcode. -4 *[3] accountcode - CDR account number to use: account - uniqueid - Unique channel identifier - userfield - CDR user-defined field - cdr_type - CDR type - see FS::cdr_type (Usage = 1, S&E = 7, OC&C = 8) - *[4] charged_party - Service number to be billed - upstream_currency - Wholesale currency from upstream - *[5] upstream_price - Wholesale price from upstream - upstream_rateplanid - Upstream rate plan ID - rated_price - Rated (or re-rated) price - distance - km (need units field?) - islocal - Local - 1, Non Local = 0 - *[6] calltypenum - Type of call - see FS::cdr_calltype - description - Description (cdr_type 7&8 only) (used for - cust_bill_pkg.itemdesc) - quantity - Number of items (cdr_type 7&8 only) - carrierid - Upstream Carrier ID (see FS::cdr_carrier) - upstream_rateid - Upstream Rate ID - svcnum - Link to customer service (see FS::cust_svc) - freesidestatus - NULL, done (or something) - -[1] Auto-populated from startdate if not present -[2] Package options available to ignore calls without a specific disposition -[3] When using 'cdr-charged_party-accountcode' config -[4] Auto-populated from src (normal calls) or dst (toll free calls) if not present -[5] When using 'upstream_simple' rating method. -[6] Set to usage class classnum when using pre-rated CDRs and usage class-based - taxation (local/intrastate/interstate/international) diff --git a/FS/FS/cdr/taqua62.pm b/FS/FS/cdr/taqua62.pm index aa9463008..c6a40eddc 100644 --- a/FS/FS/cdr/taqua62.pm +++ b/FS/FS/cdr/taqua62.pm @@ -159,7 +159,7 @@ use FS::cdr qw(_cdr_date_parser_maker); #60 - '', #OrigIPCallID + 'sipcallid', #OrigIPCallID '', #ESAIPTrunkGroup '', #ESAReason '', #BearerlessCall diff --git a/FS/FS/cdr/telapi_voip.pm b/FS/FS/cdr/telapi_voip.pm index 687c431a8..c238d2a17 100644 --- a/FS/FS/cdr/telapi_voip.pm +++ b/FS/FS/cdr/telapi_voip.pm @@ -2,27 +2,31 @@ package FS::cdr::telapi_voip; use base qw( FS::cdr ); use strict; -use vars qw( @ISA %info $CDR_TYPES ); -use FS::Record qw( qsearch ); -use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker ); +use vars qw( %info ); +use FS::cdr qw( _cdr_date_parser_maker ); %info = ( - 'name' => 'telapi_voip (csv file)', + 'name' => 'TeleAPI VoIP (CSV file)', 'weight' => 601, 'header' => 1, 'type' => 'csv', 'import_fields' => [ - skip(1), # Inbound/Outbound - _cdr_date_parser_maker('startdate'), # date - skip(1), # cost per minute - 'upstream_price', # call cost - 'billsec', # duration - 'src', # source - 'dst', # destination - skip(1), # hangup code + _cdr_date_parser_maker('startdate', 'gmt'=>1 ), # date gmt + 'src', # source + 'dst', # destination + 'clid', # callerid + 'disposition', # hangup code + 'userfield', # sip account + 'src_ip_addr', # orig ip + 'billsec', # duration + skip(1), # per minute (add "upstream_rate"? + 'upstream_price', # call cost + 'dcontext', # type + 'uniqueid', # uuid + 'lastapp', # direction ], ); sub skip { map {''} (1..$_[0]) } -1; \ No newline at end of file +1; diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 6f9c74a64..f0d710534 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -392,7 +392,7 @@ sub void { } - $error = $self->delete; + $error = $self->delete( skip_update_cust_bill_charged=>1 ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -448,6 +448,7 @@ Not recommended. sub delete { my $self = shift; + my %opt = @_; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -493,16 +494,27 @@ sub delete { } } - #fix the invoice amount + unless ( $opt{skip_update_cust_bill_charged} ) { + + #fix the invoice amount + + my $cust_bill = $self->cust_bill; + my $charged = $cust_bill->charged - $self->setup - $self->recur; + $charged = sprintf('%.2f', $charged + 0.00000001 ); + $cust_bill->charged( $charged ); - my $cust_bill = $self->cust_bill; - $cust_bill->charged( $cust_bill->charged - $self->setup - $self->recur ); + #not adding a cc surcharge, but this override lets us modify charged + $cust_bill->{'Hash'}{'cc_surcharge_replace_hack'} = 1; - #not adding a cc surcharge, but this override lets us modify charged - $cust_bill->{'Hash'}{'cc_surcharge_replace_hack'} = 1; + my $error = $cust_bill->replace; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + } - my $error = $cust_bill->replace - || $self->SUPER::delete(@_); + my $error = $self->SUPER::delete(@_); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm index 21bf92feb..73821cc14 100644 --- a/FS/FS/cust_location.pm +++ b/FS/FS/cust_location.pm @@ -252,7 +252,7 @@ sub insert { } if ( $self->censustract ) { - $self->set('censusyear' => $conf->config('census_year') || 2012); + $self->set('censusyear' => $conf->config('census_legacy') || 2020); } my $oldAutoCommit = $FS::UID::AutoCommit; @@ -419,10 +419,13 @@ sub check { ; return $error if $error; if ( $self->censustract ne '' ) { - $self->censustract =~ /^\s*(\d{9})\.?(\d{2})\s*$/ - or return "Illegal census tract: ". $self->censustract; - - $self->censustract("$1.$2"); + if ( $self->censustract =~ /^\s*(\d{9})\.?(\d{2})\s*$/ ) { #old + $self->censustract("$1.$2"); + } elsif ($self->censustract =~ /^\s*(\d{15})\s*$/ ) { #new + $self->censustract($1); + } else { + return "Illegal census tract: ". $self->censustract; + } } #yikes... this is ancient, pre-dates cust_location and will be harder to @@ -879,7 +882,7 @@ sub process_censustract_update { qsearchs( 'cust_location', { locationnum => $locationnum }) or die "locationnum '$locationnum' not found!\n"; - my $new_year = $conf->config('census_year') or return; + my $new_year = $conf->config('census_legacy') || 2020; my $loc = FS::GeocodeCache->new( $cust_location->location_hash ); $loc->set_censustract; my $error = $loc->get('censustract_error'); diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index 25216c6d6..37b8ec8a4 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -790,6 +790,20 @@ sub search { } ## + # no_censustract + ## + if ( $params->{'no_censustract'} ) { + push @where, "EXISTS( + SELECT 1 FROM cust_location + WHERE locationnum = cust_main.ship_locationnum + AND cust_location.country = 'US' + AND ( cust_location.censusyear IS NULL + OR cust_location.censusyear != '2020' + ) + )"; + } + + ## # phones ## diff --git a/FS/FS/cust_payby.pm b/FS/FS/cust_payby.pm index 410d69093..03864ef65 100644 --- a/FS/FS/cust_payby.pm +++ b/FS/FS/cust_payby.pm @@ -160,9 +160,8 @@ sub insert { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $error = $self->check_payinfo_cardtype if $self->payby =~/^(CARD|DCRD)$/; - $self->SUPER::insert unless $error; - + my $error = $self->check_payinfo_cardtype + || $self->SUPER::insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -348,14 +347,16 @@ sub check { if ( !$ignore_invalid_card && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) { - my $payinfo = $self->payinfo; - $payinfo =~ s/\D//g; - $payinfo =~ /^(\d{13,19}|\d{8,9})$/ - or return gettext('invalid_card'); #. ": ". $self->payinfo; - $payinfo = $1; - $self->payinfo($payinfo); - validate($payinfo) - or return gettext('invalid_card'); # . ": ". $self->payinfo; + unless ( $self->tokenized ) { + my $payinfo = $self->payinfo; + $payinfo =~ s/\D//g; + $payinfo =~ /^(\d{13,19}|\d{8,9})$/ + or return gettext('invalid_card'); #. ": ". $self->payinfo; + $payinfo = $1; + $self->payinfo($payinfo); + validate($payinfo) + or return gettext('invalid_card'); # . ": ". $self->payinfo; + } # see parallel checks in check_payinfo_cardtype & payinfo_Mixin::payinfo_check my $cardtype = $self->paycardtype; @@ -557,7 +558,7 @@ sub check_payinfo_cardtype { return '' if $ignore_cardtype; - return '' unless $self->payby =~ /^(CARD|CHEK)$/; + return '' unless $self->payby =~ /^(CARD|DCRD)$/; my $payinfo = $self->payinfo; $payinfo =~ s/\D//g; diff --git a/FS/FS/cust_pkg/Search.pm b/FS/FS/cust_pkg/Search.pm index 3e1ca820e..c5a7eb9f1 100644 --- a/FS/FS/cust_pkg/Search.pm +++ b/FS/FS/cust_pkg/Search.pm @@ -402,9 +402,12 @@ sub search { my $op = $params->{location_cust} ? '=' : '!='; push @where, "cust_location.locationnum $op cust_main.ship_locationnum"; } - if ( $params->{location_census} xor $params->{location_nocensus} ) { - my $op = $params->{location_census} ? "IS NOT NULL" : "IS NULL"; - push @where, "cust_location.censustract $op"; + if ( $params->{location_census} ) { + push @where, "cust_location.censustract IS NOT NULL", + "cust_location.censusyear = '2020' "; + } elsif ( $params->{location_nocensus} ) { + push @where, "( cust_location.censustract IS NULL ". + " OR cust_location.censusyear != '2020' )"; } if ( $params->{location_geocode} xor $params->{location_nogeocode} ) { my $op = $params->{location_geocode} ? "IS NOT NULL" : "IS NULL"; diff --git a/FS/FS/deploy_zone.pm b/FS/FS/deploy_zone.pm index 306b4fb44..c618fb987 100644 --- a/FS/FS/deploy_zone.pm +++ b/FS/FS/deploy_zone.pm @@ -10,8 +10,12 @@ use Cpanel::JSON::XS; use LWP::UserAgent; use HTTP::Request::Common; -# update this in 2020, along with the URL for the TIGERweb service -our $CENSUS_YEAR = 2010; +use Geo::JSON::Polygon; +use Geo::JSON::Feature; + +our $CENSUS_YEAR = 2020; + +our $tech_label = FS::part_pkg_fcc_option->technology_labels; =head1 NAME @@ -187,8 +191,15 @@ returns the error, otherwise returns false. =cut -# the replace method can be inherited from FS::Record +sub replace { + my $self = shift; + my $old = shift || $self->replace_old; + + $self->expire_date(time) + if $self->disabled eq 'Y' && ! $old->disabled && ! $self->expire_date; + $self->SUPER::replace($old, @_); +} =item check Checks all fields to make sure this is a valid zone record. If there is @@ -268,6 +279,27 @@ sub deploy_zone_vertex { }); } +=item shapefile_add SHAPEFILE + +Adds this deployment zone to the supplied Geo::Shapelib shapefile. + +=cut + +sub shapefile_add { + my( $self, $shapefile ) = @_; + + my @coordinates = map { [ $_->longitude, $_->latitude, 0, 0 ] } + $self->deploy_zone_vertex; + push @coordinates, $coordinates[0]; + + push @{$shapefile->{Shapes}}, { 'Vertices' => \@coordinates }; + push @{$shapefile->{ShapeRecords}}, [ $tech_label->{$self->technology}, + $self->adv_speed_down, + $self->adv_speed_up, + ]; + ''; +} + =item vertices_json Returns the vertex list for this zone, as a JSON string of @@ -282,6 +314,51 @@ sub vertices_json { encode_json(\@vertices); } +=item geo_json_feature + +Returns this zone as a Geo::JSON::Feature object + +=cut + +sub geo_json_feature { + my $self = shift; + + my @coordinates = map { [ $_->longitude, $_->latitude ] } + $self->deploy_zone_vertex; + push @coordinates, $coordinates[0]; + + Geo::JSON::Feature->new({ + geometry => Geo::JSON::Polygon->new({ coordinates => [ \@coordinates ] }), + properties => { 'Technology' => $tech_label->{$self->technology}, + 'Down' => $self->adv_speed_down, + 'Up' => $self->adv_speed_up, + }, + }) +} + +=item kml_add + +Adds this deployment zone to the supplied Geo::GoogleEarth::Pluggable object. + +=cut + +sub kml_polygon { + my( $self, $kml ) = @_; + + my $name = $self->description. ' ('. $self->adv_speed_down. '/'. + $self->adv_speed_up. ')'; + + $kml->Polygon( 'name' => $name, + 'coordinates' => [ [ #outerBoundary + map { [ $_->longitude, $_->latitude, 0 ] } + $self->deploy_zone_vertex + ], + #[ #innerBoundary + #] + ] + ); +} + =head2 SUBROUTINES =over 4 @@ -379,7 +456,7 @@ sub process_block_lookup { inSR => 4326, outSR => 4326, spatialRel => 'esriSpatialRelIntersects', # the test to perform - outFields => 'OID,GEOID', + outFields => 'GEOID', returnGeometry => 'false', orderByFields => 'OID', ); @@ -403,16 +480,12 @@ sub process_block_lookup { #warn "Census block lookup: $count\n"; - # we have to do our own pagination on this, because the census bureau - # doesn't support resultOffset (maybe they don't have ArcGIS 10.3 yet). - # that's why we're ordering by OID, it's globally unique - my $last_oid = 0; my $done = 0; while (!$done) { $response = $ua->request( POST $url, Content => [ %query, - where => "OID>$last_oid", + resultOffset => $inserted, ] ); die $response->status_line unless $response->is_success; @@ -437,7 +510,6 @@ sub process_block_lookup { } #warn "Inserted $inserted records\n"; - $last_oid = $data->{features}[-1]{attributes}{OID}; $done = 1 unless $data->{exceededTransferLimit}; } diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index 0a16724a8..33e150ae3 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -11,6 +11,7 @@ use FS::cust_msg; use FS::template_content; use Date::Format qw(time2str); +use PDF::WebKit; FS::UID->install_callback( sub { $conf = new FS::Conf; } ); @@ -411,8 +412,6 @@ Options are as for 'prepare', but 'from' and 'to' are meaningless. sub render { my $self = shift; - eval "use PDF::WebKit"; - die $@ if $@; my %opt = @_; my %hash = $self->prepare(%opt); my $html = $hash{'html_body'}; diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index c2c370760..89503590c 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -16,9 +16,8 @@ use HTML::TreeBuilder; use Encode; # needed to send email -use FS::Misc qw( generate_email ); +use FS::Misc qw( generate_email _sendmail ); use FS::Conf; -use Email::Sender::Simple qw( sendmail ); use FS::Record qw( qsearch qsearchs ); @@ -543,40 +542,13 @@ sub send_prepared { # through Email::Address to make sure my @env_to = map { $_->address } Email::Address->parse($cust_msg->env_to); - my %smtp_opt = ( 'host' => $conf->config('smtpmachine'), - 'helo' => $domain ); - - my($port, $enc) = split('-', ($conf->config('smtp-encryption') || '25') ); - $smtp_opt{'port'} = $port; - - my $transport; - if ( defined($enc) && $enc eq 'starttls' ) { - $smtp_opt{$_} = $conf->config("smtp-$_") for qw(username password); - $transport = Email::Sender::Transport::SMTP::TLS->new( %smtp_opt ); - } else { - if ( $conf->exists('smtp-username') && $conf->exists('smtp-password') ) { - $smtp_opt{"sasl_$_"} = $conf->config("smtp-$_") for qw(username password); - } - $smtp_opt{'ssl'} = 1 if defined($enc) && $enc eq 'tls'; - $transport = Email::Sender::Transport::SMTP->new( %smtp_opt ); - } - - warn "$me sending message\n" if $DEBUG; my $message = join("\n", $cust_msg->header, $cust_msg->body); - local $@; - eval { - sendmail( $message, { transport => $transport, - from => $cust_msg->env_from, - to => \@env_to }) - }; - my $error = ''; - if(ref($@) and $@->isa('Email::Sender::Failure')) { - $error = $@->code.' ' if $@->code; - $error .= $@->message; - } - else { - $error = $@; - } + + my $error = _sendmail( $message, { 'from' => $cust_msg->env_from, + 'to' => \@env_to, + 'domain' => $domain, + } + ); $cust_msg->set('error', $error); $cust_msg->set('status', $error ? 'failed' : 'sent'); diff --git a/FS/FS/part_export/acct_sql.pm b/FS/FS/part_export/acct_sql.pm index 8163f2017..c51ef6836 100644 --- a/FS/FS/part_export/acct_sql.pm +++ b/FS/FS/part_export/acct_sql.pm @@ -8,7 +8,7 @@ use FS::Record; #qw(qsearchs); tie my %options, 'Tie::IxHash', %{__PACKAGE__->sql_options}; $options{'crypt'} = { label => 'Password encryption', - type=>'select', options=>[qw(crypt md5 sha1_base64)], + type=>'select', options=>[qw(crypt md5 sha1_base64 sha512)], default=>'crypt', }; @@ -59,6 +59,47 @@ my $postfix_native_mailbox_map = join('\n', map "$_ $postfix_native_mailbox_map{$_}", keys %postfix_native_mailbox_map ); +tie my %libnss_pgsql_passwd_map, 'Tie::IxHash', + 'username' => 'username', + #'passwd' => literal string 'x' + 'uid' => 'uid', + 'gid' => 'gid', + 'gecos' => 'finger', + 'homedir' => 'dir', + 'shell' => 'shell', +; +my $libnss_pgsql_passwd_map = + join('\n', map "$_ $libnss_pgsql_passwd_map{$_}", + keys %libnss_pgsql_passwd_map ); + +tie my %libnss_pgsql_passwd_static, 'Tie::IxHash', + 'passwd' => 'x', +; +my $libnss_pgsql_passwd_static = + join('\n', map "$_ $libnss_pgsql_passwd_static{$_}", + keys %libnss_pgsql_passwd_static ); + +tie my %libnss_pgsql_shadow_map, 'Tie::IxHash', + 'username' => 'username', + 'passwd' => 'crypt_password', +; +my $libnss_pgsql_shadow_map = + join('\n', map "$_ $libnss_pgsql_shadow_map{$_}", + keys %libnss_pgsql_shadow_map ); + +tie my %libnss_pgsql_shadow_static, 'Tie::IxHash', + 'lastchange' => '18550', #not actually implemented.. + 'min' => '0', + 'max' => '99999', + 'warn' => '7', + 'inact' => '0', + 'expire' => '-1', + 'flag' => '0', +; +my $libnss_pgsql_shadow_static = + join('\n', map "$_ $libnss_pgsql_shadow_static{$_}", + keys %libnss_pgsql_shadow_static ); + %info = ( 'svc' => 'svc_acct', 'desc' => 'Real-time export of accounts to SQL databases '. @@ -69,14 +110,14 @@ my $postfix_native_mailbox_map = 'default_svc_class' => 'Email', 'notes' => <
In contrast to sqlmail, this is intended to export just svc_acct records only, rather than a single export for svc_acct, svc_forward and svc_domain records, to export in "default" database schemas rather than -configure the MTA or POP/IMAP server for a Freeside-specific schema, and -to be configured for different mail server setups. +configure servers for a Freeside-specific schema, and to be configured for +different mail (and authentication) server setups.

Use these buttons for some useful presets:
    @@ -100,6 +141,18 @@ to be configured for different mail server setups. this.form.schema.value = "$postfix_native_mailbox_map"; this.form.primary_key.value = "userid"; '> +
  • +
END ); diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm index 3e2082ba0..d91c4f050 100644 --- a/FS/FS/part_pkg/voip_cdr.pm +++ b/FS/FS/part_pkg/voip_cdr.pm @@ -196,6 +196,10 @@ tie my %accountcode_tollfree_field, 'Tie::IxHash', 'skip_dcontext' => { 'name' => 'Do not charge for CDRs where dcontext is set to any of these (comma-separated) values: ', }, + 'noskip_dcontext_tollfree' => { 'name' => 'Do charge for CDRs where dcontext is set to any of the specified values, if the CDR is tollfree', + 'type' => 'checkbox', + }, + 'skip_dcontext_prefix' => { 'name' => 'Do not charge for CDRs where dcontext starts with: ', }, @@ -231,6 +235,10 @@ tie my %accountcode_tollfree_field, 'Tie::IxHash', 'skip_dst_length_less' => { 'name' => 'Do not charge for CDRs where the destination is less than this many digits:', }, + 'noskip_dst_length_n11' => { 'name' => 'Do charge for CDRs where dst is less than the specified digits, when dst is N11 (i.e. 411, 611)', + 'type' => 'checkbox', + }, + 'noskip_dst_length_accountcode_tollfree' => { 'name' => 'Do charge for CDRs where dst is less than the specified digits, when accountcode is toll free', 'type' => 'checkbox', }, @@ -347,12 +355,13 @@ tie my %accountcode_tollfree_field, 'Tie::IxHash', use_cdrtypenum ignore_cdrtypenum use_calltypenum ignore_calltypenum ignore_disposition disposition_in disposition_prefix - skip_dcontext skip_dcontext_prefix skip_dcontext_suffix + skip_dcontext noskip_dcontext_tollfree + skip_dcontext_prefix skip_dcontext_suffix skip_dst_prefix skip_dstchannel_prefix skip_src_length_more noskip_src_length_accountcode_tollfree accountcode_tollfree_ratenum accountcode_tollfree_field - skip_dst_length_less + skip_dst_length_less noskip_dst_length_n11 noskip_dst_length_accountcode_tollfree skip_lastapp skip_max_callers @@ -447,8 +456,6 @@ sub calc_usage { rounding => ($self->option_cacheable('rounding') || 2), ); - my $use_duration = $self->option('use_duration'); - my($svc_table, $svc_field, $by_ip_addr) = split('\.', $cdr_svc_method); my @cust_svc; @@ -625,7 +632,10 @@ sub check_chargable { return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )" if $self->option_cacheable('skip_dcontext') =~ /\S/ - && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext')); + && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext')) + && ! ( $self->option_cacheable('noskip_dcontext_tollfree') + && $cdr->is_tollfree + ); my $len_dcontext_prefix = length($self->option_cacheable('skip_dcontext_prefix')); @@ -646,8 +656,11 @@ sub check_chargable { my $dst_length = $self->option_cacheable('skip_dst_length_less'); return "destination less than $dst_length digits" if $dst_length && length($cdr->dst) < $dst_length - && ! ( $self->option_cacheable('noskip_dst_length_accountcode_tollfree') - && $cdr->is_tollfree('accountcode') + && ! ( $self->option_cacheable('noskip_dst_length_n11') + && $cdr->dst =~ /^\d11$/ + ) + && ! ( $self->option_cacheable('noskip_dst_length_accountcode_tollfree') + && $cdr->is_tollfree('accountcode') ); return "lastapp is ". $self->option_cacheable('skip_lastapp') diff --git a/FS/FS/password_history.pm b/FS/FS/password_history.pm index 13d16010d..1915a2185 100644 --- a/FS/FS/password_history.pm +++ b/FS/FS/password_history.pm @@ -173,6 +173,7 @@ sub _upgrade_schema { push @where, " ( $fk IS NOT NULL AND NOT EXISTS(SELECT 1 FROM $table WHERE $table.$key = $fk) )"; } + return '' unless @where; my @recs = qsearch({ 'table' => 'password_history', 'extra_sql' => ' WHERE ' . join(' AND ', @where), diff --git a/FS/MANIFEST b/FS/MANIFEST index 1e26a3c69..d66bca775 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -880,3 +880,4 @@ t/access_user_session_log.t FS/svc_group.pm FS/h_svc_group.pm FS/Misc/DepositSlip.pm +bin/freeside-svc_acct-bulk_change diff --git a/FS/bin/freeside-cdr-sftp_and_import b/FS/bin/freeside-cdr-sftp_and_import index aa4bf6471..f2bf2944f 100755 --- a/FS/bin/freeside-cdr-sftp_and_import +++ b/FS/bin/freeside-cdr-sftp_and_import @@ -12,8 +12,8 @@ use FS::cdr; # parse command line ### -use vars qw( $opt_m $opt_p $opt_r $opt_e $opt_d $opt_v $opt_P $opt_a $opt_c $opt_g $opt_s $opt_b ); -getopts('c:m:p:r:e:d:v:P:agsb'); +use vars qw( $opt_m $opt_p $opt_r $opt_e $opt_d $opt_v $opt_P $opt_a $opt_c $opt_i $opt_g $opt_s $opt_b ); +getopts('c:i:m:p:r:e:d:v:P:agsb'); $opt_e ||= 'csv'; #$opt_e = ".$opt_e" unless $opt_e =~ /^\./; @@ -21,7 +21,8 @@ $opt_e =~ s/^\.//; $opt_p ||= ''; -die "invalid cdrtypenum" if $opt_c && $opt_c !~ /^\d+$/; +die "invalid cdrtypenum" if defined $opt_c && $opt_c !~ /^\d+$/; +die "invalid carrierid" if defined $opt_i && $opt_i !~ /^\d+$/; my %options = (); @@ -113,7 +114,8 @@ foreach my $filename ( @$ls ) { 'batch_namevalue' => $file_timestamp, 'empty_ok' => 1, }; - $import_options->{'cdrtypenum'} = $opt_c if $opt_c; + $import_options->{'cdrtypenum'} = $opt_c if defined $opt_c; + $import_options->{'carrierid'} = $opt_i if defined $opt_i; my $error = FS::cdr::batch_import($import_options); @@ -162,7 +164,7 @@ foreach my $filename ( @$ls ) { sub usage { "Usage: - cdr.sftp_and_import [ -m method ] [ -p prefix ] [ -e extension ] + freeside-cdr-sftp_and_import [ -m method ] [ -p prefix ] [ -e extension ] [ -r remotefolder ] [ -d donefolder ] [ -v level ] [ -P port ] [ -a ] [ -g ] [ -s ] [ -c cdrtypenum ] user format [sftpuser@]servername "; @@ -205,7 +207,8 @@ freeside-cdr-sftp_and_import - Download CDR files from a remote server via SFTP freeside-cdr-sftp_and_import [ -m method ] [ -p prefix ] [ -e extension ] [ -r remotefolder ] [ -d donefolder ] [ -v level ] [ -P port ] - [ -a ] [ -g ] [ -s ] [ -c cdrtypenum ] user format [sftpuser@]servername + [ -a ] [ -g ] [ -s ] [ -c cdrtypenum ] [ -i carrierid] + user format [sftpuser@]servername =head1 DESCRIPTION @@ -233,6 +236,8 @@ or FTP and then import them into the database. -c: cdrtypenum to set, defaults to none +-i: carrierid to set, defaults to none + -g: File is gzipped -s: Warn and skip files which could not be imported rather than abort diff --git a/FS/bin/freeside-cdrrewrited b/FS/bin/freeside-cdrrewrited index d117f569e..bcc76153b 100755 --- a/FS/bin/freeside-cdrrewrited +++ b/FS/bin/freeside-cdrrewrited @@ -54,7 +54,8 @@ while (1) { #order matters for removing dupes--only the first is preserved $extra_sql .= ' ORDER BY acctid ' - if $conf->exists('cdr-skip_duplicate_rewrite'); + if $conf->exists('cdr-skip_duplicate_rewrite') + || $conf->exists('cdr-skip_duplicate_rewrite-sipcallid'); my $found = 0; my %skip = (); #used only by taqua @@ -92,6 +93,22 @@ while (1) { } } + if ($conf->exists('cdr-skip_duplicate_rewrite-sipcallid')) { + my $sth = dbh->prepare( + 'SELECT 1 FROM cdr WHERE sipcallid=? AND acctid < ? LIMIT 1' + ) or die dbh->errstr; + $sth->execute($cdr->sipcallid, $cdr->acctid) or die $sth->errstr; + my $isdup = $sth->fetchrow_hashref; + $sth->finish; + if ($isdup) { + #we only act on this cdr, not touching previous dupes + #if a dupe somehow creeped in previously, too late to fix it + $cdr->freesidestatus('skipped'); #prevent it from being billed + push(@status,'duplicate'); + } + } + + if ( $conf->exists('cdr-asterisk_forward_rewrite') && $cdr->dstchannel =~ /^Local\/(\d+)/i && $1 ne $cdr->dst ) @@ -268,6 +285,7 @@ sub _shouldrun { || $conf->exists('cdr-intl_to_domestic_rewrite') || $conf->exists('cdr-userfield_dnis_rewrite') || $conf->exists('cdr-skip_duplicate_rewrite') + || $conf->exists('cdr-skip_duplicate_rewrite-sipcallid') || 0 ; } @@ -296,6 +314,11 @@ of the following config options are enabled: Marks as 'skipped' (prevents billing for) any CDRs with a src, dst and calldate identical to an existing CDR +=item cdr-skip_duplicate_rewrite-sipcallid + +Marks as 'skipped' (prevents billing for) any CDRs with +a sipcallid identical to an existing CDR + =item cdr-asterisk_australia_rewrite Classifies Australian numbers as domestic, mobile, tollfree, international, or diff --git a/FS/bin/freeside-censustract-update b/FS/bin/freeside-censustract-update index f9b6d1197..49505ee81 100755 --- a/FS/bin/freeside-censustract-update +++ b/FS/bin/freeside-censustract-update @@ -18,8 +18,7 @@ $FS::UID::AutoCommit = 0; my $dbh = dbh; my $conf = FS::Conf->new; -my $current_year = $conf->config('census_year') - or die "No current census year configured.\n"; +my $current_year = $conf->config('census_legacy') || '2020'; my $date = str2time($opt{d}) if $opt{d}; $date ||= time; # This now operates on cust_location, not cust_main. @@ -36,9 +35,14 @@ my %h_cust_location = map { $_->locationnum => $_ } # Find all locations that don't have censusyear = the current # year as of now. -my @cust_location = qsearch( 'cust_location', - { censusyear => { op => '!=', value => $current_year } }, -); +my @cust_location = qsearch({ + 'table' => 'cust_location', + 'hashref' => { 'country' => 'US', }, + 'extra_sql' => " AND ( censusyear != '$current_year' + OR censustract IS NULL + ) + ", +}); warn scalar(@cust_location)." records found.\n"; my $queued = 0; my $updated = 0; @@ -85,8 +89,8 @@ freeside-censustract-update - Update census tract codes to the current year. =head1 DESCRIPTION Finds all customers whose census tract codes don't appear to be current -and updates them to the current year. The "current year" is defined by -the I configuration variable, not the calendar year. +and updates them to the current year. The "current year" is 2020, unless the +I configuration variable is set. The -d option tells the script to assume that tract codes last modified after some date are already current. Those customers will just have diff --git a/FS/bin/freeside-svc_acct-bulk_change b/FS/bin/freeside-svc_acct-bulk_change new file mode 100755 index 000000000..c10d2c18a --- /dev/null +++ b/FS/bin/freeside-svc_acct-bulk_change @@ -0,0 +1,56 @@ +#!/usr/bin/perl -w + +use strict; +use vars qw( $opt_p $opt_g ); +use Getopt::Std; +use FS::UID qw(adminsuidsetup); +use FS::Record qw( qsearch ); #qsearchs ); +use FS::cust_svc; + +my $user = shift or die &usage; +adminsuidsetup $user; + +getopts('p:g:'); + +my @svc_x = (); +if ( $opt_p ) { + push @svc_x, map { $_->svc_acct } qsearch('cust_svc', { svcpart=>$opt_p } ); + die "no services with svcpart $opt_p found\n" unless @svc_x; +} else { + die &usage; +} + +foreach my $svc_x ( @svc_x ) { + next if $opt_g && $svc_x->gid == $opt_g; + $svc_x->gid($opt_g) if $opt_g; + my $error = $svc_x->replace; + die $error if $error; +} + +sub usage { + return "Usage:\n\n freeside-svc_acct-bulk_change user -p svcpart -g gid\n"; +} + +=head1 NAME + +freeside-svc_acct-bulk_change - Command line tool to make bulk changes to svc_acct (account) records + +=head1 SYNOPSIS + + freeside-svc_acct-bulk_change user -p svcpart -g gid + +=head1 DESCRIPTION + + For the servcies of the given svcpart, changes the GID as specified. + + Note: Unless you are changing the GID to match an new, fixed value in the + service definition, you will need to enable the B + configuration setting prior to running this script. + +=head1 SEE ALSO + +L + +=cut + +1; diff --git a/bin/part_pkg-bulk_change b/bin/part_pkg-bulk_change index 5347da6be..582ab39c2 100755 --- a/bin/part_pkg-bulk_change +++ b/bin/part_pkg-bulk_change @@ -39,6 +39,12 @@ foreach my $part_pkg ( qsearch({ 'table' => 'part_pkg', my $part_pkg_option = qsearchs('part_pkg_option', \%hash); + unless ( defined $opt_v ) { + my $error = $part_pkg_option && $part_pkg_option->delete; + die $error if $error; + next; + } + if ( $part_pkg_option ) { next if $part_pkg_option->optionvalue eq $opt_v; $part_pkg_option->optionvalue($opt_v); @@ -130,7 +136,7 @@ Search options: Change options: --o: part_pkg_option optionname +-o: part_pkg_option optionname (use without -v to unset) -v: part_pkg_option optionvalue diff --git a/bin/test_scrub b/bin/test_scrub index 850801f3b..ecb722943 100755 --- a/bin/test_scrub +++ b/bin/test_scrub @@ -23,13 +23,13 @@ getopts('h'); adminsuidsetup shift; -# payment_gateway -# payment_gateway_option -# agent_payment_gateway foreach my $table (qw( export_svc part_export_option part_export + payment_gateway + payment_gateway_option + agent_payment_gateway queue queue_arg )) { diff --git a/debian/control b/debian/control index 15a379ffd..043294fc8 100644 --- a/debian/control +++ b/debian/control @@ -78,7 +78,8 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip, libipc-run-safehandles-perl,libpoe-perl,libsoap-lite-perl,libxmlrpc-lite-perl, libhtml-tableextract-perl,libhtml-element-extended-perl,libcam-pdf-perl, libnet-openssh-perl,libgd-barcode-perl,sam2p,libsys-sigaction-perl, - libgeo-googleearth-pluggable-perl,libgeo-coder-googlev3-perl,libnet-snmp-perl, + libgeo-googleearth-pluggable-perl (>=0.16),libgeo-coder-googlev3-perl, + libnet-snmp-perl, libcrypt-openssl-rsa-perl,libregexp-common-perl,libnet-cidr-perl, libregexp-ipv6-perl,libhtml-quoted-perl,libtext-password-pronounceable-perl, libconvert-color-perl,liburi-perl,libhtml-rewriteattributes-perl, @@ -106,8 +107,10 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip, libnet-vitelity-perl (>= 0.05), libnet-sslglue-perl, libexpect-perl, libspreadsheet-parsexlsx-perl, libunicode-truncate-perl (>= 0.303-1), libspreadsheet-xlsx-perl, libpod-simple-perl, libwebservice-northern911-perl, - liblocale-codes-perl, liblocale-po-perl -Conflicts: libparams-classify-perl (= 0.013-6) + liblocale-codes-perl, liblocale-po-perl, libgeo-uscensus-geocoding-perl, + libnet-sftp-foreign-perl, libpdf-webkit-perl, libgeo-shapelib-perl, + libgeo-json-perl, libauth-googleauth-perl +Conflicts: libparams-classify-perl (>= 0.013-6) Replaces: freeside (<<4) Breaks: freeside (<<4) Description: Libraries for Freeside billing and trouble ticketing @@ -135,7 +138,7 @@ Architecture: all Depends: libtext-template-perl,libbusiness-creditcard-perl, libhttp-browserdetect-perl,libhtml-parser-perl,libtie-ixhash-perl, libhtml-widgets-selectlayers-perl,libtimedate-perl,libnumber-format-perl, - libsoap-lite-perl,libtext-csv-xs-perl,libcgi-perl + libsoap-lite-perl,libtext-csv-xs-perl,libcgi-pm-perl Recommends: Description: Self-service portal for Freeside billing and trouble ticketing Freeside is a web-based billing and trouble ticketing application. diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi index 6eab11dae..b1fea7da2 100755 --- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi +++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi @@ -1250,10 +1250,8 @@ sub do_template { $fill_in->{$_} = $access_info->{$_} foreach keys %$access_info; # update the user's authentication - my $timeout = $access_info->{'timeout'} || '3600'; my $cookie = CGI::Cookie->new('-name' => 'session', '-value' => $session_id, - '-expires' => '+'.$timeout.'s', #'-secure' => 1, # would be a good idea... ); if ( $name eq 'logout' ) { diff --git a/httemplate/browse/access_user.html b/httemplate/browse/access_user.html index 446bfe0be..658762763 100644 --- a/httemplate/browse/access_user.html +++ b/httemplate/browse/access_user.html @@ -49,6 +49,11 @@ my $groups_sub = sub { }; +my $goog_auth_sub = sub { + my $access_user = shift; + $access_user->totp_secret32 ? 'Enabled' : ''; +}; + my $installer_sub = sub { my $access_user = shift; my @sched_item = $access_user->sched_item or return ''; @@ -66,11 +71,23 @@ my $count_query = 'SELECT COUNT(*) FROM access_user'; my $link = [ $p.'edit/access_user.html?', 'usernum' ]; my @header = ( - 'Username', 'Full name', 'Groups', 'Installer', 'Customer' ); + 'Username', + 'Full name', + 'Groups', + 'Google Auth', + 'Installer', + 'Customer', +); my @fields = ( - 'username', 'name', $groups_sub, $installer_sub, $cust_sub, ); -my $align = 'lllcl'; -my @links = ( $link, $link, $link, '', '', $cust_link ); + 'username', + 'name', + $groups_sub, + $goog_auth_sub, + $installer_sub, + $cust_sub, +); +my $align = 'lllccl'; +my @links = ( $link, $link, $link, '', '', '', $cust_link ); #if ( FS::Conf->new->config('ticket_system') ) { # push @header, 'Ticketing'; diff --git a/httemplate/browse/deploy_zone.html b/httemplate/browse/deploy_zone.html index a1bd57f15..0533d6e0f 100644 --- a/httemplate/browse/deploy_zone.html +++ b/httemplate/browse/deploy_zone.html @@ -19,6 +19,21 @@ 'Contractual Mbps', 'Vertices', 'Census blocks', + 'Shapefile', + 'KMZ', + 'GeoJSON', + ], + footer => [ '', + 'All fixed zones', + '', + '', + '', + '', + '', + '', + 'download', + 'download', + 'download', ], fields => [ 'zonenum', 'description', @@ -48,6 +63,18 @@ sub { my $self = shift; FS::deploy_zone_block->count('zonenum = '.$self->zonenum) }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, ], sort_fields => [ 'zonenum', 'description', @@ -56,11 +83,13 @@ '(adv_speed_down, adv_speed_up)', '(cir_speed_down, cir_speed_up)', ], - links => [ $link_fixed, $link_fixed, ], + links => [ $link_fixed, $link_fixed, '', '', '', '', '', '', $link_shp, $link_kmz, $link_json, ], align => 'cllllrrr', nohtmlheader => 1, disable_maxselect => 1, disable_total => 1, + disableable => 1, + disabled_statuspos => 2, &>

Mobile Zones

<& elements/browse.html, @@ -77,6 +106,9 @@ 'Service Type', 'Advertised Mbps', 'Vertices', # number of vertices? not so useful + 'Shapefile', + 'KMZ', + 'GeoJSON', ], fields => [ 'zonenum', 'description', @@ -99,6 +131,18 @@ sub { my $self = shift; FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, ], sort_fields => [ 'zonenum', 'description', @@ -107,24 +151,35 @@ '(is_voice is not null, is_broadband is not null)', '(adv_speed_down, adv_speed_up)', ], - links => [ '', $link_mobile, ], + links => [ $link_mobile, $link_mobile, '', '', '', '', '', $link_shp, $link_kmz, $link_json, ], align => 'clllllr', nohtmlheader => 1, disable_maxselect => 1, disable_total => 1, + disableable => 1, + disabled_statuspos => 2, &> <& /elements/footer.html &> <%init> + my $curuser = $FS::CurrentUser::CurrentUser; my $acl_edit = $curuser->access_right('Edit FCC report configuration'); my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents'); die "access denied" unless $acl_edit or $acl_edit_global; -my $link_fixed = [ $p.'edit/deploy_zone-fixed.html?', 'zonenum' ]; -my $link_mobile= [ $p.'edit/deploy_zone-mobile.html?', 'zonenum' ]; +my $link_fixed = [ $p.'edit/deploy_zone-fixed.html?', 'zonenum' ]; +my $link_mobile = [ $p.'edit/deploy_zone-mobile.html?', 'zonenum' ]; +my $link_shp = [ $p.'view/deploy_zone-shp.cgi?', 'zonenum' ]; +my $link_kmz = [ $p.'view/deploy_zone-kmz.cgi?', 'zonenum' ]; +my $link_json = [ $p.'view/deploy_zone-geojson.cgi?', 'zonenum' ]; + +my $fixed_shp = $p.'view/deploy_zone-shp.cgi?zonetype=B'; +my $fixed_kmz = $p.'view/deploy_zone-kmz.cgi?zonetype=B'; +my $fixed_json = $p.'view/deploy_zone-geojson.cgi?zonetype=B'; + +my $tech_label = FS::part_pkg_fcc_option->technology_labels; +my $spec_label = FS::part_pkg_fcc_option->spectrum_labels; -my $tech_label = FS::part_pkg_fcc_option->technology_labels; -my $spec_label = FS::part_pkg_fcc_option->spectrum_labels; diff --git a/httemplate/browse/hardware_class.html b/httemplate/browse/hardware_class.html index 0bf314e3f..6423f4a25 100644 --- a/httemplate/browse/hardware_class.html +++ b/httemplate/browse/hardware_class.html @@ -22,7 +22,7 @@ my $curuser = $FS::CurrentUser::CurrentUser; die "access denied" - unless $curuser->access_right('Configuration'); + unless $curuser->access_right('Edit hardware classes and types'); my $menubar = [ 'Hardware statuses' => $p.'browse/hardware_status.html', diff --git a/httemplate/browse/hardware_status.html b/httemplate/browse/hardware_status.html index 89ae83035..64ad5ad7f 100644 --- a/httemplate/browse/hardware_status.html +++ b/httemplate/browse/hardware_status.html @@ -16,7 +16,7 @@ my $curuser = $FS::CurrentUser::CurrentUser; die "access denied" - unless $curuser->access_right('Configuration'); + unless $curuser->access_right('Edit hardware classes and types'); my $menubar = [ 'Hardware classes' => $p.'browse/hardware_class.html', 'Add a status' => $p.'edit/hardware_status.html' ]; diff --git a/httemplate/docs/about.html b/httemplate/docs/about.html index e91eb3686..b225c84f9 100644 --- a/httemplate/docs/about.html +++ b/httemplate/docs/about.html @@ -28,7 +28,7 @@ % } else { % } -© 2019 Freeside Internet Services, Inc.
+© 2022 Freeside Internet Services, Inc.
All rights reserved.
Licensed under the terms of the
GNU Affero General Public License.
diff --git a/httemplate/edit/deploy_zone-fixed.html b/httemplate/edit/deploy_zone-fixed.html index 24e03b01a..fd138543d 100644 --- a/httemplate/edit/deploy_zone-fixed.html +++ b/httemplate/edit/deploy_zone-fixed.html @@ -4,6 +4,7 @@ 'post_url' => popurl(1).'process/deploy_zone-fixed.html', 'viewall_dir' => 'browse', 'labels' => { + 'zonenum' => 'Deployment zone', 'description' => 'Description', 'agentnum' => 'Agent', 'dbaname' => 'Business name (if different from agent)', @@ -14,6 +15,7 @@ 'cir_speed_down' => 'Downstream', 'is_consumer' => 'Consumer/mass market', 'is_business' => 'Business/government', + 'disabled' => 'Disabled', 'blocknum' => '', 'active_date' => 'Active since', 'file' => 'Import blocks from text file', @@ -37,6 +39,9 @@ $cgi->param('active_date') || $object->active_date || time; }, }, + { field => 'expire_date', + type => 'hidden', + }, { field => 'agentnum', type => 'select-agent', disable_empty => 1, @@ -50,12 +55,16 @@ }, { field => 'is_consumer', type => 'checkbox', value=>'Y' }, { field => 'is_business', type => 'checkbox', value=>'Y' }, + { field => 'disabled', type=>'checkbox', value=>'Y', }, { type => 'tablebreak-tr-title', value => 'Advertised maximum speed (Mbps)' }, 'adv_speed_down', 'adv_speed_up', { type => 'tablebreak-tr-title', value => 'Contractually guaranteed speed (Mbps)' }, + { type => 'note', + value => 'Only required for filings as of June 30th, 2019 (due Sep. 3rd, 2019) and before', + }, 'cir_speed_down', 'cir_speed_up', { type => 'tablebreak-tr-title', value => 'Footprint'}, diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html index a32e99c0c..829b77615 100644 --- a/httemplate/edit/elements/edit.html +++ b/httemplate/edit/elements/edit.html @@ -263,12 +263,12 @@ Example: % unless ( $opt{'no_pkey_display'} ) { - + <% ( $opt{labels} && exists $opt{labels}->{$pkey} ) ? $opt{labels}->{$pkey} : $pkey %> - + #<% ( !$clone && $object->$pkey() ) || "(NEW)" %> % } @@ -277,9 +277,7 @@ Example: % my $tablenum = $opt{'tablenum'} || 0; + CLASS="<% $opt{html_table_class} || 'fsinnerbox' %>" > % my $g_row = 0; diff --git a/httemplate/edit/hardware_class.html b/httemplate/edit/hardware_class.html index 26f487dda..8d5412bd2 100644 --- a/httemplate/edit/hardware_class.html +++ b/httemplate/edit/hardware_class.html @@ -11,6 +11,6 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + unless $FS::CurrentUser::CurrentUser->access_right('Edit hardware classes and types'); diff --git a/httemplate/edit/hardware_status.html b/httemplate/edit/hardware_status.html index 23e5b6eba..c8cb68cd4 100644 --- a/httemplate/edit/hardware_status.html +++ b/httemplate/edit/hardware_status.html @@ -19,6 +19,6 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + unless $FS::CurrentUser::CurrentUser->access_right('Edit hardware classes and types'); diff --git a/httemplate/edit/hardware_type.html b/httemplate/edit/hardware_type.html index 717440166..5660688aa 100644 --- a/httemplate/edit/hardware_type.html +++ b/httemplate/edit/hardware_type.html @@ -14,7 +14,7 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + unless $FS::CurrentUser::CurrentUser->access_right('Edit hardware classes and types'); my @fields = ( { field => 'classnum', diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi index 78e4427e0..0a0b01e11 100755 --- a/httemplate/edit/part_pkg.cgi +++ b/httemplate/edit/part_pkg.cgi @@ -206,9 +206,8 @@ #recurring frequency #recurring fee (auto-disable) - { type => 'columnnext' }, + { type => 'columnnext', value=>'Taxation', }, - {type=>'justtitle', value=>'Taxation' }, {field=>'setuptax', type=>'checkbox', value=>'Y'}, {field=>'recurtax', type=>'checkbox', value=>'Y'}, {field=>'taxclass', type=>'select-taxclass' }, @@ -243,9 +242,7 @@ ) ), - { type => 'columnnext' }, - - {type=>'justtitle', value=>'Agent (reseller) types' }, + { type => 'columnnext', value=>'Agent (reseller) types' }, { field => 'agent_type', type => 'select-agent_type', @@ -857,8 +854,9 @@ my $javascript = <<'END'; supp_pkg_rows[0].style.display = 'none'; var button = document.getElementById('show_supp_pkgs'); button.onclick = show_supp_pkgs_click; - button.style.backgroundColor = '#cccccc'; - button.style.border = '1px solid #7e0079'; + //button.style.backgroundColor = '#cccccc'; + //button.style.border = '1px solid #7e0079'; + button.style.border = 'thin solid #999999'; button.style.padding = '1px'; } } @@ -914,7 +912,7 @@ my $html_bottom = sub { my $layer_callback = sub { my $layer = shift; - my $html = ntable("#cccccc",2); + my $html = '
'; #$html .= ' # diff --git a/httemplate/edit/process/access_user.html b/httemplate/edit/process/access_user.html index c27262017..8e264c1a9 100644 --- a/httemplate/edit/process/access_user.html +++ b/httemplate/edit/process/access_user.html @@ -5,7 +5,7 @@ <% include( 'elements/process.html', 'table' => 'access_user', 'viewall_dir' => 'browse', - 'copy_on_empty' => [ '_password', '_password_encoding' ], + 'copy_on_empty' => [ '_password', '_password_encoding', 'totp_secret32' ], 'clear_on_error' => [ '_password', '_password2' ], 'process_m2m' => { 'link_table' => 'access_usergroup', 'target_table' => 'access_group', diff --git a/httemplate/edit/process/cust_location-censustract.html b/httemplate/edit/process/cust_location-censustract.html index 40c83c630..1bf4d7682 100644 --- a/httemplate/edit/process/cust_location-censustract.html +++ b/httemplate/edit/process/cust_location-censustract.html @@ -28,7 +28,7 @@ my $cust_location = qsearchs({ }); die "unknown locationnum $locationnum" unless $cust_location; -$cust_location->set('censustract', $cgi->param('enter_censustract')); +$cust_location->set('censustract', scalar($cgi->param('enter_censustract'))); my $error = $cust_location->replace; diff --git a/httemplate/edit/process/cust_main.cgi b/httemplate/edit/process/cust_main.cgi index baacd5e7e..d4578d296 100755 --- a/httemplate/edit/process/cust_main.cgi +++ b/httemplate/edit/process/cust_main.cgi @@ -44,7 +44,7 @@ my $error = ''; $cgi->param('tax','') unless defined $cgi->param('tax'); -$cgi->param('refnum', (split(/:/, ($cgi->param('refnum'))[0] ))[0] ); +$cgi->param('refnum', (split(/:/, ($cgi->multi_param('refnum'))[0] ))[0] ); #my @invoicing_list = split( /\s*\,\s*/, $cgi->param('invoicing_list') ); #push @invoicing_list, 'POST' if $cgi->param('invoicing_list_POST'); @@ -62,7 +62,7 @@ $cgi->param('duplicate_of_custnum') =~ /^(\d+)$/; my $duplicate_of = $1; # if this is enabled, enforce it -if ( $conf->exists('agent-ship_address', $cgi->param('agentnum')) ) { +if ( $conf->exists('agent-ship_address', scalar($cgi->param('agentnum'))) ) { my $agent = FS::agent->by_key($cgi->param('agentnum')); my $agent_cust_main = $agent->agent_cust_main; if ( $agent_cust_main ) { diff --git a/httemplate/edit/process/elements/ApplicationCommon.html b/httemplate/edit/process/elements/ApplicationCommon.html index b7501d462..5007319b8 100644 --- a/httemplate/edit/process/elements/ApplicationCommon.html +++ b/httemplate/edit/process/elements/ApplicationCommon.html @@ -55,8 +55,12 @@ my $cust_main = qsearchs('cust_main', { 'custnum' => $src->custnum } ) my $custnum = $cust_main->custnum; my @subnames = grep { /.+/ } map { /^subnum(\d+)$/ ? $1 : '' } $cgi->param; -my @subitems = map { [ $cgi->param("subnum$_"), $cgi->param("subamount$_"), $cgi->param("taxXlocationnum$_") ] } - @subnames; +my @subitems = map { [ scalar($cgi->param("subnum$_")), + scalar($cgi->param("subamount$_")), + scalar($cgi->param("taxXlocationnum$_")) + ] + } + @subnames; { local $^W = 0; @subitems = grep { $_->[1] + 0 } @subitems; } my %options = (); diff --git a/httemplate/edit/process/hardware_class.html b/httemplate/edit/process/hardware_class.html index 64bc72efe..54e2af694 100644 --- a/httemplate/edit/process/hardware_class.html +++ b/httemplate/edit/process/hardware_class.html @@ -6,6 +6,6 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + unless $FS::CurrentUser::CurrentUser->access_right('Edit hardware classes and types'); diff --git a/httemplate/edit/process/hardware_status.html b/httemplate/edit/process/hardware_status.html index 61f02e215..d790eb5ba 100644 --- a/httemplate/edit/process/hardware_status.html +++ b/httemplate/edit/process/hardware_status.html @@ -6,6 +6,6 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + unless $FS::CurrentUser::CurrentUser->access_right('Edit hardware classes and types'); diff --git a/httemplate/edit/process/hardware_type.html b/httemplate/edit/process/hardware_type.html index 52787011c..954e76d03 100644 --- a/httemplate/edit/process/hardware_type.html +++ b/httemplate/edit/process/hardware_type.html @@ -6,6 +6,6 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + unless $FS::CurrentUser::CurrentUser->access_right('Edit hardware classes and types'); diff --git a/httemplate/elements/columnnext.html b/httemplate/elements/columnnext.html index 4dfe82fd8..da8286e64 100644 --- a/httemplate/elements/columnnext.html +++ b/httemplate/elements/columnnext.html @@ -1,4 +1,10 @@
- + <% $opt{value} %>
+
+<%init> + +my %opt = @_; + + diff --git a/httemplate/elements/columnstart.html b/httemplate/elements/columnstart.html index 245c308a7..40f7cad40 100644 --- a/httemplate/elements/columnstart.html +++ b/httemplate/elements/columnstart.html @@ -16,7 +16,7 @@ Pass 'aligned' => 1 to have corresponding rows in the columns line up.
+% foreach my $top_warning ( @top_warnings ) { + + + +% }
- +
% if ( $aligned ) { %# Instead of changing all the tr-* elements to sometimes output table %# cells without wrapping them in a row, we're just going to completely diff --git a/httemplate/elements/cust_payby.html b/httemplate/elements/cust_payby.html index 6404bcb33..6765df077 100644 --- a/httemplate/elements/cust_payby.html +++ b/httemplate/elements/cust_payby.html @@ -306,8 +306,11 @@ if ( $curr_value ) { $cust_payby = new FS::cust_payby {}; } my $sel_payby = $cgi->param($name.'_payby') || $cust_payby->payby; +# add a weight for CARD/CHEK imports, so we don't turn off auto-charge on edit # convert DCRD to CARD + no weight, and the same for DCHK/CHEK -if ($sel_payby eq 'DCRD') { +if ( $cust_payby->custpaybynum && $sel_payby =~ /^(CARD|CHEK)$/ && ! $cust_payby->weight ) { + $cust_payby->weight(1); +} elsif ($sel_payby eq 'DCRD') { $sel_payby = 'CARD'; $cust_payby->weight(''); } elsif ($sel_payby eq 'DCHK') { diff --git a/httemplate/elements/dashboard-topnotes.html b/httemplate/elements/dashboard-topnotes.html new file mode 100644 index 000000000..28ca025a0 --- /dev/null +++ b/httemplate/elements/dashboard-topnotes.html @@ -0,0 +1,15 @@ +% if ( $notes ) { +
+ +
+ <% $notes %> +
+
+% } +<%init> + +my $conf = new FS::Conf; + +my $notes = join('
', map encode_entities($_), $conf->config('dashboard-topnotes') ); + + diff --git a/httemplate/elements/header-full.html b/httemplate/elements/header-full.html index 5a5db9620..a56e0b183 100644 --- a/httemplate/elements/header-full.html +++ b/httemplate/elements/header-full.html @@ -96,6 +96,14 @@ Example:
+ + <% $top_warning %> +
@@ -276,4 +284,23 @@ if ( scalar(@agentnums) == 1 ) { my %status_color = ( 'status' => '#eeffee', 'warning' => '#fefbd0', 'error' => '#f97c7c', ); my %status_image = ( 'status' => 'images/tick.png', 'warning' => 'images/tick.png', 'error' => 'images/error.png', ); +my @top_warnings = (); +my $deb_version = int(slurp('/etc/debian_version')); +#per wiki.debian.org/LTS +push @top_warnings, deb_warning($deb_version) + if ( $deb_version <= 8 ) + or ( $deb_version == 9 && time > 1656658800 ) #7/1/2022 + or ( $deb_version == 10 && time > 1719817200 ) #7/1/2024 + or ( $deb_version == 11 && time > 1782889200 ) #7/1/2026 +; + +sub deb_warning { + my $ver = shift; + <<"END"; +WARNING: Your operating system (Debian v$ver) is EOL and no longer supported. +This is insecure and a violation of PCI data security standard. +Contact sales\@freeside.biz to schedule an upgrade ASAP. +END +} + diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 091ec1245..dd38cd08c 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -129,6 +129,8 @@ $report_customers_lists{'by active trouble tickets'} = [ $fsurl. 'search/cust_ma if $conf->config('ticket_system'); $report_customers_lists{'with USPS-unvalidated addresses'} = [ $fsurl. 'search/cust_main.cgi?browse=uspsunvalid', '' ] if $conf->config('usps_webtools-userid') && $conf->config('usps_webtools-password'); +$report_customers_lists{'with missing/outdated census tract'} = [ $fsurl. 'search/cust_main.html?no_censustract=1&ship_country=US', '' ] + ;#if $conf->config('cust_main-require_censustract'); $report_customers_lists{'with referrals'} = [ $fsurl. 'search/cust_main.html?with_referrals=1' ]; tie my %report_customers, 'Tie::IxHash'; @@ -693,7 +695,7 @@ $config_export_svc{'Circuits'} = [ \%config_circuit, '' ] $config_export_svc{'Fiber'} = [ \%config_fiber, '' ] if $curuser->access_right('Configuration'); $config_export_svc{'Hardware types'} = [ $fsurl.'browse/hardware_class.html', 'Set up hardware type catalog' ] - if $curuser->access_right('Configuration'); + if $curuser->access_right('Edit hardware classes and types'); tie my %config_pkg_reason, 'Tie::IxHash', 'Cancel reasons' => [ $fsurl.'browse/reason_type.html?class=C', 'Cancel reasons explain why a service was cancelled.' ], diff --git a/httemplate/elements/standardize_locations.js b/httemplate/elements/standardize_locations.js index 54a554ff2..140ed70c6 100644 --- a/httemplate/elements/standardize_locations.js +++ b/httemplate/elements/standardize_locations.js @@ -325,6 +325,12 @@ function set_censustract(tract, year) { var form = document.<% $formname %>; form.elements['censustract'].value = tract; form.elements['censusyear'].value = year; + + var enter = form.elements['enter_censustract']; + if ( enter ) { + enter.value = tract; + } + <% $post_censustract %>; } diff --git a/httemplate/elements/tablebreak-tr-title.html b/httemplate/elements/tablebreak-tr-title.html index ee2831231..ce64d4737 100644 --- a/httemplate/elements/tablebreak-tr-title.html +++ b/httemplate/elements/tablebreak-tr-title.html @@ -1,9 +1,9 @@
+
- BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0> - -<% include('tr-title.html', @_ ) %> +><% $opt{value} %> +
CLASS="fsinnerbox"> <%init> my %opt = @_; @@ -11,4 +11,7 @@ my %opt = @_; my $id = ''; $id = 'ID="'. $opt{'table_id'}. '"' if $opt{'table_id'}; +my $title_id; +my $title_id = 'ID="'.$opt{id}.'"' if $opt{id}; + diff --git a/httemplate/elements/tr-justtitle.html b/httemplate/elements/tr-justtitle.html index b87f7e128..316ea69a8 100644 --- a/httemplate/elements/tr-justtitle.html +++ b/httemplate/elements/tr-justtitle.html @@ -1,7 +1,7 @@ - + <%init> diff --git a/httemplate/elements/tr-note.html b/httemplate/elements/tr-note.html new file mode 100644 index 000000000..8493e5a83 --- /dev/null +++ b/httemplate/elements/tr-note.html @@ -0,0 +1,12 @@ + + + + +<%init> + +my %opt = @_; +my $id = 'ID="'.$opt{id}.'"' if $opt{id}; + + diff --git a/httemplate/elements/tr-pkg_svc.html b/httemplate/elements/tr-pkg_svc.html index de3f95a3a..232454576 100644 --- a/httemplate/elements/tr-pkg_svc.html +++ b/httemplate/elements/tr-pkg_svc.html @@ -131,8 +131,8 @@ provision_hold_init(); % if ( scalar(@possible_exports) > 0 || scalar(@mapped_exports) > 0 ) {
ALIGN="left" <%$id%>> - <% $opt{value} %> - ALIGN="left" <%$id%>> + <% $opt{value} %> +
ALIGN="left" <%$id%>> + <% $opt{value} %> +
- - + + % foreach my $export ( @mapped_exports ) { @@ -166,14 +166,14 @@ my $cgi = $opt{'cgi'}; my $thead_count = 0; sub pkg_svc_thead { $thead_count += 1; - return "\n\n". ntable('#cccccc', 2). + return "\n\n". '
ExportVendor Package Id (blank to delete)ExportVendor Package Id (blank to delete)
'. ''. - ''. - ''. - ''. - ''. - ''. - ''. + ''. + ''. + ''. + ''. + ''. + ''. ''. qq!!; ; diff --git a/httemplate/elements/tr-select-reason.html b/httemplate/elements/tr-select-reason.html index 64648ba54..8174420a7 100755 --- a/httemplate/elements/tr-select-reason.html +++ b/httemplate/elements/tr-select-reason.html @@ -16,7 +16,7 @@ Example: 'control_button' => 'element_name', #button to be enabled when a reason is #selected 'id' => 'element_id', - 'hide_add' => '1', # setting this will hide the add new reason link, + 'hide_addnew' => '1', # setting this will hide the add new reason link, # even if the user has access to add a new reason. 'hide_onload' => '1', # setting this will hide reason select box on page load, # allowing for it do be displayed later. diff --git a/httemplate/loginout/login.html b/httemplate/loginout/login.html index 72e9525c4..1785ea796 100644 --- a/httemplate/loginout/login.html +++ b/httemplate/loginout/login.html @@ -27,6 +27,10 @@ + + + +
Quan.PrimaryServiceHide
from
Invoices
Bulk
Charge
Remove Hold After ProvisioningQuan.PrimaryServiceHide
from
Invoices
Bulk
Charge
Remove Hold After Provisioning
Password:
One-time code:

@@ -42,7 +46,7 @@ my %error = ( 'no_cookie' => '', #First login, don't display an error 'bad_cookie' => 'Bad Cookie', #timed out? - 'bad_credentials' => 'Incorrect username / password', + 'bad_credentials' => 'Incorrect username / password / one-time code', #'logout' => 'You have been logged out.', ); diff --git a/httemplate/misc/confirm-censustract.html b/httemplate/misc/confirm-censustract.html index 0f115e5d7..02847f869 100644 --- a/httemplate/misc/confirm-censustract.html +++ b/httemplate/misc/confirm-censustract.html @@ -16,12 +16,18 @@ Confirm census tract <% $location{address1} |h %> <% $location{address2} |h %>
<% $location{city} |h %>, <% $location{state} |h %> <% $location{zip} |h %>

-% my $querystring = "census_year=$year&address=$location{address1}, $location{address2}, $location{city}, $location{state}"; +% my $address1 = $location{address1}; +% $address1 =~ s/(apt|ste|suite|unit)[\s\d]\w*\s*$//i; +% my $querystring = "census_year=$year&address=$address1, $location{address2}, $location{city}, $location{state}"; Map service module location
+ REL="opener" + TARGET="_blank" +>Map service location
% $querystring = "census_year=$year&pre=$pre&zip_code=" . $cache->get('zip'); Map zip code center
+ REL="opener" + TARGET="_blank" +>Map zip code center

@@ -103,7 +109,7 @@ my %location = ( my $old_tract = $q->{$pre.'censustract'}; my $cache = eval { FS::GeocodeCache->new(%location) }; $cache->set_censustract; -my $year = FS::Conf->new->config('census_year'); +my $year = FS::Conf->new->config('census_legacy') || '2020'; my $new_tract = $cache->get('censustract'); my $error = $cache->get('censustract_error'); diff --git a/httemplate/misc/openmap.html b/httemplate/misc/openmap.html index 88f64c1ff..15de2882a 100644 --- a/httemplate/misc/openmap.html +++ b/httemplate/misc/openmap.html @@ -44,8 +44,7 @@ function getCensusTract(lat, lon) { var url = 'xmlhttp-censustract.html?lat=' + lat + '&lon=' + lon + '&census_year=<%$census_year%>'; $.getJSON(url,function(data){ - var tract = (data.Block.FIPS.substr(0, 11) / 100).toFixed(2); - document.getElementById("mycensustract").innerHTML = tract; + document.getElementById("mycensustract").innerHTML = data.Block.FIPS; }); } @@ -79,6 +78,6 @@ my $census_year = $cgi->param('census_year'); my $pre = $cgi->param('pre'); my $zip_code = $cgi->param('zip_code'); my $address = $cgi->param('address'); -my $loc = $zip_code ? $zip_code : $address; +my $loc = $zip_code ? $zip_code.', United States' : $address; - \ No newline at end of file + diff --git a/httemplate/misc/xmlhttp-censustract.html b/httemplate/misc/xmlhttp-censustract.html index 985fb90e5..855e172e2 100644 --- a/httemplate/misc/xmlhttp-censustract.html +++ b/httemplate/misc/xmlhttp-censustract.html @@ -5,12 +5,19 @@ my $DEBUG = 0; my $conf = new FS::Conf; -my $return = {}; - ## new api link, see doc https://geo.fcc.gov/api/census/ my $url = "https://geo.fcc.gov/api/census/block/find?format=json&censusYear=" . $cgi->param('census_year') . "&latitude=" . $cgi->param('lat') . "&longitude=" . $cgi->param('lon'); -use LWP::Simple; -my $return = get $url; +my $ua = new LWP::UserAgent; +$ua->agent('Freeside/'. $FS::VERSION); #libwww* elicits "403 Forbidden" +my $res = $ua->get($url); + +my $return = ''; +if ( $res->is_success ) { + $return = $res->decoded_content; +} else { + #better error handling? well, hopefully the site is reliable enough + warn 'Error from geo.fcc.gov: '. $res->status_line. "\n"; +} - \ No newline at end of file + diff --git a/httemplate/misc/xmlhttp-cust_main-display_recurring.html b/httemplate/misc/xmlhttp-cust_main-display_recurring.html index dd9ed3bb5..6c9cf69a9 100644 --- a/httemplate/misc/xmlhttp-cust_main-display_recurring.html +++ b/httemplate/misc/xmlhttp-cust_main-display_recurring.html @@ -1,7 +1,7 @@ <% encode_json($return) %>\ <%init> -my %arg = $cgi->param('arg'); +my %arg = $cgi->multi_param('arg'); my $custnum = delete($arg{'custnum'}); my $error; diff --git a/httemplate/pref/pref.html b/httemplate/pref/pref.html index 56fde6d44..5f68d3e46 100644 --- a/httemplate/pref/pref.html +++ b/httemplate/pref/pref.html @@ -29,6 +29,18 @@

+ <% emt('Google Authenticator') %> + + +% if ( $curuser->totp_secret32 ) { + +% } else { + +% } + +
Enable
+
+ % } <% emt("Interface") %> diff --git a/httemplate/pref/set_totp_secret32.html b/httemplate/pref/set_totp_secret32.html new file mode 100644 index 000000000..f5676bc38 --- /dev/null +++ b/httemplate/pref/set_totp_secret32.html @@ -0,0 +1,19 @@ +<& /elements/header.html, mt('Google Authenticator for [_1]', $FS::CurrentUser::CurrentUser->username) &> + +Scan this code with the Google Authenticator application on your phone. +

+ + +

+ +Future logins will require a 6-digit code generated by the application. + +<& /elements/footer.html &> +<%init> + +my $access_user = $FS::CurrentUser::CurrentUser; + +my $error = $access_user->set_totp_secret32 unless length($access_user->totp_secret32); +die $error if $error; #better error handling for this "shouldn't happen" case? + + diff --git a/httemplate/search/477.html b/httemplate/search/477.html index 84fd7fb9f..9a4126121 100644 --- a/httemplate/search/477.html +++ b/httemplate/search/477.html @@ -67,6 +67,9 @@ a.download { Download % my $header = ".header_$partname"; +% $header .= '_old' if $partname eq 'fbd' && $date < 1569826800; #9/30/2019 +% # ( halfway between the two filing "as of" dates when it changed + % my $data = $this_part->{data}; % my $error = $this_part->{error}; @@ -168,7 +171,7 @@ if ( $cgi->param('type') eq 'csv' ) { my $part_titles = FS::Report::FCC_477->parts; -<%def .header_fbd> +<%def .header_fbd_old> Census Block DBA Name @@ -185,6 +188,20 @@ my $part_titles = FS::Report::FCC_477->parts; Up +<%def .header_fbd> + + Census Block + DBA Name + Technology + Consumer? + Advertised Speed (Mbps) + Business? + + + Down + Up + + <%def .header_fbs> Census Tract diff --git a/httemplate/search/cust_credit.html b/httemplate/search/cust_credit.html index 1ffa302c8..88f9bca13 100755 --- a/httemplate/search/cust_credit.html +++ b/httemplate/search/cust_credit.html @@ -195,6 +195,10 @@ if ( $cgi->param('credbatch') =~ /^([\w\-\/\.\:]+)$/ ) { push @search, "cust_credit.credbatch = '$1'"; } +if ( $cgi->param('reasonnum') =~ /^(\d+)$/ && $1 ) { + push @search, "cust_credit.reasonnum = $1"; +} + # commission_salesnum if ( $cgi->param('commission_salesnum') =~ /^(\d+)$/ ) { push @search, "commission_salesnum = $1"; diff --git a/httemplate/search/cust_event.html b/httemplate/search/cust_event.html index 8bbd1af24..56bf76778 100644 --- a/httemplate/search/cust_event.html +++ b/httemplate/search/cust_event.html @@ -163,7 +163,7 @@ die "access denied" || $cgi->param('pkgnum') =~ /^(\d+)$/ ); -my @statuses = $cgi->param('event_status'); +my @statuses = $cgi->multi_param('event_status'); my $title = 'Billing events'; if ( $statuses[0] eq 'failed' and !defined($statuses[1]) ) { # tweak the title if we're showing only failed events diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html index 46e35da7f..8ef068f26 100755 --- a/httemplate/search/cust_main.html +++ b/httemplate/search/cust_main.html @@ -70,7 +70,7 @@ for my $param ( @scalars ) { #lists for my $param (qw( classnum refnum pkg_classnum )) { - $search_hash{$param} = [ $cgi->param($param) ]; + $search_hash{$param} = [ $cgi->multi_param($param) ]; } my $params = $cgi->Vars; diff --git a/httemplate/search/elements/report_svc_Common.html b/httemplate/search/elements/report_svc_Common.html index 434197078..d57e64feb 100644 --- a/httemplate/search/elements/report_svc_Common.html +++ b/httemplate/search/elements/report_svc_Common.html @@ -20,11 +20,8 @@ Example: - - - - - + <% emt('Search options') %> +
<% mt('Search options') |h %>
% unless ( $custnum ) { @@ -38,12 +35,15 @@ Example: field => 'cust_status', &> - <& /elements/tr-select-payby.html, - label => emt('Payment method:'), - payby_type => 'cust', - multiple => 1, - all_selected => 1, - &> +%# meaning-less in the post-4.x world, customers can have multiple payment +%# methods now + +%# <& /elements/tr-select-payby.html, +%# label => emt('Payment method:'), +%# payby_type => 'cust', +%# multiple => 1, +%# all_selected => 1, +%# &> <& /elements/tr-input-money.html, label => 'Balance over', @@ -72,13 +72,11 @@ Example: 'label' => 'Services', &> - - - - - - - +
 
<% mt('Display options') |h %>
+
+ + <% emt('Display options') %> + % #"package fields" ala advanced svc_acct search? % #move to /elements/tr-select-cust_pkg-fields and use it from there if so... diff --git a/httemplate/search/log.html b/httemplate/search/log.html index 9be0b7d0a..96a6f5b64 100644 --- a/httemplate/search/log.html +++ b/httemplate/search/log.html @@ -216,7 +216,9 @@ $cgi->param('max_level', 5) unless defined($cgi->param('max_level')); my %search = (); $search{'date'} = [ FS::UI::Web::parse_beginning_ending($cgi) ]; -$search{'level'} = [ $cgi->param('min_level'), $cgi->param('max_level') ]; +$search{'level'} = [ scalar($cgi->param('min_level')), + scalar($cgi->param('max_level')) + ]; foreach my $param (qw(agentnum context context_height tablename tablenum custnum message)) { if ( $cgi->param($param) ) { $search{$param} = $cgi->param($param); diff --git a/httemplate/search/report_cust_credit.html b/httemplate/search/report_cust_credit.html index cafe556ba..c37b578b4 100644 --- a/httemplate/search/report_cust_credit.html +++ b/httemplate/search/report_cust_credit.html @@ -24,6 +24,15 @@ 'field' => 'amount', &> + <& /elements/tr-select-reason.html, + 'label' => emt('Reason').':', + 'field' => 'reasonnum', + 'reason_class' => 'R', + 'cgi' => $cgi, + 'hide_addnew' => 1, + 'pre_options' => [ 0 => emt('(any reason)') ], + &> + <& /elements/tr-checkbox.html, 'label' => emt('Show Voided Credits').':', 'field' => 'show_voided_credits', diff --git a/httemplate/search/report_cust_pkg.html b/httemplate/search/report_cust_pkg.html index 4e17d1f51..a976ef972 100755 --- a/httemplate/search/report_cust_pkg.html +++ b/httemplate/search/report_cust_pkg.html @@ -266,8 +266,8 @@ 'options' => \@location_options, 'labels' => { 'cust' => "is the customer's default location", 'nocust' => "is not the customer's default location", - 'census' => "has a census tract", - 'nocensus' => "does not have a census tract", + 'census' => "has an up-to-date census tract", + 'nocensus' => "does not have an up-to-date census tract", 'nogeocode'=> 'has an implicit tax location', 'geocode' => 'has a hardcoded tax location', }, diff --git a/httemplate/search/report_svc_broadband.html b/httemplate/search/report_svc_broadband.html index 45246c74a..377064af8 100755 --- a/httemplate/search/report_svc_broadband.html +++ b/httemplate/search/report_svc_broadband.html @@ -1,14 +1,13 @@ <% include('/elements/header.html', $title ) %> +%# extensive false laziness with svc_acct + -%# extensive false laziness with svc_acct -
- - - + <% emt('Search options') %> +
Search options
% unless ( $custnum ) { <% include( '/elements/tr-select-agent.html', @@ -44,17 +43,16 @@ % } % } - - - - - - - +
 
Display options
+
+ + <% emt('Display options') %> + + % #move to /elements/tr-select-cust_pkg-fields if anything else needs it... - +
Package fieldsPackage fields