From 69e481a4a9191b9912d6bb8202627a5dc75f74ce Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Thu, 11 Jan 2018 20:05:34 -0600 Subject: rt# 74031 implement svc_realestate --- FS/FS/Mason.pm | 3 + FS/FS/Schema.pm | 90 +++++++++++++++++++++- FS/FS/h_svc_realestate.pm | 31 ++++++++ FS/FS/realestate_location.pm | 177 +++++++++++++++++++++++++++++++++++++++++++ FS/FS/realestate_unit.pm | 163 +++++++++++++++++++++++++++++++++++++++ FS/FS/svc_Common.pm | 10 ++- FS/FS/svc_realestate.pm | 172 +++++++++++++++++++++++++++++++++++++++++ FS/t/realestate_location.t | 5 ++ FS/t/realestate_unit.t | 5 ++ FS/t/svc_realestate.t | 5 ++ 10 files changed, 658 insertions(+), 3 deletions(-) create mode 100644 FS/FS/h_svc_realestate.pm create mode 100644 FS/FS/realestate_location.pm create mode 100644 FS/FS/realestate_unit.pm create mode 100644 FS/FS/svc_realestate.pm create mode 100644 FS/t/realestate_location.t create mode 100644 FS/t/realestate_unit.t create mode 100644 FS/t/svc_realestate.t (limited to 'FS') diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 7bdb6059e..7f883dec1 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -418,6 +418,9 @@ if ( -e $addl_handler_use_file ) { use FS::part_svc_msgcat; use FS::commission_schedule; use FS::commission_rate; + use FS::realestate_location; + use FS::realestate_unit; + use FS::svc_realestate; use FS::saved_search; use FS::sector_coverage; # Sammath Naur diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 6d7520bd9..f7ac973be 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -495,8 +495,44 @@ sub tables_hashref { my $username_len = 64; #usernamemax config file - # name type nullability length default local + # Return a hashref defining the entire application database schema + # Each key of the hashref contains a structure describing a database table + # + # table_name => { + # columns => [...], + # primary_key => 'column', + # unique => [column,column,...], + # index => [[column],[column,column],...], + # foreign_keys => [{...},{...},...], + # } + # + # + # columns => [ + # + # 'column_name', + # + # 'column_type', + # + # 'NULL' or '', # 'NULL' : Allow null values + # # '' : Disallow null values + # + # 'length', # Column size value. eg: + # # 40 : VARCHAR(40) + # # '10,2' : FLOAT(10,2) + # + # 'default', # Default column value for a new record + # # (Unclear if setting this to '' results in a default + # # value of NULL or empty string?) + # + # '', # local ? + # + # name, type, nullability, length, default, local, + # name, type, nullability, length, default, local, + # ... + # + # ], + # name type nullability length default local return { 'agent' => { @@ -7601,6 +7637,57 @@ sub tables_hashref { 'foreign_keys' => [], }, + 'realestate_unit' => { + 'columns' => [ + 'realestatenum', 'serial', '', '', '', '', + 'realestatelocnum', 'int', '', '', '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'unit_title', 'varchar', '', $char_d, '', '', + 'disabled', 'char', 'NULL', 1, '', '', + ], + 'primary_key' => 'realestatenum', + 'unique' => [ ['unit_title'] ], + 'index' => [ + ['agentnum'], + ['realestatelocnum'], + ['disabled'], + ['unit_title'], + ], + 'foreign_keys' => [ + {columns => ['agentnum'], table => 'agent'}, + {columns => ['realestatelocnum'] => table => 'realestate_location'}, + ], + }, + + realestate_location => { + 'columns' => [ + 'realestatelocnum', 'serial', '', '', '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'location_title', 'varchar', '', $char_d, '', '', + 'address1', 'varchar', 'NULL', $char_d, '', '', + 'address2', 'varchar', 'NULL', $char_d, '', '', + 'city', 'varchar', 'NULL', $char_d, '', '', + 'state', 'varchar', 'NULL', $char_d, '', '', + 'zip', 'char', 'NULL', 5, '', '', + 'disabled', 'char', 'NULL', 1, '', '', + ], + primary_key => 'realestatelocnum', + 'unique' => [ ['location_title'] ], + 'index' => [ ['agentnum'], ['disabled'] ], + 'foreign_keys' => [ + {columns => ['agentnum'], table => 'agent'}, + ], + }, + + svc_realestate => { + columns => [ + 'svcnum', 'serial', '', '', '', '', + 'realestatenum', 'int', 'NULL', '', '', '', + ], + primary_key => 'svcnum', + index => [], + }, + # name type nullability length default local #'new_table' => { @@ -7627,4 +7714,3 @@ L =cut 1; - diff --git a/FS/FS/h_svc_realestate.pm b/FS/FS/h_svc_realestate.pm new file mode 100644 index 000000000..2fdd291d1 --- /dev/null +++ b/FS/FS/h_svc_realestate.pm @@ -0,0 +1,31 @@ +package FS::h_svc_realestate; + +use strict; +use vars qw( @ISA ); +use FS::h_Common; + + +@ISA = qw( FS::h_Common ); + +sub table { 'h_svc_realestate' }; + +=head1 NAME + +FS::h_svc_circuit - Historical realestate service objects + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +An FS::h_svc_realestate object + +=head1 BUGS + +=head1 SEE ALSO + +L, L, L, schema.html from the base +documentation. + +=cut + +1; diff --git a/FS/FS/realestate_location.pm b/FS/FS/realestate_location.pm new file mode 100644 index 000000000..d9cd76a58 --- /dev/null +++ b/FS/FS/realestate_location.pm @@ -0,0 +1,177 @@ +package FS::realestate_location; +use strict; +use warnings; +use Carp qw(croak); + +use base 'FS::Record'; + +use FS::Record qw(qsearchs qsearch); + +=head1 NAME + +FS::realestate_location - Object representing a realestate_location record + +=head1 SYNOPSIS + + use FS::realestate_location; + + $location = new FS::realestate_location \%values; + $location = new FS::realestate_location { + agentnum => 1, + location_title => 'Superdome', + address1 => '1500 Sugar Bowl Dr', + city => 'New Orleans', + state => 'LA', + zip => '70112', + }; + + $error = $location->insert; + $error = $new_loc->replace($location); + $error = $record->check; + + $error = $location->add_unit('Box Seat No. 42'); + @units = $location->units; + +=head1 DESCRIPTION + +An FS::realestate_location object represents a location for one or more +FS::realestate_unit objects. Expected to contain at least one unit, as only +realestate_unit objects are assignable to packages via +L. + +FS::realestate_location inherits from FS::Record. + +The following fields are currently supported: + +=over 4 + +=item realestatelocnum + +=item agentnum + +=item location_title + +=item address1 (optional) + +=item address2 (optional) + +=item city (optional) + +=item state (optional) + +=item zip (optional) + +=item disabled + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF (see L) + +=cut + +sub table {'realestate_location';} + +=item insert (see L) + +=item delete + + FS::realestate_location records should never be deleted, only disabled + +=cut + +sub delete { + # Once this record has been associated with a customer in any way, it + # should not be deleted. todo perhaps, add a is_deletable function that + # checks if the record has ever actually been used, and allows deletion + # if it hasn't. (entered in error, etc). + croak "FS::realestate_location records should never be deleted"; +} + +=item replace OLD_RECORD (see L) + +=item check (see L) + +=item agent + +Returns the associated agent + +=cut + +sub agent { + my $self = shift; + return undef unless $self->agentnum; + return exists $self->{agent} + ? $self->{agent} + : $self->{agent} = qsearchs('agent', {agentnum => $self->agentnum} ); +} + + +=item add_unit UNIT_TITLE + +Create an associated L record + +=cut + +sub add_unit { + my ($self, $unit_title) = @_; + croak "add_unit() requires a \$unit_title parameter" unless $unit_title; + + if ( + qsearchs('realestate_unit',{ + realestatelocnum => $self->realestatelocnum, + unit_title => $unit_title, + }) + ) { + return "Unit Title ($unit_title) has already been used for location (". + $self->location_title.")"; + } + + my $unit = FS::realestate_unit->new({ + realestatelocnum => $self->realestatelocnum, + agentnum => $self->agentnum, + unit_title => $unit_title, + }); + my $err = $unit->insert; + die "Error creating FS::realestate_new record: $err" if $err; + + return; +} + + +=item units + +Returns all units associated with this location + +=cut + +sub units { + my $self = shift; + return qsearch( + 'realestate_unit', + {realestatelocnum => $self->realestatelocnum} + ); +} + + +=head1 SUBROUTINES + +=over 4 + +=cut + + + + +=back + +=head1 SEE ALSO + +L, L, L + +=cut + +1; diff --git a/FS/FS/realestate_unit.pm b/FS/FS/realestate_unit.pm new file mode 100644 index 000000000..d1d1f7fda --- /dev/null +++ b/FS/FS/realestate_unit.pm @@ -0,0 +1,163 @@ +package FS::realestate_unit; +use strict; +use warnings; +use Carp qw(croak); + +use base 'FS::Record'; +use FS::Record qw(qsearch qsearchs); + +=head1 NAME + +FS::realestate_unit - Object representing a realestate_unit record + +=head1 SYNOPSIS + + use FS::realestate_unit; + + $record = new FS:realestate_unit \%values; + $record = new FS::realestate_unit { + realestatelocnum => 42, + agentnum => 1, + unit_title => 'Ste 404', + }; + + $error = $record->insert; + $error = $new_rec->replace($record) + $error = $record->check; + + $location = $record->location; + +=head1 DESCRIPTION + +An FS::realestate_unit object represents an invoicable unit of real estate. +Object may represent a single property, such as a rental house. It may also +represent a group of properties sharing a common address or identifier, such +as a shopping mall, apartment complex, or office building, concert hall. + +A FS::realestate_unit object must be associated with a FS::realestate_location + +FS::realestate_unit inherits from FS::Record. + +The following fields are currently supported: + +=over 4 + +=item realestatenum + +=item realestatelocnum + +=item agentnum + +=item unit_title + +=item disabled + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF (see L) + +=cut + +sub table {'realestate_unit';} + +=item insert (see L) + +=item delete + + FS::realestate_unit records should never be deleted, only disabled + +=cut + +sub delete { + # Once this record has been associated with a customer in any way, it + # should not be deleted. todo perhaps, add a is_deletable function that + # checks if the record has ever actually been used, and allows deletion + # if it hasn't. (entered in error, etc). + croak "FS::realestate_unit records should never be deleted"; +} + + +=item replace OLD_RECORD (see L) + +=item check (see L) + +=item agent + +Returns the associated agent, if any, for this object + +=cut + +sub agent { + my $self = shift; + return undef unless $self->agentnum; + return qsearchs('agent', {agentnum => $self->agentnum} ); +} + +=item location + + Return the associated FS::realestate_location object + +=cut + +sub location { + my $self = shift; + return $self->{location} if exists $self->{location}; + return $self->{location} = qsearchs( + 'realestate_location', + {realestatelocnum => $self->realestatelocnum} + ); +} + +=back + +=item custnum + +Pull the assigned custnum for this unit, if provisioned + +=cut + +sub custnum { + my $self = shift; + return $self->{custnum} + if $self->{custnum}; + + # select cust_pkg.custnum + # from svc_realestate + # LEFT JOIN cust_svc ON svc_realestate.svcnum = cust_svc.svcnum + # LEFT JOIN cust_pkg ON cust_svc.pkgnum = cust_pkg.pkgnum + # WHERE svc_realestate.realestatenum = $realestatenum + + my $row = qsearchs({ + select => 'cust_pkg.custnum', + table => 'svc_realestate', + addl_from => 'LEFT JOIN cust_svc ON svc_realestate.svcnum = cust_svc.svcnum ' + . 'LEFT JOIN cust_pkg ON cust_svc.pkgnum = cust_pkg.pkgnum ', + extra_sql => 'WHERE svc_realestate.realestatenum = '.$self->realestatenum, + }); + + return + unless $row && $row->custnum; + + return $self->{custnum} = $row->custnum; +} + +=head1 SUBROUTINES + +=over 4 + +=cut + + +=back + +=head1 SEE ALSO + +L, L, L + +=cut + +1; diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm index f2456a56f..afd5db64f 100644 --- a/FS/FS/svc_Common.pm +++ b/FS/FS/svc_Common.pm @@ -122,6 +122,15 @@ sub virtual_fields { =item label +Returns a label to identify a record of this service. +Label may be displayed on freeside screens, and within customer bills. + +For example, $obj->label may return: + + - A provisioned phone number for svc_phone + - The mailing list name and e-mail address for svc_mailinglist + - The address of a rental property svc_realestate + svc_Common provides a fallback label subroutine that just returns the svcnum. =cut @@ -1586,4 +1595,3 @@ from the base documentation. =cut 1; - diff --git a/FS/FS/svc_realestate.pm b/FS/FS/svc_realestate.pm new file mode 100644 index 000000000..a7512eef8 --- /dev/null +++ b/FS/FS/svc_realestate.pm @@ -0,0 +1,172 @@ +package FS::svc_realestate; +use base qw(FS::svc_Common); + +use strict; +use warnings; +use vars qw($conf); + +use FS::Record qw(qsearchs qsearch dbh); +use Tie::IxHash; + +$FS::UID::callback{'FS::svc_realestate'} = sub { + $conf = new FS::Conf; +}; + +=head1 NAME + +FS::svc_realestate - Object methods for svc_realestate records + +=head1 SYNOPSIS + + {...} TODO + +=head1 DESCRIPTION + +A FS::svc_realestate object represents a billable real estate trasnaction, +such as renting a home or office. + +FS::svc_realestate inherits from FS::svc_Common. The following fields are +currently supported: + +=over 4 + +=item svcnum - primary key + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Instantiates a new svc_realestate object. + +=cut + +sub table_info { + tie my %fields, 'Tie::IxHash', + svcnum => 'Service', + realestatenum => { + type => 'select-realestate_unit', + label => 'Real estate unit', + }; + + { + name => 'Real estate', + name_plural => 'Real estate services', + longname_plural => 'Real estate services', + display_weight => 100, + cancel_weight => 100, + fields => \%fields, + }; +} + +sub table {'svc_realestate'} + +=item label + +Returns a label formatted as: + + +=cut + +sub label { + my $self = shift; + my $unit = $self->realestate_unit; + my $location = $self->realestate_location; + + return $location->location_title.' '.$unit->unit_title + if $unit && $location; + + return $self->svcnum; # shouldn't happen +} + + +=item realestate_unit + +Returns associated L + +=cut + +sub realestate_unit { + my $self = shift; + + return $self->get('_realestate_unit') + if $self->get('_realestate_unit'); + + return unless $self->realestatenum; + + my $realestate_unit = qsearchs( + 'realestate_unit', + {realestatenum => $self->realestatenum} + ); + + $self->set('_realestate_unit', $realestate_unit); + $realestate_unit; +} + +=item realestate_location + +Returns associated L + +=cut + +sub realestate_location { + my $self = shift; + + my $realestate_unit = $self->realestate_unit; + return unless $realestate_unit; + + $realestate_unit->location; +} + +=item cust_svc + +Returns associated L + +=cut + +sub cust_svc { + qsearchs('cust_svc', { 'svcnum' => $_[0]->svcnum } ); +} + +=item search_sql + +I have an unfounded suspicion this method serves no purpose in this context + +=cut + +# sub search_sql {die "search_sql called on FS::svc_realestate"} + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=item delete + +Delete this record from the database. + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=back 4 + +=head1 BUGS + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; diff --git a/FS/t/realestate_location.t b/FS/t/realestate_location.t new file mode 100644 index 000000000..ecb1d8be9 --- /dev/null +++ b/FS/t/realestate_location.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::realestate_location; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/realestate_unit.t b/FS/t/realestate_unit.t new file mode 100644 index 000000000..bbecc1a4c --- /dev/null +++ b/FS/t/realestate_unit.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::realestate_unit; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/svc_realestate.t b/FS/t/svc_realestate.t new file mode 100644 index 000000000..4145d8b52 --- /dev/null +++ b/FS/t/svc_realestate.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::svc_realestate; +$loaded=1; +print "ok 1\n"; -- cgit v1.2.1 From 75b56ebe174c4703270670a33e1b248cf641f92c Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Fri, 12 Jan 2018 09:14:25 -0500 Subject: RT# 79121 - Added new cdr import format for ani networks, and cron script for same --- FS/FS/cdr/ani_networks.pm | 81 +++++++++++++++++ FS/bin/freeside-cdr-aninetworks-import | 162 +++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 FS/FS/cdr/ani_networks.pm create mode 100755 FS/bin/freeside-cdr-aninetworks-import (limited to 'FS') diff --git a/FS/FS/cdr/ani_networks.pm b/FS/FS/cdr/ani_networks.pm new file mode 100644 index 000000000..cac30c488 --- /dev/null +++ b/FS/FS/cdr/ani_networks.pm @@ -0,0 +1,81 @@ +package FS::cdr::ani_networks; +use base qw( FS::cdr ); + +use strict; +use vars qw( %info ); +use Time::Local; + +%info = ( + 'name' => 'ANI NETWORKS', + 'weight' => 60, + 'type' => 'fixedlength', + 'fixedlength_format' => [qw( + call_date_time:14:1:14 + bill_to_number:15:15:29 + translate_number:10:30:39 + originating_number:10:40:49 + originating_lata:3:50:52 + originating_city:30:53:82 + originating_state:2:83:84 + originating_country:4:85:88 + terminating_number:15:89:103 + terminating_lata:3:104:106 + terminating_city:30:107:136 + terminating_state:2:137:138 + terminating_citycode:3:139:141 + terminating_country:4:142:145 + call_type:2:146:147 + call_transport:1:148:148 + account_code:12:149:160 + info_digits:2:161:162 + duration:8:163:170 + wholesale_amount:9:171:179 + cic:4:180:183 + originating_lrn:10:184:193 + terminating_lrn:10:194:203 + originating_ocn:4:204:207 + terminating_ocn:4:208:211 + )], + 'import_fields' => [ + + sub { #call_date and time + my($cdr, $data, $conf, $param) = @_; + $data =~ /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/ or die "unparsable record_date: $data"; + $cdr->set('calldate', "$2/$3/$1 $4:$5:$6"); + }, + + 'charged_party', #bill to number + '', #translate number + + 'src', #originating number + + '', #originating lata + '', #originating city + '', #originating state + '', #originating country + + 'dst', #terminating number + + '', #terminating lata + '', #terminating city + '', #terminating state + '', #terminating city code + '', #terminating country + + '', #call type + '', #call transport + 'accountcode', #account code + '', #info digits + 'duration', #duration + '', #wholesale amount + '', #cic + 'src_lrn', #originating lrn + 'dst_lrn', #terminating lrn + '', #originating ocn + '', #terminating ocn + + ], + +); + +1; \ No newline at end of file diff --git a/FS/bin/freeside-cdr-aninetworks-import b/FS/bin/freeside-cdr-aninetworks-import new file mode 100755 index 000000000..0fd0659ec --- /dev/null +++ b/FS/bin/freeside-cdr-aninetworks-import @@ -0,0 +1,162 @@ +#!/usr/bin/perl + +use strict; +use Getopt::Std; +use Date::Format; +use File::Temp 'tempdir'; +use Net::SFTP::Foreign::Compat; +use FS::UID qw(adminsuidsetup datasrc dbh); +use FS::cdr; +use FS::cdr_batch; +use FS::Record qw(qsearch qsearchs); +use Date::Format 'time2str'; +use Date::Parse 'str2time'; + + +### +# parse command line +### + +use vars qw( $opt_d $opt_v $opt_c $opt_s $opt_e $opt_a ); +getopts('dvc:s:e:a'); + +my ($user, $login, $password) = @ARGV; +($user and $login and $password) or die &usage; + +my $dbh = adminsuidsetup $user; +$FS::UID::AutoCommit = 0; + +# index already-downloaded batches +my @previous = qsearch({ + 'table' => 'cdr_batch', + 'hashref' => { 'cdrbatch' => {op=>'like', value=>'ani_networks%'} }, + 'order_by' => 'ORDER BY cdrbatch DESC', +}); +my %exists = map {$_->cdrbatch => 1} @previous; + +my $format = 'ani_networks'; +my $host = 'arkftp.aninetworks.com'; + +### +# get the file list +### + +warn "Retrieving directory listing\n" if $opt_v; + +my $sftp = sftp(); + +## get the current working dir +my $cwd = $sftp->cwd; + +## switch to CDR dir +$sftp->setcwd($cwd . '/CDR') or die "can't chdir to $cwd/CDR\n"; + +my $ls = $sftp->ls('.', wanted => qr/^UYM.*.zip$/i, names_only =>1 ); +my @files = @$ls; + +warn scalar(@files)." CDR files found.\n" if $opt_v; +# apply date range +if ( $opt_a ) { + my $most_recent = $previous[0]; + if ($most_recent) { + if ($most_recent->cdrbatch =~ /^ani_networks-(\d+)/) { + my $date = $1; + warn "limiting to dates > $date (from most recent batch)\n" if $opt_v; + @files = grep { /^*Daily_(\d+)_/ && $1 > $date } @files; + } + } # else download them all +} +if ( $opt_s ) { + # start date + # normalize date format + $opt_s = time2str('%Y%m%d', str2time($opt_s)) if $opt_s =~ /\D/; + warn "limiting to dates > $opt_s\n" if $opt_v; + @files= grep { /^*Daily_(\d+)_/ && $1 >= $opt_s } @files; +} +if ( $opt_e ) { + # end date + $opt_e = time2str('%Y%m%d', str2time($opt_e)) if $opt_e =~ /\D/; + warn "limiting to dates < $opt_e\n" if $opt_v; + @files= grep { /^*Daily_(\d+)_/ && $1 < $opt_e } @files; +} +warn scalar(@files) ." files to be downloaded\n" if $opt_v; +foreach my $file (@files) { + + my $tmpdir = tempdir( CLEANUP => $opt_v ); + + warn "downloading $file to $tmpdir\n" if $opt_v; + $sftp = sftp(); + $sftp->get($file, "$tmpdir/$file"); + + ## extract zip file + if(system ("unzip $tmpdir/$file -d $tmpdir") != 0) { + unlink "$tmpdir/$file"; + my $error = "unzip of '$tmpdir/$file' failed\n"; + if ( $opt_s ) { + warn $error; + next; + } else { + die $error; + } + } + + warn "processing $file\n" if $opt_v; + + my $batchname = "$format-$file"; + if ($exists{$batchname}) { + warn "already imported $file\n"; + next; + } + + my $unzipped_file = $file; + $unzipped_file =~ s/.zip/.txt/i; + + warn "going to import file $unzipped_file" if $opt_v; + + my $import_options = { + 'file' => "$tmpdir/$unzipped_file", + 'format' => $format, + 'batch_namevalue' => $batchname, + 'empty_ok' => 1, + }; + $import_options->{'cdrtypenum'} = $opt_c if $opt_c; + + my $error = FS::cdr::batch_import($import_options); + + if ( $error ) { + die "error processing $unzipped_file: $error\n"; + } +} +warn "finished\n" if $opt_v; +$dbh->commit; + +### +# subs +### + +sub usage { + "Usage: \n freeside-cdr-aninetworks-import [ options ] user login password + Options: + -v: be verbose + -d: enable FTP debugging (very noisy) + -c num: apply a cdrtypenum to the imported CDRs + -s date: start date + -e date: end date + -a: automatically choose start date from most recently downloaded batch + +"; +} + +sub sftp { + + #reuse connections + return $sftp if $sftp && $sftp->cwd; + + my %sftp = ( host => $host, user => $login, password => $password ); + + $sftp = Net::SFTP::Foreign->new(%sftp); + $sftp->error and die "SFTP connection failed: ". $sftp->error; + + $sftp; +} + -- cgit v1.2.1 From 3229b75a85ab506a718ec684c3e0ca26c496c166 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Fri, 12 Jan 2018 17:42:02 -0500 Subject: RT# 79121 - added flag to store public key in known host file --- FS/bin/freeside-cdr-aninetworks-import | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/bin/freeside-cdr-aninetworks-import b/FS/bin/freeside-cdr-aninetworks-import index 0fd0659ec..1e6217648 100755 --- a/FS/bin/freeside-cdr-aninetworks-import +++ b/FS/bin/freeside-cdr-aninetworks-import @@ -152,7 +152,11 @@ sub sftp { #reuse connections return $sftp if $sftp && $sftp->cwd; - my %sftp = ( host => $host, user => $login, password => $password ); + my %sftp = ( host => $host, + user => $login, + password => $password, + more => [-o => 'StrictHostKeyChecking no'], + ); $sftp = Net::SFTP::Foreign->new(%sftp); $sftp->error and die "SFTP connection failed: ". $sftp->error; -- cgit v1.2.1 From 7668262421ea253ffaf95e0233e037f2857f7071 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 16 Jan 2018 12:14:54 -0500 Subject: RT# 39340 - Created minimal selfservice that only allows payments to be made, authorization is based on ip and mac address. This is not done yet need to write routine to get mac address from radius server based on ip address. --- FS/FS/ClientAPI/MyAccount.pm | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index ce887efcd..c00a13f2b 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -237,6 +237,13 @@ sub login { $svc_x = $svc_phone; + } elsif ( $p->{'domain'} eq 'ip_mac' ) { + + my $svc_broadband = qsearchs( 'svc_broadband', { 'mac_addr' => $p->{'username'} } ); + return { error => 'IP address not found' } + unless $svc_broadband; + $svc_x = $svc_broadband; + } elsif ( $p->{email} && (my $contact = FS::contact->by_selfservice_email($p->{email})) ) -- cgit v1.2.1 From ea963c68bda696def555b86a3b5a7410386ab92d Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Fri, 19 Jan 2018 15:52:53 -0500 Subject: RT# 76398 - Added auto status field or status table creation --- FS/FS/cdr/Import.pm | 142 ++++++++++++++++++++++++++++------------- FS/bin/freeside-cdr-freeswitch | 11 ++-- 2 files changed, 103 insertions(+), 50 deletions(-) (limited to 'FS') diff --git a/FS/FS/cdr/Import.pm b/FS/FS/cdr/Import.pm index f32a7bd85..aa5f5744f 100644 --- a/FS/FS/cdr/Import.pm +++ b/FS/FS/cdr/Import.pm @@ -19,7 +19,7 @@ FS::cdr::Import - CDR importing use FS::cdr::Import; FS::cdr::Import->dbi_import( - 'dbd' => 'mysql', #Pg, Sybase, etc. + 'dbd' => 'Pg', #mysql, Sybase, etc. 'table' => 'TABLE_NAME', 'primary_key' => 'BILLING_ID', 'status_table' = > 'STATUS_TABLE_NAME', # if using a table rather than field in main table @@ -46,61 +46,64 @@ sub dbi_import { my %opt; #opt is specified for each install / run of the script getopts('H:U:P:D:T:c:L:S:', \%opt); - my $user = shift(@ARGV) or die $class->cli_usage; - - $opt{D} ||= $args{database}; - - #do we want to add more types? or add as we go? - my %dbi_connect_types = { - 'Sybase' => ':server', - 'Pg' => ':host', - }; - - my $dsn = 'dbi:'. $args{dbd}; - my $dbi_connect_type = $dbi_connect_types{$args{'dbd'}} ? $dbi_connect_types{$args{'dbd'}} : ':host'; - $dsn .= $dbi_connect_type . "=$opt{H}"; - $dsn .= ";database=$opt{D}" if $opt{D}; + my $user = shift(@ARGV) or die $class->cli_usage; + my $database = $opt{D} || $args{database}; + my $table = $opt{T} || $args{table}; + my $pkey = $args{primary_key}; + my $pkey_info = $args{primary_key_info} ? $args{primary_key_info} : 'BIGINT'; + my $status_table = $opt{S} || $args{status_table}; + my $dbd_type = $args{'dbd'} ? $args{'dbd'} : 'Pg'; + my $status_column = $args{status_column} ? $args{status_column} : 'freesidestatus'; + my $status_column_info = $args{status_column_info} ? $args{status_column} : 'VARCHAR(32)'; + + my $queries = get_queries({ + 'dbd' => $dbd_type, + 'table' => $table, + 'status_column' => $status_column, + 'status_column_info' => $status_column_info, + 'status_table' => $status_table, + 'primary_key' => $pkey, + 'primary_key_info' => $pkey_info, + }); + + my $dsn = 'dbi:'. $dbd_type; + $dsn .= $queries->{connect_type} . "=$opt{H}"; + $dsn .= ";database=$database" if $database; my $dbi = DBI->connect($dsn, $opt{U}, $opt{P}) or die $DBI::errstr; adminsuidsetup $user; - #my $fsdbh = FS::UID::dbh; - - my $table = $opt{T} || $args{table}; - my $pkey = $args{primary_key}; - my $status_table = $opt{S} || $args{status_table}; - - #just doing this manually with IVR MSSQL databases for now - # # check for existence of freesidestatus - # my $status = $dbi->selectall_arrayref("SHOW COLUMNS FROM $table WHERE Field = 'freesidestatus'"); - # if( ! @$status ) { - # print "Adding freesidestatus column...\n"; - # $dbi->do("ALTER TABLE $table ADD COLUMN freesidestatus varchar(32)") - # or die $dbi->errstr; - # } - # else { - # print "freesidestatus column present\n"; - # } - # or if using a status_table: - # CREATE TABLE FREESIDE_BILLING ( - # BILLING_ID BIGINT, - # FREESIDESTATUS VARCHAR(32) - # ) + ## check for status table if using. if not there create it. + if ($status_table) { + my $status = $dbi->selectall_arrayref( $queries->{check_statustable} ); + if( ! @$status ) { + print "Adding status table $status_table ...\n"; + $dbi->do( $queries->{create_statustable} ) + or die $dbi->errstr; + } + } + ## check for column freeside status if not using status table and create it if not there. + else { + my $status = $dbi->selectall_arrayref( $queries->{check_statuscolumn} ); + if( ! @$status ) { + print "Adding $status_column column...\n"; + $dbi->do( $queries->{create_statuscolumn} ) + or die $dbi->errstr; + } + } #my @cols = values %{ $args{column_map} }; my $sql = "SELECT $table.* FROM $table "; # join(',', @cols). " FROM $table ". - $sql .= 'LEFT JOIN '. $status_table. - " ON ( $table.$pkey = ". $status_table. ".$pkey )" + $sql .= "LEFT JOIN $status_table ON ( $table.$pkey = $status_table.$pkey ) " if $status_table; - $sql .= ' WHERE freesidestatus IS NULL '; + $sql .= "WHERE $status_column IS NULL "; #$sql .= ' LIMIT '. $opt{L} if $opt{L}; my $sth = $dbi->prepare($sql); $sth->execute or die $sth->errstr. " executing $sql"; - #MySQL-specific print "Importing ".$sth->rows." records...\n"; my $cdr_batch = new FS::cdr_batch({ 'cdrbatch' => $args{batch_name} . '-import-'. time2str('%Y/%m/%d-%T',time), @@ -123,6 +126,7 @@ sub dbi_import { } $hash{$field} = '' if $hash{$field} =~ /^\s+$/; #IVR (MSSQL?) bs } + my $cdr = FS::cdr->new(\%hash); $cdr->cdrtypenum($opt{c}) if $opt{c}; @@ -145,12 +149,12 @@ sub dbi_import { if ( $status_table ) { $st_sql = - 'INSERT INTO '. $status_table. " ( $pkey, freesidestatus ) ". + 'INSERT INTO '. $status_table. " ( $pkey, $status_column ) ". " VALUES ( ?, 'done' )"; } else { - $st_sql = "UPDATE $table SET freesidestatus = 'done' WHERE $pkey = ?"; + $st_sql = "UPDATE $table SET $status_column = 'done' WHERE $pkey = ?"; } @@ -179,10 +183,58 @@ sub cli_usage { "Usage: \n $0\n\t-H hostname\n\t[ -D database ]\n\t-U user\n\t-P password\n\t[ -c cdrtypenum ]\n\t[ -L num_cdrs_limit ]\n\t[ -T table ]\n\t[ -S status table ]\n\tfreesideuser\n"; } +sub get_queries { + #my ($dbd, $table, $column, $column_create_info, $status_table, $primary_key, $primary_key_info) = @_; + my $info = shift; + + #do we want to add more types? or add as we go? + my %dbi_connect_types = ( + 'Sybase' => ':server', + 'Pg' => ':host', + ); + + #Check for freeside status table Sybase has not been tested + my %dbi_check_statustable = ( + 'Sybase' => "SELECT systables.name FROM sysobjects + JOIN systables ON sysobjects.id = systables.id + WHERE sysobjects.name LIKE '$info->{table}' AND systables.name = $info->{status_table}", + 'Pg' => "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '$info->{status_table}' AND column_name = '$info->{status_column}'", + ); + + #Check for freeside status table Sybase has not been tested + my %dbi_create_statustable = ( + 'Sybase' => "CREATE TABLE $info->{status_table} ( $info->{primary_key} $info->{primary_key_info}, $info->{status_column} $info->{status_column_info} )", + 'Pg' => "CREATE TABLE $info->{status_table} ( $info->{primary_key} $info->{primary_key_info}, $info->{status_column} $info->{status_column_info} )", + ); + + #Check for freeside status column Sybase has not been tested + my %dbi_check_statuscolumn = ( + 'Sybase' => "SELECT syscolumns.name FROM sysobjects + JOIN syscolumns ON sysobjects.id = syscolumns.id + WHERE sysobjects.name LIKE '$info->{table}' AND syscolumns.name = $info->{status_column}", + 'Pg' => "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '$info->{table}' AND column_name = '$info->{status_column}' ", + ); + + #Check for freeside status column Sybase has not been tested + my %dbi_create_statuscolumn = ( + 'Sybase' => "ALTER TABLE $info->{table} ADD COLUMN $info->{status_column} $info->{status_column_info}", + 'Pg' => "ALTER TABLE $info->{table} ADD COLUMN $info->{status_column} $info->{status_column_info}", + ); + + my $queries = { + 'connect_type' => $dbi_connect_types{$info->{dbd}}, + 'check_statustable' => $dbi_check_statustable{$info->{dbd}}, + 'create_statustable' => $dbi_create_statustable{$info->{dbd}}, + 'check_statuscolumn' => $dbi_check_statuscolumn{$info->{dbd}}, + 'create_statuscolumn' => $dbi_create_statuscolumn{$info->{dbd}}, + }; + + return $queries; +} + =head1 BUGS -Not everything has been refactored out of the various bin/cdr-*.import scripts, -let alone other places. +This has only been test with Pg -> postgresql databases Sparse documentation. diff --git a/FS/bin/freeside-cdr-freeswitch b/FS/bin/freeside-cdr-freeswitch index 7f09578d4..51f360ca7 100644 --- a/FS/bin/freeside-cdr-freeswitch +++ b/FS/bin/freeside-cdr-freeswitch @@ -5,11 +5,12 @@ use Date::Parse 'str2time'; use FS::cdr::Import; FS::cdr::Import->dbi_import( - 'dbd' => 'Pg', - 'database' => 'fusionpbx', - 'table' => 'v_xml_cdr', - 'primary_key' => 'uuid', - 'column_map' => { #freeside => fusionpbx + 'dbd' => 'Pg', + 'database' => 'fusionpbx', + 'table' => 'v_xml_cdr', + 'status_column' => 'freesidestatus', + 'primary_key' => 'uuid',:q + 'column_map' => { #freeside => fusionpbx #'cdrid' => 'uuid', #Primary key #'' => 'CALL_SESSION_ID', # Call Session Id (unique per call session – GUID) 'uniqueid' => 'uuid', # -- cgit v1.2.1 From c7caf18eb75426b50715e3bf02c2a2f92e170b83 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 23 Jan 2018 14:05:02 -0500 Subject: RT# 76398 - fixed sybase query for detecting tables and adding columns --- FS/FS/cdr/Import.pm | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'FS') diff --git a/FS/FS/cdr/Import.pm b/FS/FS/cdr/Import.pm index aa5f5744f..c0c192f40 100644 --- a/FS/FS/cdr/Import.pm +++ b/FS/FS/cdr/Import.pm @@ -195,9 +195,7 @@ sub get_queries { #Check for freeside status table Sybase has not been tested my %dbi_check_statustable = ( - 'Sybase' => "SELECT systables.name FROM sysobjects - JOIN systables ON sysobjects.id = systables.id - WHERE sysobjects.name LIKE '$info->{table}' AND systables.name = $info->{status_table}", + 'Sybase' => "SELECT * FROM sysobjects WHERE name = $info->{status_table}", 'Pg' => "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '$info->{status_table}' AND column_name = '$info->{status_column}'", ); @@ -217,7 +215,7 @@ sub get_queries { #Check for freeside status column Sybase has not been tested my %dbi_create_statuscolumn = ( - 'Sybase' => "ALTER TABLE $info->{table} ADD COLUMN $info->{status_column} $info->{status_column_info}", + 'Sybase' => "ALTER TABLE $info->{table} ADD $info->{status_column} $info->{status_column_info}", 'Pg' => "ALTER TABLE $info->{table} ADD COLUMN $info->{status_column} $info->{status_column_info}", ); -- cgit v1.2.1 From ca5b77df888ac14882c290f10f625654e39d9072 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Wed, 24 Jan 2018 14:45:43 -0500 Subject: RT# 76398 - Fixed sybase connection and cleaned up code --- FS/FS/cdr/Import.pm | 54 +++++++++++++++++++++++------------------- FS/bin/freeside-cdr-freeswitch | 2 +- 2 files changed, 31 insertions(+), 25 deletions(-) (limited to 'FS') diff --git a/FS/FS/cdr/Import.pm b/FS/FS/cdr/Import.pm index c0c192f40..f2263c552 100644 --- a/FS/FS/cdr/Import.pm +++ b/FS/FS/cdr/Import.pm @@ -19,15 +19,19 @@ FS::cdr::Import - CDR importing use FS::cdr::Import; FS::cdr::Import->dbi_import( - 'dbd' => 'Pg', #mysql, Sybase, etc. - 'table' => 'TABLE_NAME', - 'primary_key' => 'BILLING_ID', - 'status_table' = > 'STATUS_TABLE_NAME', # if using a table rather than field in main table - 'column_map' => { #freeside => remote_db - 'freeside_column' => 'remote_db_column', - 'freeside_column' => sub { my $row = shift; $row->{remote_db_column}; }, + 'dbd' => 'Pg', #mysql, Sybase, etc. + 'database' => 'DATABASE_NAME', + 'table' => 'TABLE_NAME',, + 'status_table' => 'STATUS_TABLE_NAME', # if using a table rather than field in main table + 'primary_key' => 'BILLING_ID', + 'primary_key_info' => 'BIGINT', # defaults to bigint + 'status_column' => 'STATUS_COLUMN_NAME', # defaults to freesidestatus + 'status_column_info' => 'varchar(32)', # defaults to varchar(32) + 'column_map' => { #freeside => remote_db + 'freeside_column' => 'remote_db_column', + 'freeside_column' => sub { my $row = shift; $row->{remote_db_column}; }, }, - 'batch_name' => 'batch_name', # cdr_batch name -import-date gets appended. + 'batch_name' => 'batch_name', # cdr_batch name -import-date gets appended. ); =head1 DESCRIPTION @@ -59,6 +63,7 @@ sub dbi_import { my $queries = get_queries({ 'dbd' => $dbd_type, + 'host' => $opt{H}, 'table' => $table, 'status_column' => $status_column, 'status_column_info' => $status_column_info, @@ -67,8 +72,7 @@ sub dbi_import { 'primary_key_info' => $pkey_info, }); - my $dsn = 'dbi:'. $dbd_type; - $dsn .= $queries->{connect_type} . "=$opt{H}"; + my $dsn = 'dbi:'. $dbd_type . $queries->{connect_type}; $dsn .= ";database=$database" if $database; my $dbi = DBI->connect($dsn, $opt{U}, $opt{P}) @@ -178,44 +182,46 @@ sub dbi_import { } sub cli_usage { - #"Usage: \n $0\n\t[ -H hostname ]\n\t-D database\n\t-U user\n\t-P password\n\tfreesideuser\n"; - #"Usage: \n $0\n\t-H hostname\n\t-D database\n\t-U user\n\t-P password\n\t[ -c cdrtypenum ]\n\tfreesideuser\n"; "Usage: \n $0\n\t-H hostname\n\t[ -D database ]\n\t-U user\n\t-P password\n\t[ -c cdrtypenum ]\n\t[ -L num_cdrs_limit ]\n\t[ -T table ]\n\t[ -S status table ]\n\tfreesideuser\n"; } sub get_queries { - #my ($dbd, $table, $column, $column_create_info, $status_table, $primary_key, $primary_key_info) = @_; + #my ($dbd, $host, $table, $column, $column_create_info, $status_table, $primary_key, $primary_key_info) = @_; my $info = shift; - #do we want to add more types? or add as we go? + #get host and port information. + my ($host, $port) = split /:/, $info->{host}; + $host ||= 'localhost'; + $port ||= '5000'; # check for pg default 5000 is sybase. + my %dbi_connect_types = ( - 'Sybase' => ':server', - 'Pg' => ':host', + 'Sybase' => ':host='.$host.';port='.$port, + 'Pg' => ':host='.$info->{host}, ); - #Check for freeside status table Sybase has not been tested + #Check for freeside status table my %dbi_check_statustable = ( - 'Sybase' => "SELECT * FROM sysobjects WHERE name = $info->{status_table}", + 'Sybase' => "SELECT * FROM sysobjects WHERE name = '$info->{status_table}'", 'Pg' => "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '$info->{status_table}' AND column_name = '$info->{status_column}'", ); - #Check for freeside status table Sybase has not been tested + #Create freeside status table my %dbi_create_statustable = ( 'Sybase' => "CREATE TABLE $info->{status_table} ( $info->{primary_key} $info->{primary_key_info}, $info->{status_column} $info->{status_column_info} )", 'Pg' => "CREATE TABLE $info->{status_table} ( $info->{primary_key} $info->{primary_key_info}, $info->{status_column} $info->{status_column_info} )", ); - #Check for freeside status column Sybase has not been tested + #Check for freeside status column my %dbi_check_statuscolumn = ( 'Sybase' => "SELECT syscolumns.name FROM sysobjects JOIN syscolumns ON sysobjects.id = syscolumns.id - WHERE sysobjects.name LIKE '$info->{table}' AND syscolumns.name = $info->{status_column}", + WHERE sysobjects.name LIKE '$info->{table}' AND syscolumns.name = '$info->{status_column}'", 'Pg' => "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '$info->{table}' AND column_name = '$info->{status_column}' ", ); - #Check for freeside status column Sybase has not been tested + #Create freeside status column my %dbi_create_statuscolumn = ( - 'Sybase' => "ALTER TABLE $info->{table} ADD $info->{status_column} $info->{status_column_info}", + 'Sybase' => "ALTER TABLE $info->{table} ADD $info->{status_column} $info->{status_column_info} NULL", 'Pg' => "ALTER TABLE $info->{table} ADD COLUMN $info->{status_column} $info->{status_column_info}", ); @@ -232,7 +238,7 @@ sub get_queries { =head1 BUGS -This has only been test with Pg -> postgresql databases +currently works with Pg(Postgresql) and Sybase(Sybase AES) Sparse documentation. diff --git a/FS/bin/freeside-cdr-freeswitch b/FS/bin/freeside-cdr-freeswitch index 51f360ca7..3c18ef2a6 100644 --- a/FS/bin/freeside-cdr-freeswitch +++ b/FS/bin/freeside-cdr-freeswitch @@ -9,7 +9,7 @@ FS::cdr::Import->dbi_import( 'database' => 'fusionpbx', 'table' => 'v_xml_cdr', 'status_column' => 'freesidestatus', - 'primary_key' => 'uuid',:q + 'primary_key' => 'uuid', 'column_map' => { #freeside => fusionpbx #'cdrid' => 'uuid', #Primary key #'' => 'CALL_SESSION_ID', # Call Session Id (unique per call session – GUID) -- cgit v1.2.1 From 7b5f402ed6fa6e9b82d724b7a14b341be819e5be Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Thu, 25 Jan 2018 12:06:08 -0500 Subject: RT#79121 - added POD documentation --- FS/bin/freeside-cdr-aninetworks-import | 66 +++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) (limited to 'FS') diff --git a/FS/bin/freeside-cdr-aninetworks-import b/FS/bin/freeside-cdr-aninetworks-import index 1e6217648..a8fc0cbfd 100755 --- a/FS/bin/freeside-cdr-aninetworks-import +++ b/FS/bin/freeside-cdr-aninetworks-import @@ -55,30 +55,34 @@ my $ls = $sftp->ls('.', wanted => qr/^UYM.*.zip$/i, names_only =>1 ); my @files = @$ls; warn scalar(@files)." CDR files found.\n" if $opt_v; -# apply date range +# apply date range from last downloaded batch. if ( $opt_a ) { my $most_recent = $previous[0]; if ($most_recent) { - if ($most_recent->cdrbatch =~ /^ani_networks-(\d+)/) { + if ($most_recent->cdrbatch =~ /^*Daily_(\d+)_/) { my $date = $1; warn "limiting to dates > $date (from most recent batch)\n" if $opt_v; @files = grep { /^*Daily_(\d+)_/ && $1 > $date } @files; } - } # else download them all + } } + +# apply a start date if given if ( $opt_s ) { - # start date # normalize date format $opt_s = time2str('%Y%m%d', str2time($opt_s)) if $opt_s =~ /\D/; warn "limiting to dates > $opt_s\n" if $opt_v; @files= grep { /^*Daily_(\d+)_/ && $1 >= $opt_s } @files; } + +# apply a end date if given if ( $opt_e ) { - # end date + # normalize date format $opt_e = time2str('%Y%m%d', str2time($opt_e)) if $opt_e =~ /\D/; warn "limiting to dates < $opt_e\n" if $opt_v; @files= grep { /^*Daily_(\d+)_/ && $1 < $opt_e } @files; } + warn scalar(@files) ." files to be downloaded\n" if $opt_v; foreach my $file (@files) { @@ -164,3 +168,55 @@ sub sftp { $sftp; } +=head1 NAME + +freeside-cdr-aninetworks-import - Download CDR files from a remote server via SFTP + +=head1 SYNOPSIS + + freeside-cdr-aninetworks-import [ -v ] [ -d ] [ -a ] [ -c cdrtypenum ] + [ -s startdate ] [ -e enddate ] user sftpuser sftppassword + +=head1 DESCRIPTION + +Command line tool to download CDR files from a remote server via SFTP +and then import them into the database. + +-v: be verbose + +-d: enable sftp debugging (very noisy) + +-a: automatically choose start date from most recently downloaded batch + +-c: cdrtypenum to set, defaults to none + +-s: if specified, sets a startdate. startdate starts at 00:00:00 + +-e: if specified, sets a enddate. enddate starts at 00:00:00 so if you wish to include enddate must add one more day. + +user: freeside username + +sftpuser: sftp user for arkftp.aninetworks.com + +sftppassword: password for sftp user + +=head1 EXAMPLES + +freeside-cdr-aninetworks-import -a +will get all cdr files starting from the day of the last day processed. + +freeside-cdr-aninetworks-import -s 20180120 -e 20180121 +will get all cdr files from 01/20/2018 + +freeside-cdr-aninetworks-import -v -s $(date --date="-1 day" +\%Y\%m\%d) -e $(date +\%Y\%m\%d) +will get all cdr files from yesterday + +=head1 BUGS + +=head1 SEE ALSO + +L + +=cut + +1; \ No newline at end of file -- cgit v1.2.1 From 72c6f125423327ed4e89170d484bc5e4a5760bf2 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Thu, 25 Jan 2018 12:27:36 -0500 Subject: RT# 79121 - fixed error with loading files with option a --- FS/bin/freeside-cdr-aninetworks-import | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/bin/freeside-cdr-aninetworks-import b/FS/bin/freeside-cdr-aninetworks-import index a8fc0cbfd..b5fc226a4 100755 --- a/FS/bin/freeside-cdr-aninetworks-import +++ b/FS/bin/freeside-cdr-aninetworks-import @@ -61,8 +61,8 @@ if ( $opt_a ) { if ($most_recent) { if ($most_recent->cdrbatch =~ /^*Daily_(\d+)_/) { my $date = $1; - warn "limiting to dates > $date (from most recent batch)\n" if $opt_v; - @files = grep { /^*Daily_(\d+)_/ && $1 > $date } @files; + warn "limiting to dates >= $date (from most recent batch)\n" if $opt_v; + @files = grep { /^*Daily_(\d+)_/ && $1 >= $date } @files; } } } -- cgit v1.2.1 From f975ff916152bf85b76e5c7bea48f7fdc8af1cc2 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Fri, 26 Jan 2018 16:24:28 -0500 Subject: RT# 75095 - updated mapping of ooma import file format --- FS/FS/cust_main/Import_Charges.pm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/cust_main/Import_Charges.pm b/FS/FS/cust_main/Import_Charges.pm index 0a12c8752..f90e0496f 100644 --- a/FS/FS/cust_main/Import_Charges.pm +++ b/FS/FS/cust_main/Import_Charges.pm @@ -68,7 +68,9 @@ sub batch_charge { if ( $format eq 'simple' ) { @fields = qw( custnum agent_custid amount pkg ); } elsif ( $format eq 'ooma' ) { - @fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'userfield9', 'userfield10', 'amount', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'userfield17', 'userfield18', 'pkg', 'userfield20', 'custnum', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); + #@fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'userfield9', 'userfield10', 'amount', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'userfield17', 'userfield18', 'pkg', 'userfield20', 'custnum', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); + @fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'amount', 'userfield10', 'userfield11', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'pkg', 'userfield18', 'custnum', 'userfield20', 'userfield21', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); + ##should charges to charge be a config option? %charges = ( 'DISABILITY ACCESS/ENHANCED 911 SERVICES SURCHARGE' => '1', -- cgit v1.2.1 From e517b3733dd3d19a7af586c9f4b8cf9982a8b3d2 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sat, 27 Jan 2018 21:21:59 -0600 Subject: RT# 73421 Add allow messages flag message_dest to contact_email --- FS/FS/Schema.pm | 3 ++- FS/FS/contact.pm | 7 +++---- FS/FS/cust_contact.pm | 7 ++++++- 3 files changed, 11 insertions(+), 6 deletions(-) (limited to 'FS') diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index f7ac973be..65eadad21 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -1793,7 +1793,8 @@ sub tables_hashref { 'classnum', 'int', 'NULL', '', '', '', 'comment', 'varchar', 'NULL', 255, '', '', 'selfservice_access', 'char', 'NULL', 1, '', '', - 'invoice_dest', 'char', 'NULL', 1, '', '', + 'invoice_dest', 'char', 'NULL', 1, '', '', # Y or NULL + 'message_dest', 'char', 'NULL', 1, '', '', # Y or NULL ], 'primary_key' => 'custcontactnum', 'unique' => [ [ 'custnum', 'contactnum' ], ], diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm index 44c538806..fa047f59d 100644 --- a/FS/FS/contact.pm +++ b/FS/FS/contact.pm @@ -155,7 +155,7 @@ sub insert { $self->custnum(''); my %link_hash = (); - for (qw( classnum comment selfservice_access invoice_dest )) { + for (qw( classnum comment selfservice_access invoice_dest message_dest)) { $link_hash{$_} = $self->get($_); $self->$_(''); } @@ -430,7 +430,7 @@ sub replace { $self->custnum(''); my %link_hash = (); - for (qw( classnum comment selfservice_access invoice_dest )) { + for (qw( classnum comment selfservice_access invoice_dest message_dest )) { $link_hash{$_} = $self->get($_); $self->$_(''); } @@ -955,7 +955,7 @@ sub cgi_contact_fields { my @contact_fields = qw( classnum first last title comment emailaddress selfservice_access - invoice_dest password + invoice_dest message_dest password ); push @contact_fields, 'phonetypenum'. $_->phonetypenum @@ -1028,4 +1028,3 @@ L, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/cust_contact.pm b/FS/FS/cust_contact.pm index 118a9e000..adad46e9e 100644 --- a/FS/FS/cust_contact.pm +++ b/FS/FS/cust_contact.pm @@ -59,6 +59,11 @@ empty or Y 'Y' if the customer should get invoices sent to this address, null if not +=item message_dest + +'Y' if contact should get non-invoice email messages sent to this address, +NULL if not + =back =head1 METHODS @@ -119,6 +124,7 @@ sub check { || $self->ut_textn('comment') || $self->ut_enum('selfservice_access', [ '', 'Y' ]) || $self->ut_flag('invoice_dest') + || $self->ut_flag('message_dest') ; return $error if $error; @@ -148,4 +154,3 @@ L, L, L =cut 1; - -- cgit v1.2.1 From 4747bfbea3f4abb66d05a2bd1abed69e28a4aa3d Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sun, 28 Jan 2018 02:41:17 -0600 Subject: RT# 73421 Fixed E-Mail pipeline to obey contact opt-in flags --- FS/FS/cust_main.pm | 96 ++++++++++++++++++++++++++++++++++----------- FS/FS/cust_main_Mixin.pm | 38 +++++++++++++----- FS/FS/msg_template/email.pm | 29 ++++++++++++-- 3 files changed, 129 insertions(+), 34 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 925eb4e44..b8d8f102f 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3022,48 +3022,101 @@ sub invoicing_list_emailonly_scalar { join(', ', $self->invoicing_list_emailonly); } -=item contact_list [ CLASSNUM, ... ] +=item contact_list [ CLASSNUM, DEST_FLAG... ] -Returns a list of contacts (L objects) for the customer. If -a list of contact classnums is given, returns only contacts in those -classes. If the pseudo-classnum 'invoice' is given, returns contacts that -are marked as invoice destinations. If '0' is given, also returns contacts -with no class. +Returns a list of contacts (L objects) for the customer. If no arguments are given, returns all contacts for the customer. +Arguments may contain classnums. When classnums are specified, only +contacts with a matching cust_contact.classnum are returned. When a +classnum of 0 is given, contacts with a null classnum are also included. + +Arguments may also contain the dest flag names 'invoice' or 'message'. +If given, contacts who's invoice_dest and/or invoice_message flags are +not set to 'Y' will be excluded. + =cut sub contact_list { my $self = shift; my $search = { table => 'contact', - select => 'contact.*, cust_contact.invoice_dest', + select => join(', ',( + 'contact.*', + 'cust_contact.invoice_dest', + 'cust_contact.message_dest', + )), addl_from => ' JOIN cust_contact USING (contactnum)', extra_sql => ' WHERE cust_contact.custnum = '.$self->custnum, }; - my @orwhere; + # Bugfix notes: + # Calling methods were relying on this method to use invoice_dest to + # block e-mail messages. Depending on parameters, this may or may not + # have actually happened. + # + # The bug could cause this SQL to be used to filter e-mail addresses: + # + # AND ( + # cust_contact.classnums IN (1,2,3) + # OR cust_contact.invoice_dest = 'Y' + # ) + # + # improperly including everybody with the opt-in flag AND everybody + # in the contact classes + # + # Possibility to introduce new bugs: + # If callers of this method called it incorrectly, and didn't notice + # because it seemed to send the e-mails they wanted. + + # WHERE ... + # AND ( + # ( cust_contact.classnum IN (1,2,3) ) + # OR + # ( cust_contact.classnum IS NULL ) + # + # AND ( + # ( cust_contact.invoice_dest = 'Y' ) + # OR + # ( cust_contact.message_dest = 'Y' ) + # ) + # ) + + my @and_dest; + my @or_classnum; my @classnums; - foreach (@_) { - if ( $_ eq 'invoice' ) { - push @orwhere, 'cust_contact.invoice_dest = \'Y\''; - } elsif ( $_ eq '0' ) { - push @orwhere, 'cust_contact.classnum is null'; + for (@_) { + if ($_ eq 'invoice' || $_ eq 'message') { + push @and_dest, " cust_contact.${_}_dest = 'Y' "; + } elsif ($_ eq '0') { + push @or_classnum, ' cust_contact.classnum IS NULL '; } elsif ( /^\d+$/ ) { push @classnums, $_; } else { - die "bad classnum argument '$_'"; + croak "bad classnum argument '$_'"; } } - if (@classnums) { - push @orwhere, 'cust_contact.classnum IN ('.join(',', @classnums).')'; - } - if (@orwhere) { - $search->{extra_sql} .= ' AND (' . - join(' OR ', map "( $_ )", @orwhere) . - ')'; + push @or_classnum, 'cust_contact.classnum IN ('.join(',',@classnums).')' + if @classnums; + + if (@or_classnum || @and_dest) { # catch, no arguments given + $search->{extra_sql} .= ' AND ( '; + + if (@or_classnum) { + $search->{extra_sql} .= join ' OR ', map {" ($_) "} @or_classnum; + $search->{extra_sql} .= ' AND ( ' if @and_dest; + } + + if (@and_dest) { + $search->{extra_sql} .= join ' OR ', map {" ($_) "} @and_dest; + $search->{extra_sql} .= ' ) ' if @or_classnum; + } + + $search->{extra_sql} .= ' ) '; + + warn "\$extra_sql: $search->{extra_sql} \n" if $DEBUG; } qsearch($search); @@ -5540,4 +5593,3 @@ L, L, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index 8b6569a74..169e1eb65 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -348,10 +348,21 @@ sub cust_search_sql { =item email_search_result HASHREF -Emails a notice to the specified customers. Customers without -invoice email destinations will be skipped. +Emails a notice to the specified customer's contact_email addresses. -Parameters: + +If the user has specified "Invoice recipients" on the send e-mail screen, +contact_email rows containing the invoice_dest flag will be included. +This option is default, if neither 'invoice' nor 'message' are present. + +If the user has specified "Message recipients" on the send e-mail screen, +contact_email rows containing the message_dest flag will be included. + +The selection is indicated by the presence of the text 'message' or +'invoice' within the to_contact_classnum argument. + + +Parameters: =over 4 @@ -386,9 +397,19 @@ Text body =item to_contact_classnum -The customer contact class (or classes, as a comma-separated list) to send -the message to. If unspecified, will be sent to any contacts that are marked -as invoice destinations (the equivalent of specifying 'invoice'). +This field contains a comma-separated list. This list may contain: + +- the text "invoice" indicating emails should only be sent to contact_email + addresses with the invoice_dest flag set +- the text "message" indicating emails should only be sent to contact_email + addresses with the message_dest flag set +- numbers representing classnum id values for email contact classes. + If any classnum are present, emails should only be sent to contact_email + addresses where contact_email.classnum contains one of these classes. + The classnum 0 also includes where contact_email.classnum IS NULL + +If neither 'invoice' nor 'message' has been specified, this method will +behave as if 'invoice' had been selected =back @@ -483,8 +504,8 @@ sub email_search_result { next; # unlinked object; nothing else we can do } -my %to = {}; -if ($to) { $to{'to'} = $to; } + my %to = (); + if ($to) { $to{'to'} = $to; } my $cust_msg = $msg_template->prepare( 'cust_main' => $cust_main, @@ -736,4 +757,3 @@ L, L =cut 1; - diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index 4ae89f056..753fd3d69 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -210,6 +210,22 @@ go away in the future. A L (or arrayref of them) to attach to the message. +=item to_contact_classnum + +Set a string containing a comma-separated list. This list may contain: + +- the text "invoice" indicating emails should only be sent to contact_email + addresses with the invoice_dest flag set +- the text "message" indicating emails should only be sent to contact_email + addresses with the message_dest flag set + - numbers representing classnum id values for email contact classes. + If any classnum are present, emails should only be sent to contact_email + addresses where contact_email.classnum contains one of these classes. + The classnum 0 also includes where contact_email.classnum IS NULL + +If neither 'invoice' nor 'message' has been specified, this method will +behave as if 'invoice' had been selected + =cut =back @@ -296,8 +312,16 @@ sub prepare { my $classnum = $opt{'to_contact_classnum'} || ''; my @classes = ref($classnum) ? @$classnum : split(',', $classnum); - # traditional behavior: send to all invoice recipients - @classes = ('invoice') unless @classes; + + # There are two e-mail opt-in flags per contact_email address. + # If neither 'invoice' nor 'message' has been specified, default + # to 'invoice'. + # + # This default supports the legacy behavior of + # send to all invoice recipients + push @classes,'invoice' + unless grep {$_ eq 'invoice' || $_ eq 'message'} @classes; + @to = $cust_main->contact_list_email(@classes); # not guaranteed to produce contacts, but then customers aren't # guaranteed to have email addresses on file. in that case, env_to @@ -625,4 +649,3 @@ L, schema.html from the base documentation. =cut 1; - -- cgit v1.2.1 From 280c44682a79e586af941e869e7a78ca8f367cf2 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sun, 28 Jan 2018 17:54:00 -0600 Subject: RT# 73421 Fix bug on some "Email customers" report links, docs --- FS/FS/cust_main.pm | 2 +- FS/FS/cust_main_Mixin.pm | 8 ++++---- FS/FS/msg_template/email.pm | 16 ++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index b8d8f102f..a82d8a225 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3033,7 +3033,7 @@ contacts with a matching cust_contact.classnum are returned. When a classnum of 0 is given, contacts with a null classnum are also included. Arguments may also contain the dest flag names 'invoice' or 'message'. -If given, contacts who's invoice_dest and/or invoice_message flags are +If given, contacts who's invoice_dest and/or message_dest flags are not set to 'Y' will be excluded. =cut diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index 169e1eb65..cceaa4bc7 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -399,10 +399,10 @@ Text body This field contains a comma-separated list. This list may contain: -- the text "invoice" indicating emails should only be sent to contact_email - addresses with the invoice_dest flag set -- the text "message" indicating emails should only be sent to contact_email - addresses with the message_dest flag set +- the text "invoice" indicating contacts with invoice_dest flag should + be included +- the text "message" indicating contacts with message_dest flag should + be included - numbers representing classnum id values for email contact classes. If any classnum are present, emails should only be sent to contact_email addresses where contact_email.classnum contains one of these classes. diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index 753fd3d69..37c1fab46 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -214,14 +214,14 @@ A L (or arrayref of them) to attach to the message. Set a string containing a comma-separated list. This list may contain: -- the text "invoice" indicating emails should only be sent to contact_email - addresses with the invoice_dest flag set -- the text "message" indicating emails should only be sent to contact_email - addresses with the message_dest flag set - - numbers representing classnum id values for email contact classes. - If any classnum are present, emails should only be sent to contact_email - addresses where contact_email.classnum contains one of these classes. - The classnum 0 also includes where contact_email.classnum IS NULL +- the text "invoice" indicating contacts with invoice_dest flag should + be included +- the text "message" indicating contacts with message_dest flag should + be included +- numbers representing classnum id values for email contact classes. + If any classnum are present, emails should only be sent to contact_email + addresses where contact_email.classnum contains one of these classes. + The classnum 0 also includes where contact_email.classnum IS NULL If neither 'invoice' nor 'message' has been specified, this method will behave as if 'invoice' had been selected -- cgit v1.2.1 From e803d5f00368e951f7a4b82d5d390b53c4a6c827 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Tue, 30 Jan 2018 09:27:42 -0600 Subject: RT# 73422 Changes to report Customer Contacts --- FS/FS/cust_main.pm | 19 +++++++++++-------- FS/FS/cust_main/Search.pm | 48 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 14 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index a82d8a225..7c9868d7a 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3072,14 +3072,15 @@ sub contact_list { # WHERE ... # AND ( - # ( cust_contact.classnum IN (1,2,3) ) - # OR - # ( cust_contact.classnum IS NULL ) - # + # ( + # cust_contact.classnum IN (1,2,3) + # OR + # cust_contact.classnum IS NULL + # ) # AND ( - # ( cust_contact.invoice_dest = 'Y' ) + # cust_contact.invoice_dest = 'Y' # OR - # ( cust_contact.message_dest = 'Y' ) + # cust_contact.message_dest = 'Y' # ) # ) @@ -3105,12 +3106,14 @@ sub contact_list { $search->{extra_sql} .= ' AND ( '; if (@or_classnum) { - $search->{extra_sql} .= join ' OR ', map {" ($_) "} @or_classnum; + $search->{extra_sql} .= ' ( '; + $search->{extra_sql} .= join ' OR ', map {" $_ "} @or_classnum; + $search->{extra_sql} .= ' ) '; $search->{extra_sql} .= ' AND ( ' if @and_dest; } if (@and_dest) { - $search->{extra_sql} .= join ' OR ', map {" ($_) "} @and_dest; + $search->{extra_sql} .= join ' OR ', map {" $_ "} @and_dest; $search->{extra_sql} .= ' ) ' if @or_classnum; } diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index 2ec87cd14..815304bb4 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -1,6 +1,7 @@ package FS::cust_main::Search; use strict; +use Carp qw( croak ); use base qw( Exporter ); use vars qw( @EXPORT_OK $DEBUG $me $conf @fuzzyfields ); use String::Approx qw(amatch); @@ -804,15 +805,51 @@ sub search { unless $params->{'cancelled_pkgs'}; ## - # "with email address(es)" checkbox + # "with email address(es)" checkbox, + # also optionally: with_email_dest and with_contact_type ## - push @where, - 'EXISTS ( SELECT 1 FROM contact_email + if ($params->{with_email}) { + my @email_dest; + my $email_dest_sql; + my $contact_type_sql; + + if ($params->{with_email_dest}) { + croak unless ref $params->{with_email_dest} eq 'ARRAY'; + + @email_dest = @{$params->{with_email_dest}}; + $email_dest_sql = + " AND ( ". + join(' OR ',map(" cust_contact.${_}_dest IS NOT NULL ", @email_dest)). + " ) "; + # Can't use message_dist = 'Y' because single quotes are escaped later + } + if ($params->{with_contact_type}) { + croak unless ref $params->{with_contact_type} eq 'ARRAY'; + + my @contact_type = grep {/^\d+$/ && $_ > 0} @{$params->{with_contact_type}}; + my $has_null_type = 0; + $has_null_type = 1 if grep { $_ eq 0 } @{$params->{with_contact_type}}; + my $hnt_sql; + if ($has_null_type) { + $hnt_sql = ' OR ' if @contact_type; + $hnt_sql .= ' cust_contact.classnum IS NULL '; + } + + $contact_type_sql = + " AND ( ". + join(' OR ', map(" cust_contact.classnum = $_ ", @contact_type)). + $hnt_sql. + " ) "; + } + push @where, + "EXISTS ( SELECT 1 FROM contact_email JOIN cust_contact USING (contactnum) WHERE cust_contact.custnum = cust_main.custnum - )' - if $params->{'with_email'}; + $email_dest_sql + $contact_type_sql + ) "; + } ## # "with postal mail invoices" checkbox @@ -1390,4 +1427,3 @@ L, L =cut 1; - -- cgit v1.2.1 From c71fd54c5103b16e7f7296ce6cc6750986119bd8 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Thu, 1 Feb 2018 10:23:33 -0500 Subject: RT# 75095 - added comment --- FS/FS/cust_main/Import_Charges.pm | 1 + 1 file changed, 1 insertion(+) (limited to 'FS') diff --git a/FS/FS/cust_main/Import_Charges.pm b/FS/FS/cust_main/Import_Charges.pm index f90e0496f..bff2ec2fb 100644 --- a/FS/FS/cust_main/Import_Charges.pm +++ b/FS/FS/cust_main/Import_Charges.pm @@ -68,6 +68,7 @@ sub batch_charge { if ( $format eq 'simple' ) { @fields = qw( custnum agent_custid amount pkg ); } elsif ( $format eq 'ooma' ) { + #below is gcet file. #@fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'userfield9', 'userfield10', 'amount', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'userfield17', 'userfield18', 'pkg', 'userfield20', 'custnum', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); @fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'amount', 'userfield10', 'userfield11', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'pkg', 'userfield18', 'custnum', 'userfield20', 'userfield21', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); -- cgit v1.2.1 From 42b6338522f6dc66771f49b38191b4201462fd82 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Fri, 2 Feb 2018 01:31:50 -0600 Subject: RT# 78592 CDR Import for Acme Packet VOIP Switches --- FS/FS/cdr.pm | 9 ++- FS/FS/cdr/acmepacket.pm | 157 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 FS/FS/cdr/acmepacket.pm (limited to 'FS') diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index c4e9c47a3..331ac0f2f 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -1733,6 +1733,14 @@ sub _cdr_date_parse { # Telos 2014-10-10T05:30:33Z ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 ); $options{gmt} = 1; + } elsif ( $date =~ /^(\d+):(\d+):(\d+)\.\d+ \w+ (\w+) (\d+) (\d+)$/ ) { + ($hour, $min, $sec, $mon, $day, $year) = ( $1, $2, $3, $4, $5, $6 ); + $mon = { # Acme Packet: 15:54:56.868 PST DEC 18 2017 + # My best guess of month abbv they may use + JAN => '01', FEB => '02', MAR => '03', APR => '04', + MAY => '05', JUN => '06', JUL => '07', AUG => '08', + SEP => '09', OCT => '10', NOV => '11', DEC => '12' + }->{$mon}; } else { die "unparsable date: $date"; #maybe we shouldn't die... } @@ -1932,4 +1940,3 @@ L, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/cdr/acmepacket.pm b/FS/FS/cdr/acmepacket.pm new file mode 100644 index 000000000..e0e80cdd6 --- /dev/null +++ b/FS/FS/cdr/acmepacket.pm @@ -0,0 +1,157 @@ +package FS::cdr::acmepacket; + +=head1 NAME + +FS:cdr::acmepacket - CDR import definition based on Acme Packet Net-Net 4000 + +=head1 DESCRIPTION + +The Acme Packet NetNet 4000 S-CX6.4.0 generates an odd cdr log format: + + - Each row in the CSV may be in one of many various formats, some of + them undocumented. + - Columns are inconsistently enclosed with " characters + - Quoted column values may, themselves, contain unescaped quote marks. + This breaks Text::CSV (well technically the FORMAT is broken, not + Text::CSV). + - A single call can generate multiple CDR records. The only records we're + interested in are billable calls: + - These are called "Stop Record CSV Placement" in Acme Packet documentation + - These will always contain a "2" as the first column value + - These rows may be 175 or 269 fields in length. It's unclear if the + undocumented 269 column rows are an intentional Acme Packet format, or + a bug in their switch. The extra columns are inserted at idx 115, + and can safely be disregarded. + +NOTE ON DATE PARSING: + + The Acme Packet manual doesn't describe it's date format in detail. The sample + we were given contains only records from December. Dates are formatted like + so: 15:54:56.868 PST DEC 18 2017 + + I gave my best guess how they will format the month text in the parser + FS::cdr::_cdr_date_parse(). If this format doesn't import records on a + particular month, check there. + +=cut + +use strict; +use warnings; +use vars qw(%info); +use base qw(FS::cdr); +use FS::cdr qw(_cdr_date_parse); +use Text::CSV; + +my $DEBUG = 0; + +my $cbcsv = Text::CSV->new({binary => 1}) + or die "Error loading Text::CSV - ".Text::CSV->error_diag(); + +# Used to map source format into the contrived format created for import_fields +# $cdr_premap[ IMPORT_FIELDS_IDX ] = SOURCE_FORMAT_IDX +my @cdr_premap = ( + 9, # clid + 9, # src + 10, # dst + 22, # channel + 21, # dstchannel + 26, # src_ip_addr + 28, # dst_ip_addr + 13, # startdate + 14, # answerdate + 15, # enddate + 12, # duration + 12, # billsec + 3, # userfield +); + +our %info = ( + name => 'Acme Packet', + weight => 600, + header => 0, + type => 'csv', + + import_fields => [ + # freeside # [idx] acmepacket + 'clid', # 9 Calling-Station-Id + 'src', # 9 Calling-Station-Id + 'dst', # 10 Called-Station-Id + 'channel', # 22 Acme-Session-Ingress-Realm + 'dstchannel', # 23 Acme-Session-Egress-Realm + 'src_ip_addr', # 26 Acme-Flow-In-Src-Adr_FS1_f + 'dst_ip_addr', # 28 Acme-Flow-In-Dst-Addr_FS1_f + 'startdate', # 13 h323-setup-time + 'answerdate', # 14 h323-connect-time + 'enddate', # 15 h323-disconnect-time + 'duration', # 12 Acct-Session-Time + 'billsec', # 12 Acct-Session-Time + 'userfield', # 3 Acct-Session-Id + ], + + row_callback => sub { + my $line = shift; + + # Only process records whose first column contains a 2 + return undef unless $line =~ /^2\,/; + + # Replace unescaped quote characters within quote-enclosed text cols + $line =~ s/(?parse($line) ) { + warn "Text::CSV Error parsing Acme Packet CDR: ".$cbcsv->error_diag(); + return undef; + } + + my @row = $cbcsv->fields(); + if (@row == 269) { + # Splice out the extra columns + @row = (@row[0..114], @row[209..@row-1]); + } elsif (@row != 175) { + warn "Unknown row format parsing Acme Packet CDR"; + return undef; + } + + my @out = map{ $row[$_] } @cdr_premap; + + if ($DEBUG) { + warn "~~~~~~~~~~pre-processed~~~~~~~~~~~~~~~~ \n"; + warn "$_ $out[$_] \n" for (0..@out-1); + } + + # answerdate, enddate, startdate + for (7,8,9) { + $out[$_] = _cdr_date_parse($out[$_]); + if ($out[$_] =~ /\D/) { + warn "Unparsable date in Acme Packet CDR: ($out[$_])"; + return undef; + } + } + + # clid, dst, src CDR field formatted as one of the following: + # 'WIRELESS CALLER';tag=SDepng302 + # ;tag=SDepng302 + + # clid + $out[0] = $out[0] =~ /^\'(.+)\'/ ? $1 : ""; + + # src, dst + # All of the sample data given shows sip connections. In case the same + # switch is hooked into another circuit type in the future, we'll just + # tease out a length 7+ number not contained in the caller-id-text field + for (1,2) { + $out[$_] =~ s/^\'.+\'//g; # strip caller id label portion + $out[$_] = $out[$_] =~ /(\d{7,})/ ? $1 : undef; + } + + if ($DEBUG) { + warn "~~~~~~~~~~post-processed~~~~~~~~~~~~~~~~ \n"; + warn "$_ $out[$_] \n" for (0..@out-1); + } + + # I haven't seen commas in sample data text fields. Extra caution, + # mangle commas into periods as we pass back to importer + join ',', map{ $_ =~ s/\,/\./g; $_ } @out; + }, +); + +1; -- cgit v1.2.1 From cbb6450c25690e5b3fccc6cfddd497de45f062d5 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Fri, 2 Feb 2018 03:47:41 -0600 Subject: RT# 78952 CDR Import for Acme Packet Improved caller-id parsing --- FS/FS/cdr/acmepacket.pm | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'FS') diff --git a/FS/FS/cdr/acmepacket.pm b/FS/FS/cdr/acmepacket.pm index e0e80cdd6..1f00e4086 100644 --- a/FS/FS/cdr/acmepacket.pm +++ b/FS/FS/cdr/acmepacket.pm @@ -130,17 +130,28 @@ our %info = ( # clid, dst, src CDR field formatted as one of the following: # 'WIRELESS CALLER';tag=SDepng302 # ;tag=SDepng302 + # '12513001300';tag=SDepng302 # clid $out[0] = $out[0] =~ /^\'(.+)\'/ ? $1 : ""; # src, dst - # All of the sample data given shows sip connections. In case the same - # switch is hooked into another circuit type in the future, we'll just - # tease out a length 7+ number not contained in the caller-id-text field + # Use the 7+ digit number as the src/dst phone number. + # Prefer using the number within label. If there is not one, + # allow using number from caller-id text field. for (1,2) { + my $f = $out[$_]; $out[$_] =~ s/^\'.+\'//g; # strip caller id label portion - $out[$_] = $out[$_] =~ /(\d{7,})/ ? $1 : undef; + if ($out[$_] =~ /(\d{7,})/) { + # Using number from + $out[$_] = $1; + } elsif ($f =~ /(\d{7,})/) { + # Using number from caller-id text + $out[$_] = $1; + } else { + # No phone number, perhaps an IP only call + $out[$_] = undef; + } } if ($DEBUG) { -- cgit v1.2.1 From 15d596e3090f3bde642917b56563736cd1ee2e90 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 6 Feb 2018 10:36:31 -0500 Subject: RT# 75095 - fixed ooma format to import all charges, and unified import process to allow new formats to be added quickly. consistant with cdr imports --- FS/FS/cust_main/Import_Charges.pm | 60 ++++++++++++++++++++++++-------- FS/FS/cust_main/import_charges/gcet.pm | 26 ++++++++++++++ FS/FS/cust_main/import_charges/ooma.pm | 21 +++++++++++ FS/FS/cust_main/import_charges/simple.pm | 21 +++++++++++ 4 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 FS/FS/cust_main/import_charges/gcet.pm create mode 100644 FS/FS/cust_main/import_charges/ooma.pm create mode 100644 FS/FS/cust_main/import_charges/simple.pm (limited to 'FS') diff --git a/FS/FS/cust_main/Import_Charges.pm b/FS/FS/cust_main/Import_Charges.pm index bff2ec2fb..3d2031d45 100644 --- a/FS/FS/cust_main/Import_Charges.pm +++ b/FS/FS/cust_main/Import_Charges.pm @@ -8,6 +8,48 @@ use FS::UID qw( dbh ); use FS::CurrentUser; use FS::Record qw( qsearchs ); use FS::cust_main; +use FS::Conf; + +my $DEBUG = ''; + +my %import_charges_info; +foreach my $INC ( @INC ) { + warn "globbing $INC/FS/cust_main/import_charges/[a-z]*.pm\n" if $DEBUG; + foreach my $file ( glob("$INC/FS/cust_main/import_charges/[a-z]*.pm") ) { + warn "attempting to load import charges format info from $file\n" if $DEBUG; + $file =~ /\/(\w+)\.pm$/ or do { + warn "unrecognized file in $INC/FS/cust_main/import_charges/: $file\n"; + next; + }; + my $mod = $1; + my $info = eval "use FS::cust_main::import_charges::$mod; ". + "\\%FS::cust_main::import_charges::$mod\::info;"; + if ( $@ ) { + die "error using FS::cust_main::import_charges::$mod (skipping): $@\n" if $@; + next; + } + unless ( keys %$info ) { + warn "no %info hash found in FS::cust_main::import_charges::$mod, skipping\n"; + next; + } + warn "got import charges format info from FS::cust_main::import_charges::$mod: $info\n" if $DEBUG; + if ( exists($info->{'disabled'}) && $info->{'disabled'} ) { + warn "skipping disabled import charges format FS::cust_main::import_charges::$mod" if $DEBUG; + next; + } + $import_charges_info{$mod} = $info; + } +} + +tie my %import_formats, 'Tie::IxHash', + map { $_ => $import_charges_info{$_}->{'name'} } + sort { $import_charges_info{$a}->{'weight'} <=> $import_charges_info{$b}->{'weight'} } + grep { exists($import_charges_info{$_}->{'fields'}) } + keys %import_charges_info; + +sub import_formats { + %import_formats; +} =head1 NAME @@ -65,20 +107,10 @@ sub batch_charge { my @fields; my %charges; - if ( $format eq 'simple' ) { - @fields = qw( custnum agent_custid amount pkg ); - } elsif ( $format eq 'ooma' ) { - #below is gcet file. - #@fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'userfield9', 'userfield10', 'amount', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'userfield17', 'userfield18', 'pkg', 'userfield20', 'custnum', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); - @fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'amount', 'userfield10', 'userfield11', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'pkg', 'userfield18', 'custnum', 'userfield20', 'userfield21', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); - - ##should charges to charge be a config option? - %charges = ( - 'DISABILITY ACCESS/ENHANCED 911 SERVICES SURCHARGE' => '1', - 'FEDERAL TRS FUND' => '1', - 'FEDERAL UNIVERSAL SERVICE FUND' => '1', - 'STATE SALES TAX' => '1', - ); + + if ( $import_charges_info{$format} ) { + @fields = @{$import_charges_info{$format}->{'fields'}}; + %charges = %{$import_charges_info{$format}->{'charges'}}; } else { die "unknown format $format"; } diff --git a/FS/FS/cust_main/import_charges/gcet.pm b/FS/FS/cust_main/import_charges/gcet.pm new file mode 100644 index 000000000..83f956545 --- /dev/null +++ b/FS/FS/cust_main/import_charges/gcet.pm @@ -0,0 +1,26 @@ +package FS::cust_main::import_charges::gcet; + +use strict; +use base qw( FS::cust_main::Import_Charges ); +use vars qw ( %info ); + +# gcet fields. +my @fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'userfield9', 'userfield10', 'amount', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'userfield17', 'userfield18', 'pkg', 'userfield20', 'custnum', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); +# hash of charges (pkg) to charge. if empty charge them all. +# '911 services' => '1', +my $charges = { + 'DISABILITY ACCESS/ENHANCED 911 SERVICES SURCHARGE' => '1', + 'FEDERAL TRS FUND' => '1', + 'FEDERAL UNIVERSAL SERVICE FUND' => '1', + 'STATE SALES TAX' => '1', +}; + +%info = ( + 'fields' => [@fields], + 'charges' => $charges, + 'name' => 'Gcet', + 'weight' => '30', + 'disabled' => '1', +); + +1; \ No newline at end of file diff --git a/FS/FS/cust_main/import_charges/ooma.pm b/FS/FS/cust_main/import_charges/ooma.pm new file mode 100644 index 000000000..a43def239 --- /dev/null +++ b/FS/FS/cust_main/import_charges/ooma.pm @@ -0,0 +1,21 @@ +package FS::cust_main::import_charges::ooma; + +use strict; +use base qw( FS::cust_main::Import_Charges ); +use vars qw ( %info ); + +# ooma fields +my @fields = ('userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'amount', 'userfield10', 'userfield11', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'pkg', 'userfield18', 'custnum', 'userfield20', 'userfield21', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); +# hash of charges (pkg) to charge. if empty charge them all. +# '911 services' => '1', +my $charges = {}; + +%info = ( + 'fields' => [@fields], + 'charges' => $charges, + 'name' => 'Ooma', + 'weight' => '10', + 'disabled' => '', +); + +1; \ No newline at end of file diff --git a/FS/FS/cust_main/import_charges/simple.pm b/FS/FS/cust_main/import_charges/simple.pm new file mode 100644 index 000000000..e039328ba --- /dev/null +++ b/FS/FS/cust_main/import_charges/simple.pm @@ -0,0 +1,21 @@ +package FS::cust_main::import_charges::simple; + +use strict; +use base qw( FS::cust_main::Import_Charges ); +use vars qw ( %info ); + +# simple field format +my @fields = ('custnum', 'agent_custid', 'amount', 'pkg'); +# hash of charges (pkg) to charge. if empty charge them all. +# '911 services' => '1', +my $charges = {}; + +%info = ( + 'fields' => [@fields], + 'charges' => $charges, + 'name' => 'Simple', + 'weight' => '1', + 'disabled' => '', +); + +1; \ No newline at end of file -- cgit v1.2.1