From: Ivan Kohler Date: Tue, 3 Oct 2017 16:36:51 +0000 (-0700) Subject: Merge branch 'master' of git.freeside.biz:/home/git/freeside X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=929783d1045757abbe5c84ff2439547b0f8eca23;hp=9270a9fe7ea00f5a24a9ce3b927f932db6650b23 Merge branch 'master' of git.freeside.biz:/home/git/freeside --- diff --git a/FS/FS.pm b/FS/FS.pm index 134a34cb2..9575c3db3 100644 --- a/FS/FS.pm +++ b/FS/FS.pm @@ -67,6 +67,8 @@ L - Customer searching L - Batch customer importing +L - Batch contact importing + =head2 Database record classes L - Database record base class diff --git a/FS/FS/API.pm b/FS/FS/API.pm index fd3793d4f..047bb4e60 100644 --- a/FS/FS/API.pm +++ b/FS/FS/API.pm @@ -1,6 +1,7 @@ package FS::API; use strict; +use Date::Parse; use FS::Conf; use FS::Record qw( qsearch qsearchs ); use FS::cust_main; @@ -16,7 +17,20 @@ FS::API - Freeside backend API =head1 SYNOPSIS - use FS::API; + use Frontier::Client; + use Data::Dumper; + + my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure + # the traffic + + my $xmlrpc = new Frontier::Client url=>$url; + + my $result = $xmlrpc->call( 'FS.API.customer_info', + 'secret' => 'sharingiscaring', + 'custnum' => 181318, + ); + + print Dumper($result); =head1 DESCRIPTION @@ -525,6 +539,23 @@ sub update_customer { Returns general customer information. Takes a list of keys and values as parameters with the following keys: custnum, secret +Example: + + use Frontier::Client; + use Data::Dumper; + + my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure + # the traffic + + my $xmlrpc = new Frontier::Client url=>$url; + + my $result = $xmlrpc->call( 'FS.API.customer_info', + 'secret' => 'sharingiscaring', + 'custnum' => 181318, + ); + + print Dumper($result); + =cut sub customer_info { @@ -542,6 +573,28 @@ sub customer_info { Returns customer service information. Takes a list of keys and values as parameters with the following keys: custnum, secret +Example: + + use Frontier::Client; + use Data::Dumper; + + my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure + # the traffic + + my $xmlrpc = new Frontier::Client url=>$url; + + my $result = $xmlrpc->call( 'FS.API.customer_list_svcs', + 'secret' => 'sharingiscaring', + 'custnum' => 181318, + ); + + print Dumper($result); + + foreach my $cust_svc ( @{ $result->{'cust_svc'} } ) { + #print $cust_svc->{mac_addr}."\n" if exists $cust_svc->{mac_addr}; + print $cust_svc->{circuit_id}."\n" if exists $cust_svc->{circuit_id}; + } + =cut sub customer_list_svcs { @@ -597,10 +650,128 @@ sub location_info { return \%return; } +=item order_package OPTION => VALUE, ... + +Orders a new customer package. Takes a list of keys and values as paramaters +with the following keys: + +=over 4 + +=item secret + +API Secret + +=item custnum + +=item pkgpart + +=item quantity + +=item start_date + +=item contract_end + +=item address1 + +=item address2 + +=item city + +=item county + +=item state + +=item zip + +=item country + +=item setup_fee + +Including this implements per-customer custom pricing for this package, overriding package definition pricing + +=item recur_fee + +Including this implements per-customer custom pricing for this package, overriding package definition pricing + +=item invoice_details + +A single string for just one detail line, or an array reference of one or more +lines of detail + +=cut + +sub order_package { + my( $class, %opt ) = @_; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} }) + or return { 'error' => 'Unknown custnum' }; + + #some conceptual false laziness w/cust_pkg/Import.pm + + my $cust_pkg = new FS::cust_pkg { + 'pkgpart' => $opt{'pkgpart'}, + 'quantity' => $opt{'quantity'} || 1, + }; + + #start_date and contract_end + foreach my $date_field (qw( start_date contract_end )) { + if ( $opt{$date_field} =~ /^(\d+)$/ ) { + $cust_pkg->$date_field( $opt{$date_field} ); + } elsif ( $opt{$date_field} ) { + $cust_pkg->$date_field( str2time( $opt{$date_field} ) ); + } + } + + #especially this part for custom pkg price + # (false laziness w/cust_pkg/Import.pm) + my $s = $opt{'setup_fee'}; + my $r = $opt{'recur_fee'}; + my $part_pkg = $cust_pkg->part_pkg; + if ( ( length($s) && $s != $part_pkg->option('setup_fee') ) + or ( length($r) && $r != $part_pkg->option('recur_fee') ) + ) + { + my $custom_part_pkg = $part_pkg->clone; + $custom_part_pkg->disabled('Y'); + my %options = $part_pkg->options; + $options{'setup_fee'} = $s if length($s); + $options{'recur_fee'} = $r if length($r); + my $error = $custom_part_pkg->insert( options=>\%options ); + return ( 'error' => "error customizing package: $error" ) if $error; + $cust_pkg->pkgpart( $custom_part_pkg->pkgpart ); + } + + my %order_pkg = ( 'cust_pkg' => $cust_pkg ); + + my @loc_fields = qw( address1 address2 city county state zip country ); + if ( grep length($opt{$_}), @loc_fields ) { + $order_pkg{'cust_location'} = new FS::cust_location { + map { $_ => $opt{$_} } @loc_fields, 'custnum' + }; + } + + $order_pkg{'invoice_details'} = $opt{'invoice_details'} + if $opt{'invoice_details'}; + + my $error = $cust_main->order_pkg( %order_pkg ); + + #if ( $error ) { + return { 'error' => $error, + #'pkgnum' => '', + }; + #} else { + # return { 'error' => '', + # #cust_main->order_pkg doesn't actually have a way to return pkgnum + # #'pkgnum' => $pkgnum, + # }; + #} + +} + =item change_package_location Updates package location. Takes a list of keys and values -as paramters with the following keys: +as parameters with the following keys: pkgnum @@ -719,7 +890,205 @@ sub bill_now { } -#next.. Advertising sources? +#next.. Delete Advertising sources? + +=item list_advertising_sources OPTION => VALUE, ... + +Lists all advertising sources. + +=over + +=item secret + +API Secret + +=back + +Example: + + my $result = FS::API->list_advertising_sources( + 'secret' => 'sharingiscaring', + ); + + if ( $result->{'error'} ) { + die $result->{'error'}; + } else { + # list advertising sources returns an array of hashes for sources. + print Dumper($result->{'sources'}); + } + +=cut + +#list_advertising_sources +sub list_advertising_sources { + my( $class, %opt ) = @_; + return _shared_secret_error() unless _check_shared_secret($opt{secret}); + + my @sources = qsearch('part_referral', {}, '', "") + or return { 'error' => 'No referrals' }; + + my $return = { + 'sources' => [ map $_->hashref, @sources ], + }; + + $return; +} + +=item add_advertising_source OPTION => VALUE, ... + +Add a new advertising source. + +=over + +=item secret + +API Secret + +=item referral + +Referral name + +=item disabled + +Referral disabled, Y for disabled or nothing for enabled + +=item agentnum + +Agent ID number + +=item title + +External referral ID + +=back + +Example: + + my $result = FS::API->add_advertising_source( + 'secret' => 'sharingiscaring', + 'referral' => 'test referral', + + #optional + 'disabled' => 'Y', + 'agentnum' => '2', #agent id number + 'title' => 'test title', + ); + + if ( $result->{'error'} ) { + die $result->{'error'}; + } else { + # add_advertising_source returns new source upon success. + print Dumper($result); + } + +=cut + +#add_advertising_source +sub add_advertising_source { + my( $class, %opt ) = @_; + return _shared_secret_error() unless _check_shared_secret($opt{secret}); + + use FS::part_referral; + + my $new_source = $opt{source}; + + my $source = new FS::part_referral $new_source; + + my $error = $source->insert; + + my $return = {$source->hash}; + $return = { 'error' => $error, } if $error; + + $return; +} + +=item edit_advertising_source OPTION => VALUE, ... + +Edit a advertising source. + +=over + +=item secret + +API Secret + +=item refnum + +Referral number to edit + +=item source + +hash of edited source fields. + +=over + +=item referral + +Referral name + +=item disabled + +Referral disabled, Y for disabled or nothing for enabled + +=item agentnum + +Agent ID number + +=item title + +External referral ID + +=back + +=back + +Example: + + my $result = FS::API->edit_advertising_source( + 'secret' => 'sharingiscaring', + 'refnum' => '4', # referral number to edit + 'source' => { + #optional + 'referral' => 'test referral', + 'disabled' => 'Y', + 'agentnum' => '2', #agent id number + 'title' => 'test title', + } + ); + + if ( $result->{'error'} ) { + die $result->{'error'}; + } else { + # edit_advertising_source returns updated source upon success. + print Dumper($result); + } + +=cut + +#edit_advertising_source +sub edit_advertising_source { + my( $class, %opt ) = @_; + return _shared_secret_error() unless _check_shared_secret($opt{secret}); + + use FS::part_referral; + + my $refnum = $opt{refnum}; + my $source = $opt{source}; + + my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,}); + my $new = new FS::part_referral { $old->hash }; + + foreach my $key (keys %$source) { + $new->$key($source->{$key}); + } + + my $error = $new->replace; + + my $return = {$new->hash}; + $return = { 'error' => $error, } if $error; + + $return; +} ## diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 161e466a2..471e32aff 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -291,6 +291,7 @@ tie my %rights, 'Tie::IxHash', { rightname=> 'List rating data', desc=>'Usage reports', global=>1 }, 'Billing event reports', 'Receivables report', + 'Basic payment and refund reports', 'Financial reports', { rightname=>'Send reports to customers', global=>1 }, { rightname=> 'List inventory', global=>1 }, @@ -329,7 +330,7 @@ tie my %rights, 'Tie::IxHash', 'Usage: Unrateable CDRs', 'Usage: Time worked', #gone in 4.x as a distinct ACL (for now?) { rightname=>'Employees: Commission Report', global=>1 }, - { rightname=>'Employees: Audit Report', global=>1 }, + { rightname=>'Employee Reports', global=>1 }, #{ rightname => 'List customers of all agents', global=>1 }, ], diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 5c86b7820..30ab96b49 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -882,6 +882,7 @@ sub payment_info { if ($cust_payby) { $return{payname} = $cust_payby->payname || ( $cust_main->first. ' '. $cust_main->get('last') ); + $return{custpaybynum} = $cust_payby->custpaybynum; if ( $cust_payby->payby =~ /^(CARD|DCRD)$/ ) { $return{card_type} = cardtype($cust_payby->payinfo); @@ -980,6 +981,7 @@ sub validate_payment { #false laziness w/process/payment.cgi my $payinfo; my $paycvv = ''; + my $replace_cust_payby; if ( $payby eq 'CHEK' || $payby eq 'DCHK' ) { $p->{'payinfo1'} =~ /^([\dx]+)$/ @@ -994,6 +996,7 @@ sub validate_payment { foreach my $cust_payby ($cust_main->cust_payby('CHEK','DCHK')) { if ( $cust_payby->paymask eq $payinfo ) { $payinfo = $cust_payby->payinfo; + $replace_cust_payby = $cust_payby; $achonfile = 1; last; } @@ -1014,6 +1017,7 @@ sub validate_payment { foreach my $cust_payby ($cust_main->cust_payby('CARD','DCRD')) { if ( $cust_payby->paymask eq $payinfo ) { $payinfo = $cust_payby->payinfo; + $replace_cust_payby = $cust_payby; $onfile = 1; last; } @@ -1055,6 +1059,8 @@ sub validate_payment { 'CHEK' => [ qw( ss paytype paystate stateid stateid_state payip ) ], ); + my %replace = ( 'replace' => $replace_cust_payby, ); + my $card_type = ''; $card_type = cardtype($payinfo) if $payby eq 'CARD'; @@ -1063,7 +1069,7 @@ sub validate_payment { 'amount' => sprintf('%.2f', $amount), 'payby' => $payby, 'payinfo' => $payinfo, - 'paymask' => $cust_main->mask_payinfo( $payby, $payinfo ), + 'paymask' => FS::payinfo_Mixin->mask_payinfo( $payby, $payinfo ), 'card_type' => $card_type, 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01', 'paydate_pretty' => $p->{'month'}. ' / '. $p->{'year'}, @@ -1076,6 +1082,7 @@ sub validate_payment { 'payname' => $payname, 'discount_term' => $discount_term, 'pkgnum' => $session->{'pkgnum'}, + %replace, map { $_ => $p->{$_} } ( @{ $payby2fields{$payby} }, qw( save auto ), ) @@ -1158,6 +1165,7 @@ sub do_process_payment { my $error = $cust_main->save_cust_payby( 'payment_payby' => $payby, + 'replace' => $validate->{'replace'}, # cust_payby object to replace %saveopt ); diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index d41cc741b..ed72354dd 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -898,6 +898,14 @@ my $validate_email = sub { $_[0] =~ }, { + 'key' => 'email-to-voice_domain', + 'section' => 'email_to_voice_services', + 'description' => 'The domain name that phone numbers will be attached to for sending email to voice emails via a 3rd party email to voice service. You will get this domain from your email to voice service provider. This is utilized on the email customer page or when using the email to voice billing event action. There you will be able to select the phone number for the email to voice service.', + 'type' => 'text', + 'per_agent' => 1, + }, + + { 'key' => 'next-bill-ignore-time', 'section' => 'billing', 'description' => 'Ignore the time portion of next bill dates when billing, matching anything from 00:00:00 to 23:59:59 on the billing day.', @@ -4302,6 +4310,7 @@ and customer address. Include units.', '' => 'Numeric only', '\d{7}' => 'Numeric only, exactly 7 digits', 'ww?d+' => 'Numeric with one or two letter prefix', + 'd+-w' => 'Numeric with a dash and one letter suffix', ], }, @@ -4479,7 +4488,7 @@ and customer address. Include units.', '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.', 'type' => 'select', - 'select_enum' => [ qw( 2013 2012 2011 ) ], + 'select_enum' => [ qw( 2017 2016 2015 ) ], }, { @@ -5788,8 +5797,8 @@ and customer address. Include units.', { 'key' => 'logout-timeout', - 'section' => 'UI', - 'description' => 'If set, automatically log users out of the backoffice after this many minutes.', + 'section' => 'deprecated', + 'description' => 'Deprecated. Used to automatically log users out of the backoffice after this many minutes. Set session timeouts in employee groups instead.', 'type' => 'text', }, diff --git a/FS/FS/Cron/backup.pm b/FS/FS/Cron/backup.pm index 7d868c882..5276565c5 100644 --- a/FS/FS/Cron/backup.pm +++ b/FS/FS/Cron/backup.pm @@ -25,7 +25,7 @@ sub backup { my $ext; if ( driver_name eq 'Pg' ) { - system("pg_dump -Fc $database >/var/tmp/$database.Pg"); + system("pg_dump -Fc -T h_cdr -T h_queue -T h_queue_arg $database >/var/tmp/$database.Pg"); $ext = 'Pg'; } elsif ( driver_name eq 'mysql' ) { system("mysqldump $database >/var/tmp/$database.sql"); diff --git a/FS/FS/Cron/rt_tasks.pm b/FS/FS/Cron/rt_tasks.pm index 01ea0b5dd..077f23cc6 100644 --- a/FS/FS/Cron/rt_tasks.pm +++ b/FS/FS/Cron/rt_tasks.pm @@ -31,6 +31,8 @@ sub rt_daily { my $system = $FS::TicketSystem::system; return if !defined($system) || $system ne 'RT_Internal'; + system('/opt/rt3/sbin/rt-fulltext-indexer --quiet --limit 5400 &'); + # if -d or -y is in use, bail out. There's no reliable way to tell RT # to use an alternate system time. if ( $opt{'d'} or $opt{'y'} ) { diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 956ea6210..7bdb6059e 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -262,6 +262,7 @@ if ( -e $addl_handler_use_file ) { use FS::cust_category; use FS::prospect_main; use FS::contact; + use FS::contact::Import; use FS::phone_type; use FS::svc_pbx; use FS::discount; diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index f2e9e6fba..479f9b1f1 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -2647,7 +2647,7 @@ sub ut_currency { =item ut_text COLUMN Check/untaint text. Alphanumerics, spaces, and the following punctuation -symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? / = [ ] < > +symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? / = [ ] < > ~ May not be null. If there is an error, returns the error, otherwise returns false. @@ -2661,7 +2661,7 @@ sub ut_text { # \p{Word} = alphanumerics, marks (diacritics), and connectors # see perldoc perluniprops $self->getfield($field) - =~ /^([\p{Word} \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]\<\>$money_char]+)$/ + =~ /^([\p{Word} \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]\<\>\~$money_char]+)$/ or return gettext('illegal_or_empty_text'). " $field: ". $self->getfield($field); $self->setfield($field,$1); diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index d7c7452f1..d347c0653 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -2335,6 +2335,7 @@ sub tables_hashref { 'taxratelocationnum', 'serial', '', '', '', '', 'data_vendor', 'varchar', 'NULL', $char_d, '', '', 'geocode', 'varchar', '', 20, '', '', + 'district', 'varchar', 'NULL', $char_d, '', '', 'city', 'varchar', 'NULL', $char_d, '', '', 'county', 'varchar', 'NULL', $char_d, '', '', 'state', 'char', 'NULL', 2, '', '', @@ -5796,6 +5797,25 @@ sub tables_hashref { ], }, + 'access_user_session_log' => { + 'columns' => [ + 'sessionlognum', 'serial', '', '', '', '', + 'usernum', 'int', '', '', '', '', + 'start_date', @date_type, '', '', + 'last_date', @date_type, '', '', + 'logout_date', @date_type, '', '', + 'logout_type', 'varchar', '', $char_d, '', '', + ], + 'primary_key' => 'sessionlognum', + 'unique' => [], + 'index' => [], + 'foreign_keys' => [ + { columns => [ 'usernum' ], + table => 'access_user', + }, + ], + }, + 'access_user' => { 'columns' => [ 'usernum', 'serial', '', '', '', '', @@ -5843,8 +5863,9 @@ sub tables_hashref { 'access_group' => { 'columns' => [ - 'groupnum', 'serial', '', '', '', '', - 'groupname', 'varchar', '', $char_d, '', '', + 'groupnum', 'serial', '', '', '', '', + 'groupname', 'varchar', '', $char_d, '', '', + 'session_timeout', 'int', 'NULL', '', '', '', ], 'primary_key' => 'groupnum', 'unique' => [ [ 'groupname' ] ], @@ -6995,7 +7016,7 @@ sub tables_hashref { 'vendor_order_status', 'varchar', 'NULL', $char_d, '', '', 'endpoint_ip_addr', 'varchar', 'NULL', 40, '', '', 'endpoint_mac_addr', 'varchar', 'NULL', 12, '', '', - 'internal_circuit_id', 'varchar', '', 64, '', '', + 'internal_circuit_id', 'varchar', 'NULL', 64, '', '', ], 'primary_key' => 'svcnum', 'unique' => [], diff --git a/FS/FS/TaxEngine/compliance_solutions.pm b/FS/FS/TaxEngine/compliance_solutions.pm index 92ca2ce02..1f0c16605 100644 --- a/FS/FS/TaxEngine/compliance_solutions.pm +++ b/FS/FS/TaxEngine/compliance_solutions.pm @@ -263,7 +263,7 @@ sub make_taxlines { # create a tax rate location if there isn't one yet my $taxname = $tax_data->{descript}; my $tax_rate = FS::tax_rate->new({ - data_vendor => 'compliance solutions', + data_vendor => 'compliance_solutions', taxname => $taxname, taxclassnum => '', taxauth => $tax_data->{'taxauthtype'}, # federal / state / city / district @@ -277,13 +277,16 @@ sub make_taxlines { $tax_rate = $tax_rate->replace_old; my $tax_rate_location = FS::tax_rate_location->new({ - data_vendor => 'compliance solutions', - state => $tax_data->{'state'}, - country => $tax_data->{'country'}, + data_vendor => 'compliance_solutions', geocode => $tax_data->{'geocode'}, + district => $tax_data->{'geo_district'}, + state => $tax_data->{'geo_state'}, + county => $tax_data->{'geo_county'}, + country => 'US', }); $error = $tax_rate_location->find_or_insert; - die "error inserting tax_rate_location record: $error\n" + die 'error inserting tax_rate_location record for '. $tax_data->{state}. + '/'. $tax_data->{country}. ' ('. $tax_data->{'geocode'}. "): $error\n" if $error; $tax_rate_location = $tax_rate_location->replace_old; diff --git a/FS/FS/TaxEngine/internal.pm b/FS/FS/TaxEngine/internal.pm index dbe9a99e0..5f5d2295a 100644 --- a/FS/FS/TaxEngine/internal.pm +++ b/FS/FS/TaxEngine/internal.pm @@ -23,7 +23,8 @@ sub add_sale { my ($self, $cust_bill_pkg) = @_; my $part_item = $cust_bill_pkg->part_X; - my $location = $cust_bill_pkg->tax_location; + my $location = $cust_bill_pkg->tax_location + or return; my $custnum = $self->{cust_main}->custnum; push @{ $self->{items} }, $cust_bill_pkg; diff --git a/FS/FS/TaxEngine/suretax.pm b/FS/FS/TaxEngine/suretax.pm index 1a00cdaaa..356f5f318 100644 --- a/FS/FS/TaxEngine/suretax.pm +++ b/FS/FS/TaxEngine/suretax.pm @@ -14,7 +14,7 @@ our $DEBUG = 1; # prints progress messages # $DEBUG = 2; # prints decoded request and response (noisy, be careful) # $DEBUG = 3; # prints raw response from the API, ridiculously unreadable -our $json = Cpanel::JSON::XS->new->pretty(1); +our $json = Cpanel::JSON::XS->new->pretty(0)->shrink(1); our %taxproduct_cache; @@ -328,13 +328,14 @@ sub make_taxlines { return; } - warn "sending SureTax request\n" if $DEBUG; + warn "encoding SureTax request\n" if $DEBUG; my $request_json = $json->encode($request); warn $request_json if $DEBUG > 1; my $host = $conf->config('suretax-hostname'); $host ||= 'testapi.taxrating.net'; + warn "sending SureTax request\n" if $DEBUG; # We are targeting the "V05" interface: # - accepts both telecom and general sales transactions # - produces results broken down by "invoice" (Freeside line item) @@ -346,8 +347,11 @@ sub make_taxlines { 'Accept' => 'application/json', ); + warn 'received SureTax response: '. $http_response->status_line. "\n" + if $DEBUG; + die $http_response->status_line. "\n" unless $http_response->is_success; + my $raw_response = $http_response->content; - warn "received response\n" if $DEBUG; warn $raw_response if $DEBUG > 2; my $response; if ( $raw_response =~ /^<\?xml/ ) { @@ -356,8 +360,10 @@ sub make_taxlines { $response = XMLin( $raw_response ); $raw_response = $response->{content}; } + + warn "decoding SureTax response\n" if $DEBUG; $response = eval { $json->decode($raw_response) } - or die "$raw_response\n"; + or die "Can't JSON-decode response: $raw_response\n"; # documentation implies this might be necessary $response = $response->{'d'} if exists $response->{'d'}; @@ -375,6 +381,7 @@ sub make_taxlines { } return if !$response->{GroupList}; + warn "creating FS objects from SureTax data\n" if $DEBUG; foreach my $taxable ( @{ $response->{GroupList} } ) { # each member of this array here corresponds to what SureTax calls an # "invoice" and we call a "line item". The invoice number is @@ -420,6 +427,7 @@ sub make_taxlines { }); } } + warn "TaxEngine/suretax.pm make_taxlines done; returning FS objects\n" if $DEBUG; return @elements; } diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index 6edec9072..0069e207a 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -497,6 +497,10 @@ sub upgrade_data { #mark certain taxes as system-maintained, # and fix whitespace 'cust_main_county' => [], + + #'compliance solutions' -> 'compliance_solutions' + 'tax_rate' => [], + 'tax_rate_location' => [], ; \%hash; diff --git a/FS/FS/access_right.pm b/FS/FS/access_right.pm index 409b44136..155da739e 100644 --- a/FS/FS/access_right.pm +++ b/FS/FS/access_right.pm @@ -155,6 +155,7 @@ sub _upgrade_data { # class method 'Refund payment' => [ 'Refund credit card payment', 'Refund Echeck payment' ], 'Regular void' => [ 'Void payments' ], 'Unvoid' => [ 'Unvoid payments', 'Unvoid invoices' ], + 'Employees: Audit Report' => [ 'Employee Reports' ], ); foreach my $oldright (keys %migrate) { @@ -233,9 +234,7 @@ sub _upgrade_data { # class method 'Usage: Unrateable CDRs', ], 'Provision customer service' => [ 'Edit password' ], - 'Financial reports' => [ 'Employees: Commission Report', - 'Employees: Audit Report', - ], + 'Financial reports' => 'Employee Reports', 'Change customer package' => 'Detach customer package', 'Services: Accounts' => 'Services: Cable Subscribers', 'Bulk change customer packages' => 'Bulk move customer services', @@ -261,6 +260,7 @@ sub _upgrade_data { # class method 'List customers' => 'Customers: Customer churn report', 'Edit customer note' => 'Delete customer note', 'Edit customer' => 'Edit customer invoice terms', + 'Financial reports' => 'Basic payment and refund reports', ); # foreach my $old_acl ( keys %onetime ) { diff --git a/FS/FS/access_user_session_log.pm b/FS/FS/access_user_session_log.pm new file mode 100644 index 000000000..d28ec8586 --- /dev/null +++ b/FS/FS/access_user_session_log.pm @@ -0,0 +1,124 @@ +package FS::access_user_session_log; +use base qw( FS::Record ); + +use strict; +#use FS::Record qw( qsearch qsearchs ); + +=head1 NAME + +FS::access_user_session_log - Object methods for access_user_session_log records + +=head1 SYNOPSIS + + use FS::access_user_session_log; + + $record = new FS::access_user_session_log \%hash; + $record = new FS::access_user_session_log { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::access_user_session_log object represents an log of an employee session. +FS::access_user_session_log inherits from FS::Record. The following fields +are currently supported: + +=over 4 + +=item sessionlognum + +primary key + +=item usernum + +usernum + +=item start_date + +start_date + +=item last_date + +last_date + +=item logout_date + +logout_date + +=item logout_type + +logout_type + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new log entry. To add the entry to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'access_user_session_log'; } + +=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 log entry. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_number('usernum') + || $self->ut_numbern('start_date') + || $self->ut_numbern('last_date') + || $self->ut_numbern('logout_date') + || $self->ut_text('logout_type') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L + +=cut + +1; + diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm index 568d46f07..44c538806 100644 --- a/FS/FS/contact.pm +++ b/FS/FS/contact.pm @@ -10,6 +10,7 @@ use FS::Record qw( qsearch qsearchs dbh ); use FS::Cursor; use FS::contact_phone; use FS::contact_email; +use FS::contact::Import; use FS::queue; use FS::phone_type; #for cgi_contact_fields use FS::cust_contact; diff --git a/FS/FS/contact/Import.pm b/FS/FS/contact/Import.pm new file mode 100644 index 000000000..26bdcfa6e --- /dev/null +++ b/FS/FS/contact/Import.pm @@ -0,0 +1,161 @@ +package FS::contact::Import; + +use strict; +use vars qw( $DEBUG ); #$conf ); +use Data::Dumper; +use FS::Misc::DateTime qw( parse_datetime ); +use FS::Record qw( qsearchs ); +use FS::contact; +use FS::cust_main; + +$DEBUG = 0; + +=head1 NAME + +FS::contact::Import - Batch contact importing + +=head1 SYNOPSIS + + use FS::contact::Import; + + #import + FS::contact::Import::batch_import( { + file => $file, #filename + type => $type, #csv or xls + format => $format, #default + agentnum => $agentnum, + job => $job, #optional job queue job, for progressbar updates + pkgbatch => $pkgbatch, #optional batch unique identifier + } ); + die $error if $error; + + #ajax helper + use FS::UI::Web::JSRPC; + my $server = + new FS::UI::Web::JSRPC 'FS::contact::Import::process_batch_import', $cgi; + print $server->process; + +=head1 DESCRIPTION + +Batch contact importing. + +=head1 SUBROUTINES + +=item process_batch_import + +Load a batch import as a queued JSRPC job + +=cut + +sub process_batch_import { + my $job = shift; + my $param = shift; + warn Dumper($param) if $DEBUG; + + my $files = $param->{'uploaded_files'} + or die "No files provided.\n"; + + my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files; + + my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/'; + #my $dir = '/usr/local/etc/freeside/cache.'. $FS::UID::datasrc. '/'; + my $file = $dir. $files{'file'}; + + my $type; + if ( $file =~ /\.(\w+)$/i ) { + $type = lc($1); + } else { + #or error out??? + warn "can't parse file type from filename $file; defaulting to CSV"; + $type = 'csv'; + } + + my $error = + FS::contact::Import::batch_import( { + job => $job, + file => $file, + type => $type, + agentnum => $param->{'agentnum'}, + 'format' => $param->{'format'}, + } ); + + unlink $file; + + die "$error\n" if $error; + +} + +=item batch_import + +=cut + +my %formatfields = ( + 'default' => [ qw( custnum last first title comment selfservice_access emailaddress phonetypenum1 phonetypenum3 phonetypenum2 ) ], +); + +sub _formatfields { + \%formatfields; +} + +## not tested but maybe allow 2nd format to attach location in the future +my %import_options = ( + 'table' => 'contact', + + 'preinsert_callback' => sub { + my($record, $param) = @_; + my @location_params = grep /^location\./, keys %$param; + if (@location_params) { + my $cust_location = FS::cust_location->new({ + 'custnum' => $record->custnum, + }); + foreach my $p (@location_params) { + $p =~ /^location.(\w+)$/; + $cust_location->set($1, $param->{$p}); + } + + my $error = $cust_location->find_or_insert; # this avoids duplicates + return "error creating location: $error" if $error; + $record->set('locationnum', $cust_location->locationnum); + } + ''; + }, + +); + +sub _import_options { + \%import_options; +} + +sub batch_import { + my $opt = shift; + + my $iopt = _import_options; + $opt->{$_} = $iopt->{$_} foreach keys %$iopt; + + my $format = delete $opt->{'format'}; + + my $formatfields = _formatfields(); + die "unknown format $format" unless $formatfields->{$format}; + + my @fields; + foreach my $field ( @{ $formatfields->{$format} } ) { + push @fields, $field; + } + + $opt->{'fields'} = \@fields; + + FS::Record::batch_import( $opt ); + +} + +=head1 BUGS + +Not enough documentation. + +=head1 SEE ALSO + +L + +=cut + +1; \ No newline at end of file diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index f8157c478..925eb4e44 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -4617,6 +4617,8 @@ PAYBYLOOP: next if grep(/^$field$/, qw( custpaybynum payby weight ) ); next if grep(/^$field$/, @preserve ); next PAYBYLOOP unless $new->get($field) eq $cust_payby->get($field); + # check if paymask exists, if so stop and don't save, no need for a duplicate. + return '' if $new->get('paymask') eq $cust_payby->get('paymask'); } # now check fields that can replace if one value is blank my $replace = 0; diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index d62120b3f..f16752ba4 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -6,6 +6,7 @@ use vars qw( $realtime_bop_decline_quiet ); #ugh use Carp; use Data::Dumper; use Business::CreditCard 0.35; +use Business::OnlinePayment; use FS::UID qw( dbh myconnect ); use FS::Record qw( qsearch qsearchs ); use FS::payby; diff --git a/FS/FS/cust_main/Import.pm b/FS/FS/cust_main/Import.pm index 646476162..9624529fa 100644 --- a/FS/FS/cust_main/Import.pm +++ b/FS/FS/cust_main/Import.pm @@ -325,6 +325,7 @@ sub batch_import { my %svc_x = (); my %bill_location = (); my %ship_location = (); + my $cust_payby = ''; foreach my $field ( @fields ) { if ( $field =~ /^cust_pkg\.(pkgpart|setup|bill|susp|adjourn|expire|cancel)$/ ) { @@ -409,17 +410,24 @@ sub batch_import { if ( $cust_main{'payinfo'} =~ /^\s*(\d+\@[\d\.]+)\s*$/ ) { - $cust_main{'payby'} = 'CHEK'; - $cust_main{'payinfo'} = $1; + delete $cust_main{'payinfo'}; - } else { + $cust_payby = new FS::cust_payby { + 'payby' => 'CHEK', + 'payinfo' => $1, + }; - $cust_main{'payby'} = 'CARD'; + } elsif ($cust_main{'payinfo'} =~ /^\s*([AD]?)(.*)\s*$/) { - if ($cust_main{'payinfo'} =~ /^\s*([AD]?)(.*)\s*$/) { - $cust_main{'payby'} = 'DCRD' if $1 eq 'D'; - $cust_main{'payinfo'} = $2; - } + delete $cust_main{'payinfo'}; + + $cust_payby = new FS::cust_payby { + 'payby' => ($1 eq 'D') ? 'DCRD' : 'CARD', + 'payinfo' => $2, + 'paycvv' => delete $cust_main{'paycvv'}, + 'paydate' => delete $cust_main{'paydate'}, + 'payname' => $cust_main{'first'}. ' '. $cust_main{'last'}, + }; } @@ -502,7 +510,10 @@ sub batch_import { $hash{$cust_pkg} = \@svc_x; } - my $error = $cust_main->insert( \%hash, $invoicing_list ); + my %options = ('invoicing_list' => $invoicing_list); + $options{'cust_payby'} = [ $cust_payby ] if $cust_payby; + + my $error = $cust_main->insert( \%hash, %options ); if ( $error ) { $dbh->rollback if $oldAutoCommit; diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index d66809404..2ec87cd14 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -93,7 +93,7 @@ sub smart_search { my $phonenum = "$1$2$3"; #my $extension = $4; - #cust_main phone numbers + #cust_main phone numbers and contact phone number push @cust_main, qsearch( { 'table' => 'cust_main', 'hashref' => { %options }, @@ -102,20 +102,12 @@ sub smart_search { join(' OR ', map "$_ = '$phonen'", qw( daytime night mobile fax ) ). + " OR phonenum = '$phonenum' ". ' ) '. " AND $agentnums_sql", #agent virtualization + 'addl_from' => ' left join cust_contact using (custnum) left join contact_phone using (contactnum) ', } ); - #contact phone numbers - push @cust_main, - grep $agentnums_href->{$_->agentnum}, #agent virt - grep $_, #skip contacts that don't have cust_main records - map $_->contact->cust_main, - qsearch({ - 'table' => 'contact_phone', - 'hashref' => { 'phonenum' => $phonenum }, - }); - unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match #try looking for matches with extensions unless one was specified @@ -136,45 +128,40 @@ sub smart_search { } - if ( $search =~ /@/ ) { #email address - - # invoicing email address - push @cust_main, - grep $agentnums_href->{$_->agentnum}, #agent virt - map $_->cust_main, - qsearch( { - 'table' => 'cust_main_invoice', - 'hashref' => { 'dest' => $search }, - } - ); - - # contact email address - push @cust_main, - grep $agentnums_href->{$_->agentnum}, #agent virt - grep $_, #skip contacts that don't have cust_main records - map $_->contact->cust_main, - qsearch( { - 'table' => 'contact_email', - 'hashref' => { 'emailaddress' => $search }, - } - ); + if ( $search =~ /@/ ) { #email address from cust_main_invoice and contact_email + + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'hashref' => { %options }, + 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ). + ' ( '. + join(' OR ', map "$_ = '$search'", + qw( dest emailaddress ) + ). + ' ) '. + " AND $agentnums_sql", #agent virtualization + 'addl_from' => ' left join cust_main_invoice using (custnum) left join cust_contact using (custnum) left join contact_email using (contactnum) ', + } ); # custnum search (also try agent_custid), with some tweaking options if your # legacy cust "numbers" have letters - } elsif ( $search =~ /^\s*(\d+)\s*$/ - || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+' - && $search =~ /^\s*(\w\w?\d+)\s*$/ - ) - || ( $conf->config('cust_main-custnum-display_special') - # it's not currently possible for special prefixes to contain - # digits, so just strip off any alphabetic prefix and match - # the rest to custnum - && $search =~ /^\s*[[:alpha:]]*(\d+)\s*$/ - ) - || ( $conf->exists('address1-search' ) - && $search =~ /^\s*(\d+\-?\w*)\s*$/ #i.e. 1234A or 9432-D - ) - ) + } elsif ( $search =~ /^\s*(\d+)\s*$/ + or ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+' + && $search =~ /^\s*(\w\w?\d+)\s*$/ + ) + or ( $conf->config('cust_main-agent_custid-format') eq 'd+-w' + && $search =~ /^\s*(\d+-\w)\s*$/ + ) + or ( $conf->config('cust_main-custnum-display_special') + # it's not currently possible for special prefixes to contain + # digits, so just strip off any alphabetic prefix and match + # the rest to custnum + && $search =~ /^\s*[[:alpha:]]*(\d+)\s*$/ + ) + or ( $conf->exists('address1-search' ) + && $search =~ /^\s*(\d+\-?\w*)\s*$/ #i.e. 1234A or 9432-D + ) + ) { my $num = $1; @@ -278,8 +265,8 @@ sub smart_search { } elsif ( ! $NameParse->parse($value) ) { my %name = $NameParse->components; - $first = $name{'given_name_1'} || $name{'initials_1'}; #wtf NameParse, Ed? - $last = $name{'surname_1'}; + $first = lc($name{'given_name_1'}) || $name{'initials_1'}; #wtf NameParse, Ed? + $last = lc($name{'surname_1'}); } @@ -289,28 +276,18 @@ sub smart_search { #exact my $sql = scalar(keys %options) ? ' AND ' : ' WHERE '; - $sql .= "( LOWER(cust_main.last) = $q_last AND LOWER(cust_main.first) = $q_first )"; + $sql .= "( (LOWER(cust_main.last) = $q_last AND LOWER(cust_main.first) = $q_first) + OR (LOWER(contact.last) = $q_last AND LOWER(contact.first) = $q_first) )"; - #cust_main + #cust_main and contacts push @cust_main, qsearch( { 'table' => 'cust_main', - 'hashref' => \%options, + 'select' => 'cust_main.*, cust_contact.*, contact.contactnum, contact.last as contact_last, contact.first as contact_first, contact.title', + 'hashref' => { %options }, 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization + 'addl_from' => ' left join cust_contact on cust_main.custnum = cust_contact.custnum left join contact using (contactnum) ', } ); - #contacts - push @cust_main, - grep $agentnums_href->{$_->agentnum}, #agent virt - grep $_, #skip contacts that don't have cust_main records - map $_->cust_main, - qsearch( { - 'table' => 'contact', - 'hashref' => { 'first' => $first, - 'last' => $last, - }, - } - ); - # or it just be something that was typed in... (try that in a sec) } @@ -323,7 +300,9 @@ sub smart_search { OR LOWER(cust_main.last) = $q_value OR LOWER(cust_main.company) = $q_value OR LOWER(cust_main.ship_company) = $q_value - "; + OR LOWER(contact.first) = $q_value + OR LOWER(contact.last) = $q_value + )"; #address1 (yes, it's a kludge) $sql .= " OR EXISTS ( @@ -333,20 +312,12 @@ sub smart_search { )" if $conf->exists('address1-search'); - #contacts (look, another kludge) - $sql .= " OR EXISTS ( SELECT 1 FROM contact - WHERE ( LOWER(contact.first) = $q_value - OR LOWER(contact.last) = $q_value - ) - AND contact.custnum IS NOT NULL - AND contact.custnum = cust_main.custnum - ) - ) "; - push @cust_main, qsearch( { 'table' => 'cust_main', - 'hashref' => \%options, + 'select' => 'cust_main.*, cust_contact.*, contact.contactnum, contact.last as contact_last, contact.first as contact_first, contact.title', + 'hashref' => { %options }, 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization + 'addl_from' => 'left join cust_contact on cust_main.custnum = cust_contact.custnum left join contact using (contactnum) ', } ); #no exact match, trying substring/fuzzy @@ -872,10 +843,24 @@ sub search { ## # with referrals ## - if ( $params->{'with_referrals'} ) { + if ( $params->{with_referrals} =~ /^\s*(\d+)\s*$/ ) { + + my $n = $1; + + # referral status + my $and_status = ''; + if ( grep { $params->{referral_status} eq $_ } FS::cust_main->statuses() ) { + my $method = $params->{referral_status}. '_sql'; + $and_status = ' AND '. FS::cust_main->$method(); + $and_status =~ s/ cust_main\./ referred_cust_main./g; + } + push @where, - ' EXISTS ( SELECT 1 FROM cust_main AS referred_cust_main - WHERE cust_main.custnum = referred_cust_main.referral_custnum )'; + " $n <= ( SELECT COUNT(*) FROM cust_main AS referred_cust_main + WHERE cust_main.custnum = referred_cust_main.referral_custnum + $and_status + )"; + } ## @@ -1064,8 +1049,48 @@ sub search { FS::UI::Web::cust_sql_fields($params->{'cust_fields'}), ); - my(@extra_headers) = (); - my(@extra_fields) = (); + my @extra_headers = (); + my @extra_fields = (); + my @extra_sort_fields = (); + + ## search contacts + if ($params->{'contacts'}) { + my $contact_params = $params->{'contacts'}; + + $addl_from .= + ' LEFT JOIN cust_contact ON ( cust_main.custnum = cust_contact.custnum ) '; + + if ($contact_params->{'contacts_firstname'} || $contact_params->{'contacts_lastname'}) { + $addl_from .= ' LEFT JOIN contact ON ( cust_contact.contactnum = contact.contactnum ) '; + my $first_query = " AND contact.first = '" . $contact_params->{'contacts_firstname'} . "'" + unless !$contact_params->{'contacts_firstname'}; + my $last_query = " AND contact.last = '" . $contact_params->{'contacts_lastname'} . "'" + unless !$contact_params->{'contacts_lastname'}; + $extra_sql .= " AND ( '1' $first_query $last_query )"; + } + + if ($contact_params->{'contacts_email'}) { + $addl_from .= ' LEFT JOIN contact_email ON ( cust_contact.contactnum = contact_email.contactnum ) '; + $extra_sql .= " AND ( contact_email.emailaddress = '" . $contact_params->{'contacts_email'} . "' )"; + } + + if ($contact_params->{'contacts_homephone'} || $contact_params->{'contacts_workphone'} || $contact_params->{'contacts_mobilephone'}) { + $addl_from .= ' LEFT JOIN contact_phone ON ( cust_contact.contactnum = contact_phone.contactnum ) '; + my $contacts_mobilephone; + foreach my $phone (qw( contacts_homephone contacts_workphone contacts_mobilephone )) { + (my $num = $contact_params->{$phone}) =~ s/\W//g; + if ( $num =~ /^1?(\d{3})(\d{3})(\d{4})(\d*)$/ ) { $contact_params->{$phone} = "$1$2$3"; } + } + my $home_query = " AND ( contact_phone.phonetypenum = '2' AND contact_phone.phonenum = '" . $contact_params->{'contacts_homephone'} . "' )" + unless !$contact_params->{'contacts_homephone'}; + my $work_query = " AND ( contact_phone.phonetypenum = '1' AND contact_phone.phonenum = '" . $contact_params->{'contacts_workphone'} . "' )" + unless !$contact_params->{'contacts_workphone'}; + my $mobile_query = " AND ( contact_phone.phonetypenum = '3' AND contact_phone.phonenum = '" . $contact_params->{'contacts_mobilephone'} . "' )" + unless !$contact_params->{'contacts_mobilephone'}; + $extra_sql .= " AND ( '1' $home_query $work_query $mobile_query )"; + } + + } if ($params->{'flattened_pkgs'}) { @@ -1110,6 +1135,7 @@ sub search { my $p = $a[!.--$headercount. q!]; $p; };!; + unshift @extra_sort_fields, ''; } } @@ -1125,21 +1151,23 @@ sub search { unshift @extra_headers, 'Referrals'; unshift @extra_fields, 'num_referrals'; + unshift @extra_sort_fields, 'num_referrals'; } my $select = join(', ', @select); my $sql_query = { - 'table' => 'cust_main', - 'select' => $select, - 'addl_from' => $addl_from, - 'hashref' => {}, - 'extra_sql' => $extra_sql, - 'order_by' => $orderby, - 'count_query' => $count_query, - 'extra_headers' => \@extra_headers, - 'extra_fields' => \@extra_fields, + 'table' => 'cust_main', + 'select' => $select, + 'addl_from' => $addl_from, + 'hashref' => {}, + 'extra_sql' => $extra_sql, + 'order_by' => $orderby, + 'count_query' => $count_query, + 'extra_headers' => \@extra_headers, + 'extra_fields' => \@extra_fields, + 'extra_sort_fields' => \@extra_sort_fields, }; $sql_query; diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index 195574627..8b6569a74 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -262,6 +262,17 @@ sub cust_statuscolor { : '000000'; } +=item agent_name + +=cut + +sub agent_name { + my $self = shift; + $self->cust_linked + ? $self->cust_main->agent_name + : $self->cust_unlinked_msg; +} + =item prospect_sql =item active_sql @@ -397,14 +408,21 @@ use Digest::SHA qw(sha1); # for duplicate checking sub email_search_result { my($class, $param) = @_; + my $conf = FS::Conf->new; + my $send_to_domain = $conf->config('email-to-voice_domain'); + my $msgnum = $param->{msgnum}; my $from = delete $param->{from}; my $subject = delete $param->{subject}; my $html_body = delete $param->{html_body}; my $text_body = delete $param->{text_body}; my $to_contact_classnum = delete $param->{to_contact_classnum}; + my $emailtovoice_name = delete $param->{emailtovoice_contact}; + my $error = ''; + my $to = $emailtovoice_name . '@' . $send_to_domain unless !$emailtovoice_name; + my $job = delete $param->{'job'} or die "email_search_result must run from the job queue.\n"; @@ -465,10 +483,14 @@ sub email_search_result { next; # unlinked object; nothing else we can do } +my %to = {}; +if ($to) { $to{'to'} = $to; } + my $cust_msg = $msg_template->prepare( 'cust_main' => $cust_main, 'object' => $obj, 'to_contact_classnum' => $to_contact_classnum, + %to, ); # For non-cust_main searches, we avoid duplicates based on message diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index b256daedc..c70a6795f 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -2622,9 +2622,9 @@ sub change { foreach my $old_discount ($self->cust_pkg_discount_active) { # don't remove the old discount, we may still need to bill that package. my $new_discount = new FS::cust_pkg_discount { - 'pkgnum' => $cust_pkg->pkgnum, - 'discountnum' => $old_discount->discountnum, - 'months_used' => $old_discount->months_used, + 'pkgnum' => $cust_pkg->pkgnum, + map { $_ => $old_discount->$_() } + qw( discountnum months_used end_date usernum setuprecur ), }; $error = $new_discount->insert; if ( $error ) { diff --git a/FS/FS/cust_pkg/Import.pm b/FS/FS/cust_pkg/Import.pm index 63a9909e5..b827bcfe1 100644 --- a/FS/FS/cust_pkg/Import.pm +++ b/FS/FS/cust_pkg/Import.pm @@ -105,6 +105,7 @@ my %formatfields = ( 'svc_phone' => [qw( countrycode phonenum sip_password pin )], 'svc_external' => [qw( id title )], 'location' => [qw( address1 address2 city state zip country )], + 'quan_price' => [qw( quantity setup_fee recur_fee invoice_details )], ); sub _formatfields { @@ -116,8 +117,11 @@ my %import_options = ( 'preinsert_callback' => sub { my($record, $param) = @_; - my @location_params = grep /^location\./, keys %$param; + + my @location_params = grep { /^location\./ && length($param->{$_}) } + keys %$param; if (@location_params) { +warn join('-', @location_params); my $cust_location = FS::cust_location->new({ 'custnum' => $record->custnum, }); @@ -130,12 +134,53 @@ my %import_options = ( return "error creating location: $error" if $error; $record->set('locationnum', $cust_location->locationnum); } + + $record->quantity( $param->{'quan_price.quantity'} ) + if $param->{'quan_price.quantity'} > 0; + + my $s = $param->{'quan_price.setup_fee'}; + my $r = $param->{'quan_price.recur_fee'}; + my $part_pkg = $record->part_pkg; + if ( ( length($s) && $s != $part_pkg->option('setup_fee') ) + or ( length($r) && $r != $part_pkg->option('recur_fee') ) + ) + { + my $custom_part_pkg = $part_pkg->clone; + $custom_part_pkg->disabled('Y'); + my %options = $part_pkg->options; + $options{'setup_fee'} = $s if length($s); + $options{'recur_fee'} = $r if length($r); + my $error = $custom_part_pkg->insert( options=>\%options ); + return "error customizing package: $error" if $error; + $record->pkgpart( $custom_part_pkg->pkgpart ); + } + + ''; }, 'postinsert_callback' => sub { my( $record, $param ) = @_; + if ( $param->{'quan_price.invoice_details'} ) { + + my $weight = 0; + foreach my $detail (split(/\|/, $param->{'quan_price.invoice_details'})) { + + my $cust_pkg_detail = new FS::cust_pkg_detail { + 'pkgnum' => $record->pkgnum, + 'detail' => $detail, + 'detailtype' => 'I', + 'weight' => $weight++, + }; + + my $error = $cust_pkg_detail->insert; + return "error inserting invoice detail: $error" if $error; + + } + + } + my $formatfields = _formatfields; foreach my $svc_x ( grep /^svc/, keys %$formatfields ) { @@ -283,17 +328,20 @@ sub batch_import { }; } - my $formatfields = _formatfields(); + my @formats = split /-/, $format; + foreach my $f (@formats){ - die "unknown format $format" unless $formatfields->{$format}; + my $formatfields = _formatfields(); + die "unknown format $format" unless $formatfields->{$f}; - foreach my $field ( @{ $formatfields->{$format} } ) { + foreach my $field ( @{ $formatfields->{$f} } ) { - push @fields, sub { - my( $self, $value, $conf, $param ) = @_; - $param->{"$format.$field"} = $value; - }; + push @fields, sub { + my( $self, $value, $conf, $param ) = @_; + $param->{"$f.$field"} = $value; + }; + } } $opt->{'fields'} = \@fields; diff --git a/FS/FS/part_event.pm b/FS/FS/part_event.pm index 1c2389989..ded57154f 100644 --- a/FS/FS/part_event.pm +++ b/FS/FS/part_event.pm @@ -582,9 +582,11 @@ sub actions { my( $class, $eventtable ) = @_; ( map { $_ => $actions{$_} } - sort { $actions{$a}->{'default_weight'}<=>$actions{$b}->{'default_weight'} } - # || $actions{$a}->{'description'} cmp $actions{$b}->{'description'} } - $class->all_actions( $eventtable ) + sort { + $actions{$a}->{'default_weight'} <=> $actions{$b}->{'default_weight'} + || $actions{$a}->{'description'} cmp $actions{$b}->{'description'} + } + $class->all_actions( $eventtable ) ); } diff --git a/FS/FS/part_event/Action/notice_to_emailtovoice.pm b/FS/FS/part_event/Action/notice_to_emailtovoice.pm new file mode 100644 index 000000000..3eaa73850 --- /dev/null +++ b/FS/FS/part_event/Action/notice_to_emailtovoice.pm @@ -0,0 +1,84 @@ +package FS::part_event::Action::notice_to_emailtovoice; + +use strict; +use base qw( FS::part_event::Action ); +use FS::Record qw( qsearchs ); +use FS::msg_template; +use FS::Conf; + +sub description { 'Email a email to voice notice'; } + +sub eventtable_hashref { + { + 'cust_main' => 1, + 'cust_bill' => 1, + 'cust_pkg' => 1, + 'cust_pay' => 1, + 'cust_pay_batch' => 1, + 'cust_statement' => 1, + 'svc_acct' => 1, + }; +} + +sub option_fields { + + #my $conf = new FS::Conf; + #my $to_domain = $conf->config('email-to-voice_domain'); + +( + 'to_name' => { 'label' => 'Address To', + 'type' => 'select', + 'options' => [ 'mobile', 'fax', 'daytime' ], + 'option_labels' => { 'mobile' => 'Mobile Phone #', + 'fax' => 'Fax #', + 'daytime' => 'Day Time #', + }, + 'post_field_label' => ' Make sure you have setup your email-to-voice_domain config option in your Configuration settings.', + }, + + 'msgnum' => { 'label' => 'Template', + 'type' => 'select-table', + 'table' => 'msg_template', + 'name_col' => 'msgname', + 'hashref' => { disabled => '' }, + 'disable_empty' => 1, + }, + ); + +} + +sub default_weight { 56; } #? + +sub do_action { + my( $self, $object ) = @_; + + my $conf = new FS::Conf; + my $to_domain = $conf->config('email-to-voice_domain') + or die "Can't send notice with out send-to-domain, being set in global config \n"; + + my $cust_main = $self->cust_main($object); + + my $msgnum = $self->option('msgnum'); + my $name = $self->option('to_name'); + + my $msg_template = qsearchs('msg_template', { 'msgnum' => $msgnum } ) + or die "Template $msgnum not found"; + + my $to_name = $cust_main->$name + or die "Can't send notice with out " . $cust_main->$name . " number set"; + + ## remove - from phone number + $to_name =~ s/-//g; + + #my $to = $to_name . '@' . $self->option('to_domain'); + my $to = $to_name . '@' . $to_domain; + + $msg_template->send( + 'to' => $to, + 'cust_main' => $cust_main, + 'object' => $object, + ); + +} + +1; diff --git a/FS/FS/part_event/Condition/referred_cust_base_recur.pm b/FS/FS/part_event/Condition/referred_cust_base_recur.pm new file mode 100644 index 000000000..4ad4da763 --- /dev/null +++ b/FS/FS/part_event/Condition/referred_cust_base_recur.pm @@ -0,0 +1,51 @@ +package FS::part_event::Condition::referred_cust_base_recur; +use base qw( FS::part_event::Condition ); + +use List::Util qw( sum ); + +sub description { 'Referred customers recurring per month'; } + +sub option_fields { + ( + 'recur_times' => { label => 'Base recurring per month of referred customers is at least this many times base recurring per month of referring customer', + type => 'text', + value => '1', + }, + 'if_pkg_class' => { label => 'Only considering package of class', + type => 'select-pkg_class', + multiple => 1, + }, + ); +} + +sub condition { + my($self, $object, %opt) = @_; + + my $cust_main = $self->cust_main($object); + my @cust_pkg = $cust_main->billing_pkgs; + + my @referral_cust_main = $cust_main->referral_cust_main; + my @referral_cust_pkg = map $_->billing_pkgs, @referral_cust_main; + + my $if_pkg_class = $self->option('if_pkg_class') || {}; + if ( keys %$if_pkg_class ) { + @cust_pkg = grep $_->part_pkg->classnum, @cust_pkg; + @referral_cust_pkg = grep $_->part_pkg->classnum, @referral_cust_pkg; + } + + return 0 unless @cust_pkg && @referral_cust_pkg; + + my $recur = sum map $_->part_pkg->base_recur_permonth, @cust_pkg; + my $ref_recur = sum map $_->part_pkg->base_recur_permonth, @referral_cust_pkg; + + $ref_recur >= $self->option('recur_times') * $recur; +} + +#sub condition_sql { +# my( $class, $table ) = @_; +# +# #XXX TODO: this optimization +#} + +1; + diff --git a/FS/FS/part_export/acct_http.pm b/FS/FS/part_export/acct_http.pm index 414350bba..b84e008b9 100644 --- a/FS/FS/part_export/acct_http.pm +++ b/FS/FS/part_export/acct_http.pm @@ -69,10 +69,16 @@ tie %options, 'Tie::IxHash', 'no_machine' => 1, 'notes' => <<'END' Send an HTTP or HTTPS GET or POST to the specified URL on account addition, -modification and deletion. For HTTPS support, -Crypt::SSLeay -or IO::Socket::SSL -is required. +modification and deletion. +

Each "Data" option takes a list of name value pairs on successive +lines. +

  • name is an unquoted, literal string without whitespace.
  • +
  • value is a Perl expression that will be evaluated. If it's a +literal string, it must be quoted. This expression has access to the +svc_acct object as '$svc_x' (or '$new' and '$old' in "Replace Data") +and the customer record as '$cust_main'.
+If "Success Regexp" is specified, the response from the server will be +tested against it to determine if the export succeeded.

END ); diff --git a/FS/FS/part_export/broadband_http.pm b/FS/FS/part_export/broadband_http.pm index 097ff34c3..cc1e45071 100644 --- a/FS/FS/part_export/broadband_http.pm +++ b/FS/FS/part_export/broadband_http.pm @@ -58,15 +58,12 @@ tie %options, 'Tie::IxHash', %info = ( 'svc' => 'svc_broadband', - 'desc' => 'Send an HTTP or HTTPS GET or POST request, for accounts.', + 'desc' => 'Send an HTTP or HTTPS GET or POST request, for wireless broadband services.', 'options' => \%options, 'no_machine' => 1, 'notes' => <<'END' -

Send an HTTP or HTTPS GET or POST to the specified URL on account addition, -modification and deletion. For HTTPS support, -Crypt::SSLeay -or IO::Socket::SSL -is required.

+

Send an HTTP or HTTPS GET or POST to the specified URL on wireless broadband service addition, +modification and deletion.

Each "Data" option takes a list of name value pairs on successive lines.

  • name is an unquoted, literal string without whitespace.
  • diff --git a/FS/FS/part_export/broadband_shellcommands.pm b/FS/FS/part_export/broadband_shellcommands.pm index 44280a200..d3e495c45 100644 --- a/FS/FS/part_export/broadband_shellcommands.pm +++ b/FS/FS/part_export/broadband_shellcommands.pm @@ -70,7 +70,18 @@ sub _export_command { my $command = $self->option($action); return '' if $command =~ /^\s*$/; - #set variables for the command + my $command_string = $self->_export_subvars( $svc_broadband, $command ); + + $self->shellcommands_queue( $svc_broadband->svcnum, + user => $self->option('user')||'root', + host => $self->machine, + command => $command_string, + ); +} + +sub _export_subvars { + my( $self, $svc_broadband, $command ) = @_; + no strict 'vars'; { no strict 'refs'; @@ -85,20 +96,25 @@ sub _export_command { $locationnum = $cust_pkg ? $cust_pkg->locationnum : ''; $custnum = $cust_pkg ? $cust_pkg->custnum : ''; - #done setting variables for the command + eval(qq("$command")); +} - $self->shellcommands_queue( $svc_broadband->svcnum, +sub _export_replace { + my($self, $new, $old ) = (shift, shift, shift); + my $command = $self->option('replace'); + + my $command_string = $self->_export_subvars_replace( $new, $old, $command ); + + $self->shellcommands_queue( $new->svcnum, user => $self->option('user')||'root', host => $self->machine, - command => eval(qq("$command")), + command => $command_string, ); } -sub _export_replace { - my($self, $new, $old ) = (shift, shift, shift); - my $command = $self->option('replace'); +sub _export_subvars_replace { + my( $self, $new, $old, $command ) = @_; - #set variable for the command no strict 'vars'; { no strict 'refs'; @@ -120,15 +136,10 @@ sub _export_replace { $new_locationnum = $new_cust_pkg ? $new_cust_pkg->locationnum : ''; $new_custnum = $new_cust_pkg ? $new_cust_pkg->custnum : ''; - #done setting variables for the command - - $self->shellcommands_queue( $new->svcnum, - user => $self->option('user')||'root', - host => $self->machine, - command => eval(qq("$command")), - ); + eval(qq("$command")); } + #a good idea to queue anything that could fail or take any time sub shellcommands_queue { my( $self, $svcnum ) = (shift, shift); diff --git a/FS/FS/part_export/broadband_shellcommands_expect.pm b/FS/FS/part_export/broadband_shellcommands_expect.pm new file mode 100644 index 000000000..ec525d38a --- /dev/null +++ b/FS/FS/part_export/broadband_shellcommands_expect.pm @@ -0,0 +1,19 @@ +package FS::part_export::broadband_shellcommands_expect; +use base qw( FS::part_export::shellcommands_expect ); + +use strict; +use FS::part_export::broadband_shellcommands; + +our %info = %FS::part_export::shellcommands_expect::info; +$info{'svc'} = 'svc_broadband'; +$info{'desc'} = 'Real time export via remote SSH, with interactive ("Expect"-like) scripting, for svc_broadband services'; + +sub _export_subvars { + FS::part_export::broadband_shellcommands::_export_subvars(@_) +} + +sub _export_subvars_replace { + FS::part_export::broadband_shellcommands::_export_subvars_replace(@_) +} + +1; diff --git a/FS/FS/part_export/dsl_http.pm b/FS/FS/part_export/dsl_http.pm new file mode 100644 index 000000000..ac61ec88d --- /dev/null +++ b/FS/FS/part_export/dsl_http.pm @@ -0,0 +1,72 @@ +package FS::part_export::dsl_http; +use base qw( FS::part_export::http ); + +use Tie::IxHash; + +tie our %options, 'Tie::IxHash', + 'method' => { label =>'Method', + type =>'select', + #options =>[qw(POST GET)], + options =>[qw(POST)], + default =>'POST' }, + 'url' => { label => 'URL', default => 'http://', }, + 'ssl_no_verify' => { label => 'Skip SSL certificate validation', + type => 'checkbox', + }, + 'insert_data' => { + label => 'Insert data', + type => 'textarea', + default => join("\n", + ), + }, + 'delete_data' => { + label => 'Delete data', + type => 'textarea', + default => join("\n", + ), + }, + 'replace_data' => { + label => 'Replace data', + type => 'textarea', + default => join("\n", + ), + }, + 'suspend_data' => { + label => 'Suspend data', + type => 'textarea', + default => join("\n", + ), + }, + 'unsuspend_data' => { + label => 'Unsuspend data', + type => 'textarea', + default => join("\n", + ), + }, + 'success_regexp' => { + label => 'Success Regexp', + default => '', + }, +; + +%info = ( + 'svc' => 'svc_dsl', + 'desc' => 'Send an HTTP or HTTPS GET or POST request, for DSL services.', + 'options' => \%options, + 'no_machine' => 1, + 'notes' => <<'END' +Send an HTTP or HTTPS GET or POST to the specified URL on account addition, +modification and deletion. +

    Each "Data" option takes a list of name value pairs on successive +lines. +

    • name is an unquoted, literal string without whitespace.
    • +
    • value is a Perl expression that will be evaluated. If it's a +literal string, it must be quoted. This expression has access to the +svc_dsl object as '$svc_x' (or '$new' and '$old' in "Replace Data") +and the customer record as '$cust_main'.
    +If "Success Regexp" is specified, the response from the server will be +tested against it to determine if the export succeeded.

    +END +); + +1; diff --git a/FS/FS/part_export/fiber_http.pm b/FS/FS/part_export/fiber_http.pm new file mode 100644 index 000000000..38b23c44e --- /dev/null +++ b/FS/FS/part_export/fiber_http.pm @@ -0,0 +1,73 @@ +package FS::part_export::fiber_http; +use base qw( FS::part_export::http ); + +use Tie::IxHash; + +tie our %options, 'Tie::IxHash', + 'method' => { label =>'Method', + type =>'select', + #options =>[qw(POST GET)], + options =>[qw(POST)], + default =>'POST' }, + 'url' => { label => 'URL', default => 'http://', }, + 'ssl_no_verify' => { label => 'Skip SSL certificate validation', + type => 'checkbox', + }, + 'insert_data' => { + label => 'Insert data', + type => 'textarea', + default => join("\n", + ), + }, + 'delete_data' => { + label => 'Delete data', + type => 'textarea', + default => join("\n", + ), + }, + 'replace_data' => { + label => 'Replace data', + type => 'textarea', + default => join("\n", + ), + }, + 'suspend_data' => { + label => 'Suspend data', + type => 'textarea', + default => join("\n", + ), + }, + 'unsuspend_data' => { + label => 'Unsuspend data', + type => 'textarea', + default => join("\n", + ), + }, + 'success_regexp' => { + label => 'Success Regexp', + default => '', + }, +; + +%info = ( + 'svc' => 'svc_fiber', + 'desc' => 'Send an HTTP or HTTPS GET or POST request, for FTTx services.', + 'options' => \%options, + 'no_machine' => 1, + 'notes' => <<'END' +Send an HTTP or HTTPS GET or POST to the specified URL on account addition, +modification and deletion. +

    Each "Data" option takes a list of name value pairs on successive +lines. +

    • name is an unquoted, literal string without whitespace.
    • +
    • value is a Perl expression that will be evaluated. If it's a +literal string, it must be quoted. This expression has access to the +svc_fiber object as '$svc_x' (or '$new' and '$old' in "Replace Data") +and the customer record as '$cust_main'.
    +If "Success Regexp" is specified, the response from the server will be +tested against it to determine if the export succeeded.

    +END +); + +1; + diff --git a/FS/FS/part_export/http.pm b/FS/FS/part_export/http.pm index 42a35cb07..43ccfc525 100644 --- a/FS/FS/part_export/http.pm +++ b/FS/FS/part_export/http.pm @@ -59,14 +59,21 @@ tie %options, 'Tie::IxHash', %info = ( 'svc' => 'svc_domain', - 'desc' => 'Send an HTTP or HTTPS GET or POST request', + 'desc' => 'Send an HTTP or HTTPS GET or POST request, for domains1', 'options' => \%options, 'no_machine' => 1, 'notes' => <<'END' -Send an HTTP or HTTPS GET or POST to the specified URL. For HTTPS support, -Crypt::SSLeay -or IO::Socket::SSL -is required. +Send an HTTP or HTTPS GET or POST to the specified URL on domain addition, +modification and deletion. +

    Each "Data" option takes a list of name value pairs on successive +lines. +

    • name is an unquoted, literal string without whitespace.
    • +
    • value is a Perl expression that will be evaluated. If it's a +literal string, it must be quoted. This expression has access to the +svc_domain object as '$svc_x' (or '$new' and '$old' in "Replace Data") +and the customer record as '$cust_main'.
    +If "Success Regexp" is specified, the response from the server will be +tested against it to determine if the export succeeded.

    END ); diff --git a/FS/FS/part_export/pbxware.pm b/FS/FS/part_export/pbxware.pm index 4373e7ad5..9458fca0c 100644 --- a/FS/FS/part_export/pbxware.pm +++ b/FS/FS/part_export/pbxware.pm @@ -137,7 +137,7 @@ sub import_cdrs { # page's IDs or something. my $uniqueid = md5_hex(join(',',@$row)); if ( FS::cdr->row_exists('uniqueid = ?', $uniqueid) ) { - warn "skipped duplicate row in page $page\n" if $DEBUG > 1; + warn "skipped duplicate row in page $page\n" if $DEBUG; next CDR; } @@ -186,7 +186,7 @@ local $ENV{'PERL_LWP_SSL_VERIFY_HOSTNAME'} = 0; ] ); warn "$me $method\n" if $DEBUG; - warn $request->as_string."\n" if $DEBUG > 1; + warn $request->as_string."\n" if $DEBUG; my $ua = LWP::UserAgent->new; my $response = $ua->request($request); diff --git a/FS/FS/part_export/shellcommands.pm b/FS/FS/part_export/shellcommands.pm index 647dc5f4d..775af17ae 100644 --- a/FS/FS/part_export/shellcommands.pm +++ b/FS/FS/part_export/shellcommands.pm @@ -4,6 +4,7 @@ use vars qw(@ISA %info); use Tie::IxHash; use Date::Format; use String::ShellQuote; +use Net::OpenSSH; use FS::part_export; use FS::Record qw( qsearch qsearchs ); @@ -296,7 +297,7 @@ sub _export_command_or_super { } else { $self->_export_command($action, @_); } -}; +} sub _export_command { my ( $self, $action, $svc_acct) = (shift, shift, shift); @@ -305,6 +306,41 @@ sub _export_command { return '' if $command =~ /^\s*$/; my $stdin = $self->option($action."_stdin"); + my( $command_string, $stdin_string ) = + $self->_export_subvars( $svc_acct, $command, $stdin ); + + $self->ssh_or_queue( $svc_acct, $command_string, $stdin_string ); +} + +sub ssh_or_queue { + my( $self, $svc_acct, $command_string, $stdin_string ) = @_; + + my @ssh_cmd_args = ( + user => $self->option('user') || 'root', + host => $self->svc_machine($svc_acct), + command => $command_string, + stdin_string => $stdin_string, + ignored_errors => $self->option('ignored_errors') || '', + ignore_all_errors => $self->option('ignore_all_errors'), + fail_on_output => $self->option('fail_on_output'), + ); + + if ( $self->option($action. '_no_queue') ) { + # discard return value just like freeside-queued. + eval { ssh_cmd(@ssh_cmd_args) }; + $error = $@; + $error = $error->full_message if ref $error; # Exception::Class::Base + return $error. + ' ('. $self->exporttype. ' to '. $self->svc_machine($svc_acct). ')' + if $error; + } else { + $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args ); + } +} + +sub _export_subvars { + my( $self, $svc_acct, $command, $stdin ) = @_; + no strict 'vars'; { no strict 'refs'; @@ -412,27 +448,7 @@ sub _export_command { my $command_string = eval(qq("$command")); return "error filling in command: $@" if $@; - my @ssh_cmd_args = ( - user => $self->option('user') || 'root', - host => $self->svc_machine($svc_acct), - command => $command_string, - stdin_string => $stdin_string, - ignored_errors => $self->option('ignored_errors') || '', - ignore_all_errors => $self->option('ignore_all_errors'), - fail_on_output => $self->option('fail_on_output'), - ); - - if ( $self->option($action. '_no_queue') ) { - # discard return value just like freeside-queued. - eval { ssh_cmd(@ssh_cmd_args) }; - $error = $@; - $error = $error->full_message if ref $error; # Exception::Class::Base - return $error. - ' ('. $self->exporttype. ' to '. $self->svc_machine($svc_acct). ')' - if $error; - } else { - $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args ); - } + ( $command_string, $stdin_string ); } sub _export_replace { @@ -440,6 +456,16 @@ sub _export_replace { my $command = $self->option('usermod'); return '' if $command =~ /^\s*$/; my $stdin = $self->option('usermod_stdin'); + + my( $command_string, $stdin_string ) = + $self->_export_subvars_replace( $new, $old, $command, $stdin ); + + $self->ssh_or_queue( $new, $command_string, $stdin_string ); +} + +sub _export_subvars_replace { + my( $self, $new, $old, $command, $stdin ) = @_; + no strict 'vars'; { no strict 'refs'; @@ -511,27 +537,7 @@ sub _export_replace { my $command_string = eval(qq("$command")); - my @ssh_cmd_args = ( - user => $self->option('user') || 'root', - host => $self->svc_machine($new), - command => $command_string, - stdin_string => $stdin_string, - ignored_errors => $self->option('ignored_errors') || '', - ignore_all_errors => $self->option('ignore_all_errors'), - fail_on_output => $self->option('fail_on_output'), - ); - - if($self->option('usermod_no_queue')) { - # discard return value just like freeside-queued. - eval { ssh_cmd(@ssh_cmd_args) }; - $error = $@; - $error = $error->full_message if ref $error; # Exception::Class::Base - return $error. ' ('. $self->exporttype. ' to '. $self->svc_machine($new). ')' - if $error; - } - else { - $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args ); - } + ( $command_string, $stdin_string ); } #a good idea to queue anything that could fail or take any time @@ -545,7 +551,6 @@ sub shellcommands_queue { } sub ssh_cmd { #subroutine, not method - use Net::OpenSSH; my $opt = { @_ }; open my $def_in, '<', '/dev/null' or die "unable to open /dev/null\n"; my $ssh = Net::OpenSSH->new( diff --git a/FS/FS/part_export/shellcommands_expect.pm b/FS/FS/part_export/shellcommands_expect.pm new file mode 100644 index 000000000..c2a4118e2 --- /dev/null +++ b/FS/FS/part_export/shellcommands_expect.pm @@ -0,0 +1,128 @@ +package FS::part_export::shellcommands_expect; +use base qw( FS::part_export::shellcommands ); + +use strict; +use Tie::IxHash; +use Net::OpenSSH; +use Expect; +#use FS::Record qw( qsearch qsearchs ); + +tie my %options, 'Tie::IxHash', + 'user' => { label =>'Remote username', default=>'root' }, + 'useradd' => { label => 'Insert commands', type => 'textarea', }, + 'userdel' => { label => 'Delete commands', type => 'textarea', }, + 'usermod' => { label => 'Modify commands', type => 'textarea', }, + 'suspend' => { label => 'Suspend commands', type => 'textarea', }, + 'unsuspend' => { label => 'Unsuspend commands', type => 'textarea', }, + 'debug' => { label => 'Enable debugging', + type => 'checkbox', + value => 1, + }, +; + +our %info = ( + 'svc' => 'svc_acct', + 'desc' => 'Real time export via remote SSH, with interactive ("Expect"-like) scripting, for svc_acct services', + 'options' => \%options, + 'notes' => q[ +Interactively run commands via SSH in a remote terminal, like "Expect". In +most cases, you probably want a regular shellcommands (or broadband_shellcommands, etc.) export instead, unless +you have a specific need to interact with a terminal-based interface in an +"Expect"-like fashion. +

    + +Each line specifies a string to match and a command to +run after that string is found, separated by the first space. For example, to +run "exit" after a prompt ending in "#" is sent, "# exit". You will need to +setup SSH for unattended operation. +

    + +In commands, all variable substitutions of the regular shellcommands (or +broadband_shellcommands, etc.) export are available (use a backslash to escape +a literal $). +] +); + +sub _export_command { + my ( $self, $action, $svc_acct) = (shift, shift, shift); + my @lines = split("\n", $self->option($action) ); + + return '' unless @lines; + + my @commands = (); + foreach my $line (@lines) { + my($match, $command) = split(' ', $line, 2); + my( $command_string ) = $self->_export_subvars( $svc_acct, $command, '' ); + push @commands, [ $match, $command_string ]; + } + + $self->shellcommands_expect_queue( $svc_acct->svcnum, @commands ); +} + +sub _export_replace { + my( $self, $new, $old ) = (shift, shift, shift); + my @lines = split("\n", $self->option('replace') ); + + return '' unless @lines; + + my @commands = (); + foreach my $line (@lines) { + my($match, $command) = split(' ', $line, 2); + my( $command_string ) = $self->_export_subvars_replace( $new, $old, $command, '' ); + push @commands, [ $match, $command_string ]; + } + + $self->shellcommands_expect_queue( $new->svcnum, @commands ); +} + +sub shellcommands_expect_queue { + my( $self, $svcnum, @commands ) = @_; + + my $queue = new FS::queue { + 'svcnum' => $svcnum, + 'job' => "FS::part_export::shellcommands_expect::ssh_expect", + }; + $queue->insert( + user => $self->option('user') || 'root', + host => $self->machine, + debug => $self->option('debug'), + commands => \@commands, + ); +} + +sub ssh_expect { #subroutine, not method + my $opt = { @_ }; + + my $dest = $opt->{'user'}.'@'.$opt->{'host'}; + + open my $def_in, '<', '/dev/null' or die "unable to open /dev/null\n"; + my $ssh = Net::OpenSSH->new( $dest, 'default_stdin_fh' => $def_in ); + # ignore_all_errors doesn't override SSH connection/auth errors-- + # probably correct + die "Couldn't establish SSH connection to $dest: ". $ssh->error + if $ssh->error; + + my ($pty, $pid) = $ssh->open2pty + or die "Couldn't start a remote terminal session"; + my $expect = Expect->init($pty); + #not useful #$expect->debug($opt->{debug} ? 3 : 0); + + foreach my $line ( @{ $opt->{commands} } ) { + my( $match, $command ) = @$line; + + warn "Waiting for '$match'\n" if $opt->{debug}; + + my $matched = $expect->expect(30, $match); + unless ( $matched ) { + my $err = "Never saw '$match'\n"; + warn $err; + die $err; + } + warn "Running '$command'\n" if $opt->{debug}; + $expect->send("$command\n"); + } + + ''; +} + +1; diff --git a/FS/FS/part_export/vitelity.pm b/FS/FS/part_export/vitelity.pm index 332e45712..d71553529 100644 --- a/FS/FS/part_export/vitelity.pm +++ b/FS/FS/part_export/vitelity.pm @@ -286,8 +286,8 @@ sub _export_insert { my $cust_main = $svc_phone->cust_svc->cust_pkg->cust_main; - return 'Customer company is required' - unless $cust_main->company; + #return 'Customer company is required' + # unless $cust_main->company; return 'Customer day phone (for contact, not porting) is required' unless $cust_main->daytime; @@ -306,7 +306,7 @@ sub _export_insert { 'partial' => 'no', 'wireless' => 'no', 'carrier' => $svc_phone->lnp_other_provider, - 'company' => $cust_main->company, + 'company' => $cust_main->company || $cust_main->contact, 'accnumber' => $svc_phone->lnp_other_provider_account, 'name' => $svc_phone->phone_name_or_cust, 'streetnumber' => $sa->{number}, @@ -410,6 +410,7 @@ sub e911_send { return '' if $self->option('disable_e911'); my %location = $svc_phone->location_hash; + $location{'zip'} =~ s/\-\d{4}$//; my %e911send = ( 'did' => $svc_phone->phonenum, 'name' => $svc_phone->phone_name_or_cust, @@ -425,7 +426,7 @@ sub e911_send { my $e911_result = $self->vitelity_command('e911send', %e911send); - unless ( $e911_result =~ /^(missingdata|invalid)/i ) { + unless ( $e911_result =~ /status=(missingdata|invalid)/i ) { warn "Vitelity response: $e911_result" if $self->option('debug'); return ''; } diff --git a/FS/FS/part_pkg/recur_Common.pm b/FS/FS/part_pkg/recur_Common.pm index 4ed83a46b..729fb6125 100644 --- a/FS/FS/part_pkg/recur_Common.pm +++ b/FS/FS/part_pkg/recur_Common.pm @@ -43,12 +43,17 @@ sub cutoff_day { my $recur_method = $self->option('recur_method',1) || 'anniversary'; my $cust_main = $cust_pkg->cust_main; - if ( $cust_main->force_prorate_day and $cust_main->prorate_day ) { - return ( $cust_main->prorate_day ); - } elsif ($recur_method eq 'prorate' || $recur_method eq 'subscription') { + return ( $cust_main->prorate_day ) + if $cust_main->prorate_day and ( $cust_main->force_prorate_day + || $recur_method eq 'prorate' + || $recur_method eq 'subscription' + ); - return split(/\s*,\s*/, $self->option('cutoff_day', 1) || '1'); - } + return split(/\s*,\s*/, $self->option('cutoff_day', 1) || '1') + if $recur_method eq 'prorate' + || $recur_method eq 'subscription'; + + return (); } sub calc_recur_Common { diff --git a/FS/FS/svc_circuit.pm b/FS/FS/svc_circuit.pm index eb0750c8b..7f49715b9 100644 --- a/FS/FS/svc_circuit.pm +++ b/FS/FS/svc_circuit.pm @@ -201,6 +201,7 @@ sub check { || $self->ut_textn('vendor_order_status') || $self->ut_ipn('endpoint_ip_addr') || $self->ut_textn('endpoint_mac_addr') + || $self->ut_textn('internal_circuit_id') ; # no canonical values yet for vendor_order_status or _type diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm index 5416ff565..8bc0c6ef3 100644 --- a/FS/FS/tax_rate.pm +++ b/FS/FS/tax_rate.pm @@ -2335,7 +2335,15 @@ EOF } +sub _upgrade_data { + my $class = shift; + my $sql = "UPDATE tax_rate SET data_vendor = 'compliance_solutions' WHERE data_vendor = 'compliance solutions'"; + + my $sth = dbh->prepare($sql) or die $DBI::errstr; + $sth->execute() or die $sth->errstr; + +} =back diff --git a/FS/FS/tax_rate_location.pm b/FS/FS/tax_rate_location.pm index d9646e4bc..e33859123 100644 --- a/FS/FS/tax_rate_location.pm +++ b/FS/FS/tax_rate_location.pm @@ -111,6 +111,7 @@ sub check { $self->ut_numbern('taxratelocationnum') || $self->ut_textn('data_vendor') || $self->ut_alpha('geocode') + || $self->ut_textn('district') || $self->ut_textn('city') || $self->ut_textn('county') || $self->ut_textn('state') @@ -118,16 +119,12 @@ sub check { ; return $error if $error; - my $t; - $t = qsearchs( 'tax_rate_location', - { disabled => '', - ( map { $_ => $self->$_ } qw( data_vendor geocode ) ), - }, - ) + my $t = ''; + $t = $self->existing_search unless $self->disabled; $t = $self->by_key( $self->taxratelocationnum ) - if ( !$t && $self->taxratelocationnum ); + if !$t && $self->taxratelocationnum; return "geocode ". $self->geocode. " already in use for this vendor" if ( $t && $t->taxratelocationnum != $self->taxratelocationnum ); @@ -153,11 +150,7 @@ record. sub find_or_insert { my $self = shift; - my $existing = qsearchs('tax_rate_location', { - disabled => '', - data_vendor => $self->data_vendor, - geocode => $self->geocode - }); + my $existing = $self->existing_search; if ($existing) { my $update = 0; foreach (qw(city county state country)) { @@ -176,6 +169,16 @@ sub find_or_insert { } } +sub existing_search { + my $self = shift; + + qsearchs( 'tax_rate_location', + { disabled => '', + map { $_ => $self->$_ } qw( data_vendor geocode ) + } + ); +} + =back =head1 CLASS METHODS @@ -392,6 +395,17 @@ sub batch_import { } +sub _upgrade_data { +#actually no, we want to leave those records behind now that they're giving us +# geo_state etc. +# my $class = shift; +# +# my $sql = "UPDATE tax_rate_location SET data_vendor = 'compliance_solutions' WHERE data_vendor = 'compliance solutions'"; +# +# my $sth = dbh->prepare($sql) or die $DBI::errstr; +# $sth->execute() or die $sth->errstr; +} + =head1 BUGS Currently somewhat specific to CCH supplied data. diff --git a/FS/MANIFEST b/FS/MANIFEST index f6a640066..81087dea7 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -510,6 +510,7 @@ t/class_Common.t FS/category_Common.pm t/category_Common.t FS/contact.pm +FS/contact/Import.pm t/contact.t FS/contact_phone.pm t/contact_phone.t @@ -872,3 +873,7 @@ FS/saved_search.pm t/saved_search.t FS/sector_coverage.pm t/sector_coverage.t +FS/access_user_session_log.pm +t/access_user_session_log.t +FS/access_user_session_log.pm +t/access_user_session_log.t diff --git a/FS/bin/freeside-voipinnovations-cdrimport b/FS/bin/freeside-voipinnovations-cdrimport index 23ea6bbdc..d64c8708f 100755 --- a/FS/bin/freeside-voipinnovations-cdrimport +++ b/FS/bin/freeside-voipinnovations-cdrimport @@ -4,7 +4,8 @@ use strict; use Getopt::Std; use Date::Format; use File::Temp 'tempdir'; -use Net::FTP; +use Net::SSLGlue::FTP; #at least until the Deb 9 transition is done, then + # regular Net::FTP has SSL support use FS::UID qw(adminsuidsetup datasrc dbh); use FS::cdr; use FS::cdr_batch; @@ -39,11 +40,14 @@ my $tempdir = tempdir( CLEANUP => !$opt_v ); my $format = 'voip_innovations'; my $hostname = 'customercdr.voipinnovations.com'; -my $ftp = Net::FTP->new($hostname, Debug => $opt_d) +my $ftp = Net::FTP->new($hostname, Passive => 1, Debug => $opt_d) or die "Can't connect to $hostname: $@\n"; +$ftp->starttls() + or die "TLS initialization failed: ". $ftp->message. "\n"; + $ftp->login($login, $password) - or die "Login failed: ".$ftp->message."\n"; + or die "Login failed: ". $ftp->message. "\n"; ### # get the file list @@ -51,7 +55,7 @@ $ftp->login($login, $password) warn "Retrieving directory listing\n" if $opt_v; -$ftp->cwd('/'); +#$ftp->cwd('/'); my @dirs = $ftp->ls(); warn scalar(@dirs)." directories found.\n" if $opt_v; # apply date range diff --git a/FS/t/access_user_session_log.t b/FS/t/access_user_session_log.t new file mode 100644 index 000000000..630637474 --- /dev/null +++ b/FS/t/access_user_session_log.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::access_user_session_log; +$loaded=1; +print "ok 1\n"; diff --git a/Makefile b/Makefile index e18d39d3e..3486b7967 100644 --- a/Makefile +++ b/Makefile @@ -236,7 +236,7 @@ perl-modules: " blib/lib/FS/part_export/*.pm;\ perl -p -i -e "\ s|%%%FREESIDE_CACHE%%%|${FREESIDE_CACHE}|g;\ - " blib/lib/FS/cust_main/*.pm blib/lib/FS/cust_pkg/*.pm;\ + " blib/lib/FS/cust_main/*.pm blib/lib/FS/cust_pkg/*.pm blib/lib/FS/contact/*.pm;\ perl -p -i -e "\ s|%%%FREESIDE_LOG%%%|${FREESIDE_LOG}|g;\ " blib/lib/FS/Daemon/*.pm;\ diff --git a/bin/cust_main-email_and_rebill b/bin/cust_main-email_and_rebill new file mode 100644 index 000000000..dea1319d6 --- /dev/null +++ b/bin/cust_main-email_and_rebill @@ -0,0 +1,73 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Date::Parse; +use FS::UID qw( adminsuidsetup ); +use FS::Record qw( qsearchs ); +use FS::cust_pkg; +use FS::msg_template; + +adminsuidsetup shift or die 'Usage: cust_main-email_and_rebill username\n'; + +my $DRY_RUN = 1; +my $msgnum = 17; + +my $sep1 = str2time('9/1/2017'); +my $aug15 = str2time('8/15/2017') + 1802; + +my $msg_template = qsearchs('msg_template', { 'msgnum' => $msgnum } ) + or die "unknown msg_template $msgnum\n"; + +while (<>) { + chomp; + my $pkgnum = $_; + + #find the package + my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum'=>$pkgnum } ) + or die "pkgnum $pkgnum not found\n"; + + #reset its next bill date back to sep 1 + $cust_pkg->set('bill', $sep1); + unless ( $DRY_RUN ) { + warn "updating cust_pkg $pkgnum bill to $sep1\n"; + my $error = $cust_pkg->replace; + die $error if $error; + } else { + warn "DRY RUN: would update cust_pkg $pkgnum bill to $sep1\n"; + } + + #customer + my $cust_main = $cust_pkg->cust_main; + my $custnum = $cust_main->custnum; + + #send the custoemr a notice + unless ( $DRY_RUN ) { + warn "emailing msg_template $msgnum to customer $custnum\n"; + $msg_template->send( 'cust_main' => $cust_main, + 'object' => $cust_main, + ); + } else { + warn "DRY RUN: emailing msg_template $msgnum to customer $custnum\n"; + } + + #bill the package + unless ( $DRY_RUN ) { + warn "billing customer $custnum for package $pkgnum as of $sep1\n"; + $cust_main->bill( 'time' => $sep1, + 'invoice_time' => $aug15, + 'pkg_list' => [ $cust_pkg ], + ); + } else { + warn "DRY RUN: billing customer $custnum for package $pkgnum as of $sep1\n"; + } + + #something about removing their pending batch payment?? + #hmm, there doesn't appear to be anything in a batch + #dating the invoices aug 15th will ensure payments for them are batched + + #events will take care of the rest... + +} + +1; diff --git a/bin/freeside-debian-releases.sh b/bin/freeside-debian-releases.sh index c774dd868..84d4e92dc 100755 --- a/bin/freeside-debian-releases.sh +++ b/bin/freeside-debian-releases.sh @@ -12,7 +12,7 @@ fi DATE=`date +"%Y%m%d"` DIR="/home/autobuild/packages/staging/freeside$FS_VERSION/$FS_REPO" -TARGET="/home/jeremyd/public_html/freeside$FS_VERSION-$DISTRO-$FS_REPO" +TARGET="/home/autobuild/public_html/freeside$FS_VERSION-$DISTRO-$FS_REPO" if [ ! -d "$DIR" -a -d $TARGET ]; then @@ -31,13 +31,6 @@ git checkout -- debian/changelog git pull #STATUS=`git pull` -#Assign the proper config files for freeside-ng-selfservice -if [ $DISTRO = "wheezy" ]; then - ln -s $DIR/freeside/debian/freeside-ng-selfservice.deb7 $DIR/freeside/debian/freeside-ng-selfservice.conffiles -else - ln -s $DIR/freeside/debian/freeside-ng-selfservice.deb8 $DIR/freeside/debian/freeside-ng-selfservice.conffiles -fi - # Add the build information to changelog if [ $FS_REPO != "stable" ]; then dch -b --newversion $GIT_VERSION-$DATE "Auto-Build" @@ -49,7 +42,14 @@ pdebuild --pbuilderroot sudo --debbuildopts "-b -rfakeroot -uc -us" --buildresul #--buildresult gets the file where it needs to be, may need to clean up DIR -cd $DIR; rm -f freeside_* -cd $TARGET; rm -f *.gz - -$TARGET/APT +cd $DIR && rm -f freeside_* +cd $TARGET && rm -f *.gz + +apt-ftparchive -qq packages ./ >Packages +gzip -c Packages >Packages.gz +#bzip2 -c Packages >Packagez.bz2 +apt-ftparchive -qq sources ./ >Sources +gzip -c Sources >Sources.gz +#bzip2 -c Sources >Sources.bz2 +rm *bz2 || true +apt-ftparchive -qq release ./ >Release diff --git a/bin/recover-cust_location b/bin/recover-cust_location new file mode 100755 index 000000000..6318eb343 --- /dev/null +++ b/bin/recover-cust_location @@ -0,0 +1,33 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use FS::UID qw( adminsuidsetup ); +use FS::Record qw( qsearchs ); +use FS::h_cust_location; +use FS::cust_location; + +adminsuidsetup shift or &usage; +my $start = shift or &usage; +my $end = shift or &usage; + +for my $locationnum ( $start .. $end ) { + + my $h_cust_location = qsearchs({ + 'table' => 'h_cust_location', + 'hashref' => { 'locationnum' => $locationnum, }, + 'extra_sql' => " AND ( history_action = 'insert' OR history_action = 'replace_new' )", + 'order_by' => 'ORDER BY historynum DESC LIMIT 1', + }) + or die "h_cust_location for $locationnum not found\n"; + + warn "recovering cust_locaiton $locationnum\n"; + my $cust_location = new FS::cust_location { $h_cust_location->hash }; + my $error = $cust_location->insert; + die $error if $error; + +} + +sub usage { + die "Usage: recover-cust_location username start_locationnum end_locationnum\n"; +} diff --git a/bin/xmlrpc-advertising_sources-add.pl b/bin/xmlrpc-advertising_sources-add.pl new file mode 100755 index 000000000..4800ad0c7 --- /dev/null +++ b/bin/xmlrpc-advertising_sources-add.pl @@ -0,0 +1,28 @@ +#!/usr/bin/perl + +use strict; +use Frontier::Client; +use Data::Dumper; + +my $uri = new URI 'http://localhost:8008/'; + +my $server = new Frontier::Client ( 'url' => $uri ); + +my $result = $server->call( + 'FS.API.add_advertising_source', + 'secret' => 'MySecretCode', + 'source' => { + 'referral' => 'API test referral', + 'disabled' => '', + 'agentnum' => '', + 'title' => 'API test title', + }, +); + +die $result->{'error'} if $result->{'error'}; + +print Dumper($result); + +print "\nAll Done\n"; + +exit; \ No newline at end of file diff --git a/bin/xmlrpc-advertising_sources-edit.pl b/bin/xmlrpc-advertising_sources-edit.pl new file mode 100755 index 000000000..80f9139c1 --- /dev/null +++ b/bin/xmlrpc-advertising_sources-edit.pl @@ -0,0 +1,30 @@ +#!/usr/bin/perl + +use strict; +use Frontier::Client; +use Data::Dumper; + +my $uri = new URI 'http://localhost:8008/'; + +my $server = new Frontier::Client ( 'url' => $uri ); + +my $result = $server->call( + 'FS.API.edit_advertising_source', + 'secret' => 'MySecretCode', + 'refnum' => '4', + 'source' => { + 'referral' => 'Edit referral', + 'title' => 'Edit Referral title', + #'disabled' => 'Y', + #'disabled' => '', + #'agentnum' => '2', + }, +); + +die $result->{'error'} if $result->{'error'}; + +print Dumper($result); + +print "\nAll Done\n"; + +exit; \ No newline at end of file diff --git a/bin/xmlrpc-advertising_sources-list.pl b/bin/xmlrpc-advertising_sources-list.pl new file mode 100755 index 000000000..317a38b7b --- /dev/null +++ b/bin/xmlrpc-advertising_sources-list.pl @@ -0,0 +1,22 @@ +#!/usr/bin/perl + +use strict; +use Frontier::Client; +use Data::Dumper; + +my $uri = new URI 'http://localhost:8008/'; + +my $server = new Frontier::Client ( 'url' => $uri ); + +my $result = $server->call( + 'FS.API.list_advertising_sources', + 'secret' => 'MySecretCode', +); + +die $result->{'error'} if $result->{'error'}; + +print Dumper($result); + +print "\nAll Done\n"; + +exit; \ No newline at end of file diff --git a/bin/xmlrpc-order_package.php b/bin/xmlrpc-order_package.php new file mode 100755 index 000000000..fccf77a63 --- /dev/null +++ b/bin/xmlrpc-order_package.php @@ -0,0 +1,81 @@ +#!/usr/bin/php5 + +order_package( array( + 'secret' => 'sharingiscaring', #config setting api_shared_secret + 'custnum' => 619797, + 'pkgpart' => 2, + + #the rest is optional + 'quantity' => 5, + 'start_date' => '12/1/2017', + 'invoice_details' => [ 'detail', 'even more detail' ], + 'address1' => '5432 API Lane', + 'city' => 'API Town', + 'state' => 'AZ', + 'zip' => '54321', + 'country' => 'US', + 'setup_fee' => '23', + 'recur_fee' => '19000', +)); + +var_dump($result); + +#pre-php 5.4 compatible version? +function flatten($hash) { + if ( !is_array($hash) ) return $hash; + $flat = array(); + + array_walk($hash, function($value, $key, &$to) { + array_push($to, $key, $value); + }, $flat); + + if ( PHP_VERSION_ID >= 50400 ) { + + #php 5.4+ (deb 7+) + foreach ($hash as $key => $value) { + $flat[] = $key; + $flat[] = $value; + } + + } + + return($flat); +} + +class FreesideAPI { + + //Change this to match the location of your backoffice XML-RPC interface + #var $URL = 'https://localhost/selfservice/xmlrpc.cgi'; + var $URL = 'http://localhost:8008/'; + + function FreesideAPI() { + $this; + } + + public function __call($name, $arguments) { + + error_log("[FreesideAPI] $name called, sending to ". $this->URL); + + $request = xmlrpc_encode_request("FS.API.$name", flatten($arguments[0])); + $context = stream_context_create( array( 'http' => array( + 'method' => "POST", + 'header' => "Content-Type: text/xml", + 'content' => $request + ))); + $file = file_get_contents($this->URL, false, $context); + $response = xmlrpc_decode($file); + if (isset($response) && is_array($response) && xmlrpc_is_fault($response)) { + trigger_error("[FreesideAPI] XML-RPC communication error: $response[faultString] ($response[faultCode])"); + } else { + //error_log("[FreesideAPI] $response"); + return $response; + } + } + +} + +?> diff --git a/conf/invoice_latex b/conf/invoice_latex index 5c6090e08..a710cddec 100644 --- a/conf/invoice_latex +++ b/conf/invoice_latex @@ -63,6 +63,10 @@ \LTchunksize=40 + +\begin{document} + + \renewcommand{\headrulewidth}{0pt} \renewcommand{\footrulewidth}{1pt} @@ -268,7 +272,6 @@ \\ } -\begin{document} % Headers and footers defined for the first page \addressinset \rule{0.5cm}{0cm} \makebox{ diff --git a/conf/quotation_latex b/conf/quotation_latex index 25228dcb9..13fc42978 100644 --- a/conf/quotation_latex +++ b/conf/quotation_latex @@ -39,6 +39,10 @@ \LTchunksize=40 + +\begin{document} + + \renewcommand{\headrulewidth}{0pt} \renewcommand{\footrulewidth}{1pt} @@ -200,7 +204,6 @@ } -\begin{document} % Headers and footers defined for the first page \addressinset \rule{0.5cm}{0cm} \makebox{ diff --git a/debian/control b/debian/control index a268ffdad..fc3bae1ce 100644 --- a/debian/control +++ b/debian/control @@ -2,22 +2,22 @@ Source: freeside Section: misc Priority: extra Maintainer: Ivan Kohler -Uploaders: Jeremy Davis Standards-Version: 3.7.2 Homepage: http://www.freeside.biz/freeside Build-Depends: debhelper (>=5), perl (>=5.8), rrdtool,librrds-perl, libxml-libxml-perl, libberkeleydb-perl, libtemplate-perl, libproc-daemon-perl, libnet-snmp-perl, libapache-session-perl, libjson-perl, libdbix-abstract-perl, - libdbix-sequence-perl, librrds-perl, apache2, texlive-binaries, + libdbix-sequence-perl, apache2, texlive-binaries, autotools-dev, liburi-perl, db-util, libtimedate-perl, libcgi-fast-perl Package: freeside Architecture: all Pre-Depends: freeside-lib # dbconfig-common -Depends: ${perl:Depends}, ${shlibs:Depends}, ${misc:Depends}, freeside-webui, - debconf, cron, openbsd-inetd, tcpd, undersmtpd, ssmtp, freeside-lib (>= 4.0~git-20160211) +Depends: ${perl:Depends}, ${shlibs:Depends}, ${misc:Depends}, + freeside-webui (= ${binary:Version}), freeside-lib (= ${binary:Version}), + debconf, cron, openbsd-inetd, tcpd, undersmtpd, ssmtp Description: Billing and trouble ticketing for service providers Freeside is a web-based billing, trouble ticketing and network monitoring application. It includes features for ISPs and WISPs, hosting providers and @@ -44,7 +44,8 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip,latex-xcolor, libipc-run-perl,libipc-run3-perl,libipc-sharelite-perl,libjavascript-rpc-perl, libjson-perl,liblingua-en-inflect-perl,liblingua-en-nameparse-perl, liblocale-gettext-perl,liblocale-maketext-fuzzy-perl, - liblocale-maketext-lexicon-perl,liblocale-subcountry-perl,liblog-dispatch-perl, + liblocale-maketext-lexicon-perl,liblocale-subcountry-perl (<< 2), + liblog-dispatch-perl, libmailtools-perl (>=2.12), libmime-tools-perl (>= 5.504), libmodule-versions-report-perl, libnet-daemon-perl,libnet-ping-external-perl,libnet-scp-perl,libnet-ssh-perl, @@ -99,7 +100,8 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip,latex-xcolor, libxml-writer-perl, libio-socket-ssl-perl, libmap-splat-perl, libdatetime-format-ical-perl, librest-client-perl, libgeo-streetaddress-us-perl, libbusiness-onlinepayment-perl, - libnet-vitelity-perl (>= 0.05) + libnet-vitelity-perl (>= 0.05), libnet-sslglue-perl, libexpect-perl, + libspreadsheet-parsexlsx-perl Conflicts: libparams-classify-perl (>= 0.013-6) Replaces: freeside (<<4) Breaks: freeside (<<4) @@ -111,7 +113,7 @@ Description: Libraries for Freeside billing and trouble ticketing Package: freeside-webui Architecture: all -Depends: freeside-lib,apache2,apache2-mpm-prefork,apache2-utils, +Depends: freeside-lib,apache2,apache2-utils, libapache-dbi-perl,libapache2-mod-perl2,libapache2-request-perl, libapache-session-perl,openssl, libcgi-emulate-psgi-perl, libplack-perl (>= 1.0002) @@ -173,7 +175,7 @@ Description: Self-service portal html/cgi filesfor Freeside billing and trouble Package: freeside-ng-selfservice Architecture: all -Depends: libapache2-mod-php5,php5-xmlrpc,apache2-mpm-prefork +Depends: libapache2-mod-php5,php5-xmlrpc,apache2 Recommends: Description: Next Generation Self-service portal for Freeside billing and trouble ticketing Freeside is a web-based billing and trouble ticketing application. diff --git a/debian/freeside-ng-selfservice.conffiles b/debian/freeside-ng-selfservice.conffiles new file mode 100644 index 000000000..d6537dda8 --- /dev/null +++ b/debian/freeside-ng-selfservice.conffiles @@ -0,0 +1 @@ +/var/www/html/ng_selfservice-DIST/freeside.class.php diff --git a/debian/freeside-ng-selfservice.deb7 b/debian/freeside-ng-selfservice.deb7 deleted file mode 100644 index 58f0d3ad3..000000000 --- a/debian/freeside-ng-selfservice.deb7 +++ /dev/null @@ -1 +0,0 @@ -/var/www/ng_selfservice-DIST/freeside.class.php diff --git a/debian/freeside-ng-selfservice.deb8 b/debian/freeside-ng-selfservice.deb8 deleted file mode 100644 index d6537dda8..000000000 --- a/debian/freeside-ng-selfservice.deb8 +++ /dev/null @@ -1 +0,0 @@ -/var/www/html/ng_selfservice-DIST/freeside.class.php diff --git a/fs_selfservice/FS-SelfService/cgi/change_pay.html b/fs_selfservice/FS-SelfService/cgi/change_pay.html index e38ba762d..f90f6d92b 100644 --- a/fs_selfservice/FS-SelfService/cgi/change_pay.html +++ b/fs_selfservice/FS-SelfService/cgi/change_pay.html @@ -60,8 +60,8 @@ delete $options{'DCRD'} unless $payby eq 'DCRD' || ! exists $options{'CARD'}; delete $options{'DCHK'} unless $payby eq 'DCHK' || ! exists $options{'CHEK'}; - ## setting payby to default to layer if only one. should we always display first layer? - if (keys %options == 1) { @p = keys %options; $payby = $p[0]; } + ## set default layer to first payby. + @p = keys %options; $payby = $p[0]; HTML::Widgets::SelectLayers->new( options => \%options, diff --git a/fs_selfservice/FS-SelfService/cgi/myaccount_menu.html b/fs_selfservice/FS-SelfService/cgi/myaccount_menu.html index 6af5e5ec0..1bc35e34c 100644 --- a/fs_selfservice/FS-SelfService/cgi/myaccount_menu.html +++ b/fs_selfservice/FS-SelfService/cgi/myaccount_menu.html @@ -93,9 +93,10 @@ push @menu, unless ( $access_pkgnum ) { push @menu, - { title=>'Change billing address', url=>'change_bill', indent=>2 }, - { title=>'Change service address', url=>'change_ship', indent=>2 }, - { title=>'Change payment information', url=>'change_pay', indent=>2 }, + { title=>'Change billing address', url=>'change_bill', indent=>2 }, + { title=>'Change service address', url=>'change_ship', indent=>2 }, + { title=>'Change credit card information', url=>'change_creditcard_pay', indent=>2 }, + { title=>'Change check information', url=>'change_check_pay', indent=>2 }, ; } diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi index cd9e32c78..f194746c5 100755 --- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi +++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi @@ -12,8 +12,8 @@ use Date::Format; use Date::Parse 'str2time'; use Number::Format 1.50; use FS::SelfService qw( - access_info login_info login customer_info edit_info invoice - payment_info process_payment realtime_collect process_prepay + access_info login_info login customer_info edit_info insert_payby update_payby + invoice payment_info process_payment realtime_collect process_prepay list_pkgs order_pkg signup_info order_recharge part_svc_info provision_acct provision_external provision_phone provision_forward unprovision_svc change_pkg suspend_pkg domainselector @@ -59,6 +59,10 @@ my @actions = ( qw( change_bill change_ship change_pay + change_creditcard_pay + change_check_pay + process_change_creditcard_pay + process_change_check_pay process_change_bill process_change_ship process_change_pay @@ -261,19 +265,30 @@ sub myaccount { customer_info( 'session_id' => $session_id ); } -sub change_bill { my $payment_info = - payment_info( 'session_id' => $session_id ); - return $payment_info if ( $payment_info->{'error'} ); - my $customer_info = - customer_info( 'session_id' => $session_id ); - return { - %$payment_info, - %$customer_info, - }; - } +sub change_bill { + my $payby = shift; + my $payment_info; + if ($payby) { + $payment_info = payment_info( 'session_id' => $session_id, 'payment_payby' => $payby, ); + } + else { + $payment_info = payment_info( 'session_id' => $session_id, ); + } + + return $payment_info if ( $payment_info->{'error'} ); + my $customer_info = + customer_info( 'session_id' => $session_id ); + return { + %$payment_info, + %$customer_info, + }; +} sub change_ship { change_bill(@_); } sub change_pay { change_bill(@_); } +sub change_creditcard_pay { change_bill('CARD'); } +sub change_check_pay { change_bill('CHEK'); } + sub _process_change_info { my ($erroraction, @fields) = @_; @@ -298,6 +313,30 @@ sub _process_change_info { } } +sub _process_change_payby { + my ($erroraction, @fields) = @_; + + my $results = ''; + + $results ||= update_payby ( + 'session_id' => $session_id, + map { ($_ => $cgi->param($_)) } grep { defined($cgi->param($_)) } @fields, + ); + + + if ( $results->{'error'} ) { + no strict 'refs'; + $action = $erroraction; + return { + $cgi->Vars, + %{&$action()}, + 'error' => ''. $results->{'error'}. '', + }; + } else { + return $results; + } +} + sub process_change_bill { _process_change_info( 'change_bill', qw( first last company address1 address2 city state @@ -342,6 +381,30 @@ sub process_change_pay { _process_change_info( 'change_pay', @list ); } +sub process_change_creditcard_pay { + my $payby = $cgi->param( 'payby' ); + $cgi->param('paydate', $cgi->param('year') . '-' . $cgi->param('month') . '-01'); + my @list = + qw( payby payinfo payinfo1 payinfo2 paydate payname custpaybynum + address1 address2 city county state zip country auto paytype + paystate ss stateid stateid_state invoicing_list + ); + + _process_change_payby( 'change_creditcard_pay', @list ); +} + +sub process_change_check_pay { + my $payby = $cgi->param( 'payby' ); + $cgi->param('paydate', $cgi->param('year') . '-' . $cgi->param('month') . '-01'); + my @list = + qw( payby payinfo payinfo1 payinfo2 paydate payname custpaybynum + address1 address2 city county state zip country auto paytype + paystate ss stateid stateid_state invoicing_list + ); + + _process_change_payby( 'change_check_pay', @list ); +} + sub view_invoice { $cgi->param('invnum') =~ /^(\d+)$/ or die "illegal invnum"; diff --git a/httemplate/browse/part_pkg_taxproduct/compliance_solutions.html b/httemplate/browse/part_pkg_taxproduct/compliance_solutions.html index bbcb6877f..b5936daef 100644 --- a/httemplate/browse/part_pkg_taxproduct/compliance_solutions.html +++ b/httemplate/browse/part_pkg_taxproduct/compliance_solutions.html @@ -53,6 +53,9 @@ function category_changed(what) { jopt( $('#product_code'), '', 'Select product code' ); var part_pkg_taxproduct = reply.part_pkg_taxproduct; + if ( part_pkg_taxproduct.length == 0 ) { + alert('No compliance solutions product codes found; did you run freeside-compliance_solutions-import?'); + } for ( var s = 0; s < part_pkg_taxproduct.length; s=s+2 ) { var product_code = part_pkg_taxproduct[s]; var description = part_pkg_taxproduct[s+1]; diff --git a/httemplate/config/config-view.cgi b/httemplate/config/config-view.cgi index edbda5ca9..5f09b128a 100644 --- a/httemplate/config/config-view.cgi +++ b/httemplate/config/config-view.cgi @@ -212,11 +212,15 @@ invoice language options: -
    <% encode_entities(join("\n",
    -     map { length($_) > 88 ? substr($_,0,88).'...' : $_ }
    -         $conf->config($i->key, $agentnum)
    -   ) )
    -%>
    + +% my $escaped = eval { encode_entities(join("\n", +% map { length($_) > 88 ? substr($_,0,88).'...' : $_ } +% $conf->config($i->key, $agentnum) +% ) ); +% }; +% $escaped = $@ ? '('.encode_entities($@).')' : $escaped; +
    <% $escaped %>
    + @@ -435,7 +439,7 @@ my @sections = (qw( important billing payments payment_batching credit_cards e-checks taxation packages suspension cancellation - printing print_services + printing print_services email_to_voice_services invoicing invoice_email invoice_balances invoice_templates quotations notification UI addresses customer_number customer_fields reporting localization scalability backup diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi index e58441d24..05bf4377a 100755 --- a/httemplate/edit/cust_main.cgi +++ b/httemplate/edit/cust_main.cgi @@ -296,8 +296,13 @@ if ( $cgi->param('error') ) { $custnum=''; $cust_main = new FS::cust_main ( {} ); + + my @agentnums = $curuser->agentnums; + $cust_main->agentnum( $agentnums[0] ) + if scalar(@agentnums) == 1; $cust_main->agentnum( $conf->config('default_agentnum') ) if $conf->exists('default_agentnum'); + $cust_main->referral_custnum( $cgi->param('referral_custnum') ); $cust_main->set('postal_invoice', 'Y') unless $conf->exists('disablepostalinvoicedefault'); diff --git a/httemplate/edit/deploy_zone-fixed.html b/httemplate/edit/deploy_zone-fixed.html index b8d9f8bbc..24e03b01a 100644 --- a/httemplate/edit/deploy_zone-fixed.html +++ b/httemplate/edit/deploy_zone-fixed.html @@ -19,6 +19,7 @@ 'file' => 'Import blocks from text file', 'censusyear' => 'as census year', }, + 'fields' => [ { field => 'zonetype', type => 'hidden', @@ -30,8 +31,11 @@ }, 'description', { field => 'active_date', - type => 'fixed-date', - value => time, + type => 'input-date-field', + curr_value_callback => sub { + my ($cgi, $object) = @_; + $cgi->param('active_date') || $object->active_date || time; + }, }, { field => 'agentnum', type => 'select-agent', diff --git a/httemplate/edit/deploy_zone-mobile.html b/httemplate/edit/deploy_zone-mobile.html index 8cec298bf..e7f534cb8 100644 --- a/httemplate/edit/deploy_zone-mobile.html +++ b/httemplate/edit/deploy_zone-mobile.html @@ -23,8 +23,11 @@ }, 'description', { field => 'active_date', - type => 'fixed-date', - value => time, + type => 'input-date-field', + curr_value_callback => sub { + my ($cgi, $object) = @_; + $cgi->param('active_date') || $object->active_date || time; + }, }, { field => 'agentnum', type => 'select-agent', diff --git a/httemplate/edit/process/deploy_zone-fixed.html b/httemplate/edit/process/deploy_zone-fixed.html index 0033bbe52..b22e63066 100644 --- a/httemplate/edit/process/deploy_zone-fixed.html +++ b/httemplate/edit/process/deploy_zone-fixed.html @@ -28,6 +28,10 @@ my $precheck_callback = sub { $i++; } } + if ( length $cgi->param('active_date') ) { + my $date = parse_datetime( $cgi->param('active_date') ); + $cgi->param('active_date', $date); + } ''; }; diff --git a/httemplate/edit/process/deploy_zone-mobile.html b/httemplate/edit/process/deploy_zone-mobile.html index d36d5d448..9b205ab6e 100644 --- a/httemplate/edit/process/deploy_zone-mobile.html +++ b/httemplate/edit/process/deploy_zone-mobile.html @@ -21,6 +21,10 @@ my $precheck_callback = sub { $i++; } } + if ( length $cgi->param('active_date') ) { + my $date = parse_datetime( $cgi->param('active_date') ); + $cgi->param('active_date', $date); + } ''; }; diff --git a/httemplate/edit/router.cgi b/httemplate/edit/router.cgi index 0df9b457e..a756c617c 100755 --- a/httemplate/edit/router.cgi +++ b/httemplate/edit/router.cgi @@ -43,8 +43,7 @@ my $callback = sub { { 'type' => 'tablebreak-tr-title', 'value' => 'Select the service types available on this router', }, - { 'field' => 'svc_part', - 'type' => 'checkboxes-table', + { 'type' => 'checkboxes-table', 'target_table' => 'part_svc', 'link_table' => 'part_svc_router', 'name_col' => 'svc', diff --git a/httemplate/edit/svc_acct.cgi b/httemplate/edit/svc_acct.cgi index 7be5eabb7..27ea3c5ea 100755 --- a/httemplate/edit/svc_acct.cgi +++ b/httemplate/edit/svc_acct.cgi @@ -310,11 +310,11 @@ % } % } -% my %label = ( seconds => 'Time', -% upbytes => 'Upload bytes', -% downbytes => 'Download bytes', -% totalbytes => 'Total bytes', -% ); +% tie my %label, 'Tie::IxHash', seconds => 'Time', +% upbytes => 'Upload bytes', +% downbytes => 'Download bytes', +% totalbytes => 'Total bytes', +% ; % foreach my $uf (keys %label) { % my $tf = $uf . "_threshold"; % if ( $curuser->access_right('Edit usage') ) { diff --git a/httemplate/elements/change_password.html b/httemplate/elements/change_password.html index 463384f2f..7d95e19dc 100644 --- a/httemplate/elements/change_password.html +++ b/httemplate/elements/change_password.html @@ -13,7 +13,7 @@ % }
    % if (!$opt{'noformtag'}) { -
    + % } <% $change_id_input %> @@ -33,9 +33,6 @@ 'contactnum' => $opt{'contact_num'}, 'submitid' => $change_button_id, &> -% if ( $error ) { -
    <% $error |h %> -% } % if (!$opt{'noformtag'}) {
    @@ -58,6 +55,16 @@ function <%$pre%>toggle(toggle, clear) { toggle ? 'none' : 'inline'; % } } + +function checkPasswordValidation() { + var validationResult = document.getElementById('<%$pre%>password_result').innerHTML; + if (validationResult.match(/Password valid!/)) { + return true; + } + else { + return false; + } +} <%init> my %opt = @_; diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 58a7d5783..defcc494f 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -147,6 +147,7 @@ if ( $curuser->access_right('List contacts') ) { $report_customers{'separator'} = ''; $report_customers{'Customer contacts'} = [ $fsurl. 'search/report_contact.html?link=cust_main' ]; $report_customers{'Customer stored payment information'} = [ $fsurl. 'search/report_cust_payby.html' ]; + $report_customers{'Customer timespan'} = [ $fsurl. 'search/report_cust_timespan.html' ]; } tie my %report_invoices_open, 'Tie::IxHash', @@ -343,20 +344,25 @@ tie my %report_bill_event, 'Tie::IxHash', 'Billing event errors' => [ $fsurl.'search/report_cust_event.html?failed=1', 'Failed credit cards, processor or printer problems, etc.' ], ; -tie my %report_payments, 'Tie::IxHash', - 'Payments' => [ $fsurl.'search/report_cust_pay.html', 'Payment report (by type and/or date range)' ], - 'Payment application detail' => [ $fsurl.'search/report_cust_bill_pay_pkg.html', 'Line item application detail' ], -; +tie my %report_payments, 'Tie::IxHash'; +$report_payments{'Payments'} = [ $fsurl.'search/report_cust_pay.html', 'Payment report (by type and/or date range)' ] + if $curuser->access_right('Basic payment and refund reports'); +$report_payments{'Payment application detail'} = [ $fsurl.'search/report_cust_bill_pay_pkg.html', 'Line item application detail' ] + if $curuser->access_right('Financial reports'); $report_payments{'Pending Payments'} = [ $fsurl.'search/cust_pay_pending.html?magic=_date;statusNOT=done', 'Pending real-time payments' ] if $curuser->access_right('View customer pending payments'); -$report_payments{'Unapplied Payments'} = [ $fsurl.'search/report_cust_pay.html?unapplied=1', 'Unapplied payment report (by type and/or date range)' ]; +$report_payments{'Unapplied Payments'} = [ $fsurl.'search/report_cust_pay.html?unapplied=1', 'Unapplied payment report (by type and/or date range)' ] + if $curuser->access_right('Financial reports'); #not enforced $report_payments{'Voided Payments'} = [ $fsurl.'search/report_cust_pay.html?void=1', 'Voided payment report (by type and/or date range)' ] - if $curuser->access_right('View customer pending payments'); + if $curuser->access_right('Financial reports'); #not enforced $report_payments{'Payment Batches'} = [ $fsurl.'search/pay_batch.html', 'Payment batches (by status and/or date range)' ] - if $conf->exists('batch-enable') || $conf->config('batch-enable_payby'); -$report_payments{'Unapplied Payment Aging'} = [ $fsurl.'search/report_unapplied_cust_pay.html', 'Unapplied payment aging report' ]; + if ( $conf->exists('batch-enable') || $conf->config('batch-enable_payby') ) + && $curuser->access_right('Financial reports'); +$report_payments{'Unapplied Payment Aging'} = [ $fsurl.'search/report_unapplied_cust_pay.html', 'Unapplied payment aging report' ] + if $curuser->access_right('Financial reports'); $report_payments{'Deleted Payments / Payment history table'} = [ $fsurl.'search/report_h_cust_pay.html', 'Deleted payments / payment history table' ] - if $conf->exists('payment-history-report'); + if $conf->exists('payment-history-report') + && $curuser->access_right('Financial reports'); tie my %report_credits, 'Tie::IxHash', 'Credit Report' => [ $fsurl.'search/report_cust_credit.html', 'Credit report (by employee and/or date range)' ], @@ -428,8 +434,6 @@ $report_logs{'Billing events'} = [ $fsurl.'search/report_cust_event.html', 'Sea if $curuser->access_right('Billing event reports'); $report_logs{'Credit limit incidents'} = [ $fsurl.'search/report_cust_main_credit_limit.html', '' ] if $curuser->access_right('List rating data'); -$report_logs{'Employee activity'} = [ $fsurl.'search/report_employee_audit.html', '' ] - if $curuser->access_right('Employees: Audit Report'); $report_logs{'System log'} = [ $fsurl.'search/log.html', 'View system events and debugging information.' ], if $curuser->access_right('View system logs') || $curuser->access_right('Configuration'); @@ -437,6 +441,12 @@ $report_logs{'Outgoing messages'} = [ $fsurl.'search/cust_msg.html', 'View outgo if $curuser->access_right('View email logs') || $curuser->access_right('Configuration'); +tie my %report_employee, 'Tie::IxHash', + 'Employee activity' => [ $fsurl.'search/report_employee_audit.html', '' ], + 'Employee sessions' => [ $fsurl.'search/report_access_user_session_log.html', '' ], + 'Access log statistics' => [ $fsurl.'search/report_access_user_log.html?group_by=path', '' ], +; + tie my %report_menu, 'Tie::IxHash'; $report_menu{'Saved searches'} = [ \%report_saved_searches, 'My saved searches' ] if keys(%report_saved_searches); @@ -457,7 +467,7 @@ $report_menu{'Invoices'} = [ \%report_invoices, 'Invoice reports' ] $report_menu{'Discounts'} = [ \%report_discounts, 'Discount reports' ] if $curuser->access_right('Financial reports'); $report_menu{'Payments'} = [ \%report_payments, 'Payment reports' ] - if $curuser->access_right('Financial reports'); + if keys %report_payments; $report_menu{'Packages'} = [ \%report_packages, 'Package reports' ] if $curuser->access_right('List packages'); $report_menu{'Services'} = [ \%report_services, 'Services reports' ] @@ -475,6 +485,8 @@ $report_menu{'Financial (Receivables)'} = [ \%report_financial, 'Financial repor $report_menu{'Financial (Payables)'} = [ \%report_payable, 'Financial reports (Payables)' ] if $curuser->access_right('Financial reports'); +$report_menu{'Employees'} = [ \%report_employee, 'Employee reports' ] + if $curuser->access_right('Employee Reports'); $report_menu{'Logs'} = [ \%report_logs, 'System and email logs' ] if (keys %report_logs); # empty if the user has no rights to it $report_menu{'SQL Query'} = [ $fsurl.'search/report_sql.html', 'SQL Query'] @@ -505,6 +517,7 @@ tie my %tools_importing, 'Tie::IxHash', 'Package definitions' => [ $fsurl.'misc/part_pkg-import.html', '' ], 'Customer packages' => [ $fsurl.'misc/cust_pkg-import.html', '' ], 'Customer notes' => [ $fsurl.'misc/cust_main_note-import.html', '' ], + 'Customer Contacts' => [ $fsurl.'misc/contact-import.cgi', '' ], 'One-time charges' => [ $fsurl.'misc/cust_main-import_charges.cgi', '' ], 'Payments' => [ $fsurl.'misc/cust_pay-import.cgi', '' ], 'Credits' => [ $fsurl.'misc/cust_credit-import.html', '' ], @@ -554,8 +567,6 @@ $tools_system{'Status'} = [ $fsurl.'view/Status.html', 'System status' ] if $curuser->access_right('Configuration'); # 'View system status'); $tools_system{'Job Queue'} = [ $fsurl.'search/queue.html', 'View pending job queue' ] if $curuser->access_right('Job queue'); -$tools_system{'Access log statistics'} = [ $fsurl.'search/report_access_user_log.html?group_by=path', '' ] - if $curuser->access_right('Configuration'); # 'View profiling data'); tie my %tools_menu, 'Tie::IxHash', (); $tools_menu{'Customers'} = [ \%tools_customers, 'Customer tools' ] diff --git a/httemplate/elements/searchbar-cust_main.html b/httemplate/elements/searchbar-cust_main.html index 5bfef484a..7f8f9d850 100644 --- a/httemplate/elements/searchbar-cust_main.html +++ b/httemplate/elements/searchbar-cust_main.html @@ -26,7 +26,7 @@ my $curuser = $FS::CurrentUser::CurrentUser; my $menu_position = $curuser->option('menu_position') || 'top'; my $cust_width = 246; -my $cust_label = '(cust #, name, company'; +my $cust_label = '(cust #, name, company, email'; if ( $conf->exists('address1-search') ) { $cust_label .= ', address'; $cust_width += 56; diff --git a/httemplate/elements/select-cust_phone.html b/httemplate/elements/select-cust_phone.html new file mode 100644 index 000000000..94cd41322 --- /dev/null +++ b/httemplate/elements/select-cust_phone.html @@ -0,0 +1,31 @@ + + +<%init> + +my %opt = @_; +my $cust_num = $opt{'cust_num'}; +my $phone_types = $opt{'phone_types'}; +my $format = $opt{'format'}; + +my $cust_phones = qsearchs('cust_main', { 'custnum' => $cust_num }) + or die 'unknown custnum' . $cust_num; + +my %phones_formatted = map { + $_ => format_phone_number($cust_phones->$_, $format) +} @$phone_types; + +sub format_phone_number { + my ($n, $f) = @_; + if ($f eq 'xxxxxxxxxx') { $n =~ s/-//g; } + return $n; +} + + \ No newline at end of file diff --git a/httemplate/elements/select.html b/httemplate/elements/select.html index 689566e36..100df94d0 100644 --- a/httemplate/elements/select.html +++ b/httemplate/elements/select.html @@ -69,7 +69,7 @@ % % } - + <% $opt{'post_field_label'} %> % } <%init> diff --git a/httemplate/elements/tr-select-cust_location.html b/httemplate/elements/tr-select-cust_location.html index 5ff320b06..7a2d886d3 100644 --- a/httemplate/elements/tr-select-cust_location.html +++ b/httemplate/elements/tr-select-cust_location.html @@ -214,7 +214,7 @@ Example: '' ) ); -% } else { +% } elsif ( $locationnum != -1 ) { locationnum_changed(document.getElementById('locationnum')); % } diff --git a/httemplate/elements/tr-select-cust_phone.html b/httemplate/elements/tr-select-cust_phone.html new file mode 100644 index 000000000..cf88392b0 --- /dev/null +++ b/httemplate/elements/tr-select-cust_phone.html @@ -0,0 +1,12 @@ + + <% $opt{'label'} || 'Customer Phones' %> + + <% include( '/elements/select-cust_phone.html', %opt ) %> + + + +<%init> + +my %opt = @_; + + diff --git a/httemplate/elements/validate_password.html b/httemplate/elements/validate_password.html index 3d23a552c..4057f5d3f 100644 --- a/httemplate/elements/validate_password.html +++ b/httemplate/elements/validate_password.html @@ -22,6 +22,17 @@ should be the input id plus '_result'. + <& /elements/footer.html &> <%init> @@ -227,12 +264,16 @@ my %opt = @_; $opt{'acl'} ||= 'Bulk send customer notices'; +my $email_to; + die "access denied" unless $FS::CurrentUser::CurrentUser->access_right($opt{'acl'}); my $conf = FS::Conf->new; my @no_search_fields = qw( table from subject html_body text_body popup url ); +my $send_to_domain = $conf->config('email-to-voice_domain'); + my $form_action = $opt{'form_action'} || 'email-customers.html'; my $process_url = $opt{'process_url'} || 'process/email-customers.html'; my $title = $opt{'title'} || 'Send customer notices'; @@ -341,12 +382,14 @@ if ( !$cgi->param('preview') ) { # unless creating the msg_template failed, we now have one, so construct a # preview message from the first customer/whatever in the search results + my $cust; + if ( $msg_template ) { $sql_query->{'extra_sql'} .= ' LIMIT 1'; $sql_query->{'select'} = "$table.*"; $sql_query->{'order_by'} = ''; my $object = qsearchs($sql_query); - my $cust = $object->cust_main; + $cust = $object->cust_main; my %msgopts = ( 'cust_main' => $cust, 'object' => $object, @@ -367,14 +410,22 @@ if ( !$cgi->param('preview') ) { # contact_class_X params #we can't switch to multi_param until we're done supporting deb 7 local($CGI::LIST_CONTEXT_WARN) = 0; - foreach my $param ( $cgi->param ) { - if ( $param =~ /^contact_class_(\w+)$/ ) { - push @contact_classnum, $1; - if ( $1 eq 'invoice' ) { - push @contact_classname, 'Invoice recipients'; - } else { - my $contact_class = FS::contact_class->by_key($1); - push @contact_classname, encode_entities($contact_class->classname); + + if ($cgi->param('emailtovoice_contact')) { + $email_to = $cgi->param('emailtovoice_contact') . '@' . $send_to_domain; + push @contact_classnum, 'emailtovoice'; + push @contact_classname, $email_to; + } + else { + foreach my $param ( $cgi->param ) { + if ( $param =~ /^contact_class_(\w+)$/ ) { + push @contact_classnum, $1; + if ( $1 eq 'invoice' ) { + push @contact_classname, 'Invoice recipients'; + } else { + my $contact_class = FS::contact_class->by_key($1); + push @contact_classname, encode_entities($contact_class->classname); + } } } } @@ -383,10 +434,12 @@ if ( !$cgi->param('preview') ) { my @contact_checkboxes = ( [ 'invoice' => { label => 'Invoice recipients' } ] ); + foreach my $class (qsearch('contact_class', { disabled => '' })) { push @contact_checkboxes, [ $class->classnum, { label => $class->classname } ]; } + diff --git a/httemplate/misc/openmap.html b/httemplate/misc/openmap.html new file mode 100644 index 000000000..6ccc72491 --- /dev/null +++ b/httemplate/misc/openmap.html @@ -0,0 +1,88 @@ + + + Find Census Tract Map + + + + +

    Please select your location on the map

    + + + + + +
    + +

    + Census Tract:

    + +
    +
    +
    + + + + + +<%init> + +local $SIG{__DIE__}; #disable Mason error trap + +my $DEBUG = 0; + +my $census_year = $cgi->param('census_year'); +my $pre = $cgi->param('pre'); +my $zip_code = $cgi->param('zip_code'); +my $address = $cgi->param('address'); +my $loc = $zip_code ? $zip_code : $address; + + \ No newline at end of file diff --git a/httemplate/misc/process/change-password.html b/httemplate/misc/process/change-password.html index be83786cd..a3e060168 100644 --- a/httemplate/misc/process/change-password.html +++ b/httemplate/misc/process/change-password.html @@ -7,6 +7,7 @@ % $cgi->param('contactnum', $contactnum); % $cgi->param("changepw${contactnum}_error", $error); % } +% $cgi->param('error', $error); % } else { % if ($svcnum) { $cgi->query_string($svcnum); } % elsif ($contactnum) { $cgi->query_string($contactnum); } @@ -52,7 +53,7 @@ if ($svcnum) { ! $part_svc->restrict_edit_password ) ); - my $error = $svc_acct->is_password_allowed($newpass) + $error = $svc_acct->is_password_allowed($newpass) || $svc_acct->set_password($newpass) || $svc_acct->replace; @@ -63,7 +64,7 @@ elsif ($contactnum) { my $contact = qsearchs('contact', { 'contactnum' => $contactnum } ) or return { 'error' => "Contact not found" . $contactnum }; - my $error = $contact->is_password_allowed($newpass) + $error = $contact->is_password_allowed($newpass) || $contact->change_password($newpass); # annoyingly specific to view/svc_acct.cgi, for now... diff --git a/httemplate/misc/process/contact-import.cgi b/httemplate/misc/process/contact-import.cgi new file mode 100644 index 000000000..108ee93e9 --- /dev/null +++ b/httemplate/misc/process/contact-import.cgi @@ -0,0 +1,10 @@ +<% $server->process %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Import'); + +my $server = + new FS::UI::Web::JSRPC 'FS::contact::Import::process_batch_import', $cgi; + + diff --git a/httemplate/misc/xmlhttp-censustract.html b/httemplate/misc/xmlhttp-censustract.html new file mode 100644 index 000000000..8a7686047 --- /dev/null +++ b/httemplate/misc/xmlhttp-censustract.html @@ -0,0 +1,14 @@ +<% $return %>\ +<%init> + +my $DEBUG = 0; + +my $conf = new FS::Conf; + +my $return = {}; +my $url = "http://data.fcc.gov/api/block/find?format=json&censusYear=" . $cgi->param('census_year') . "&latitude=" . $cgi->param('lat') . "&longitude=" . $cgi->param('lon'); + +use LWP::Simple; +my $return = get $url; + + \ No newline at end of file diff --git a/httemplate/search/access_user_log.html b/httemplate/search/access_user_log.html index 529ae8698..75bfa13ef 100644 --- a/httemplate/search/access_user_log.html +++ b/httemplate/search/access_user_log.html @@ -8,37 +8,60 @@ '#', 'Avg time', ], - 'fields' => [ sub { shift->path }, + 'fields' => [ sub { shift->pretty_path }, sub { shift->num_entries }, sub { shift->avg_render_seconds }, ], - 'sort_fields' => [qw( path num_entries avg_render_seconds )], + 'sort_fields' => [qw( pretty_path num_entries avg_render_seconds )], &> <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + unless $FS::CurrentUser::CurrentUser->access_right('Employee Reports'); my %hashref = (); my @extra_sql = (); -my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, '_date'); +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); push @extra_sql, "_date >= $beginning" if $beginning; push @extra_sql, "_date <= $ending" if $ending; -$hashref{usernum} = $cgi->param('usernum') if $cgi->param('usernum') =~ /^\d+$/; +my $count_hr = ''; +if ( $cgi->param('usernum') =~ /^\d+$/ ) { + $hashref{usernum} = $cgi->param('usernum'); + $count_hr = "WHERE usernum = ".$hashref{usernum}; +} + +if ( $cgi->param('skip_components') ) { + push @extra_sql, "path NOT IN ( '". join("','", + map "/var/www/html/freeside/$_", qw( + view/REAL_logo.cgi + misc/jsrsServer.html + misc/xmlhttp-reason-hint.html + config/config-image.cgi + misc/xmlhttp-part_pkg_taxproduct.html + misc/progress-popup.html + misc/file-upload.html + misc/confirm-censustract.html + misc/xmlhttp-address_standardize.html + misc/confirm-address_standardize.html + misc/xmlhttp-part_pkg_usageprice.html + ) + ). + "' )"; +} my $extra_sql = ''; $extra_sql .= (keys(%hashref) ? ' AND ' : ' WHERE ' ). join(' AND ', @extra_sql) if @extra_sql; -$extra_sql .= ' GROUP BY path ' +my $count_extra = $extra_sql; +$extra_sql .= ' GROUP BY pretty_path ' if $cgi->param('group_by') eq 'path'; -warn $extra_sql; my $query = { 'select' => join(' , ', - 'path', + "regexp_replace(path, '^/var/www/(html/)?freeside/', '') as pretty_path", 'COUNT(*) AS num_entries', 'ROUND(AVG(COALESCE(render_seconds,0)),1) '. ' AS avg_render_seconds', @@ -48,7 +71,6 @@ my $query = { 'extra_sql' => $extra_sql, }; -#uuh, no, need dates and usernum too, but whatever for now -my $count_query = 'SELECT COUNT(DISTINCT path) FROM access_user_log'; +my $count_query = "SELECT COUNT(DISTINCT path) FROM access_user_log $count_hr $count_extra"; diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html index 759f09521..5f02fef2f 100644 --- a/httemplate/search/contact.html +++ b/httemplate/search/contact.html @@ -29,6 +29,30 @@ my $email_sub = sub { join(', ', map $_->emailaddress, @contact_email); }; +my $work_phone_sub = sub { + my $contact = shift; + my $phone_type = qsearchs('phone_type', { 'typename' => 'Work' }); + #can't because contactnum is in the wrong field + my @contact_workphone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } ); + join(', ', map $_->phonenum, @contact_workphone); +}; + +my $mobile_phone_sub = sub { + my $contact = shift; + my $phone_type = qsearchs('phone_type', { 'typename' => 'Mobile' }); + #can't because contactnum is in the wrong field + my @contact_mobilephone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } ); + join(', ', map $_->phonenum, @contact_mobilephone); +}; + +my $home_phone_sub = sub { + my $contact = shift; + my $phone_type = qsearchs('phone_type', { 'typename' => 'Home' }); + #can't because contactnum is in the wrong field + my @contact_homephone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } ); + join(', ', map $_->phonenum, @contact_homephone); +}; + my $link; #for closure in this sub, we'll define it later my $contact_classname_sub = sub { my $contact = shift; @@ -44,9 +68,9 @@ my $contact_classname_sub = sub { $X_contact->contact_classname; }; -my @header = ( 'First', 'Last', 'Title', 'Email', 'Type' ); -my @fields = ( 'first', 'last', 'title', $email_sub, $contact_classname_sub ); -my @links = ( '', '', '', '', '', ); +my @header = ( 'First', 'Last', 'Title', 'Email', 'Work Phone', 'Mobile Phone', 'Home Phone', 'Type' ); +my @fields = ( 'first', 'last', 'title', $email_sub, $work_phone_sub, $mobile_phone_sub, $home_phone_sub, $contact_classname_sub ); +my @links = ( '', '', '', '', '', '', '', '', ); my $company_link = ''; diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html index 41309fdd1..30162506f 100755 --- a/httemplate/search/cust_main.html +++ b/httemplate/search/cust_main.html @@ -13,6 +13,10 @@ \&FS::UI::Web::cust_fields, @extra_fields, ], + 'sort_fields' => [ + FS::UI::Web::cust_sort_fields(), + @extra_sort_fields, + ], 'color' => [ FS::UI::Web::cust_colors(), map '', @extra_fields ], @@ -55,7 +59,7 @@ my @scalars = qw ( all_tags all_pkg_classnums any_pkg_status - with_referrals + with_referrals referral_status ); for my $param ( @scalars ) { @@ -68,11 +72,20 @@ for my $param (qw( classnum refnum pkg_classnum )) { $search_hash{$param} = [ $cgi->param($param) ]; } -#tags my $params = $cgi->Vars; + +#contacts +$search_hash{'contacts'} = { + map { $_ => $cgi->param($_), } + grep { /^(contacts_*)/ && $cgi->param($_) } + keys %$params +}; + +#tags $search_hash{'tagnum'} = [ map { /^tagnum(\d+)/ && $1 } - grep { /^tagnum(\d+)/ && $cgi->param($_) } keys %$params + grep { /^tagnum(\d+)/ && $cgi->param($_) } + keys %$params ]; ### @@ -110,9 +123,10 @@ $search_hash{'current_balance'} = ### my $sql_query = FS::cust_main::Search->search(\%search_hash); -my $count_query = delete($sql_query->{'count_query'}); -my @extra_headers = @{ delete($sql_query->{'extra_headers'}) }; -my @extra_fields = @{ delete($sql_query->{'extra_fields'}) }; +my $count_query = delete($sql_query->{'count_query'}); +my @extra_headers = @{ delete($sql_query->{'extra_headers'}) }; +my @extra_fields = @{ delete($sql_query->{'extra_fields'}) }; +my @extra_sort_fields = @{ delete($sql_query->{'extra_sort_fields'}) }; my $link = [ "${p}view/cust_main.cgi?", 'custnum' ]; diff --git a/httemplate/search/cust_timespan.html b/httemplate/search/cust_timespan.html new file mode 100644 index 000000000..a380b78ab --- /dev/null +++ b/httemplate/search/cust_timespan.html @@ -0,0 +1,117 @@ +<& elements/search.html, + 'title' => emt('Customer Timespan Report'), + 'name' => emt('customers'), + 'query' => { + select => join(', ', @select), + table => $table, + addl_from => $addl_from, + extra_sql => $extra_sql, + }, + 'count_query' => $count_query, + 'header' => \@header, + 'fields' => \@fields, + 'links' => \@links, + +&> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Advanced customer search'); + +my $table = 'cust_main'; +my $customer_link = [ "${p}view/cust_main.cgi?", 'custnum' ]; +my $agent_sql; + +## get agent numbers +if (length($cgi->param('agentnum'))) { + $cgi->param('agentnum') =~ /^(\d+)$/ or errorpage("Illegal agentnum"); + $agent_sql = ' and cust_main.agentnum = ' . $1; +} + + +## get selected requested customers +my $cust_status = $cgi->param('cust_status'); + +my %type_sql_map = ( + 'cancelled' => 'cancel_sql', + 'suspended' => 'susp_sql', +); + +my $type_sql = $type_sql_map{$cust_status}; +$type_sql = 'cancel_sql' unless $type_sql; + +my @custs = qsearch({ + table => 'cust_main', + extra_sql => ' where ' . FS::cust_main->$type_sql, +}); +my @customers = ('0'); +foreach my $cust (@custs) { push @customers, $cust->custnum; } + +## get locations +my $location_sub = sub { + my $customer = shift; + my @cust_location = qsearch({ + table => 'cust_location', + select => 'cust_location.*', + addl_from => ' LEFT JOIN cust_main ON (cust_location.locationnum = cust_main.bill_locationnum) ', + extra_sql => ' WHERE cust_main.custnum = ' . $customer->custnum , + } ); + + my $location; + foreach my $loc (@cust_location) { + $location .= $loc->address1 unless !$loc->address1; + $location .= "
    " . $loc->address2 unless !$loc->address2; + $location .= "
    " . $loc->city . ", " . $loc->state . ' ' . $loc->zip unless !$loc->city; + } + $location; +}; + +## get contact emails for customer +my $email_sub = sub { + my $customer = shift; + #can't because contactnum is in the wrong field #my @contact_email = $contact->contact_email; + my @contact_email = qsearch({ + table => 'contact_email', + addl_from => ' LEFT JOIN cust_contact ON (contact_email.contactnum = cust_contact.contactnum) LEFT JOIN cust_main ON (cust_contact.custnum = cust_main.custnum) ', + extra_sql => ' WHERE cust_main.custnum = ' . $customer->custnum , + } ); + join('
    ', map $_->emailaddress, @contact_email); +}; + +## sql to get only canceled customers +my @status = ('active', 'on hold', 'suspended', 'not yet billed', 'one-time charge'); +my $active_pkg_sql = 'select pkgnum from cust_pkg where cust_pkg.custnum = cust_main.custnum and ' . FS::cust_pkg->status_sql . " in ('".join( "', '", @status )."') limit 1"; + +## sql to get the first active date, last cancel date, and last reason. +my $active_date = 'select min(setup) from cust_pkg left join part_pkg using (pkgpart) where cust_pkg.custnum = cust_main.custnum and part_pkg.freq > \'0\''; +my $cancel_date = 'select max(cancel) from cust_pkg where cust_pkg.custnum = cust_main.custnum'; +my $cancel_reason = 'select reason.reason from cust_pkg + left join cust_pkg_reason on (cust_pkg.pkgnum = cust_pkg_reason.pkgnum) + left join reason on (cust_pkg_reason.reasonnum = reason.reasonnum) + where cust_pkg.custnum = cust_main.custnum and cust_pkg_reason.date = ('.$cancel_date.') +'; + +my @header = ( '#', 'Name', 'Address', 'Phone', 'Email', 'Active Date', 'Cancelled Date', 'Reason', 'Active Days' ); +my @fields = ( 'custnum', 'custname', $location_sub, 'daytime', $email_sub, 'active_date', 'cancel_date', 'cancel_reason', 'active_days' ); +my @links = ( $customer_link, $customer_link, '', '', '', '', '', '', '' ); +my @select = ( + 'cust_main.*', + 'cust_location.*', + 'part_pkg.*', + "(select to_char((select to_timestamp((".$active_date."))), 'Mon DD YYYY')) AS active_date", + "(select to_char((select to_timestamp((".$cancel_date."))), 'Mon DD YYYY')) AS cancel_date", + "($cancel_reason) AS cancel_reason", + "(select date_part('day', (select to_timestamp((".$cancel_date."))) - (select to_timestamp((".$active_date."))) )) AS active_days", + "CONCAT_WS(', ', last, first) AS custname", +); +my $addl_from = ' + LEFT JOIN cust_location ON (cust_main.bill_locationnum = cust_location.locationnum) + LEFT JOIN cust_pkg ON (cust_main.custnum = cust_pkg.custnum) + LEFT JOIN part_pkg ON (cust_pkg.pkgpart = part_pkg.pkgpart) +'; +my $extra_sql = " WHERE (".$active_date.") IS NOT NULL AND (".$cancel_date.") IS NOT NULL AND cust_main.custnum IN ('" . join( "', '", @customers ). "') $agent_sql "; + +## sql to get record count +my $count_query = 'select COUNT(*) from ' . $table . ' ' . $extra_sql; + + \ No newline at end of file diff --git a/httemplate/search/elements/cust_pay_or_refund.html b/httemplate/search/elements/cust_pay_or_refund.html index 99f9ba0eb..fbcae9d4d 100755 --- a/httemplate/search/elements/cust_pay_or_refund.html +++ b/httemplate/search/elements/cust_pay_or_refund.html @@ -87,7 +87,7 @@ my $conf = FS::Conf->new; my $money = ($conf->config('money_char') || '$') . '%.2f'; die "access denied" - unless $curuser->access_right('Financial reports'); + unless $curuser->access_right('Basic payment and refund reports'); my $table = $opt{'table'} || 'cust_'.$opt{'thing'}; diff --git a/httemplate/search/elements/options_cust_contacts.html b/httemplate/search/elements/options_cust_contacts.html new file mode 100644 index 000000000..cfbf834b0 --- /dev/null +++ b/httemplate/search/elements/options_cust_contacts.html @@ -0,0 +1,36 @@ + + <% mt('First name') |h %> + + + + + <% mt('Last name') |h %> + + + + + <% mt('Email') |h %> + + + + + <% mt('Home Phone') |h %> + + + + + <% mt('Work Phone') |h %> + + + + + <% mt('Mobile Phone') |h %> + + + +<%init> + +my %opt = @_; +my $field_prefix = $opt{'pre_fix'} ? $opt{'pre_fix'} : ''; + + \ No newline at end of file diff --git a/httemplate/search/elements/report_cust_pay_or_refund.html b/httemplate/search/elements/report_cust_pay_or_refund.html index bff470a69..f7a81a066 100644 --- a/httemplate/search/elements/report_cust_pay_or_refund.html +++ b/httemplate/search/elements/report_cust_pay_or_refund.html @@ -174,7 +174,7 @@ my $table = 'cust_'.$opt{'thing'}; my $name_singular = $opt{'name_singular'}; die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + unless $FS::CurrentUser::CurrentUser->access_right('Basic payment and refund reports'); my $conf = new FS::Conf; diff --git a/httemplate/search/employee_audit.html b/httemplate/search/employee_audit.html index 2bc6ff46e..991758c4a 100644 --- a/httemplate/search/employee_audit.html +++ b/httemplate/search/employee_audit.html @@ -7,7 +7,7 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Employees: Audit Report'); + unless $FS::CurrentUser::CurrentUser->access_right('Employee Reports'); my %tables = ( cust_pay => 'Payments', diff --git a/httemplate/search/report_access_user_log.html b/httemplate/search/report_access_user_log.html index 0c8acb35e..209adfa8a 100644 --- a/httemplate/search/report_access_user_log.html +++ b/httemplate/search/report_access_user_log.html @@ -2,20 +2,21 @@
    - + - - - - - + <% emt('Search options') %> +
    - Search options -
    <& /elements/tr-input-beginning_ending.html &> <& /elements/tr-select-user.html &> + <& /elements/tr-checkbox.html, + label => 'Omit components', + field => 'skip_components', + value => 'Y', + &> +

    @@ -27,7 +28,7 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + unless $FS::CurrentUser::CurrentUser->access_right('Employee Reports'); my $group_by = ''; if ( $cgi->param('group_by') =~ /^(\w+)$/ ) { diff --git a/httemplate/search/report_cust_main.html b/httemplate/search/report_cust_main.html index c458bb4af..0a6726215 100755 --- a/httemplate/search/report_cust_main.html +++ b/httemplate/search/report_cust_main.html @@ -110,6 +110,24 @@
    + <% emt('Referral search options') %> + + + <& /elements/tr-input-text.html, + label => emt('At least this many'), + field => 'with_referrals', + size => 4, + maxlength => 4, + &> + + <& /elements/tr-select-cust_main-status.html, + label => emt('Referral status'), + field => 'referral_status', + &> + +
    +
    + <% emt('Package search options') %> @@ -148,6 +166,14 @@

    + <% emt('Contacts search options') %> + + <& elements/options_cust_contacts.html, + 'pre_fix' => 'contacts_', + &> +
    +
    + <% emt('Billing search options') %> diff --git a/httemplate/search/report_cust_timespan.html b/httemplate/search/report_cust_timespan.html new file mode 100644 index 000000000..4ff3bb892 --- /dev/null +++ b/httemplate/search/report_cust_timespan.html @@ -0,0 +1,42 @@ +<& /elements/header.html, mt($title) &> + + + +
    + + <& /elements/tr-select-agent.html, + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'label' => emt('Contacts for agent: '), + 'disable_empty' => 0, + &> + + <& /elements/tr-select.html, + 'label' => 'Customer status', + 'field' => 'cust_status', + 'options' => [ 'cancelled', 'suspended'], + 'labels' => { 'cancelled' => 'Cancelled Customers', + 'suspended' => 'Suspended Customers', + }, + 'curr_value' => scalar( $cgi->param('cust_status') ), + &> + + + +
    + +
    + + + + +<& /elements/footer.html &> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List contacts'); + +my $conf = new FS::Conf; + +my $title = 'Customer Timespan Report'; + + \ No newline at end of file diff --git a/httemplate/search/report_employee_audit.html b/httemplate/search/report_employee_audit.html index 461849b76..6008e1caa 100644 --- a/httemplate/search/report_employee_audit.html +++ b/httemplate/search/report_employee_audit.html @@ -23,7 +23,7 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Employees: Audit Report'); + unless $FS::CurrentUser::CurrentUser->access_right('Employee Reports'); my %tables = ( cust_pay => 'Payments', diff --git a/httemplate/view/Status-db_size_detail.html b/httemplate/view/Status-db_size_detail.html new file mode 100644 index 000000000..96c2da006 --- /dev/null +++ b/httemplate/view/Status-db_size_detail.html @@ -0,0 +1,39 @@ +<& /elements/header-popup.html, { + 'title' => 'Database size details', + } +&> + +<& /search/elements/search.html, + 'name_singular' => 'table', + 'header' => [ 'Table', 'Size' ], + 'query' => $query, + 'count_query' => $count_query, + 'nohtmlheader' => 1, +&> + +<& /elements/footer-popup.html &> +<%init> + +my $query = q{ + + SELECT table_name, pg_size_pretty(total_bytes) AS total + FROM ( + SELECT * FROM ( + SELECT relname AS TABLE_NAME, + pg_total_relation_size(c.oid) AS total_bytes + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE relkind = 'r' + AND nspname = 'public' + ) a + ) a order by total_bytes desc +}; + +my $count_query = q{ + SELECT count(*) FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE relkind = 'r' + AND nspname = 'public' +}; + + diff --git a/httemplate/view/Status.html b/httemplate/view/Status.html index e08bfe44b..7fb03eb2f 100644 --- a/httemplate/view/Status.html +++ b/httemplate/view/Status.html @@ -1,4 +1,7 @@ <& /elements/header.html, 'System Status' &> + +<& /elements/init_overlib.html &> + % foreach my $section ( keys %status ) { <% mt($section) |h %> @@ -11,6 +14,7 @@


    % } + <& /elements/footer.html &> <%init> @@ -40,8 +44,13 @@ if ( $db eq 'PostgreSQL' && $db_ver =~ /^\s*PostgreSQL\s+([\w\.]+)\s+on\s+/ ) { my $db_size = 'Unknown'; if ( $db eq 'PostgreSQL' ) { $db_size = FS::Record->scalar_sql(qq( - SELECT pg_size_pretty(pg_database_size('freeside')) - )); + SELECT pg_size_pretty(pg_database_size('freeside')) + )). ' '. + include('/elements/popup_link.html', + 'action' => 'Status-db_size_detail.html', + 'label' => '(details)', + 'actionlabel' => 'Database size details', + ); } tie my %status, 'Tie::IxHash', diff --git a/httemplate/view/cust_main/packages/package.html b/httemplate/view/cust_main/packages/package.html index 61143413c..ab19ff94a 100644 --- a/httemplate/view/cust_main/packages/package.html +++ b/httemplate/view/cust_main/packages/package.html @@ -258,7 +258,7 @@ actionlabel => emt('Change package'), #width => 768, width => 960, - height => 538, + height => 650, }, { label => 'Discount package', @@ -589,13 +589,13 @@ "zip=;country=$countrydefault", actionlabel => emt('Change location'), width => 960, - height => 530, + height => 650, }, { label => 'Edit location', acl => 'Change customer package', condition => sub { ! $change_from && $cust_pkg->locationnum != $cust_pkg->cust_main->ship_locationnum }, - popup => 'misc/cust_location.cgi?locationnum='. + popup => 'edit/cust_location.cgi?locationnum='. $cust_pkg->locationnum, actionlabel => emt('Edit location'), width => 700, diff --git a/httemplate/view/cust_main/payment_history/payment.html b/httemplate/view/cust_main/payment_history/payment.html index 6241f11b8..6402383d8 100644 --- a/httemplate/view/cust_main/payment_history/payment.html +++ b/httemplate/view/cust_main/payment_history/payment.html @@ -180,17 +180,18 @@ if ( $cust_pay->closed !~ /^Y/i } my $void = ''; -# note: "TOKN" is not yet supported in stock freeside -my $voidmsg = $cust_pay->payby =~ /^(CARD|CHEK|TOKN)$/ +my $voidmsg = $cust_pay->payby =~ /^(CARD|CHEK)$/ ? ' (' . emt('do not send anything to the payment gateway').')' : ''; $void = ' ('. - include( '/elements/popup_link.html', - 'label' => emt('void'), - 'action' => "${p}misc/void-cust_pay.html?".$cust_pay->paynum, - 'actionlabel' => emt('Void payment'), - ). - ')' + include( '/elements/popup_link.html', + 'label' => emt('void'), + 'action' => "${p}misc/void-cust_pay.html?".$cust_pay->paynum, + 'actionlabel' => emt('Void payment'), + 'title' => emt('Void this payment from the database'). + $voidmsg, + ). + ')' if $cust_pay->closed !~ /^Y/i && ( ( $cust_pay->payby eq 'CARD' && $opt{'Credit card void'} ) || ( $cust_pay->payby eq 'CHEK' && $opt{'Echeck void'} ) diff --git a/httemplate/view/svc_acct.cgi b/httemplate/view/svc_acct.cgi index 747477989..026effb45 100755 --- a/httemplate/view/svc_acct.cgi +++ b/httemplate/view/svc_acct.cgi @@ -20,8 +20,9 @@ "javascript:areyousure(\'${p}misc/cancel-unaudited.cgi?$svcnum\')", ) &> -% } +% } +<& /elements/error.html &> <& elements/svc_radius_usage.html, 'svc' => $svc_acct, diff --git a/ng_selfservice/payment_cc.php b/ng_selfservice/payment_cc.php index 5c49dab36..37f57c057 100644 --- a/ng_selfservice/payment_cc.php +++ b/ng_selfservice/payment_cc.php @@ -88,8 +88,8 @@ if ( $receipt_html ) { ?> diff --git a/rt/share/html/Search/Build.html b/rt/share/html/Search/Build.html index 306519242..cffcbf426 100644 --- a/rt/share/html/Search/Build.html +++ b/rt/share/html/Search/Build.html @@ -67,7 +67,6 @@ %# <& /Elements/Header, Title => $title &> <& /Elements/Tabs, %TabArgs &> -
    @@ -200,7 +199,7 @@ my $cf_field_names = # Try to find if we're adding a clause foreach my $arg ( keys %ARGS ) { - next unless $arg =~ m/^ValueOf(\w+|($cf_field_names).\{.*?\})$/ + next unless $arg =~ m/^ValueOf([\w\.]+|($cf_field_names).\{.*?\})$/ && ( ref $ARGS{$arg} eq "ARRAY" ? grep $_ ne '', @{ $ARGS{$arg} } : $ARGS{$arg} ne '' );