{ 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
{ 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, },
use FS::access_user;
sub authenticate {
- my($self, $username, $check_password ) = @_;
+ my($self, $username, $check_password, $totp_code ) = @_;
my $access_user =
ref($username) ? $username
)
or return 0;
+ my $pw_check;
if ( $access_user->_password_encoding eq 'bcrypt' ) {
my( $cost, $salt, $hash ) = split(',', $access_user->_password);
)
);
- $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; }
}
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;
}
{
'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',
},
'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) ],
},
{
},
{
+ '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',
{
'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 <a href ="#census_legacy">census_legacy</a> 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.',
},
{
+ '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*.',
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");
On error, sets 'error' and returns nothing.
This uses the "get_censustract_*" methods in L<FS::Misc::Geo>; 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);
}
die $@ if $@;
}
use Text::CSV_XS;
+ use Archive::Zip;
use Spreadsheet::WriteExcel;
use Spreadsheet::WriteExcel::Utility;
use OLE::Storage_Lite;
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;
#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
#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
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') ) {
}
+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:
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 {
}
}
+=item get_censustract_uscensus LOCATION YEAR
+
+Given a location hash (see L<FS::location_Mixin>) 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',
($subloc, $addr2);
}
+#is anyone still using this?
sub standardize_melissa {
my $class = shift;
my $location = shift;
'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)
);
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
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",
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",
# 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' => [],
[ '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.
'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', '', '', '',
'is_business', 'char', 'NULL', 1, '', '',
'active_date', @date_type, '', '',
'expire_date', @date_type, '', '',
+ 'disabled', 'char', 'NULL', 1, '', '',
],
'primary_key' => 'zonenum',
'unique' => [],
'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 ) {
use FS::cust_main;
use FS::sales;
use Carp qw( croak );
+use Auth::GoogleAuth;
$DEBUG = 0;
$me = '[FS::access_user]';
$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')
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
=item detailnum - Link to invoice detail (L<FS::cust_bill_pkg_detail>)
+=item sipcallid - SIP Call-ID
+
=back
=head1 METHODS
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);
'';
};
}
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/(?<!\\),/","/g; # all commas not after a \ become ","
+ $line =~ s/\\,/,/g; # and now we can turn \, into ,
+ #XXX embedded \r and \n ? none in my test data, and might be better to
+ # leave escaped and deal with it from there?
+ $line =~ s/\\\\/\\/g; # undo double backslashes? none in my test data
+
+ #and now we have a properly formed CSV line
+ $line;
+ },
+
+ import_fields => [
+
+ # 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',
+
],
);
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)
#60
- '', #OrigIPCallID
+ 'sipcallid', #OrigIPCallID
'', #ESAIPTrunkGroup
'', #ESAReason
'', #BearerlessCall
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;
}
- $error = $self->delete;
+ $error = $self->delete( skip_update_cust_bill_charged=>1 );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
sub delete {
my $self = shift;
+ my %opt = @_;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
}
}
- #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;
}
if ( $self->censustract ) {
- $self->set('censusyear' => $conf->config('census_year') || 2012);
+ $self->set('censusyear' => $conf->config('census_legacy') || 2020);
}
my $oldAutoCommit = $FS::UID::AutoCommit;
;
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
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');
}
##
+ # 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
##
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;
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;
return '' if $ignore_cardtype;
- return '' unless $self->payby =~ /^(CARD|CHEK)$/;
+ return '' unless $self->payby =~ /^(CARD|DCRD)$/;
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
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";
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
=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
});
}
+=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
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
inSR => 4326,
outSR => 4326,
spatialRel => 'esriSpatialRelIntersects', # the test to perform
- outFields => 'OID,GEOID',
+ outFields => 'GEOID',
returnGeometry => 'false',
orderByFields => 'OID',
);
#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;
}
#warn "Inserted $inserted records\n";
- $last_oid = $data->{features}[-1]{attributes}{OID};
$done = 1 unless $data->{exceededTransferLimit};
}
use FS::template_content;
use Date::Format qw(time2str);
+use PDF::WebKit;
FS::UID->install_callback( sub { $conf = new FS::Conf; } );
sub render {
my $self = shift;
- eval "use PDF::WebKit";
- die $@ if $@;
my %opt = @_;
my %hash = $self->prepare(%opt);
my $html = $hash{'html_body'};
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 );
# 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');
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',
};
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 '.
'default_svc_class' => 'Email',
'notes' => <<END
Export accounts (svc_acct records) to SQL databases. Currently has default
-configurations for vpopmail and Postfix+Courier IMAP but intended to be
-configurable for other schemas as well.
+configurations for vpopmail, Postfix+Courier IMAP, Postfix native and ,
+but can be configured for other schemas.
<BR><BR>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.
<BR><BR>Use these buttons for some useful presets:
<UL>
this.form.schema.value = "$postfix_native_mailbox_map";
this.form.primary_key.value = "userid";
'>
+ <LI><INPUT TYPE="button" VALUE="libnss-pgsql passwd" onClick='
+ this.form.table.value = "passwd_table";
+ this.form.schema.value = "$libnss_pgsql_passwd_map";
+ this.form.static.value = "$libnss_pgsql_passwd_static";
+ this.form.primary_key.value = "uid";
+ '>
+ <LI><INPUT TYPE="button" VALUE="libnss-pgsql shadow" onClick='
+ this.form.table.value = "shadow_table";
+ this.form.schema.value = "$libnss_pgsql_shadow_map";
+ this.form.static.value = "$libnss_pgsql_shadow_static";
+ this.form.primary_key.value = "username";
+ '>
</UL>
END
);
'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: ',
},
'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',
},
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
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;
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'));
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')
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),
FS/svc_group.pm
FS/h_svc_group.pm
FS/Misc/DepositSlip.pm
+bin/freeside-svc_acct-bulk_change
# 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 =~ /^\./;
$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 = ();
'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);
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
";
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
-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
#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
}
}
+ 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
)
|| $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
;
}
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
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.
# 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;
=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<census_tract> configuration variable, not the calendar year.
+and updates them to the current year. The "current year" is 2020, unless the
+I<census_legacy> 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
--- /dev/null
+#!/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<svc_acct-edit_gid>
+ configuration setting prior to running this script.
+
+=head1 SEE ALSO
+
+L<FS::svc_acct>
+
+=cut
+
+1;
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);
Change options:
--o: part_pkg_option optionname
+-o: part_pkg_option optionname (use without -v to unset)
-v: part_pkg_option optionvalue
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
)) {
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,
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
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.
$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' ) {
};
+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 '';
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';
'Contractual Mbps',
'Vertices',
'Census blocks',
+ 'Shapefile',
+ 'KMZ',
+ 'GeoJSON',
+ ],
+ footer => [ '',
+ 'All fixed zones',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '<A HREF="'. $fixed_shp. '">download</A>',
+ '<A HREF="'. $fixed_kmz. '">download</A>',
+ '<A HREF="'. $fixed_json. '">download</A>',
],
fields => [ 'zonenum',
'description',
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',
'(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,
&>
<P><FONT SIZE="+1"><B>Mobile Zones</B></FONT></P>
<& elements/browse.html,
'Service Type',
'Advertised Mbps',
'Vertices', # number of vertices? not so useful
+ 'Shapefile',
+ 'KMZ',
+ 'GeoJSON',
],
fields => [ 'zonenum',
'description',
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',
'(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;
</%init>
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',
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' ];
% } else {
<FONT SIZE="-1">
% }
-© 2019 Freeside Internet Services, Inc.<BR>
+© 2022 Freeside Internet Services, Inc.<BR>
All rights reserved.<BR>
Licensed under the terms of the<BR>
GNU <b>Affero</b> General Public License.<BR>
'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)',
'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',
$cgi->param('active_date') || $object->active_date || time;
},
},
+ { field => 'expire_date',
+ type => 'hidden',
+ },
{ field => 'agentnum',
type => 'select-agent',
disable_empty => 1,
},
{ 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'},
% unless ( $opt{'no_pkey_display'} ) {
- <FONT SIZE="+1"><B>
+ <FONT CLASS="fsinnerbox-title">
<% ( $opt{labels} && exists $opt{labels}->{$pkey} )
? $opt{labels}->{$pkey}
: $pkey
%>
- </B></FONT>
+ </FONT>
#<% ( !$clone && $object->$pkey() ) || "(NEW)" %>
% }
% my $tablenum = $opt{'tablenum'} || 0;
<TABLE ID="TableNumber<% $tablenum++ %>"
- <% $opt{html_table_class} ? 'CLASS="'. $opt{html_table_class}. '"'
- : 'BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0'
- %>
+ CLASS="<% $opt{html_table_class} || 'fsinnerbox' %>"
>
% my $g_row = 0;
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+ unless $FS::CurrentUser::CurrentUser->access_right('Edit hardware classes and types');
</%init>
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+ unless $FS::CurrentUser::CurrentUser->access_right('Edit hardware classes and types');
</%init>
<%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',
#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' },
)
),
- { type => 'columnnext' },
-
- {type=>'justtitle', value=>'Agent (reseller) types' },
+ { type => 'columnnext', value=>'Agent (reseller) types' },
{ field => 'agent_type',
type => 'select-agent_type',
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';
}
}
my $layer_callback = sub {
my $layer = shift;
- my $html = ntable("#cccccc",2);
+ my $html = '<TABLE CLASS="fsinnerbox">';
#$html .= '
# <TR>
<% 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',
});
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;
</%init>
$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');
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 ) {
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 = ();
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+ unless $FS::CurrentUser::CurrentUser->access_right('Edit hardware classes and types');
</%init>
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+ unless $FS::CurrentUser::CurrentUser->access_right('Edit hardware classes and types');
</%init>
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+ unless $FS::CurrentUser::CurrentUser->access_right('Edit hardware classes and types');
</%init>
</TABLE>
</TD>
<TD VALIGN="top" STYLE="padding-left:12px">
- <TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
+ <FONT CLASS="fsinnerbox-title"><% $opt{value} %></FONT><BR>
+ <TABLE CLASS="fsinnerbox">
+<%init>
+
+my %opt = @_;
+
+</%init>
<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=0 id="<%$id%>">
<TR>
<TD VALIGN="top">
- <TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
+ <TABLE CLASS="fsinnerbox">
% 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
$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') {
--- /dev/null
+% if ( $notes ) {
+ <TABLE CLASS="fsinnerbox">
+ <TR><TD>
+ <% $notes %>
+ </TD></TR>
+ </TABLE>
+ <BR>
+% }
+<%init>
+
+my $conf = new FS::Conf;
+
+my $notes = join('<BR>', map encode_entities($_), $conf->config('dashboard-topnotes') );
+
+</%init>
<BR></FONT>
</td>
</tr>
+% foreach my $top_warning ( @top_warnings ) {
+ <TR>
+ <TD COLSPAN=4>
+ <IMG SRC="<% $fsurl %>images/error.png">
+ <FONT COLOR="#FF0000" SIZE="+1"><% $top_warning %></FONT>
+ </TD>
+ </TR>
+% }
</table>
<TABLE WIDTH="100%" CELLSPACING=0 CELLPADDING=0>
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 <a href="mailto:sales\@freeside.biz?subject=Debian OS upgrade">sales\@freeside.biz</a> to schedule an upgrade ASAP.
+END
+}
+
</%init>
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';
$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.' ],
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 %>;
}
</TABLE>
+<BR>
-<TABLE <% $id %> BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
-
-<% include('tr-title.html', @_ ) %>
+<FONT CLASS="fsinnerbox-title" <%$title_id%>><% $opt{value} %></FONT>
+<TABLE <% $id %> CLASS="fsinnerbox">
<%init>
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};
+
</%init>
<TR>
- <TH CLASS="background" COLSPAN=<% $opt{colspan} || 2 %> ALIGN="left" <%$id%>>
- <FONT SIZE="+1"><% $opt{value} %></FONT>
- </TH>
+ <TD CLASS="background" COLSPAN=<% $opt{colspan} || 2 %> ALIGN="left" <%$id%>>
+ <FONT CLASS="fsinnerbox-title"><% $opt{value} %></FONT>
+ </TD>
</TR>
<%init>
--- /dev/null
+<TR>
+ <TD COLSPAN=<% $opt{colspan} || 2 %> ALIGN="left" <%$id%>>
+ <I><% $opt{value} %></I>
+ </TD>
+</TR>
+
+<%init>
+
+my %opt = @_;
+my $id = 'ID="'.$opt{id}.'"' if $opt{id};
+
+</%init>
% if ( scalar(@possible_exports) > 0 || scalar(@mapped_exports) > 0 ) {
<TABLE><TR>
- <TH BGCOLOR="#dcdcdc">Export</TH>
- <TH BGCOLOR="#dcdcdc">Vendor Package Id <FONT SIZE="-2">(blank to delete)</FONT></TH>
+ <TH>Export</TH>
+ <TH>Vendor Package Id <FONT SIZE="-2">(blank to delete)</FONT></TH>
</TR>
% foreach my $export ( @mapped_exports ) {
<TR>
my $thead_count = 0;
sub pkg_svc_thead {
$thead_count += 1;
- return "\n\n". ntable('#cccccc', 2).
+ return "\n\n". '<TABLE CLASS="fsinnerbox">'.
'<TR>'.
- '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Quan.</FONT></TH>'.
- '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Primary</FONT></TH>'.
- '<TH BGCOLOR="#dcdcdc">Service</TH>'.
- '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Hide<BR>from<BR>Invoices</FONT></TH>'.
- '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Bulk<BR>Charge</FONT></TH>'.
- '<TH BGCOLOR="#dcdcdc" ID="th_provision_hold' . $thead_count . '"><FONT SIZE=-1>Remove Hold After Provisioning</FONT></TH>'.
+ '<TH><FONT SIZE=-1>Quan.</FONT></TH>'.
+ '<TH><FONT SIZE=-1>Primary</FONT></TH>'.
+ '<TH>Service</TH>'.
+ '<TH><FONT SIZE=-1>Hide<BR>from<BR>Invoices</FONT></TH>'.
+ '<TH><FONT SIZE=-1>Bulk<BR>Charge</FONT></TH>'.
+ '<TH ID="th_provision_hold' . $thead_count . '"><FONT SIZE=-1>Remove Hold After Provisioning</FONT></TH>'.
'</TR>'.
qq!<SCRIPT>provision_hold_td.push(document.getElementById('th_provision_hold$thead_count'))</SCRIPT>!;
;
'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.
<TD ALIGN="right">Password: </TD>
<TD><INPUT TYPE="password" NAME="credential_1" SIZE="13"></TD>
</TR>
+ <TR>
+ <TD ALIGN="right">One-time code: </TD>
+ <TD><INPUT TYPE="text" NAME="credential_2" SIZE="13"></TD>
+ </TR>
</TABLE>
<BR>
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.',
);
<% $location{address1} |h %> <% $location{address2} |h %><BR>
<% $location{city} |h %>, <% $location{state} |h %> <% $location{zip} |h %><BR>
<BR>
-% 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}";
<A HREF="<%$p%>misc/openmap.html?<% $querystring %>"
- TARGET="_blank">Map service module location</A><BR>
+ REL="opener"
+ TARGET="_blank"
+>Map service location</A><BR>
% $querystring = "census_year=$year&pre=$pre&zip_code=" . $cache->get('zip');
<A HREF="<%$p%>misc/openmap.html?<% $querystring %>"
- TARGET="_blank">Map zip code center</A><BR>
+ REL="opener"
+ TARGET="_blank"
+>Map zip code center</A><BR>
<BR>
<input type="hidden" id="new_tract" name="new_tract" value="<%$new_tract%>">
<TABLE>
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');
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;
});
}
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;
-</%init>
\ No newline at end of file
+</%init>
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";
+}
-</%init>
\ No newline at end of file
+</%init>
<% encode_json($return) %>\
<%init>
-my %arg = $cgi->param('arg');
+my %arg = $cgi->multi_param('arg');
my $custnum = delete($arg{'custnum'});
my $error;
</TABLE>
<BR>
+ <FONT CLASS="fsinnerbox-title"><% emt('Google Authenticator') %></FONT>
+ <TABLE CLASS="fsinnerbox">
+ <TR>
+% if ( $curuser->totp_secret32 ) {
+ <TD><IMG SRC="<% $curuser->totp_qr_code_url %>"</IMG></TD>
+% } else {
+ <TD><A HREF="<%$p%>pref/set_totp_secret32.html">Enable</A></TD>
+% }
+ </TR>
+ </TABLE>
+ <BR>
+
% }
<FONT CLASS="fsinnerbox-title"><% emt("Interface") %></FONT>
--- /dev/null
+<& /elements/header.html, mt('Google Authenticator for [_1]', $FS::CurrentUser::CurrentUser->username) &>
+
+Scan this code with the Google Authenticator application on your phone.
+<BR><BR>
+
+<IMG SRC="<% $access_user->totp_qr_code_url %>"></IMG>
+<BR><BR>
+
+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?
+
+</%init>
<a class="download" href="<% $cgi->self_url %>">Download</a>
</caption>
% 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};
<thead>
my $part_titles = FS::Report::FCC_477->parts;
</%init>
-<%def .header_fbd>
+<%def .header_fbd_old>
<TR CLASS="head">
<TD ROWSPAN=2>Census Block</TD>
<TD ROWSPAN=2>DBA Name</TD>
<TD>Up</TD>
</TR>
</%def>
+<%def .header_fbd>
+ <TR CLASS="head">
+ <TD ROWSPAN=2>Census Block</TD>
+ <TD ROWSPAN=2>DBA Name</TD>
+ <TD ROWSPAN=2>Technology</TD>
+ <TD ROWSPAN=2>Consumer?</TD>
+ <TD COLSPAN=2>Advertised Speed (Mbps)</TD>
+ <TD ROWSPAN=2>Business?</TD>
+ </TR>
+ <TR CLASS="subhead">
+ <TD>Down</TD>
+ <TD>Up</TD>
+ </TR>
+</%def>
<%def .header_fbs>
<TR CLASS="head">
<TD ROWSPAN=2>Census Tract</TD>
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";
|| $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
#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;
<INPUT TYPE="hidden" NAME="magic" VALUE="advanced">
<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
- <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
-
- <TR>
- <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1"><% mt('Search options') |h %></FONT></TH>
- </TR>
+ <FONT CLASS="fsinnerbox-title"><% emt('Search options') %></FONT>
+ <TABLE CLASS="fsinnerbox">
% unless ( $custnum ) {
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',
'label' => 'Services',
&>
- <TR>
- <TH CLASS="background" COLSPAN=2> </TH>
- </TR>
-
- <TR>
- <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1"><% mt('Display options') |h %></FONT></TH>
- </TR>
+ </TABLE>
+ <BR>
+
+ <FONT CLASS="fsinnerbox-title"><% emt('Display options') %></FONT>
+ <TABLE CLASS="fsinnerbox">
% #"package fields" ala advanced svc_acct search?
% #move to /elements/tr-select-cust_pkg-fields and use it from there if so...
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);
'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',
'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',
},
<% include('/elements/header.html', $title ) %>
+%# extensive false laziness with svc_acct
+
<FORM ACTION="svc_broadband.cgi" METHOD="POST">
<INPUT TYPE="hidden" NAME="magic" VALUE="advanced">
<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
-%# extensive false laziness with svc_acct
- <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
- <TR>
- <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Search options</FONT></TH>
- </TR>
+ <FONT CLASS="fsinnerbox-title"><% emt('Search options') %></FONT>
+ <TABLE CLASS="fsinnerbox">
% unless ( $custnum ) {
<% include( '/elements/tr-select-agent.html',
% }
% }
- <TR>
- <TH CLASS="background" COLSPAN=2> </TH>
- </TR>
-
- <TR>
- <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Display options</FONT></TH>
- </TR>
+ </TABLE>
+ <BR>
+
+ <FONT CLASS="fsinnerbox-title"><% emt('Display options') %></FONT>
+ <TABLE CLASS="fsinnerbox">
+
% #move to /elements/tr-select-cust_pkg-fields if anything else needs it...
<TR>
- <TD ALIGN="right">Package fields</TD>
+ <TH ALIGN="right">Package fields</TD>
<TD>
<SELECT NAME="cust_pkg_fields">
<OPTION VALUE="">(none)
actionlabel => 'Order new package',
color => '#333399',
width => 960,
- height => 740,
+ height => 850,
acl => 'Order customer package',
},
{
# acl => [ 'Post payment', ],
## condition => sub { $payby{MCHK} },
#},
+ {
+ label => 'Record manual (non-Freeside) Paypal payment',
+ popup => "edit/cust_pay.cgi?popup=1;payby=PPAL;custnum=$custnum",
+ actionlabel => 'Enter Paypal payment',
+ width => 763,
+ height => 392,
+ acl => [ 'Post Payment', 'Post Paypal payment', ],
+ condition => sub { $payby{PPAL} },
+ },
],
},
--- /dev/null
+<% $content %>\
+<%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($name, $content);
+
+my($query) = $cgi->keywords;
+if ( $query =~ /^(\d+)$/ || $cgi->param('zonenum') =~ /^(\d+$)/ ) {
+ my $zonenum = $1;
+ $name = $zonenum;
+ my $deploy_zone = qsearchs('deploy_zone', { 'zonenum' => $zonenum })
+ or die 'unknown zonenum';
+
+ $content = $deploy_zone->geo_json_feature->to_json;
+
+} elsif ( $cgi->param('zonetype') =~ /^(\w)$/ ) {
+ my $zonetype = $1;
+ $name = $zonetype;
+ my @deploy_zone = qsearch('deploy_zone', { 'zonetype' => $zonetype,
+ 'disabled' => '', });
+
+ my $fc = Geo::JSON::FeatureCollection->new({
+ features => [ map $_->geo_json_feature, @deploy_zone ],
+ });
+
+ $content = $fc->to_json;
+
+} else {
+ die "no zonenum or zonetype\n";
+}
+
+http_header('Content-Type' => 'application/geo+json' );
+http_header('Content-Disposition' => "filename=$name.geojson" );
+http_header('Content-Length' => length($content) );
+http_header('Cache-control' => 'max-age=60' );
+
+</%init>
--- /dev/null
+<% $content %>\
+<%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 $kml = Geo::GoogleEarth::Pluggable->new;
+
+my $name;
+
+my($query) = $cgi->keywords;
+if ( $query =~ /^(\d+)$/ || $cgi->param('zonenum') =~ /^(\d+$)/ ) {
+ my $zonenum = $1;
+ $name = $zonenum;
+ my $deploy_zone = qsearchs('deploy_zone', { 'zonenum' => $zonenum })
+ or die 'unknown zonenum';
+
+ $deploy_zone->kml_polygon($kml);
+
+} elsif ( $cgi->param('zonetype') =~ /^(\w)$/ ) {
+ my $zonetype = $1;
+ $name = $zonetype;
+ my @deploy_zone = qsearch('deploy_zone', { 'zonetype' => $zonetype,
+ 'disabled' => '', });
+
+ $_->kml_polygon($kml) foreach @deploy_zone;
+
+} else {
+ die "no zonenum or zonetype\n";
+}
+
+my $content = $kml->archive;
+
+http_header('Content-Type' => 'application/vnd.google-earth.kmz' ); #kmz
+http_header('Content-Disposition' => "filename=$name.kmz" );
+http_header('Content-Length' => length($content) );
+http_header('Cache-control' => 'max-age=60' );
+
+</%init>
--- /dev/null
+<% $content %>\
+<%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 $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+
+my %shapelib_opts = (
+ Shapetype => Geo::Shapelib::POLYGON,
+ FieldNames => [ 'Tech', 'Down', 'Up' ],
+ FieldTypes => [ 'String:32', 'Double', 'Double' ],
+);
+
+my( $name, $shapefile );
+
+my($query) = $cgi->keywords;
+if ( $query =~ /^(\d+)$/ || $cgi->param('zonenum') =~ /^(\d+$)/ ) {
+ my $zonenum = $1;
+ $name = $zonenum;
+ my $deploy_zone = qsearchs('deploy_zone', { 'zonenum' => $zonenum })
+ or die 'unknown zonenum';
+
+ $shapefile = new Geo::Shapelib {
+ Name => "$dir/$zonenum-$$",
+ %shapelib_opts
+ };
+
+ $deploy_zone->shapefile_add($shapefile);
+
+} elsif ( $cgi->param('zonetype') =~ /^(\w)$/ ) {
+ my $zonetype = $1;
+ $name = $zonetype;
+ my @deploy_zone = qsearch('deploy_zone', { 'zonetype' => $zonetype,
+ 'disabled' => '', });
+
+ $shapefile = new Geo::Shapelib {
+ Name => "$dir/$zonetype-$$",
+ %shapelib_opts
+ };
+
+ $_->shapefile_add($shapefile) foreach @deploy_zone;
+
+} else {
+ die "no zonenum or zonetype\n";
+}
+
+$shapefile->set_bounds;
+
+$shapefile->save;
+
+#slurp up .shp .shx and .dbf files and put them in a zip.. return that
+#and delete the files
+
+my $content = '';
+open(my $fh, '>', \$content);
+
+my $zip = new Archive::Zip;
+$zip->addFile("$dir/$name-$$.$_", "$name.$_") foreach qw( shp shx dbf );
+unless ( $zip->writeToFileHandle($fh) == Archive::Zip::AZ_OK() ) {
+ die "failed to create .shz file\n";
+}
+close $fh;
+
+unlink("$dir/$name-$$.$_") foreach qw( shp shx dbf );
+
+#http_header('Content-Type' => 'x-gis/x-shapefile' );
+http_header('Content-Type' => 'archive/zip' );
+http_header('Content-Disposition' => "filename=$name.shz" );
+http_header('Content-Length' => length($content) );
+http_header('Cache-control' => 'max-age=60' );
+
+</%init>