From 7b90424db790634149932f49e253e4b33f8186a5 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Tue, 21 May 2019 13:42:16 -0400 Subject: RT# 83401 Catch exception and display suitable error --- httemplate/misc/process/payment.cgi | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/httemplate/misc/process/payment.cgi b/httemplate/misc/process/payment.cgi index 56bcfd872..5b18367f7 100644 --- a/httemplate/misc/process/payment.cgi +++ b/httemplate/misc/process/payment.cgi @@ -186,15 +186,22 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) { %saveopt = map { $_ => scalar($cgi->param($_)) } @{$payby2fields{$payby}}; } - my $error = $cust_main->save_cust_payby( - 'saved_cust_payby' => \$cust_payby, - 'payment_payby' => $payby, - 'auto' => scalar($cgi->param('auto')), - 'weight' => scalar($cgi->param('weight')), - 'payinfo' => $payinfo, - 'payname' => $payname, - %saveopt - ); + my $error; + { + local $@; + eval { + $error = $cust_main->save_cust_payby( + 'saved_cust_payby' => \$cust_payby, + 'payment_payby' => $payby, + 'auto' => scalar($cgi->param('auto')), + 'weight' => scalar($cgi->param('weight')), + 'payinfo' => $payinfo, + 'payname' => $payname, + %saveopt + ); + }; + $error ||= $@; + } errorpage("error saving info, payment not processed: $error") if $error; -- cgit v1.2.1 From 7bfdea32633df161273631bcdc6b33b93867f5b2 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Tue, 21 May 2019 15:54:13 -0400 Subject: RT# 83401 Send country field to B::OP on tokenize --- FS/FS/cust_main/Billing_Realtime.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 1ac12ce6b..89d63dd26 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -323,7 +323,8 @@ sub _bop_cust_payby_options { if ( $cust_payby->locationnum ) { my $cust_location = $cust_payby->cust_location; - $options->{$_} = $cust_location->$_() for qw( address1 address2 city state zip ); + $options->{$_} = $cust_location->$_() + for qw( address1 address2 city state zip country ); } } } -- cgit v1.2.1 From a98de57aee063b3ff737c283336f83b2e50e14a8 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Wed, 22 May 2019 12:58:05 -0400 Subject: RT 77532 - added contact phone numbers to advanced customer report --- FS/FS/ConfDefaults.pm | 20 ++++++++--------- FS/FS/UI/Web.pm | 21 ++++++++++++++++- FS/FS/cust_main.pm | 26 ++++++++++++++++++++++ FS/FS/cust_main/Search.pm | 19 ++++++++-------- httemplate/elements/select-cust-fields.html | 10 +++++++-- httemplate/elements/tr-select-cust-fields.html | 9 +++++++- httemplate/search/contact.html | 18 +++++++-------- .../search/elements/options_cust_contacts.html | 17 ++++---------- 8 files changed, 94 insertions(+), 46 deletions(-) diff --git a/FS/FS/ConfDefaults.pm b/FS/FS/ConfDefaults.pm index bd5893439..d81e8e2dc 100644 --- a/FS/FS/ConfDefaults.pm +++ b/FS/FS/ConfDefaults.pm @@ -36,9 +36,9 @@ sub cust_fields_avail { ( 'Agent | Agent Cust# or Cust# | Cust. Status | Customer' => 'Agent | Agent Cust# | Status | Last, First or Company (Last, First)', - 'Customer | Day phone | Night phone | Mobile phone | Fax number' => + "Customer | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s)" => 'Customer | (all phones)', - 'Cust# | Customer | Day phone | Night phone | Mobile phone | Fax number' => + 'Cust# | Customer | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s)' => 'custnum | Customer | (all phones)', 'Cust. Status | Name | Company' => @@ -56,28 +56,28 @@ sub cust_fields_avail { ( 'Cust# | Cust. Status | Name | Company' => 'custnum | Status | Last, First | Company', - 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Mobile phone | Fax number | Contact email(s) | Invoices | Messages' => + 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | Contact email(s) | Invoices | Messages' => 'custnum | Status | Last, First | Company | (address) | (all phones) | Contact email(s)', - 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Mobile phone | Fax number | Invoicing email(s)' => + 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | Invoicing email(s)' => 'custnum | Status | Last, First | Company | (address) | (all phones) | Invoicing email(s)', - 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Mobile phone | Fax number | Invoicing email(s) | Current Balance' => + 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | Invoicing email(s) | Current Balance' => 'custnum | Status | Last, First | Company | (address) | (all phones) | Invoicing email(s) | Current Balance', - 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s)' => + 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s)' => 'custnum | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s)', - 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Current Balance' => + 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Current Balance' => 'custnum | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Current Balance', - 'Cust# | Agent Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Current Balance' => + 'Cust# | Agent Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Current Balance' => 'custnum | Agent Cust# | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Current Balance', - 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Latitude | (bill) Longitude | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Latitude | (service) Longitude | Invoicing email(s) | Current Balance' => + 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Latitude | (bill) Longitude | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Latitude | (service) Longitude | Invoicing email(s) | Current Balance' => 'custnum | Status | Last, First | Company | (address+coord) | (all phones) | (service address+coord) | Invoicing email(s) | Current Balance', - 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Latitude | (bill) Longitude | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Latitude | (service) Longitude | Invoicing email(s) | Current Balance | Advertising Source' => + 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Latitude | (bill) Longitude | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Latitude | (service) Longitude | Invoicing email(s) | Current Balance | Advertising Source' => 'custnum | Status | Last, First | Company | (address+coord) | (all phones) | (service address+coord) | Invoicing email(s) | Current Balance | Advertising Source', 'Invoicing email(s)' => 'Invoicing email(s)', diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm index b6dda8fcc..9eb49c430 100644 --- a/FS/FS/UI/Web.pm +++ b/FS/FS/UI/Web.pm @@ -7,7 +7,7 @@ use Carp qw( confess ); use HTML::Entities; use FS::Conf; use FS::Misc::DateTime qw( parse_datetime day_end ); -use FS::Record qw(dbdef); +use FS::Record qw(dbdef qsearch); use FS::cust_main; # are sql_balance and sql_date_balance in the right module? #use vars qw(@ISA); @@ -357,6 +357,25 @@ sub cust_header { $header2method{'Cust#'} = 'display_custnum' if $conf->exists('cust_main-default_agent_custid'); +foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { + $header2method{'Contact '.$phone_type->typename.' phone(s)'} = sub { + my $self = shift; + my $num = $phone_type->phonetypenum; + + my @phones; + foreach ($self->contact_list_name_phones) { + my $data = [ + { + 'data' => $_->first.' '.$_->last.' '.FS::contact_phone::phonenum_pretty($_), + }, + ]; + push @phones, $data if $_->phonetypenum eq $phone_type->phonetypenum; + } + return \@phones; + }; + +} + my %header2colormethod = ( 'Cust. Status' => 'cust_statuscolor', ); diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 10433ed95..57d598a74 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3175,6 +3175,32 @@ sub contact_list_email_destinations { }); } +=item contact_list_name_phones + +Returns a list of contact phone numbers. +{ phonetypenum => '1', phonenum => 'xxxxxxxxxx', first => 'firstname', last => 'lastname', countrycode => '1' } + +=cut + +sub contact_list_name_phones { + my $self = shift; + my $phone_type = shift; + + warn "$me contact_list_phones" if $DEBUG; + + return () if !$self->custnum; # not yet inserted + return map { $_ } + qsearch({ + table => 'cust_contact', + select => 'phonetypenum, phonenum, first, last, countrycode', + addl_from => ' JOIN contact USING (contactnum) '. + ' JOIN contact_phone USING (contactnum)', + hashref => { 'custnum' => $self->custnum, 'phonetypenum' => $phone_type, }, + order_by => 'ORDER BY custcontactnum DESC', + extra_sql => '', + }); +} + =item contact_list_emailonly Returns an array of hashes containing the emails. Used for displaying contact email field in advanced customer reports. diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index ae219c801..63f10fb64 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -1090,25 +1090,24 @@ sub search { ) "; } - if ($contact_params->{'contacts_homephone'} || $contact_params->{'contacts_workphone'} || $contact_params->{'contacts_mobilephone'}) { - foreach my $phone (qw( contacts_homephone contacts_workphone contacts_mobilephone )) { + if ( grep { /^contacts_phonetypenum(\d+)$/ } keys %{ $contact_params } ) { + my $phone_query; + foreach my $phone ( grep { /^contacts_phonetypenum(\d+)$/ } keys %{ $contact_params } ) { + $phone =~ /^contacts_phonetypenum(\d+)$/ or die "No phone type num $1 from $phone"; + my $phonetypenum = $1; (my $num = $contact_params->{$phone}) =~ s/\W//g; if ( $num =~ /^1?(\d{3})(\d{3})(\d{4})(\d*)$/ ) { $contact_params->{$phone} = "$1$2$3"; } + $phone_query .= " AND ( contact_phone.phonetypenum = '".$phonetypenum."' AND contact_phone.phonenum = '" . $contact_params->{$phone} . "' )" + unless !$contact_params->{$phone}; } - 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'}; push @where, "EXISTS ( SELECT 1 FROM contact_phone JOIN cust_contact USING (contactnum) WHERE cust_contact.custnum = cust_main.custnum - $home_query $work_query $mobile_query + $phone_query ) "; } -} + } ## diff --git a/httemplate/elements/select-cust-fields.html b/httemplate/elements/select-cust-fields.html index 5e3063877..7396808b1 100644 --- a/httemplate/elements/select-cust-fields.html +++ b/httemplate/elements/select-cust-fields.html @@ -1,8 +1,14 @@ <%init> my( $cust_fields, %opt ) = @_; - use FS::ConfDefaults; - $opt{'avail_fields'} ||= [ FS::ConfDefaults->cust_fields_avail() ]; + my @fields = FS::ConfDefaults->cust_fields_avail(); + my $contact_phone_list; + foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { + $contact_phone_list .= " | Contact ".$phone_type->typename." phone(s)"; + } + @fields = map {s/\| Contact phone\(s\)/$contact_phone_list/g; $_; } @fields; + + $opt{'avail_fields'} ||= [ @fields ]; tie my %hash, 'Tie::IxHash', @{ $opt{'avail_fields'} }; diff --git a/httemplate/elements/tr-select-cust-fields.html b/httemplate/elements/tr-select-cust-fields.html index dd8513316..62b8144e3 100644 --- a/httemplate/elements/tr-select-cust-fields.html +++ b/httemplate/elements/tr-select-cust-fields.html @@ -6,6 +6,13 @@ my( $cust_fields, %opt ) = @_; -$opt{'avail_fields'} ||= [ FS::ConfDefaults->cust_fields_avail() ]; +my @fields = FS::ConfDefaults->cust_fields_avail(); +my $contact_phone_list; +foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { + $contact_phone_list .= " | Contact ".$phone_type->typename." phone(s)"; +} +@fields = map {s/\| Contact phone\(s\)/$contact_phone_list/g; $_; } @fields; + +$opt{'avail_fields'} ||= [ @fields ]; diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html index 24cb237c3..0f2b283b5 100644 --- a/httemplate/search/contact.html +++ b/httemplate/search/contact.html @@ -38,7 +38,6 @@ my $classnum_null = grep{ $_ eq 0 } $cgi->param('classnum'); # Catch destination values from dest multi-checkbox, default to message # irrelevant to prospect contacts my @dest = grep{ /^(message|invoice)$/ } $cgi->param('dest'); -@dest = ('message') unless @dest; # Cache the contact_class table my %classname = @@ -125,7 +124,7 @@ if (@classnum || $classnum_null) { if (@dest && $link eq 'cust_main') { my @stm; push @stm, "cust_contact.${_}_dest IS NOT NULL" for @dest; - $extra_sql .= "\nAND (".join(' OR ',@stm).') '; + $extra_sql .= "\nAND (".join(' AND ',@stm).') '; } if ($DEBUG) { @@ -141,20 +140,23 @@ if ($DEBUG) { # Prepare to display phone numbers # adds 3 additional queries per table record :-( -my %phonetype = (qw/1 Work 2 Home 3 Mobile 4 Fax/); -my %phoneid = (qw/Work 1 Home 2 Mobile 3 Fax 4/); my $get_phone_sub = sub { my $type = shift; return sub { my $rec = shift; my @p = qsearch('contact_phone', { contactnum => $rec->contact_contactnum, - phonetypenum => $phoneid{$type} + phonetypenum => $type, }); - @p ? (join ', ',map{$_->phonenum} @p) : undef; + @p ? (join ', ',map{$_->phonenum_pretty} @p) : undef; }; }; +my @phones; +foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { + push @phones, { label => $phone_type->typename.' Phone', field => $get_phone_sub->($phone_type->phonetypenum), }; +} + # Cache contact types my %classname = map {$_->classnum => $_->classname} @@ -166,9 +168,7 @@ my @report = ( { label => 'Last', field => 'contact_last' }, { label => 'Title', field => 'contact_title' }, { label => 'E-Mail', field => 'contact_email_emailaddress' }, - { label => 'Work Phone', field => $get_phone_sub->('Work') }, - { label => 'Mobile Phone', field => $get_phone_sub->('Mobile') }, - { label => 'Home Phone', field => $get_phone_sub->('Home') }, + @phones, { label => 'Type', field => sub { my $rec = shift; diff --git a/httemplate/search/elements/options_cust_contacts.html b/httemplate/search/elements/options_cust_contacts.html index cfbf834b0..8a6b76913 100644 --- a/httemplate/search/elements/options_cust_contacts.html +++ b/httemplate/search/elements/options_cust_contacts.html @@ -12,21 +12,12 @@ <% mt('Email') |h %> - - - <% mt('Home Phone') |h %> - - - - - <% mt('Work Phone') |h %> - - - +% foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { - <% mt('Mobile Phone') |h %> - + <% $phone_type->typename. ' Phone' |h %> + +% } <%init> -- cgit v1.2.1 From 2b199169bd62b8582eedb872b4e76bee48899e81 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Wed, 22 May 2019 21:15:35 -0400 Subject: RT# 77532 - created method to display phone types --- FS/FS/UI/Web.pm | 3 +-- FS/FS/phone_type.pm | 12 ++++++++++++ httemplate/elements/contact.html | 2 +- httemplate/elements/select-cust-fields.html | 2 +- httemplate/elements/tr-select-cust-fields.html | 2 +- httemplate/search/contact.html | 2 +- httemplate/search/elements/options_cust_contacts.html | 2 +- httemplate/view/cust_main/contacts_new.html | 2 +- 8 files changed, 19 insertions(+), 8 deletions(-) diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm index 9eb49c430..76071d900 100644 --- a/FS/FS/UI/Web.pm +++ b/FS/FS/UI/Web.pm @@ -357,7 +357,7 @@ sub cust_header { $header2method{'Cust#'} = 'display_custnum' if $conf->exists('cust_main-default_agent_custid'); -foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { +foreach my $phone_type ( FS::phone_type->get_phone_types() ) { $header2method{'Contact '.$phone_type->typename.' phone(s)'} = sub { my $self = shift; my $num = $phone_type->phonetypenum; @@ -373,7 +373,6 @@ foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { } return \@phones; }; - } my %header2colormethod = ( diff --git a/FS/FS/phone_type.pm b/FS/FS/phone_type.pm index ded6b918a..3d9dced6f 100644 --- a/FS/FS/phone_type.pm +++ b/FS/FS/phone_type.pm @@ -91,6 +91,18 @@ sub check { $self->SUPER::check; } +=item get_phone_types + +returns a list of phone_types. + +=cut + +sub get_phone_types { + ## only using mobile(3) and work(1) right now. + my @phone_types = qsearch({table=>'phone_type', order_by=>'ORDER BY weight DESC', extra_sql => " WHERE phonetypenum IN ('1','3')"}); + return @phone_types; +} + # Used by FS::Setup to initialize a new database. sub _populate_initial_data { my ($class, %opts) = @_; diff --git a/httemplate/elements/contact.html b/httemplate/elements/contact.html index 599f6629d..308b846bc 100644 --- a/httemplate/elements/contact.html +++ b/httemplate/elements/contact.html @@ -180,7 +180,7 @@ unless ($opt{'for_prospect'}) { } my $first = 0; -foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { +foreach my $phone_type ( FS::phone_type->get_phone_types() ) { next if $phone_type->typename =~ /^(Home|Fax)$/; my $f = 'phonetypenum'.$phone_type->phonetypenum; $label{$f} = $phone_type->typename. ' phone'; diff --git a/httemplate/elements/select-cust-fields.html b/httemplate/elements/select-cust-fields.html index 7396808b1..63e92ac3d 100644 --- a/httemplate/elements/select-cust-fields.html +++ b/httemplate/elements/select-cust-fields.html @@ -3,7 +3,7 @@ my @fields = FS::ConfDefaults->cust_fields_avail(); my $contact_phone_list; - foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { + foreach my $phone_type ( FS::phone_type->get_phone_types() ) { $contact_phone_list .= " | Contact ".$phone_type->typename." phone(s)"; } @fields = map {s/\| Contact phone\(s\)/$contact_phone_list/g; $_; } @fields; diff --git a/httemplate/elements/tr-select-cust-fields.html b/httemplate/elements/tr-select-cust-fields.html index 62b8144e3..0af432ae7 100644 --- a/httemplate/elements/tr-select-cust-fields.html +++ b/httemplate/elements/tr-select-cust-fields.html @@ -8,7 +8,7 @@ my( $cust_fields, %opt ) = @_; my @fields = FS::ConfDefaults->cust_fields_avail(); my $contact_phone_list; -foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { +foreach my $phone_type ( FS::phone_type->get_phone_types() ) { $contact_phone_list .= " | Contact ".$phone_type->typename." phone(s)"; } @fields = map {s/\| Contact phone\(s\)/$contact_phone_list/g; $_; } @fields; diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html index 0f2b283b5..5def389cc 100644 --- a/httemplate/search/contact.html +++ b/httemplate/search/contact.html @@ -153,7 +153,7 @@ my $get_phone_sub = sub { }; my @phones; -foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { +foreach my $phone_type ( FS::phone_type->get_phone_types() ) { push @phones, { label => $phone_type->typename.' Phone', field => $get_phone_sub->($phone_type->phonetypenum), }; } diff --git a/httemplate/search/elements/options_cust_contacts.html b/httemplate/search/elements/options_cust_contacts.html index 8a6b76913..372bc6755 100644 --- a/httemplate/search/elements/options_cust_contacts.html +++ b/httemplate/search/elements/options_cust_contacts.html @@ -12,7 +12,7 @@ <% mt('Email') |h %> -% foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { +% foreach my $phone_type ( FS::phone_type->get_phone_types() ) { <% $phone_type->typename. ' Phone' |h %> diff --git a/httemplate/view/cust_main/contacts_new.html b/httemplate/view/cust_main/contacts_new.html index 9252b2197..bd213d1dd 100644 --- a/httemplate/view/cust_main/contacts_new.html +++ b/httemplate/view/cust_main/contacts_new.html @@ -80,7 +80,7 @@ %} <%once> -my @phone_type = qsearch({table=>'phone_type', order_by=>'weight'}); +my @phone_type = FS::phone_type->get_phone_types(); <%init> -- cgit v1.2.1 From 606acab48f55da2b8846e5539839d591b63ae18c Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Thu, 23 May 2019 11:07:47 -0400 Subject: RT# 75680 - telapi_voip cdr csv import --- FS/FS/cdr/telapi_voip.pm | 52 -------------------------------------------- FS/FS/cdr/telapi_voip_csv.pm | 28 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 52 deletions(-) delete mode 100644 FS/FS/cdr/telapi_voip.pm create mode 100644 FS/FS/cdr/telapi_voip_csv.pm diff --git a/FS/FS/cdr/telapi_voip.pm b/FS/FS/cdr/telapi_voip.pm deleted file mode 100644 index abc7d5bd2..000000000 --- a/FS/FS/cdr/telapi_voip.pm +++ /dev/null @@ -1,52 +0,0 @@ -package FS::cdr::telapi_voip; -use base qw( FS::cdr ); - -use strict; -use vars qw( @ISA %info $CDR_TYPES ); -use FS::Record qw( qsearch ); -use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker ); - -%info = ( - 'name' => 'telapi_voip', - 'weight' => 601, - 'header' => 1, - 'type' => 'csv', - 'import_fields' => [ - _cdr_date_parser_maker('startdate'), #'date gmt' - 'src', # source - 'dst', # destination - 'clid', # callerid - skip(1), # hangup code - skip(1), # sip account - 'src_ip_addr', # orig ip - 'duration', # duration - skip(1), # per minute - 'upstream_price', # callcost - sub { - my($cdr, $cdrtypename, $conf, $param) = @_; - return unless length($cdrtypename); - _init_cdr_types(); - unless (defined $CDR_TYPES->{$cdrtypename}) { - warn "Skipping Record: CDR type name $cdrtypename does not exist!"; - $param->{skiprow} = 1; - } - $cdr->cdrtypenum($CDR_TYPES->{$cdrtypename}); - }, # type - _cdr_min_parser_maker('billsec'), #PriceDurationMins - ], -); - -sub skip { map {''} (1..$_[0]) } - -sub _init_cdr_types { - unless ($CDR_TYPES) { - $CDR_TYPES = {}; - foreach my $cdr_type ( qsearch('cdr_type') ) { - die "multiple cdr_types with same cdrtypename".$cdr_type->cdrtypename - if defined $CDR_TYPES->{$cdr_type->cdrtypename}; - $CDR_TYPES->{$cdr_type->cdrtypename} = $cdr_type->cdrtypenum; - } - } -} - -1; \ No newline at end of file diff --git a/FS/FS/cdr/telapi_voip_csv.pm b/FS/FS/cdr/telapi_voip_csv.pm new file mode 100644 index 000000000..ee38edbcf --- /dev/null +++ b/FS/FS/cdr/telapi_voip_csv.pm @@ -0,0 +1,28 @@ +package FS::cdr::telapi_voip_csv; +use base qw( FS::cdr ); + +use strict; +use vars qw( @ISA %info $CDR_TYPES ); +use FS::Record qw( qsearch ); +use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker ); + +%info = ( + 'name' => 'telapi_voip (csv file)', + 'weight' => 601, + 'header' => 1, + 'type' => 'csv', + 'import_fields' => [ + skip(1), # Inbound/Outbound + _cdr_date_parser_maker('startdate'), # date + skip(1), # cost per minute + 'upstream_price', # call cost + 'billsec', # duration + 'src', # source + 'dst', # destination + skip(1), # hangup code + ], +); + +sub skip { map {''} (1..$_[0]) } + +1; -- cgit v1.2.1 From 269c5484bb13430773443a2ae376368be0b56c2c Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Thu, 23 May 2019 11:35:25 -0400 Subject: Revert "RT# 75680 - telapi_voip cdr csv import" This reverts commit 606acab48f55da2b8846e5539839d591b63ae18c. --- FS/FS/cdr/telapi_voip.pm | 52 ++++++++++++++++++++++++++++++++++++++++++++ FS/FS/cdr/telapi_voip_csv.pm | 28 ------------------------ 2 files changed, 52 insertions(+), 28 deletions(-) create mode 100644 FS/FS/cdr/telapi_voip.pm delete mode 100644 FS/FS/cdr/telapi_voip_csv.pm diff --git a/FS/FS/cdr/telapi_voip.pm b/FS/FS/cdr/telapi_voip.pm new file mode 100644 index 000000000..abc7d5bd2 --- /dev/null +++ b/FS/FS/cdr/telapi_voip.pm @@ -0,0 +1,52 @@ +package FS::cdr::telapi_voip; +use base qw( FS::cdr ); + +use strict; +use vars qw( @ISA %info $CDR_TYPES ); +use FS::Record qw( qsearch ); +use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker ); + +%info = ( + 'name' => 'telapi_voip', + 'weight' => 601, + 'header' => 1, + 'type' => 'csv', + 'import_fields' => [ + _cdr_date_parser_maker('startdate'), #'date gmt' + 'src', # source + 'dst', # destination + 'clid', # callerid + skip(1), # hangup code + skip(1), # sip account + 'src_ip_addr', # orig ip + 'duration', # duration + skip(1), # per minute + 'upstream_price', # callcost + sub { + my($cdr, $cdrtypename, $conf, $param) = @_; + return unless length($cdrtypename); + _init_cdr_types(); + unless (defined $CDR_TYPES->{$cdrtypename}) { + warn "Skipping Record: CDR type name $cdrtypename does not exist!"; + $param->{skiprow} = 1; + } + $cdr->cdrtypenum($CDR_TYPES->{$cdrtypename}); + }, # type + _cdr_min_parser_maker('billsec'), #PriceDurationMins + ], +); + +sub skip { map {''} (1..$_[0]) } + +sub _init_cdr_types { + unless ($CDR_TYPES) { + $CDR_TYPES = {}; + foreach my $cdr_type ( qsearch('cdr_type') ) { + die "multiple cdr_types with same cdrtypename".$cdr_type->cdrtypename + if defined $CDR_TYPES->{$cdr_type->cdrtypename}; + $CDR_TYPES->{$cdr_type->cdrtypename} = $cdr_type->cdrtypenum; + } + } +} + +1; \ No newline at end of file diff --git a/FS/FS/cdr/telapi_voip_csv.pm b/FS/FS/cdr/telapi_voip_csv.pm deleted file mode 100644 index ee38edbcf..000000000 --- a/FS/FS/cdr/telapi_voip_csv.pm +++ /dev/null @@ -1,28 +0,0 @@ -package FS::cdr::telapi_voip_csv; -use base qw( FS::cdr ); - -use strict; -use vars qw( @ISA %info $CDR_TYPES ); -use FS::Record qw( qsearch ); -use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker ); - -%info = ( - 'name' => 'telapi_voip (csv file)', - 'weight' => 601, - 'header' => 1, - 'type' => 'csv', - 'import_fields' => [ - skip(1), # Inbound/Outbound - _cdr_date_parser_maker('startdate'), # date - skip(1), # cost per minute - 'upstream_price', # call cost - 'billsec', # duration - 'src', # source - 'dst', # destination - skip(1), # hangup code - ], -); - -sub skip { map {''} (1..$_[0]) } - -1; -- cgit v1.2.1 From 562a3c85e49cd3ea8fb80e5c2fd7f4c5f60333e9 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Thu, 23 May 2019 11:48:15 -0400 Subject: RT# 75680 - fixed telapi_voip cdr csv import --- FS/FS/cdr/telapi_voip.pm | 36 ++++++------------------------------ 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/FS/FS/cdr/telapi_voip.pm b/FS/FS/cdr/telapi_voip.pm index abc7d5bd2..687c431a8 100644 --- a/FS/FS/cdr/telapi_voip.pm +++ b/FS/FS/cdr/telapi_voip.pm @@ -7,46 +7,22 @@ use FS::Record qw( qsearch ); use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker ); %info = ( - 'name' => 'telapi_voip', + 'name' => 'telapi_voip (csv file)', 'weight' => 601, 'header' => 1, 'type' => 'csv', 'import_fields' => [ - _cdr_date_parser_maker('startdate'), #'date gmt' + skip(1), # Inbound/Outbound + _cdr_date_parser_maker('startdate'), # date + skip(1), # cost per minute + 'upstream_price', # call cost + 'billsec', # duration 'src', # source 'dst', # destination - 'clid', # callerid skip(1), # hangup code - skip(1), # sip account - 'src_ip_addr', # orig ip - 'duration', # duration - skip(1), # per minute - 'upstream_price', # callcost - sub { - my($cdr, $cdrtypename, $conf, $param) = @_; - return unless length($cdrtypename); - _init_cdr_types(); - unless (defined $CDR_TYPES->{$cdrtypename}) { - warn "Skipping Record: CDR type name $cdrtypename does not exist!"; - $param->{skiprow} = 1; - } - $cdr->cdrtypenum($CDR_TYPES->{$cdrtypename}); - }, # type - _cdr_min_parser_maker('billsec'), #PriceDurationMins ], ); sub skip { map {''} (1..$_[0]) } -sub _init_cdr_types { - unless ($CDR_TYPES) { - $CDR_TYPES = {}; - foreach my $cdr_type ( qsearch('cdr_type') ) { - die "multiple cdr_types with same cdrtypename".$cdr_type->cdrtypename - if defined $CDR_TYPES->{$cdr_type->cdrtypename}; - $CDR_TYPES->{$cdr_type->cdrtypename} = $cdr_type->cdrtypenum; - } - } -} - 1; \ No newline at end of file -- cgit v1.2.1 From 6b5dda03831aef0cb5689cf2acf3fac47e4b12cb Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Fri, 24 May 2019 10:51:08 -0400 Subject: RT# 77532 - can search cust main phone numbers in advanced customer search --- FS/FS/cust_main/Search.pm | 15 +++++++++++++ httemplate/search/cust_main.html | 1 + httemplate/search/elements/cust_main_phones.html | 27 ++++++++++++++++++++++++ httemplate/search/report_cust_main.html | 1 + 4 files changed, 44 insertions(+) create mode 100644 httemplate/search/elements/cust_main_phones.html diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index 63f10fb64..479ebf5c4 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -789,6 +789,21 @@ sub search { )"; } + ## + # phones + ## + + foreach my $phonet (qw(daytime night mobile)) { + if ($params->{$phonet}) { + $params->{$phonet} =~ s/\D//g; + $params->{$phonet} =~ /^(\d{3})(\d{3})(\d{4})(\d*)$/ + or next; + my $phonen = "$1-$2-$3"; + if ($4) { push @where, "cust_main.".$phonet." = '".$phonen." x$4'"; } + else { push @where, "cust_main.".$phonet." like '".$phonen."%'"; } + } + } + ### # refnum ### diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html index 83ca7217d..06a0d5428 100755 --- a/httemplate/search/cust_main.html +++ b/httemplate/search/cust_main.html @@ -51,6 +51,7 @@ my %search_hash = (); my @scalars = qw ( agentnum salesnum status address city county state zip country location_history + daytime night mobile invoice_terms no_censustract with_geocode with_email tax no_tax POST no_POST custbatch usernum diff --git a/httemplate/search/elements/cust_main_phones.html b/httemplate/search/elements/cust_main_phones.html new file mode 100644 index 000000000..61aa1be6e --- /dev/null +++ b/httemplate/search/elements/cust_main_phones.html @@ -0,0 +1,27 @@ + + <% mt('Phones') |h %> + + + +% foreach my $phone (qw(daytime night mobile)) { + + +% } + +
+ +
<% mt($phone_label{$phone}) |h %> +
 
+ + +<%init> +my %phone_label = ( + daytime => 'Day Phone', + night => 'Night Phone', + mobile => 'Mobile Phone', +); + \ No newline at end of file diff --git a/httemplate/search/report_cust_main.html b/httemplate/search/report_cust_main.html index da2f1a4e9..3dd92af2e 100755 --- a/httemplate/search/report_cust_main.html +++ b/httemplate/search/report_cust_main.html @@ -165,6 +165,7 @@ <% emt('Location search options') %> <& elements/options_cust_location.html &> + <& elements/cust_main_phones.html &>

-- cgit v1.2.1 From d397c0135075feca088abf09e801ceb18d425f10 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sun, 26 May 2019 15:29:16 -0400 Subject: RT# 83402 CLI tool to repair wa state tax tables --- FS/FS/Cron/tax_rate_update.pm | 26 +++ FS/FS/cust_main_county.pm | 34 ++++ FS/bin/freeside-wa-tax-table-resolve | 304 +++++++++++++++++++++++++++++++++++ 3 files changed, 364 insertions(+) create mode 100755 FS/bin/freeside-wa-tax-table-resolve diff --git a/FS/FS/Cron/tax_rate_update.pm b/FS/FS/Cron/tax_rate_update.pm index bb9d4d13d..4383bc501 100755 --- a/FS/FS/Cron/tax_rate_update.pm +++ b/FS/FS/Cron/tax_rate_update.pm @@ -294,6 +294,11 @@ sub wa_sales_update_tax_table { ) ); + # The checks themselves will fully log details about the problem, + # so simple error message is sufficient here + log_error_and_die('abort tax table update, sanity checks failed') + unless wa_sales_update_tax_table_sanity_check(); + $args->{temp_dir} ||= tempdir(); $args->{filename} ||= wa_sales_fetch_xlsx_file( $args ); @@ -635,6 +640,26 @@ sub wa_sales_fetch_xlsx_file { } +=head2 wa_sales_update_tax_table_sanity_check + +There should be no duplicate tax table entries in the tax table, +with the same district value, within a tax class, where source=wa_sales. + +If there are, custome taxes may have been user-entered in the +freeside UI, and incorrectly labelled as source=wa_sales. Or, the +dupe record may have been created by issues with older wa_sales code. + +If these dupes exist, the sysadmin must solve the problem by hand +with the freeeside-wa-tax-table-resolve script + +Returns 1 unless problem sales tax entries are detected + +=cut + +sub wa_sales_update_tax_table_sanity_check { + FS::cust_main_county->find_wa_tax_dupes ? 0 : 1; +} + sub log { state $log = FS::Log->new('tax_rate_update'); $log; @@ -655,6 +680,7 @@ sub log_warn_and_warn { sub log_error_and_die { my $log_message = shift; &log()->error( $log_message ); + warn( "$log_message\n" ); die( "$log_message\n" ); } diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm index 2bd7342ca..958233440 100644 --- a/FS/FS/cust_main_county.pm +++ b/FS/FS/cust_main_county.pm @@ -562,6 +562,40 @@ sub taxline { return $tax_item; } +=head1 find_wa_tax_dupes + +Return a list of cust_main_county Record objects that are detected +as duplicate washington state sales tax rows (source=wa_state) +within their respective tax classes + +=cut + +sub find_wa_tax_dupes { + my %cust_main_county; + my @dupes; + + for my $row ( qsearch( cust_main_county => { source => 'wa_sales' } ) ) { + my $taxclass = $row->taxclass || 'none'; + $cust_main_county{$taxclass} ||= {}; + + my $district = $row->district || 'none'; + $cust_main_county{$taxclass}->{$district} ||= []; + + push @{ $cust_main_county{$taxclass}->{$district} }, $row; + } + + for my $taxclass ( keys %cust_main_county ) { + for my $district ( keys %{ $cust_main_county{$taxclass} } ) { + my $tax_rows = $cust_main_county{$taxclass}->{$district}; + if ( scalar @$tax_rows > 1 ) { + push @dupes, @$tax_rows; + } + } + } + + @dupes; +} + =back =head1 SUBROUTINES diff --git a/FS/bin/freeside-wa-tax-table-resolve b/FS/bin/freeside-wa-tax-table-resolve new file mode 100755 index 000000000..fa6db3e39 --- /dev/null +++ b/FS/bin/freeside-wa-tax-table-resolve @@ -0,0 +1,304 @@ +#!/usr/bin/env perl +use v5.10; +use strict; +use warnings; + +our $VERSION = '1.0'; + +use Data::Dumper; +use FS::cust_main_county; +use FS::Log; +use FS::Record qw( qsearch qsearchs ); +use FS::UID qw( adminsuidsetup ); +use Getopt::Long; +use Pod::Usage; + +# Begin transaction +local $FS::UID::AutoCommit = 0; + +my( + $dbh, + $freeside_user, + $opt_check, + @opt_merge, + @opt_set_source_null, +); + +GetOptions( + 'check' => \$opt_check, + 'merge=s' => \@opt_merge, + 'set-source-null=s' => \@opt_set_source_null, +); +@opt_merge = split(',',join(',',@opt_merge)); +@opt_set_source_null = split(',',join(',',@opt_set_source_null)); + + +# say Dumper({ +# check => $opt_check, +# merge => \@opt_merge, +# set_source_numm => \@opt_set_source_null, +# }); + +validate_opts(); + +$dbh = adminsuidsetup( $freeside_user ) + or die "Bad username: $freeside_user\n"; + +my $log = FS::Log->new('freeside-wa-tax-table-resolve'); + +if ( $opt_check ) { + check(); +} elsif ( @opt_merge ) { + merge(); +} elsif ( @opt_set_source_null ) { + set_source_null(); +} else { + error_and_help('No options selected'); +} + +# Commit transaction +$dbh->commit; +local $FS::UID::AutoCommit = 1; + +exit; + + +sub set_source_null { + my @cust_main_county; + for my $taxnum ( @opt_set_source_null ) { + my $row = qsearchs( cust_main_county => { taxnum => $taxnum } ); + if ( $row ) { + push @cust_main_county, $row; + } else { + error_and_help("Invalid taxnum specified: $taxnum"); + } + } + + say "=== Specified tax rows ==="; + print_taxnum($_) for @cust_main_county; + + confirm_to_continue(" + + The source column will be set to NULL for each of the + tax rows listed. The tax row will no longer be managed + by the washington state sales tax table update utilities. + + The listed taxes should be manually created taxes, that + were never intended to be managed by the auto updater. + + "); + + for my $row ( @cust_main_county ) { + + $row->setfield( source => undef ); + my $error = $row->replace; + + if ( $error ) { + $dbh->rollback; + + my $message = sprintf 'Error setting source=null taxnum %s: %s', + $row->taxnum, $error; + + $log->error( $message ); + say $message; + + return; + } + + my $message = sprintf 'Source column set to null for taxnum %s', + $row->taxnum; + + $log->warn( $message ); + say $message; + } +} + +sub merge { + my $source = qsearchs( cust_main_county => { taxnum => $opt_merge[0] }); + my $target = qsearchs( cust_main_county => { taxnum => $opt_merge[1] }); + + error_and_help("Invalid source taxnum: $opt_merge[0]") + unless $source; + error_and_help("Invalid target taxnum: $opt_merge[1]") + unless $target; + + local $| = 1; # disable output buffering + + say '==== source row ===='; + print_taxnum( $source ); + + say '==== target row ===='; + print_taxnum( $target ); + + confirm_to_continue(" + + The source tax will be merged into the target tax. + All references to the source tax on customer invoices + will be replaced with references to the target tax. + The source tax will be removed from the tax tables. + + "); + + local $@; + eval { $source->_merge_into( $target, { identical_record_check => 0 } ) }; + if ( $@ ) { + $dbh->rollback; + + my $message = sprintf 'Failed to merge wa sales tax %s into %s: %s', + $source->taxnum, $target->taxnum, $@; + + say $message; + $log->error( $message ); + + } else { + my $message = sprintf 'Merged wa sales tax %s into %s - success', + $source->taxnum, $target->taxnum; + + say $message; + $log->warn( $message ); + } +} + +sub validate_opts { + + $freeside_user = shift @ARGV + or error_and_help('freeside_user parameter required'); + + if ( @opt_merge ) { + error_and_help(( '--merge requires a comma separated list of two taxnums')) + unless scalar(@opt_merge) == 2 + && $opt_merge[0] =~ /^\d+$/ + && $opt_merge[1] =~ /^\d+$/; + } + + for my $taxnum ( @opt_set_source_null ) { + if ( $taxnum =~ /\D/ ) { + error_and_help( "Invalid taxnum ($taxnum)" ); + } + } +} + +sub check { + my @dupes = FS::cust_main_county->find_wa_tax_dupes; + + unless ( @dupes ) { + say 'No duplicate tax rows detected for WA sales tax districts'; + return; + } + + say sprintf '=== Detected %s duplicate tax rows ===', scalar @dupes; + + print_taxnum($_) for @dupes; + + $log->error( + sprintf 'Detected %s duplicate wa sales tax rows: %s', + scalar( @dupes ), + join( ',', map{ $_->taxnum } @dupes ) + ); + +} + +sub print_taxnum { + my $taxnum = shift; + die unless ref $taxnum; + + say 'taxnum: '.$taxnum->taxnum; + say join "\n" => ( + map { sprintf(' %s:%s', $_, $taxnum->$_ ) } + qw/district city county state tax taxname taxclass source/ + ); + print "\n"; +} + +sub confirm_to_continue { + say shift; + print "Confirm: [y/N]: "; + my $yn = ; + chomp $yn; + if ( lc $yn ne 'y' ) { + say "\nAborted\n"; + exit; + } +} + +sub error_and_help { + pod2usage({ + -message => sprintf( "\n\nError:\n\t%s\n\n", shift ), + -exitval => 2, + verbose => 1, + }); + exit; +} + +__END__ + +=head1 name + +freeside-wa-tax-table-resolve + +=head1 SYNOPSIS + +freeside-issue-credit-for-taxnums --help +freeside-issue-credit-for-taxnums --check [freeside_user] +freeside-issue-credit-for-taxnums --merge 123,234 [freeside_user] +freeside-issue-credit-for-taxnums --set-source-null 1337,6553 [freeside_user] + +=head1 OPTIONS + +=over 4 + +=item B<--help> + +Display help and exit + +=item B<--check> + +Display info on any taxnums considered blocking duplicates + +=item B<--merge> [source-taxnum],[target-taxnum] + +Update all records referring to [source-taxnum], so they now +refer to [target-taxnum]. [source-taxnum] is deleted. + +Used to merge duplicate taxnums + +=item B<--set-source-null> [taxnum],[taxnum],... + +Update all records for the given taxnums, by setting the +I column to NULL. + +Used for manually entered tax entries, incorrectly labelled +as created and managed for Washington State Sales Taxes + +=back + +=head1 DESCRIPTION + +Tool to resolve tax table issues for customer using Washington state +sales tax districts. + +If Freeside detects duplicate rows within the wa sales tax tables, +tax table updates are blocked, and a log message directs the +sysadmin to this tool. + +Duplicate rows may be manually entered taxes, not related +to WA sales tax. Or duplicate rows may have been manually entered +into freeside for other tax purposes. + +Use --check to display which tax entries were detected as dupes. + +For each tax entry, decide if it is a duplicate wa sales tax entry, +or some other manually entered tax. + +if the row is a duplicate, merge the duplicates with the --merge +option of this script + +If the row is a manually entered tax, not for WA state sales taxes, +keep the tax but remove the flag incorrectly labeling it as WA state +sales taxes with the --set-source-null option of this script + +Once --check no longer returns problematic tax entries, the +wa state tax tables will be able to complete their automatic +tax rate updates + +=cut -- cgit v1.2.1 From 5e3e809798a51b3b1535eaeedb9747ea74a91560 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 27 May 2019 17:12:47 -0400 Subject: RT# 82906 Fix typos in script help text --- FS/bin/freeside-issue-credit-for-taxnums | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/FS/bin/freeside-issue-credit-for-taxnums b/FS/bin/freeside-issue-credit-for-taxnums index 8b7a12252..016c14ed6 100755 --- a/FS/bin/freeside-issue-credit-for-taxnums +++ b/FS/bin/freeside-issue-credit-for-taxnums @@ -293,18 +293,18 @@ sub validate_opts { error_and_help( '--csv_dir is required' ) unless $csv_dir; - error_and_help( '--start_date is required' ) + error_and_help( '--start-date is required' ) unless $start_date; error_and_help( '--end-date is required' ) unless $end_date; error_and_help( '--taxnums is required' ) unless @taxnums; - error_and_help( '--credit-reasonnum is required with --apply-credits' ) + error_and_help( '--credit-reasonnum is required with --insert-credits' ) if $insert_credits && !$credit_reasonnum; - error_and_help( '--credit-addlinfo is required with --apply-credits' ) + error_and_help( '--credit-addlinfo is required with --insert-credits' ) if $insert_credits && !$credit_addlinfo; - error_and_help( "csv dir ($csv_dir) is not a writable directoryu" ) + error_and_help( "csv dir ($csv_dir) is not a writable directory" ) unless -d $csv_dir && -r $csv_dir; error_and_help( "start_date($start_date) is not a valid date string") -- cgit v1.2.1 From 7f10b76569406cadce292208389c644c84e90979 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 27 May 2019 17:20:27 -0400 Subject: RT# 83320 Fix UI bug managing taxes No longer incorrectly carrying the source and district columns from other tax rows into new tax rows --- httemplate/edit/process/cust_main_county-add.cgi | 71 +++++++++++++++++------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/httemplate/edit/process/cust_main_county-add.cgi b/httemplate/edit/process/cust_main_county-add.cgi index fcc138f49..fcd6be48f 100755 --- a/httemplate/edit/process/cust_main_county-add.cgi +++ b/httemplate/edit/process/cust_main_county-add.cgi @@ -27,26 +27,59 @@ my @expansion = split /[\n\r]{1,2}/, $cgi->param('expansion'); $1; } @expansion; -foreach ( @expansion ) { - my(%hash)=$cust_main_county->hash; - my($new)=new FS::cust_main_county \%hash; - $new->setfield('taxnum',''); - $new->setfield('taxclass', ''); - if ( $cgi->param('what') eq 'state' ) { #?? - $new->setfield('state',$_); - $new->setfield('county', ''); - $new->setfield('city', ''); - } elsif ( $cgi->param('what') eq 'county' ) { - $new->setfield('county',$_); - $new->setfield('city', ''); - } elsif ( $cgi->param('what') eq 'city' ) { - #uppercase cities in the US to try and agree with USPS validation - $new->setfield('city', $new->country eq 'US' ? uc($_) : $_ ); - } else { #??? - die 'unknown what '. $cgi->param('what'); +my $what = $cgi->param('what'); +foreach my $new_tax_area ( @expansion ) { + + # Clone specific tax columns from original tax row + # + # UI Note: Preserving original behavior, of cloning + # tax amounts into new tax record, against better + # judgement. If the new city/county/state has a + # different tax value than the one being populated + # (rather likely?) now the user must remember to + # revisit each newly created tax row, and correct + # the possibly incorrect tax values that were populated. + # Values would be easier to identify and correct if + # they were initially populated with 0% tax rates + # District Note: The 'district' column is NOT cloned + # to the new tax row. Manually entered taxes + # are not be divided into road maintenance districts + # like Washington state sales taxes + my $new = FS::cust_main_county->new({ + map { $_ => $cust_main_county->getfield($_) } + qw/ + charge_prediscount + exempt_amount + exempt_amount_currency + recurtax + setuptax + tax + taxname + / + }); + + # Clone additional location columns, based on the $what value + my %clone_cols_for = ( + state => [qw/country /], + county => [qw/country state/], + city => [qw/country state county/], + ); + + die "unknown what: $what" + unless grep { $_ eq $what } keys %clone_cols_for; + + $new->setfield( $_ => $cust_main_county->getfield($_) ) + for @{ $clone_cols_for{ $cgi->param('what') } }; + + # In the US, store cities upper case for USPS validation + $new_tax_area = uc($new_tax_area) + if $what eq 'city' + && $new->country eq 'US'; + + $new->setfield( $what, $new_tax_area ); + if ( my $error = $new->insert ) { + die $error; } - my $error = $new->insert; - die $error if $error; } -- cgit v1.2.1 From 4ef042d6aea8b8922fc5cfb596023d04da4690be Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 27 May 2019 17:26:06 -0400 Subject: RT# 83122 Do not auto-repair wa state sales tax rows Dupe rows may actually be dupes, or they may be manually created taxes for other purposes! Pulled out auto-repair code, because it could be harming user's manually entered tax tables. When dupes are detected, wa sales taxes will not auto update, and instead generate error and log messages pointing user towardst the freeside-wa-tax-table-resolve CLI tool --- FS/FS/Cron/tax_rate_update.pm | 50 +++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/FS/FS/Cron/tax_rate_update.pm b/FS/FS/Cron/tax_rate_update.pm index 4383bc501..5111ef4d0 100755 --- a/FS/FS/Cron/tax_rate_update.pm +++ b/FS/FS/Cron/tax_rate_update.pm @@ -294,10 +294,13 @@ sub wa_sales_update_tax_table { ) ); - # The checks themselves will fully log details about the problem, - # so simple error message is sufficient here - log_error_and_die('abort tax table update, sanity checks failed') - unless wa_sales_update_tax_table_sanity_check(); + unless ( wa_sales_update_tax_table_sanity_check() ) { + log_error_and_die( + 'Duplicate district rows exist in the Washington state sales tax table. '. + 'These must be resolved before updating the tax tables. '. + 'See "freeside-wa-tax-table-resolve --check" to repair the tax tables. ' + ); + } $args->{temp_dir} ||= tempdir(); @@ -356,7 +359,7 @@ sub wa_sales_update_cust_main_county { cust_main_county => { source => 'wa_sales', district => { op => '!=', value => undef }, - tax_class => $taxclass, + taxclass => $taxclass, } ) ) { @@ -381,19 +384,30 @@ sub wa_sales_update_cust_main_county { $cust_main_county{$district} = $row; } - # Merge any dupes, place resulting non-dupe row in %cust_main_county - # Merge, even if one of the dupes has a $0 tax, or some other - # variation on tax row data. Data for this row will get corrected - # during the following tax import - for my $dupe_district_aref ( values %cust_main_county_dupe ) { - my $row_to_keep = shift @$dupe_district_aref; - while ( my $row_to_merge = shift @$dupe_district_aref ) { - $row_to_merge->_merge_into( - $row_to_keep, - { identical_record_check => 0 }, - ); - } - $cust_main_county{$row_to_keep->district} = $row_to_keep; + # # Merge any dupes, place resulting non-dupe row in %cust_main_county + # # Merge, even if one of the dupes has a $0 tax, or some other + # # variation on tax row data. Data for this row will get corrected + # # during the following tax import + # for my $dupe_district_aref ( values %cust_main_county_dupe ) { + # my $row_to_keep = shift @$dupe_district_aref; + # while ( my $row_to_merge = shift @$dupe_district_aref ) { + # $row_to_merge->_merge_into( + # $row_to_keep, + # { identical_record_check => 0 }, + # ); + # } + # $cust_main_county{$row_to_keep->district} = $row_to_keep; + # } + + # If there are duplicate rows, it may be unsafe to auto-resolve them + if ( %cust_main_county_dupe ) { + warn "Unable to continue!"; + log_error_and_die( sprintf( + 'Tax district duplicate rows detected(%s) - '. + 'WA Sales tax tables cannot be updated without resolving duplicates - '. + 'Please use tool freeside-wa-tax-table-resolve for tax table repair', + join( ',', keys %cust_main_county_dupe ) + )); } for my $district ( @{ $args->{tax_districts} } ) { -- cgit v1.2.1 From 939cd87eba0cdb7a275ddce2382880f3954e8762 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 27 May 2019 17:32:29 -0400 Subject: RT# 83122 Fix update tool to properly display caught error --- FS/bin/freeside-wa-tax-table-update | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/bin/freeside-wa-tax-table-update b/FS/bin/freeside-wa-tax-table-update index ad14687c9..b197ac845 100755 --- a/FS/bin/freeside-wa-tax-table-update +++ b/FS/bin/freeside-wa-tax-table-update @@ -106,8 +106,8 @@ $log->info('Begin wa_tax_rate_update'); }; if ( $@ ) { - $log->error( "Error: $@" ); warn "Error: $@\n"; + $log->error( "Error: $@" ); } else { $log->info( 'Finished wa_tax_rate_update' ); warn "Finished wa_tax_rate_update\n"; -- cgit v1.2.1 From 5ae6df67cbcac031e4b2731f337f42a17af5777a Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 27 May 2019 19:13:59 -0400 Subject: RT# 83402 Fix typo --- FS/bin/freeside-wa-tax-table-resolve | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/FS/bin/freeside-wa-tax-table-resolve b/FS/bin/freeside-wa-tax-table-resolve index fa6db3e39..928408a50 100755 --- a/FS/bin/freeside-wa-tax-table-resolve +++ b/FS/bin/freeside-wa-tax-table-resolve @@ -238,10 +238,10 @@ freeside-wa-tax-table-resolve =head1 SYNOPSIS -freeside-issue-credit-for-taxnums --help -freeside-issue-credit-for-taxnums --check [freeside_user] -freeside-issue-credit-for-taxnums --merge 123,234 [freeside_user] -freeside-issue-credit-for-taxnums --set-source-null 1337,6553 [freeside_user] +freeside-wa-tax-table-resolve --help +freeside-wa-tax-table-resolve --check [freeside_user] +freeside-wa-tax-table-resolve --merge 123,234 [freeside_user] +freeside-wa-tax-table-resolve --set-source-null 1337,6553 [freeside_user] =head1 OPTIONS -- cgit v1.2.1 From 5b0df166c22e13eba83fabee858846ead6a54141 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 28 May 2019 10:55:29 -0400 Subject: RT# 77532 - fixed error where cust main fax not showing, and fixed checking of phonetypenum --- FS/FS/ConfDefaults.pm | 2 +- FS/FS/UI/Web.pm | 2 +- FS/FS/cust_main/Search.pm | 2 +- FS/FS/phone_type.pm | 4 ++-- httemplate/elements/select-cust-fields.html | 2 +- httemplate/elements/tr-select-cust-fields.html | 2 +- httemplate/search/cust_main.html | 2 +- httemplate/search/elements/cust_main_phones.html | 3 ++- httemplate/view/cust_main/contacts_new.html | 2 +- 9 files changed, 11 insertions(+), 10 deletions(-) diff --git a/FS/FS/ConfDefaults.pm b/FS/FS/ConfDefaults.pm index d81e8e2dc..34987f4b4 100644 --- a/FS/FS/ConfDefaults.pm +++ b/FS/FS/ConfDefaults.pm @@ -36,7 +36,7 @@ sub cust_fields_avail { ( 'Agent | Agent Cust# or Cust# | Cust. Status | Customer' => 'Agent | Agent Cust# | Status | Last, First or Company (Last, First)', - "Customer | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s)" => + 'Customer | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s)' => 'Customer | (all phones)', 'Cust# | Customer | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s)' => 'custnum | Customer | (all phones)', diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm index 76071d900..84f397920 100644 --- a/FS/FS/UI/Web.pm +++ b/FS/FS/UI/Web.pm @@ -475,7 +475,7 @@ sub cust_sql_fields { } } - foreach my $field (qw(daytime night mobile fax )) { + foreach my $field (qw(daytime night mobile fax)) { push @fields, $field if (grep { $_ eq $field } @cust_fields); } push @fields, 'agent_custid'; diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index 479ebf5c4..25216c6d6 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -793,7 +793,7 @@ sub search { # phones ## - foreach my $phonet (qw(daytime night mobile)) { + foreach my $phonet (qw(daytime night mobile fax)) { if ($params->{$phonet}) { $params->{$phonet} =~ s/\D//g; $params->{$phonet} =~ /^(\d{3})(\d{3})(\d{4})(\d*)$/ diff --git a/FS/FS/phone_type.pm b/FS/FS/phone_type.pm index 3d9dced6f..de565270a 100644 --- a/FS/FS/phone_type.pm +++ b/FS/FS/phone_type.pm @@ -98,8 +98,8 @@ returns a list of phone_types. =cut sub get_phone_types { - ## only using mobile(3) and work(1) right now. - my @phone_types = qsearch({table=>'phone_type', order_by=>'ORDER BY weight DESC', extra_sql => " WHERE phonetypenum IN ('1','3')"}); + ## not using Home and Fax right now. false laziness with /elements/contact.html + my @phone_types = qsearch({table=>'phone_type', order_by=>'ORDER BY weight DESC', extra_sql => " WHERE typename NOT IN ('Home','Fax')"}); return @phone_types; } diff --git a/httemplate/elements/select-cust-fields.html b/httemplate/elements/select-cust-fields.html index 63e92ac3d..833f7c1af 100644 --- a/httemplate/elements/select-cust-fields.html +++ b/httemplate/elements/select-cust-fields.html @@ -6,7 +6,7 @@ foreach my $phone_type ( FS::phone_type->get_phone_types() ) { $contact_phone_list .= " | Contact ".$phone_type->typename." phone(s)"; } - @fields = map {s/\| Contact phone\(s\)/$contact_phone_list/g; $_; } @fields; + @fields = map {s/ \| Contact phone\(s\)/$contact_phone_list/g; $_; } @fields; $opt{'avail_fields'} ||= [ @fields ]; diff --git a/httemplate/elements/tr-select-cust-fields.html b/httemplate/elements/tr-select-cust-fields.html index 0af432ae7..cbac20280 100644 --- a/httemplate/elements/tr-select-cust-fields.html +++ b/httemplate/elements/tr-select-cust-fields.html @@ -11,7 +11,7 @@ my $contact_phone_list; foreach my $phone_type ( FS::phone_type->get_phone_types() ) { $contact_phone_list .= " | Contact ".$phone_type->typename." phone(s)"; } -@fields = map {s/\| Contact phone\(s\)/$contact_phone_list/g; $_; } @fields; +@fields = map {s/ \| Contact phone\(s\)/$contact_phone_list/g; $_; } @fields; $opt{'avail_fields'} ||= [ @fields ]; diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html index 06a0d5428..46e35da7f 100755 --- a/httemplate/search/cust_main.html +++ b/httemplate/search/cust_main.html @@ -51,7 +51,7 @@ my %search_hash = (); my @scalars = qw ( agentnum salesnum status address city county state zip country location_history - daytime night mobile + daytime night mobile fax invoice_terms no_censustract with_geocode with_email tax no_tax POST no_POST custbatch usernum diff --git a/httemplate/search/elements/cust_main_phones.html b/httemplate/search/elements/cust_main_phones.html index 61aa1be6e..366d0983b 100644 --- a/httemplate/search/elements/cust_main_phones.html +++ b/httemplate/search/elements/cust_main_phones.html @@ -3,7 +3,7 @@ -% foreach my $phone (qw(daytime night mobile)) { +% foreach my $phone (qw(daytime night mobile fax)) { -- cgit v1.2.1 From f5481d63043954fa6fa8a34a5820b23408192f8b Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 4 Jun 2019 08:11:19 -0400 Subject: RT# 83365 - Added city select to work like back end --- fs_selfservice/FS-SelfService/SelfService.pm | 127 +++++++++++++++++++++++-- fs_selfservice/FS-SelfService/cgi/contact.html | 11 +-- 2 files changed, 123 insertions(+), 15 deletions(-) diff --git a/fs_selfservice/FS-SelfService/SelfService.pm b/fs_selfservice/FS-SelfService/SelfService.pm index a516a9719..af989ed8a 100644 --- a/fs_selfservice/FS-SelfService/SelfService.pm +++ b/fs_selfservice/FS-SelfService/SelfService.pm @@ -2440,7 +2440,10 @@ sub regionselector { my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : ''; - my $countyflag = 0; + my $disabled = $param->{'disabled'}; + + my $countyflag = $param->{selected_county} ? 1 : 0; + my $cityflag = $param->{selected_city} ? 1 : 0; my %cust_main_county; @@ -2450,17 +2453,17 @@ sub regionselector { foreach my $c ( @{ $param->{'locales'} } ) { #$countyflag=1 if $c->county; $countyflag=1 if $c->{county}; + $cityflag=1 if ($c->{city} && $cityflag); #push @{$cust_main_county{$c->country}{$c->state}}, $c->county; #$cust_main_county{$c->country}{$c->state}{$c->county} = 1; - $cust_main_county{$c->{country}}{$c->{state}}{$c->{county}} = 1; + $cust_main_county{$c->{country}}{$c->{state}}{$c->{county}}{$c->{city}} = 1; } # } - $countyflag=1 if $param->{selected_county}; my $script_html = < - function opt(what,value,text) { - var optionName = new Option(text, value, false, false); + function opt(what,value,text,selected) { + var optionName = new Option(text, value, false, selected); var length = what.length; what.options[length] = optionName; } @@ -2500,8 +2503,37 @@ END #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) { foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) { my $text = $county || '(n/a)'; - $script_html .= - qq!opt(what.form.${prefix}county, "$county", "$text");\n!; + if (!$county) { + if ( $cityflag) { + $script_html .= qq!what.form.${prefix}city.style.display='';\n + what.form.${prefix}city_select.style.display='none';\n! + } + $script_html .= qq!opt(what.form.${prefix}county, "$county", "$text");\n! + #$script_html .= qq!what.form.${prefix}county.style.display='none';\n! + } + else { + $script_html .= qq!var countySelected = false; if ("$param->{selected_county}" == "$text") { countySelected = true; }\n + opt(what.form.${prefix}county, "$county", "$text", countySelected);\n + what.form.${prefix}county.style.display='';\n + county = what.form.${prefix}county.options[what.form.${prefix}county.selectedIndex].text;\n!; + if ( $cityflag) { + $script_html .= qq!\nif ( county == \"$county\" ) {\n!; + foreach my $city ( sort keys %{$cust_main_county{$country}{$state}{$county}} ) { + my $text = $city || '(n/a)'; + if (!$city) { + $script_html .= qq!what.form.${prefix}city.style.display='';\n + what.form.${prefix}city_select.style.display='none';\n! + } + else { + $script_html .= qq!var citySelected = false; if ("$param->{selected_city}" == "$text") { citySelected = true; }\n + opt(what.form.${prefix}city_select, "$city", "$text", citySelected);\n + what.form.${prefix}city.style.display='none';\n + what.form.${prefix}city_select.style.display='';\n! + } + } + $script_html .= "}\n"; + } + } } $script_html .= "}\n"; } @@ -2509,14 +2541,91 @@ END } } + $script_html .= <= 0; i-- ) + what.form.${prefix}city_select.options[i] = null; +END + + foreach my $country ( sort keys %cust_main_county ) { + $script_html .= "\nif ( country == \"$country\" ) {\n"; + foreach my $state ( sort keys %{$cust_main_county{$country}} ) { + $script_html .= "\nif ( state == \"$state\" ) {\n"; + #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) { + foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) { + $script_html .= "\nif ( county == \"$county\" ) {\n"; + foreach my $city ( sort keys %{$cust_main_county{$country}{$state}{$county}} ) { + my $text = $city || '(n/a)'; + if (!$city) { + $script_html .= qq!what.form.${prefix}city.style.display='';\n + what.form.${prefix}city_select.style.display='none';\n! + } + else { + $script_html .= qq!var citySelected = false; if (saved_city == "$text") { citySelected = true; }\n + opt(what.form.${prefix}city_select, "$city", "$text", citySelected);\n + what.form.${prefix}city.style.display='none';\n + what.form.${prefix}city_select.style.display='';\n! + } + } + $script_html .= "}\n"; + } + $script_html .= "}\n"; + } + $script_html .= "}\n"; + } + } + + $script_html .= < END + my $city_html = ''; + if ( $cityflag ) { + if ( scalar (keys %{ $cust_main_county{$param->{'selected_country'}}{$param->{'selected_state'}}{$param->{'selected_county'}} }) > 1 ) { + $city_html .= qq!!; + } else { + $city_html .= qq! + !; + } + } + my $county_html = $script_html; if ( $countyflag ) { - $county_html .= qq!!; foreach my $county ( sort keys %{ $cust_main_county{$param->{'selected_country'}}{$param->{'selected_state'}} } ) { @@ -2570,7 +2679,7 @@ END } - ($county_html, $state_html, $country_html); + ($county_html, $state_html, $country_html, $city_html); } diff --git a/fs_selfservice/FS-SelfService/cgi/contact.html b/fs_selfservice/FS-SelfService/cgi/contact.html index 7ae0d4880..798af7090 100644 --- a/fs_selfservice/FS-SelfService/cgi/contact.html +++ b/fs_selfservice/FS-SelfService/cgi/contact.html @@ -42,14 +42,11 @@ - - <%= - ($county_html, $state_html, $country_html) = + ($county_html, $state_html, $country_html, $city_html) = FS::SelfService::regionselector( { prefix => $pre, + selected_city => ${$pre.'city'}, selected_county => ${$pre.'county'}, selected_state => ${$pre.'state'}, selected_country => ${$pre.'country'}, @@ -58,6 +55,8 @@ locales => \@cust_main_county, } ); + $OUT .= qq!!; + $OUT .= qq!!; $OUT .= qq!!; $OUT .= qq!!; $OUT .= qq!!; @@ -73,7 +72,7 @@ <%= if ( $disabled ) { $OUT .= qq!var what = document.getElementById("${pre}city");!; - for (qw( county state country ) ) { + for (qw( city county state country ) ) { $OUT .= "what.form.$pre$_.disabled = true;"; $OUT .= "what.form.$pre$_.style.backgroundColor = '#dddddd';"; } -- cgit v1.2.1 From 3c1daaa7219fa4eef418a0a1e3ae70018ef6791e Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 4 Jun 2019 13:55:03 -0400 Subject: RT# 83251 - changed name of script in documentation --- FS/bin/freeside-svcbroadband_update_speeds | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FS/bin/freeside-svcbroadband_update_speeds b/FS/bin/freeside-svcbroadband_update_speeds index 3a7759286..b3b257a5a 100755 --- a/FS/bin/freeside-svcbroadband_update_speeds +++ b/FS/bin/freeside-svcbroadband_update_speeds @@ -19,7 +19,7 @@ adminsuidsetup $user; sub usage { " Usage: - svc_broadband_update_speeds: [ -h help] [ -v verbose] [ -n only update services with a null up/down speed] [ -e export service ] [ -a update tower_sector_num ] [ -s service_part_num (required) ] [ -c sibling service_part_num ] [ -r (speed rate in KB 'up,down') ] [ -p (get speed from package fcc rate) ] [ -t tower_sector_num ] [ -d directory for exception file (required) ] user (required)\n + freeside-svcbroadband_update_speeds: [ -h help] [ -v verbose] [ -n only update services with a null up/down speed] [ -e export service ] [ -a update tower_sector_num ] [ -s service_part_num (required) ] [ -c sibling service_part_num ] [ -r (speed rate in KB 'up,down') ] [ -p (get speed from package fcc rate) ] [ -t tower_sector_num ] [ -d directory for exception file (required) ] user (required)\n A directory for the exception file, freeside user name and a service to update is required.\n Must set one or more of options p, c, or r. \n Also must run this report as user freeside.\n @@ -144,7 +144,7 @@ sub _update_service { exit; -=head2 svc_broadband_update_speeds +=head2 freeside-svcbroadband_update_speeds This script allows for the mas update of up and down speeds for a svc_broadband service. @@ -159,7 +159,7 @@ Script must be run as user freeside. Options -s, -d and freeside user are required. example: -sudo -u freeside ./svc_broadband_update_speeds -v -s 4 -c 2 -r 148000,248000 -p -d /home/freeside/ freesideuser +sudo -u freeside ./freeside-svcbroadband_update_speeds -v -s 4 -c 2 -r 148000,248000 -p -d /home/freeside/ freesideuser available options: [ -h help] -- cgit v1.2.1 From a3092d901441a8640c1f7f9be4b0374146e43b0a Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Wed, 5 Jun 2019 09:37:06 -0400 Subject: RT# 83436 - fixed Advanced Broadband Service Report when displaying phone numbers --- FS/FS/UI/Web.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm index 84f397920..2d12f7d14 100644 --- a/FS/FS/UI/Web.pm +++ b/FS/FS/UI/Web.pm @@ -363,7 +363,7 @@ foreach my $phone_type ( FS::phone_type->get_phone_types() ) { my $num = $phone_type->phonetypenum; my @phones; - foreach ($self->contact_list_name_phones) { + foreach (FS::cust_main::contact_list_name_phones($self)) { my $data = [ { 'data' => $_->first.' '.$_->last.' '.FS::contact_phone::phonenum_pretty($_), -- cgit v1.2.1 From c8de09c1c19c34eb9384c391b81b90bd0f0ba8a5 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Thu, 6 Jun 2019 10:00:26 -0400 Subject: RT# 83251 - removed search for unprovisioned services --- FS/bin/freeside-svcbroadband_update_speeds | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/FS/bin/freeside-svcbroadband_update_speeds b/FS/bin/freeside-svcbroadband_update_speeds index b3b257a5a..8a6f33f7d 100755 --- a/FS/bin/freeside-svcbroadband_update_speeds +++ b/FS/bin/freeside-svcbroadband_update_speeds @@ -47,12 +47,12 @@ my @services = qsearch({ }); ### get list of all unprovisioned services -my $ups_extra_sql = "where cust_pkg.cancel is null and pkg_svc.quantity > 0 and pkg_svc.quantity > (select count(1) from cust_svc where cust_svc.pkgnum = cust_pkg.pkgnum and cust_svc.svcpart = pkg_svc.svcpart) and pkg_svc.svcpart = $opt_s"; -my @unprovisioned_services = qsearchs({ - 'table' => 'cust_pkg', - 'addl_from' => 'JOIN pkg_svc using (pkgpart)', - 'extra_sql' => $ups_extra_sql, -}); +#my $ups_extra_sql = "where cust_pkg.cancel is null and pkg_svc.quantity > 0 and pkg_svc.quantity > (select count(1) from cust_svc where cust_svc.pkgnum = cust_pkg.pkgnum and cust_svc.svcpart = pkg_svc.svcpart) and pkg_svc.svcpart = $opt_s"; +#my @unprovisioned_services = qsearch({ +# 'table' => 'cust_pkg', +# 'addl_from' => 'JOIN pkg_svc using (pkgpart)', +# 'extra_sql' => $ups_extra_sql, +#}); my $speed; $speed = 'package' if $opt_p; -- cgit v1.2.1 From 38e34bbc53a4222c7507e95914e1364a5a74623f Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Mon, 10 Jun 2019 15:34:03 -0400 Subject: RT# 83450 - added fields interface and map_location to export --- FS/FS/part_export/saisei.pm | 163 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 126 insertions(+), 37 deletions(-) diff --git a/FS/FS/part_export/saisei.pm b/FS/FS/part_export/saisei.pm index 8376c7e2e..0f18ca36a 100644 --- a/FS/FS/part_export/saisei.pm +++ b/FS/FS/part_export/saisei.pm @@ -42,6 +42,7 @@ Create a tower and add a sector to that tower. The sector name will be the name Make sure you have set the up and down rate limit for the tower and the sector. This is required to be able to export the access point. The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector. They will be modified at Saisei when modified in freeside. Each sector will be attached to its tower access point using the Saisei uplink field. +Each access point will be attached to the interface set in the export config. If left blank access point will be attached to the default interface. Most setups can leave this blank. Create a package for the above created service, and order this package for a customer. @@ -65,6 +66,13 @@ tie my %scripts, 'Tie::IxHash', error_url => '/edit/part_export.cgi?', success_message => 'Saisei export of provisioned services successful', }, + 'export_all_towers_sectors' => { component => '/elements/popup_link.html', + label => 'Export of all towers and sectors', + description => 'Will force an export of all towers and sectors to Saisei as access points.', + html_label => 'Export all towers and sectors.', + error_url => '/edit/part_export.cgi?', + success_message => 'Saisei export of towers and sectors as access points successful', + }, ; tie my %options, 'Tie::IxHash', @@ -74,6 +82,8 @@ tie my %options, 'Tie::IxHash', default => '' }, 'password' => { label => 'Saisei API Password', default => '' }, + 'interface' => { label => 'Saisei Access Point Interface', + default => '' }, 'debug' => { type => 'checkbox', label => 'Enable debug warnings' }, ; @@ -105,6 +115,7 @@ Create a tower and add a sector to that tower. The sector name will be the name Make sure you have set the up and down rate limit for the tower and the sector. This is required to be able to export the access point. The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector. They will be modified at Saisei when modified in freeside. Each sector will be attached to its tower access point using the Saisei uplink field. +Each access point will be attached to the interface set in the export config. If left blank access point will be attached to the default interface. Most setups can leave this blank.

  • @@ -147,6 +158,8 @@ sub _export_insert { my $username = $svc_broadband->{Hash}->{svcnum}; my $description = $svc_broadband->{Hash}->{description}; + my $svc_location; + $svc_location = $svc_broadband->{Hash}->{latitude}.','.$svc_broadband->{Hash}->{longitude} if ($svc_broadband->{Hash}->{latitude} && $svc_broadband->{Hash}->{longitude}); if (!$username) { $self->{'__saisei_error'} = 'no username - can not export'; @@ -158,7 +171,7 @@ sub _export_insert { $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'}; # if no existing user create one. - $self->api_create_user($username, $description) unless $existing_user; + $self->api_create_user($username, $description, $svc_location) unless $existing_user; return $self->api_error if $self->{'__saisei_error'}; # set user to existing one or newly created one. @@ -173,13 +186,18 @@ sub _export_insert { tower_sector.sectorname, tower_sector.towernum, tower_sector.up_rate_limit as sector_upratelimit, - tower_sector.down_rate_limit as sector_downratelimit ', + tower_sector.down_rate_limit as sector_downratelimit, + tower.latitude, + tower.longitude', 'addl_from' => 'LEFT JOIN tower USING ( towernum )', 'hashref' => { 'sectornum' => $svc_broadband->{Hash}->{sectornum}, }, }); + my $tower_location; + $tower_location = $tower_sector->{Hash}->{latitude}.','.$tower_sector->{Hash}->{longitude} if ($tower_sector->{Hash}->{latitude} && $tower_sector->{Hash}->{longitude}); + my $tower_name = $tower_sector->{Hash}->{towername}; $tower_name =~ s/\s/_/g; @@ -189,6 +207,7 @@ sub _export_insert { 'tower_uprate_limit' => $tower_sector->{Hash}->{tower_upratelimit}, 'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit}, }; + $tower_opt->{'location'} = $tower_location if $tower_location; my $tower_ap = process_tower($self, $tower_opt); return $self->api_error if $self->{'__saisei_error'}; @@ -204,6 +223,8 @@ sub _export_insert { 'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit}, 'rateplan' => $rateplan_name, }; + $sector_opt->{'location'} = $tower_location if $tower_location; + my $accesspoint = process_sector($self, $sector_opt); return $self->api_error if $self->{'__saisei_error'}; @@ -224,12 +245,15 @@ sub _export_insert { return $self->api_error if $self->{'__saisei_error'}; ## tie host to user add sector name as access point. - $self->api_add_host_to_user( - $user->{collection}->[0]->{name}, - $rateplan->{collection}->[0]->{name}, - $svc_broadband->{Hash}->{ip_addr}, - $virtual_ap->{collection}->[0]->{name}, - ) unless $self->{'__saisei_error'}; + my $host_opt = { + 'user' => $user->{collection}->[0]->{name}, + 'rateplan' => $rateplan->{collection}->[0]->{name}, + 'ip' => $svc_broadband->{Hash}->{ip_addr}, + 'accesspoint' => $virtual_ap->{collection}->[0]->{name}, + 'location' => $svc_location, + }; + $self->api_add_host_to_user($host_opt) + unless $self->{'__saisei_error'}; } return $self->api_error; @@ -331,6 +355,9 @@ sub export_tower_sector { return; } + my $tower_location; + $tower_location = $tower->{Hash}->{latitude}.','.$tower->{Hash}->{longitude} if ($tower->{Hash}->{latitude} && $tower->{Hash}->{longitude}); + #modify tower or create it. my $tower_name = $tower->{Hash}->{towername}; $tower_name =~ s/\s/_/g; @@ -341,6 +368,7 @@ sub export_tower_sector { 'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit}, 'modify_existing' => '1', # modify an existing access point with this info }; + $tower_opt->{'location'} = $tower_location if $tower_location; my $tower_access_point = process_tower($self, $tower_opt); return $tower_access_point if $tower_access_point->{error}; @@ -354,6 +382,7 @@ sub export_tower_sector { #for each one modify or create it. foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) { + next if $tower_sector->{Hash}->{sectorname} eq "_default"; my $sector_name = $tower_sector->{Hash}->{sectorname}; $sector_name =~ s/\s/_/g; my $sector_opt = { @@ -364,6 +393,8 @@ sub export_tower_sector { 'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit}, 'modify_existing' => '1', # modify an existing access point with this info }; + $sector_opt->{'location'} = $tower_location if $tower_location; + my $sector_access_point = process_sector($self, $sector_opt) unless ($sector_name eq "_default"); return $sector_access_point if $sector_access_point->{error}; } @@ -449,7 +480,7 @@ sub api_call { return; } else { - $self->{'__saisei_error'} = "Received Bad response from server during $method , we received responce code: " . $client->responseCode(); + $self->{'__saisei_error'} = "Received Bad response from server during $method $path $data, we received responce code: " . $client->responseCode() . " " . $client->responseContent; warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug'); return; } @@ -650,14 +681,17 @@ Creates a user. =cut sub api_create_user { - my ($self,$user, $description) = @_; + my ($self,$user, $description, $location) = @_; + + my $user_hash = { + 'description' => $description, + }; + $user_hash->{'map_location'} = $location if $location; my $new_user = $self->api_call( "PUT", "/users/$user", - { - 'description' => $description, - }, + $user_hash, ); $self->{'__saisei_error'} = "Saisei could not create the user $user" @@ -674,15 +708,19 @@ Creates a access point. =cut sub api_create_accesspoint { - my ($self,$accesspoint, $upratelimit, $downratelimit) = @_; + my ($self,$accesspoint, $upratelimit, $downratelimit, $location) = @_; + + my $ap_hash = { + 'downstream_rate_limit' => $downratelimit, + 'upstream_rate_limit' => $upratelimit, + 'interface' => $self->option('interface'), + }; + $ap_hash->{'map_location'} = $location if $location; my $new_accesspoint = $self->api_call( "PUT", "/access_points/$accesspoint", - { - 'downstream_rate_limit' => $downratelimit, - 'upstream_rate_limit' => $upratelimit, - }, + $ap_hash, ); $self->{'__saisei_error'} = "Saisei could not create the access point $accesspoint" @@ -698,14 +736,18 @@ Modify a new access point. =cut sub api_modify_accesspoint { - my ($self, $accesspoint, $uplink) = @_; + my ($self, $accesspoint, $uplink, $location) = @_; + + my $ap_hash = { + 'uplink' => $uplink, + 'interface' => $self->option('interface'), + }; + $ap_hash->{'map_location'} = $location if $location; my $modified_accesspoint = $self->api_call( "PUT", "/access_points/$accesspoint", - { - 'uplink' => $uplink, # name of attached access point - }, + $ap_hash, ); $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint after it was created." @@ -722,20 +764,24 @@ Modify a existing accesspoint. =cut sub api_modify_existing_accesspoint { - my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_; + my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit, $location) = @_; + + my $ap_hash = { + 'downstream_rate_limit' => $downratelimit, + 'upstream_rate_limit' => $upratelimit, + 'interface' => $self->option('interface'), +# 'uplink' => $uplink, # name of attached access point + }; + $ap_hash->{'map_location'} = $location if $location; my $modified_accesspoint = $self->api_call( "PUT", "/access_points/$accesspoint", - { - 'downstream_rate_limit' => $downratelimit, - 'upstream_rate_limit' => $upratelimit, -# 'uplink' => $uplink, # name of attached access point - }, + $ap_hash, ); - $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint." - unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen + $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint." + unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen return; @@ -748,16 +794,22 @@ ties host to user, rateplan and default access point. =cut sub api_add_host_to_user { - my ($self,$user, $rateplan, $ip, $accesspoint) = @_; +# my ($self,$user, $rateplan, $ip, $accesspoint, $location) = @_; + my ($self,$opt) = @_; + my $ip = $opt->{'ip'}; + my $location = $opt->{'location'}; + + my $newhost_hash = { + 'user' => $opt->{'user'}, + 'rate_plan' => $opt->{'rateplan'}, + 'access_point' => $opt->{'accesspoint'}, + }; + $newhost_hash->{'map_location'} = $location if $location; my $new_host = $self->api_call( "PUT", "/hosts/$ip", - { - 'user' => $user, - 'rate_plan' => $rateplan, - 'access_point' => $accesspoint, - }, + $newhost_hash, ); $self->{'__saisei_error'} = "Saisei could not create the host $ip" @@ -811,6 +863,7 @@ sub process_tower { my $existing_tower_ap; my $tower_name = $opt->{tower_name}; + my $location = $opt->{location}; #check if tower has been set up as an access point. $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'}; @@ -821,6 +874,7 @@ sub process_tower { '', # tower does not have a uplink on sectors. $opt->{tower_uprate_limit}, $opt->{tower_downrate_limit}, + $location, ) if $existing_tower_ap->{collection} && $opt->{modify_existing}; #if tower does not exist as an access point create it. @@ -828,6 +882,7 @@ sub process_tower { $tower_name, $opt->{tower_uprate_limit}, $opt->{tower_downrate_limit}, + $location, ) unless $existing_tower_ap->{collection}; my $accesspoint = $self->api_get_accesspoint($tower_name); @@ -851,6 +906,7 @@ sub process_sector { my $existing_sector_ap; my $sector_name = $opt->{sector_name}; + my $location = $opt->{location}; #check if sector has been set up as an access point. $existing_sector_ap = $self->api_get_accesspoint($sector_name); @@ -861,6 +917,7 @@ sub process_sector { $opt->{tower_name}, $opt->{sector_uprate_limit}, $opt->{sector_downrate_limit}, + $location, ) if $existing_sector_ap && $opt->{modify_existing}; #if sector does not exist as an access point create it. @@ -868,10 +925,11 @@ sub process_sector { $sector_name, $opt->{sector_uprate_limit}, $opt->{sector_downrate_limit}, + $location, ) unless $existing_sector_ap; # Attach newly created sector to it's tower. - $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap); + $self->api_modify_accesspoint($sector_name, $opt->{tower_name}, $location) unless ($self->{'__saisei_error'} || $existing_sector_ap); # set access point to existing one or newly created one. my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name); @@ -1001,6 +1059,37 @@ sub export_provisioned_services { } +sub export_all_towers_sectors { + my $job = shift; + my $param = shift; + + my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } ) + or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}. This export does not exist.\n"; + bless $part_export; + + my @towers = FS::Record::qsearch({ + 'table' => 'tower', + }); + my $tower_count = scalar @towers; + + my %status = {}; + for (my $c=1; $c <=100; $c=$c+1) { $status{int($tower_count * ($c/100))} = $c; } + + my $process_count=0; + foreach my $tower (@towers) { + if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); } + my $export_error = export_tower_sector($part_export,$tower); + if ($export_error->{'error'}) { + warn "Error exporting tower/sector (".$tower->{Hash}->{towername}.")\n" if ($part_export->option('debug')); + die ($export_error->{'error'}."\n"); + } + $process_count++; + } + + return; + +} + sub test_export_report { my ($self, $opts) = @_; my @export_error; -- cgit v1.2.1 From f38153f6d439071fa467af22c3c727906d49d497 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 10 Jun 2019 16:51:08 -0400 Subject: RT# 83460 Fix validation bug on part_event_option --- FS/FS/part_event_option.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FS/FS/part_event_option.pm b/FS/FS/part_event_option.pm index 1421f6f0f..6c34c6878 100644 --- a/FS/FS/part_event_option.pm +++ b/FS/FS/part_event_option.pm @@ -189,7 +189,8 @@ sub check { if ( my %option_fields = $self->option_fields ) { if ( my $option_field = $option_fields{ $self->optionname } ) { - if ( my $validation_method = $option_field->{validation} ) { + if ( ref $option_field && $option_field->{validation} ) { + my $validation_method = $option_field->{validation}; $error = $self->$validation_method('optionvalue'); } } -- cgit v1.2.1 From faab9e1f2627768991e283034e773c38ca25b21a Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Wed, 12 Jun 2019 03:15:31 -0400 Subject: RT# 80488 freeside-wa-tax-table-resolve --merge-all and --fix-usf --- FS/bin/freeside-wa-tax-table-resolve | 162 +++++++++++++++++++++++++++++++++-- 1 file changed, 155 insertions(+), 7 deletions(-) diff --git a/FS/bin/freeside-wa-tax-table-resolve b/FS/bin/freeside-wa-tax-table-resolve index 928408a50..790dce20b 100755 --- a/FS/bin/freeside-wa-tax-table-resolve +++ b/FS/bin/freeside-wa-tax-table-resolve @@ -20,13 +20,17 @@ my( $dbh, $freeside_user, $opt_check, + $opt_fix_usf, @opt_merge, + $opt_merge_all, @opt_set_source_null, ); GetOptions( 'check' => \$opt_check, + 'fix-usf' => \$opt_fix_usf, 'merge=s' => \@opt_merge, + 'merge-all' => \$opt_merge_all, 'set-source-null=s' => \@opt_set_source_null, ); @opt_merge = split(',',join(',',@opt_merge)); @@ -52,6 +56,10 @@ if ( $opt_check ) { merge(); } elsif ( @opt_set_source_null ) { set_source_null(); +} elsif ( $opt_merge_all ) { + merge_all(); +} elsif ( $opt_fix_usf ) { + fix_usf(); } else { error_and_help('No options selected'); } @@ -139,6 +147,12 @@ sub merge { "); + merge_into( $source, $target ); +} + +sub merge_into { + my ( $source, $target ) = @_; + local $@; eval { $source->_merge_into( $target, { identical_record_check => 0 } ) }; if ( $@ ) { @@ -151,14 +165,117 @@ sub merge { $log->error( $message ); } else { - my $message = sprintf 'Merged wa sales tax %s into %s - success', - $source->taxnum, $target->taxnum; + my $message = sprintf 'Merged wa sales tax %s into %s for district %s', + $source->taxnum, $target->taxnum, $source->district; say $message; $log->warn( $message ); } } +sub merge_all { + my @dupes = FS::cust_main_county->find_wa_tax_dupes; + + unless ( @dupes ) { + say 'No duplicate tax rows detected for WA sales tax districts'; + return; + } + + confirm_to_continue(sprintf " + + %s blocking duplicate rows detected + + Duplicate rows will be merged using FS::cust_main_county::_merge_into() + + Rows are considered duplicates when they: + - Share the same tax class + - Share the same district + - Contain 'wa_sales' in the source column + + ", scalar @dupes); + + # Sort dupes into buckets to be merged, by taxclass and district + # $to_merge{taxclass}->{district} = [ @rows_to_merge ] + my %to_merge; + for my $row ( @dupes ) { + my $taxclass = $row->taxclass || 'none'; + $to_merge{$taxclass} ||= {}; + $to_merge{$taxclass}->{$row->district} ||= []; + push @{ $to_merge{$taxclass}->{$row->district} }, $row; + } + + # Merge the duplicates + for my $taxclass ( keys %to_merge ) { + for my $district ( keys %{ $to_merge{$taxclass} }) { + + # Keep the first row in the list as the target. + # Merge the remaining rows into the target + my $rows = $to_merge{$taxclass}->{$district}; + my $target = shift @$rows; + + while ( @$rows ) { + merge_into( shift(@$rows), $target ); + } + } + } + + say " + + Merge operations completed + + Please run freeside-wa-tax-table-update. This will update + the merged district rows with correct county and city names + + "; + +} + +sub fix_usf { + confirm_to_continue(" + + Search for duplicate districts within the tax tables with + - duplicate district column values + - source = NULL + - district = NOT NULL + - taxclass = USF + - tax > 17 + + Merge these rows into a single USF row for each tax district + + "); + + my @rows = qsearch( cust_main_county => { + taxclass => 'USF', + source => undef, + state => 'WA', + country => 'US', + tax => { op => '>', value => 17 }, + district => { op => '!=', value => undef }, + }); + + my %to_merge; + for my $row (@rows) { + $to_merge{$row->district} ||= []; + push @{ $to_merge{$row->district} }, $row; + } + + for my $dist_rows ( values %to_merge ) { + my $target = shift @$dist_rows; + while ( @$dist_rows ) { + merge_into( shift(@$dist_rows), $target ); + } + } + + say " + + USF clean up completed + + Please run freeside-wa-tax-table-update. This will update + the merged district rows with correct county and city names + + "; +} + sub validate_opts { $freeside_user = shift @ARGV @@ -188,7 +305,7 @@ sub check { say sprintf '=== Detected %s duplicate tax rows ===', scalar @dupes; - print_taxnum($_) for @dupes; + print_taxnum($_) for sort { $a->district <=> $b->district } @dupes; $log->error( sprintf 'Detected %s duplicate wa sales tax rows: %s', @@ -196,6 +313,14 @@ sub check { join( ',', map{ $_->taxnum } @dupes ) ); + say " + + Rows are considered duplicates when they: + - Share the same tax class + - Share the same district + - Contain 'wa_sales' in the source column + + "; } sub print_taxnum { @@ -238,10 +363,12 @@ freeside-wa-tax-table-resolve =head1 SYNOPSIS -freeside-wa-tax-table-resolve --help -freeside-wa-tax-table-resolve --check [freeside_user] -freeside-wa-tax-table-resolve --merge 123,234 [freeside_user] -freeside-wa-tax-table-resolve --set-source-null 1337,6553 [freeside_user] + freeside-wa-tax-table-resolve --help + freeside-wa-tax-table-resolve --check [freeside_user] + freeside-wa-tax-table-resolve --merge 123,234 [freeside_user] + freeside-wa-tax-table-resolve --set-source-null 1337,6553 [freeside_user] + freeside-wa-tax-table-resolve --merge-all [freeside_user] + freeside-wa-tax-table-resolve --fix-usf [freeside_user] =head1 OPTIONS @@ -270,6 +397,27 @@ I column to NULL. Used for manually entered tax entries, incorrectly labelled as created and managed for Washington State Sales Taxes +=item B<--merge-all> + +Automatically merge all blocking duplicate taxnums. + +If after reviewing all blocking duplicate taxnum rows with --check, +if all duplicate rows are safe to merge, this option will merge them all. + +=item B<--fix-usf> + +Fix routine for a particular USF issue + +Search for duplicate districts within the tax tables with + + - duplicate district column values + - source = NULL + - district = NOT NULL + - taxclass = USF + - tax > 17 + +Merge these rows into a single USF row for each tax district + =back =head1 DESCRIPTION -- cgit v1.2.1 From be2ed6c905de5b480b370e89f8092d8d21ef2a64 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Wed, 12 Jun 2019 03:21:10 -0400 Subject: RT# 80488 Improve WA tax table update utility --- FS/FS/Cron/tax_rate_update.pm | 56 ++++++++++++++++++++++++++++++++++--- FS/bin/freeside-wa-tax-table-update | 6 ++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/FS/FS/Cron/tax_rate_update.pm b/FS/FS/Cron/tax_rate_update.pm index 5111ef4d0..fd291afbd 100755 --- a/FS/FS/Cron/tax_rate_update.pm +++ b/FS/FS/Cron/tax_rate_update.pm @@ -334,6 +334,8 @@ sub wa_sales_update_cust_main_county { my $update_count = 0; my $same_count = 0; + $args->{taxname} ||= 'State Sales Tax'; + # Work within a SQL transaction local $FS::UID::AutoCommit = 0; @@ -410,9 +412,12 @@ sub wa_sales_update_cust_main_county { )); } - for my $district ( @{ $args->{tax_districts} } ) { + DIST: for my $district ( @{ $args->{tax_districts} } ) { if ( my $row = $cust_main_county{ $district->{district} } ) { + # Strip whitespace from input + $district->{$_} =~ s/(^\s+|\s+$)//g for keys %$district; + # District already exists in this taxclass, update if necessary # # If admin updates value of conf tax_district_taxname, instead of @@ -424,20 +429,20 @@ sub wa_sales_update_cust_main_county { no warnings 'uninitialized'; if ( - $row->tax == ( $district->{tax_combined} * 100 ) + sprintf('%.4f',$row->tax) == sprintf('%.4f',($district->{tax_combined} * 100)) && $row->taxname eq $args->{taxname} && uc $row->county eq uc $district->{county} && uc $row->city eq uc $district->{city} ) { $same_count++; - next; + next DIST; } } $row->city( uc $district->{city} ); $row->county( uc $district->{county} ); $row->taxclass( $taxclass ); - $row->taxname( $args->{taxname} || undef ); + $row->taxname( $args->{taxname} ); $row->tax( $district->{tax_combined} * 100 ); if ( my $error = $row->replace ) { @@ -485,6 +490,8 @@ sub wa_sales_update_cust_main_county { $insert_count++; } + update_non_sales_tax_rows( $taxclass, $district ); + } # /foreach $district } # /foreach $taxclass @@ -502,6 +509,47 @@ sub wa_sales_update_cust_main_county { } +=head2 update_non_sales_tax_rows tax_class, $district_href + +The customer may have created additional taxes, such as Universal Service Fund. + +Ensure the columns for city and county are consistant between +the user-created tax rows and the wa-sales-managed tax rows. + +=cut + +sub update_non_sales_tax_rows { + my ( $taxclass, $district ) = @_; + + return unless ref $district && $district->{district}; + + my @rows = qsearch( cust_main_county => { + taxclass => $taxclass, + district => $district->{district}, + state => 'WA', + country => 'US', + source => { op => '!=', value => 'wa_sales' }, + }); + + for my $row ( @rows ) { + $row->city( uc $district->{city} ); + $row->county( uc $district->{county} ); + + if ( my $error = $row->replace ) { + dbh->rollback; + local $FS::UID::AutoCommit = 1; + log_error_and_die( + sprintf + "Error updating cust_main_county row %s for district %s: %s", + $row->taxnum, + $district->{district}, + $error + ); + } + } + +} + =head2 wa_sales_parse_xlsx_file \%args Parse given XLSX file for tax district information diff --git a/FS/bin/freeside-wa-tax-table-update b/FS/bin/freeside-wa-tax-table-update index b197ac845..53c7324d7 100755 --- a/FS/bin/freeside-wa-tax-table-update +++ b/FS/bin/freeside-wa-tax-table-update @@ -60,6 +60,12 @@ https://dor.wa.gov/sites/default/files/legacy/Docs/forms/ExcsTx/LocSalUseTx/Exce https://dor.wa.gov/sites/default/files/legacy/downloads/Add_DataRates2018Q4.zip +=item Other district tax rows + +When this tool updates the tax tables, any additional tax table rows with +a district set, where the 'source' column is not 'wa_sales', will have the +country, state, county, and city values kept updated to match the data +provided in the state tax tables =item Address lookup API tool -- cgit v1.2.1 From 4e282b89c158949c1726f044e0102e126fbf5bf2 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Wed, 12 Jun 2019 03:22:57 -0400 Subject: RT# 80488 Ensure WA distrct taxes are properly applied --- FS/FS/TaxEngine/internal.pm | 20 +++++++++++++++++++- FS/FS/cust_main/Billing.pm | 20 +++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/FS/FS/TaxEngine/internal.pm b/FS/FS/TaxEngine/internal.pm index 6fb1ca756..d680af86e 100644 --- a/FS/FS/TaxEngine/internal.pm +++ b/FS/FS/TaxEngine/internal.pm @@ -39,10 +39,28 @@ sub add_sale { my @taxes = (); # entries are cust_main_county objects my %taxhash_elim = %taxhash; my @elim = qw( district city county state ); + + # WA state district city names are not stable in the WA tax tables + # Allow districts to match with just a district id + if ( $taxhash{district} ) { + @taxes = qsearch( cust_main_county => { + district => $taxhash{district}, + taxclass => $taxhash{taxclass}, + }); + if ( !scalar(@taxes) && $taxhash{taxclass} ) { + qsearch( cust_main_county => { + district => $taxhash{district}, + taxclass => '', + }); + } + } + do { #first try a match with taxclass - @taxes = qsearch( 'cust_main_county', \%taxhash_elim ); + if ( !scalar(@taxes) ) { + @taxes = qsearch( 'cust_main_county', \%taxhash_elim ); + } if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) { #then try a match without taxclass diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 5f8dd9b4c..aadc8e1e9 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -1649,10 +1649,28 @@ sub _handle_taxes { my @taxes = (); # entries are cust_main_county objects my %taxhash_elim = %taxhash; my @elim = qw( district city county state ); + + # WA state district city names are not stable in the WA tax tables + # Allow districts to match with just a district id + if ( $taxhash{district} ) { + @taxes = qsearch( cust_main_county => { + district => $taxhash{district}, + taxclass => $taxhash{taxclass}, + }); + if ( !scalar(@taxes) && $taxhash{taxclass} ) { + qsearch( cust_main_county => { + district => $taxhash{district}, + taxclass => '', + }); + } + } + do { #first try a match with taxclass - @taxes = qsearch( 'cust_main_county', \%taxhash_elim ); + if ( !scalar(@taxes) ) { + @taxes = qsearch( 'cust_main_county', \%taxhash_elim ); + } if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) { #then try a match without taxclass -- cgit v1.2.1 From 978a10cd56e76e763b15f21c533d507d5bc84dec Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Wed, 12 Jun 2019 03:24:26 -0400 Subject: RT# 80488 Current city value always exists in cities selectbox --- httemplate/elements/city.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/httemplate/elements/city.html b/httemplate/elements/city.html index 3c5e91782..f710d04db 100644 --- a/httemplate/elements/city.html +++ b/httemplate/elements/city.html @@ -131,6 +131,10 @@ function <% $pre %>county_changed(what, callback) {} <% $select_style %> > +% if ( $opt{city} ) { + +% } + % unless ( $opt{'disable_empty'} ) { % } -- cgit v1.2.1 From ac3296dc4d9c1c7ff3646df6496a4f49d9e07b9b Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Fri, 14 Jun 2019 21:38:46 -0400 Subject: RT# 83450 - added location to virtual ap and added script to force update of all virtual ap --- FS/FS/part_export/saisei.pm | 154 ++++++++++++++++++++++++++++-- httemplate/edit/part_export.cgi | 3 +- httemplate/elements/progress-init.html | 2 + httemplate/view/svc_export/run_script.cgi | 10 +- 4 files changed, 154 insertions(+), 15 deletions(-) diff --git a/FS/FS/part_export/saisei.pm b/FS/FS/part_export/saisei.pm index 0f18ca36a..78dda7190 100644 --- a/FS/FS/part_export/saisei.pm +++ b/FS/FS/part_export/saisei.pm @@ -73,6 +73,20 @@ tie my %scripts, 'Tie::IxHash', error_url => '/edit/part_export.cgi?', success_message => 'Saisei export of towers and sectors as access points successful', }, + 'force_export_all_users' => { component => '/elements/popup_link.html', + label => 'Force update of all Saisei users from freeside provisioned services', + description => 'Will force an update of Saisei users description and map location from freeside provisioned services.', + html_label => 'Force update of all Saisei users from freeside provisioned services', + error_url => '/edit/part_export.cgi?', + success_message => 'Export of freeside provisioned services as Saisei users was successful', + }, + 'force_export_all_virtual_ap' => { component => '/elements/popup_link.html', + label => 'Force update of all virtual Access Points', + description => 'Will force an update of all virtual access points.', + html_label => 'Force update of all virtual Access Points', + error_url => '/edit/part_export.cgi?', + success_message => 'Export of all virtual access points to Saisei was successful', + }, ; tie my %options, 'Tie::IxHash', @@ -140,7 +154,7 @@ END ); sub _export_insert { - my ($self, $svc_broadband) = @_; + my ($self, $svc_broadband, $force_update) = @_; my $rateplan_name = $self->get_rateplan_name($svc_broadband); @@ -148,6 +162,8 @@ sub _export_insert { my $existing_rateplan; $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'}; + die ("Please double check your credentials as ".$existing_rateplan->{message}."\n") if $existing_rateplan->{message}; + # if no existing rate plan create one and modify it. $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan; $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan); @@ -158,8 +174,7 @@ sub _export_insert { my $username = $svc_broadband->{Hash}->{svcnum}; my $description = $svc_broadband->{Hash}->{description}; - my $svc_location; - $svc_location = $svc_broadband->{Hash}->{latitude}.','.$svc_broadband->{Hash}->{longitude} if ($svc_broadband->{Hash}->{latitude} && $svc_broadband->{Hash}->{longitude}); + my $svc_location = get_svc_location($self, $svc_broadband); if (!$username) { $self->{'__saisei_error'} = 'no username - can not export'; @@ -233,13 +248,17 @@ sub _export_insert { 'table' => 'cust_pkg', 'hashref' => { 'pkgnum' => $svc_broadband->{Hash}->{pkgnum}, }, }); + my $virtual_ap_name = $cust_pkg->{Hash}->{custnum}.'_'.$cust_pkg->{Hash}->{pkgpart}.'_'.$svc_broadband->{Hash}->{speed_down}.'_'.$svc_broadband->{Hash}->{speed_up}; + my $modify_existing_virtual_ap = '1' if $force_update->{'update_virtual_ap'}; my $virtual_ap_opt = { 'virtual_name' => $virtual_ap_name, 'sector_name' => $sector_name, 'virtual_uprate_limit' => $svc_broadband->{Hash}->{speed_up}, 'virtual_downrate_limit' => $svc_broadband->{Hash}->{speed_down}, + 'location' => $svc_location, + 'modify_existing' => $modify_existing_virtual_ap, }; my $virtual_ap = process_virtual_ap($self, $virtual_ap_opt); return $self->api_error if $self->{'__saisei_error'}; @@ -402,6 +421,16 @@ sub export_tower_sector { return { error => $self->api_error, }; } +sub export_user { + my ($self, $username, $description, $location) = @_; + + $self->api_create_user($username, $description, $location); + + return $self->api_error if $self->{'__saisei_error'}; + + return ''; +} + ## creates the rateplan name sub get_rateplan_name { my ($self, $svc_broadband, $svc_name) = @_; @@ -689,7 +718,7 @@ sub api_create_user { $user_hash->{'map_location'} = $location if $location; my $new_user = $self->api_call( - "PUT", + "PUT", "/users/$user", $user_hash, ); @@ -701,6 +730,33 @@ sub api_create_user { } +=head2 api_modify_user + +Modify a user. + +=cut + +sub api_modify_user { + my ($self,$user, $description, $location) = @_; + + my $user_hash = { + 'description' => $description, + }; + $user_hash->{'map_location'} = $location if $location; + + my $modify_user = $self->api_call( + "PUT", + "/users/$user", + $user_hash, + ); + + $self->{'__saisei_error'} = "Saisei could not modify the user $user" + unless ($modify_user || $self->{'__saisei_error'}); # should never happen + + return $modify_user; + +} + =head2 api_create_accesspoint Creates a access point. @@ -938,6 +994,30 @@ sub process_sector { return $accesspoint; } +=head2 get_svc_location + +sets location to lat and long from service, if no service location gets it from package, if still no location returns null. + +=cut + +sub get_svc_location { + my ($self, $svc) = @_; + + my $svc_location = ''; + $svc_location = $svc->{Hash}->{latitude}.','.$svc->{Hash}->{longitude} if ($svc->{Hash}->{latitude} && $svc->{Hash}->{longitude}); + + if (!$svc_location) { + my $pkg_location = FS::Record::qsearchs({ + 'table' => 'cust_pkg', + 'addl_from' => 'LEFT JOIN cust_location USING (locationnum)', + 'hashref' => { 'pkgnum' => $svc->{Hash}->{pkgnum} }, + }); + $svc_location = $pkg_location->{Hash}->{latitude}.','.$pkg_location->{Hash}->{longitude} if ($pkg_location); + } + + return $svc_location; +} + =head2 require_tower_and_sector sets whether the service export requires a sector with it's tower. @@ -989,6 +1069,7 @@ sub process_virtual_ap { $opt->{sector_name}, $opt->{virtual_uprate_limit}, $opt->{virtual_downrate_limit}, + $opt->{location}, ) if $existing_virtual_ap && $opt->{modify_existing}; #if virtual ap does not exist as an access point create it. @@ -996,6 +1077,7 @@ sub process_virtual_ap { $virtual_name, $opt->{virtual_uprate_limit}, $opt->{virtual_downrate_limit}, + $opt->{location}, ) unless $existing_virtual_ap; my $update_sector; @@ -1004,7 +1086,7 @@ sub process_virtual_ap { } # Attach newly created virtual ap to tower sector ap or if sector has changed. - $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector)); + $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}, $opt->{location}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector)); # set access point to existing one or newly created one. my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name); @@ -1015,6 +1097,7 @@ sub process_virtual_ap { sub export_provisioned_services { my $job = shift; my $param = shift; + my $force_update = shift; my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } ) or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}. This export does not exist.\n"; @@ -1047,7 +1130,9 @@ sub export_provisioned_services { my $host = api_get_host($part_export, $svc->{Hash}->{ip_addr}); die ("Please double check your credentials as ".$host->{message}."\n") if $host->{message}; warn "Exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug')); - my $export_error = _export_insert($part_export,$svc) unless $host->{collection}; + my $export_error; + if ($force_update) { $export_error = _export_insert($part_export,$svc,$force_update); } + else { $export_error = _export_insert($part_export,$svc) unless $host->{collection}; } if ($export_error) { warn "Error exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug')); die ("$export_error\n"); @@ -1090,6 +1175,63 @@ sub export_all_towers_sectors { } +sub force_export_all_virtual_ap { + my $job = shift; + my $param = shift; + my $force_update = { 'update_virtual_ap' => '1', }; + + export_provisioned_services($job,$param,$force_update); + + return; +} + +sub force_export_all_users { + my $job = shift; + my $param = shift; + + my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } ) + or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}. This export does not exist.\n"; + bless $part_export; + + my @svcparts = FS::Record::qsearch({ + 'table' => 'export_svc', + 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart ) ', + 'hashref' => { 'exportnum' => $param->{export_provisioned_services_exportnum}, }, + }); + my $part_count = scalar @svcparts; + + my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts; + + my @svcs = FS::Record::qsearch({ + 'table' => 'cust_svc', + 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ', + 'extra_sql' => " WHERE svcpart in ('".$parts."')", + }) unless !$parts; + + my $svc_count = scalar @svcs; + + my %status = {}; + for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; } + + my $process_count=0; + foreach my $svc (@svcs) { + my $description = $svc->{Hash}->{description}; + my $user = $svc->{Hash}->{svcnum}; + my $svc_location = get_svc_location($job, $svc); + if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); } + warn "Exporting user ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug')); + my $export_error = export_user($part_export,$user,$description, $svc_location); + if ($export_error) { + warn "Error exporting user ".$svc->{Hash}->{svcnum}."\n" if ($part_export->option('debug')); + die ($export_error->{'error'}."\n"); + } + $process_count++; + } + + return; + +} + sub test_export_report { my ($self, $opts) = @_; my @export_error; diff --git a/httemplate/edit/part_export.cgi b/httemplate/edit/part_export.cgi index f8a46c7fd..30e4218e2 100644 --- a/httemplate/edit/part_export.cgi +++ b/httemplate/edit/part_export.cgi @@ -296,7 +296,7 @@ my $widget = new HTML::Widgets::SelectLayers( $html .= '
  • '; } diff --git a/httemplate/elements/progress-init.html b/httemplate/elements/progress-init.html index 2a62c5e99..de3c6b761 100644 --- a/httemplate/elements/progress-init.html +++ b/httemplate/elements/progress-init.html @@ -126,6 +126,8 @@ function <%$key%>process () { } } + Hash.push('key', '<%$key%>'); + // jsrsPOST = true; // jsrsExecute( '<% $action %>', <%$key%>myCallback, 'start_job', Hash ); diff --git a/httemplate/view/svc_export/run_script.cgi b/httemplate/view/svc_export/run_script.cgi index ba58bbdd7..f0524991b 100644 --- a/httemplate/view/svc_export/run_script.cgi +++ b/httemplate/view/svc_export/run_script.cgi @@ -14,17 +14,13 @@ my %param = (); } } -my $exportnum; -my $method; -for (grep /^*_script$/, keys %param) { - $exportnum = $param{$param{$_}.'_exportnum'}; - $method = $param{$param{$_}.'_script'}; -} +my $run_script = $param{'key'}; +my $exportnum = $param{$run_script.'_exportnum'}; my $part_export = qsearchs('part_export', { 'exportnum'=> $exportnum, } ) or die "unknown exportnum $exportnum"; -my $class = 'FS::part_export::'.$part_export->{Hash}->{exporttype}.'::'.$method; +my $class = 'FS::part_export::'.$part_export->{Hash}->{exporttype}.'::'.$run_script; my $server = new FS::UI::Web::JSRPC $class, $cgi; -- cgit v1.2.1 From 5372897f367498972c96f5494e142e6e11b29eb8 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Sat, 15 Jun 2019 00:42:58 -0400 Subject: RT# 83450 - fixed rateplan export --- FS/FS/part_export/saisei.pm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/FS/FS/part_export/saisei.pm b/FS/FS/part_export/saisei.pm index 78dda7190..9dba56b49 100644 --- a/FS/FS/part_export/saisei.pm +++ b/FS/FS/part_export/saisei.pm @@ -165,12 +165,12 @@ sub _export_insert { die ("Please double check your credentials as ".$existing_rateplan->{message}."\n") if $existing_rateplan->{message}; # if no existing rate plan create one and modify it. - $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan; - $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan); + $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan->{collection}; + $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan->{collection}); return $self->api_error if $self->{'__saisei_error'}; # set rateplan to existing one or newly created one. - my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name); + my $rateplan = $existing_rateplan->{collection} ? $existing_rateplan : $self->api_get_rateplan($rateplan_name); my $username = $svc_broadband->{Hash}->{svcnum}; my $description = $svc_broadband->{Hash}->{description}; @@ -1012,7 +1012,7 @@ sub get_svc_location { 'addl_from' => 'LEFT JOIN cust_location USING (locationnum)', 'hashref' => { 'pkgnum' => $svc->{Hash}->{pkgnum} }, }); - $svc_location = $pkg_location->{Hash}->{latitude}.','.$pkg_location->{Hash}->{longitude} if ($pkg_location); + $svc_location = $pkg_location->{Hash}->{latitude}.','.$pkg_location->{Hash}->{longitude} if ($pkg_location->{Hash}->{latitude} && $pkg_location->{Hash}->{longitude}); } return $svc_location; -- cgit v1.2.1 From 4fd1280540e2c9b90fa59c0c32d691f5222f65d4 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Mon, 24 Jun 2019 13:27:17 -0400 Subject: RT# 82137 - Added ability for processing fee to be pain on seperate invoice. --- FS/FS/Conf.pm | 16 ++++++ FS/FS/cust_main/Billing_Realtime.pm | 67 +++++++++++++++++----- FS/FS/cust_pay.pm | 4 ++ httemplate/config/config-process.cgi | 6 +- httemplate/elements/tr-amount_fee.html | 2 +- httemplate/elements/tr-select-payment_options.html | 2 +- 6 files changed, 79 insertions(+), 18 deletions(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 0f774d16c..9e68ffc67 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2682,6 +2682,17 @@ and customer address. Include units.', 'per_agent' => 1, }, + { + 'key' => 'processing-fee_on_separate_invoice', + 'section' => 'payments', + 'description' => 'Places the processing fee on a separate invoice by itself. Only works with real time processing.', + 'type' => 'checkbox', + 'validate' => sub { + my $conf = new FS::Conf; + !$conf->config('batch-enable_payby') ? '' : 'You can not set this option while batch processing is enabled.'; + }, + }, + { 'key' => 'banned_pay-pad', 'section' => 'credit_cards', @@ -3840,6 +3851,11 @@ and customer address. Include units.', 'description' => 'Enable batch processing for the specified payment types.', 'type' => 'selectmultiple', 'select_enum' => [qw( CARD CHEK )], + 'validate' => sub { + ## can not create a new invoice and pay it silently with batch processing, only realtime processing. + my $conf = new FS::Conf; + !$conf->exists('processing-fee_on_separate_invoice') ? '' : 'You can not enable batch processing while processing-fee_on_separate_invoice option is enabled.'; + }, }, { diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 89d63dd26..b65860e9b 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -985,17 +985,20 @@ sub _realtime_bop_result { savepoint_create( $savepoint_label ); #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction - - my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () ); + my $error = $cust_pay->insert( + $options{'manual'} ? ( 'manual' => 1 ) : (), + $options{'processing-fee'} > 0 ? ( 'processing-fee' => $options{'processing-fee'} ) : (), + ); if ( $error ) { savepoint_rollback( $savepoint_label ); $cust_pay->invnum(''); #try again with no specific invnum $cust_pay->paynum(''); - my $error2 = $cust_pay->insert( $options{'manual'} ? - ( 'manual' => 1 ) : () - ); + my $error2 = $cust_pay->insert( + $options{'manual'} ? ( 'manual' => 1 ) : (), + $options{'processing-fee'} > 0 ? ( 'processing-fee' => $options{'processing-fee'} ) : (), + ); if ( $error2 ) { # gah. but at least we have a record of the state we had to abort in # from cust_pay_pending now. @@ -1137,11 +1140,23 @@ sub _realtime_bop_result { if ($options{'processing-fee'} > 0) { my $pf_cust_pkg; my $processing_fee_text = 'Payment Processing Fee'; + + my $conf = new FS::Conf; + + my $pf_seperate_bill; + my $pf_bill_now; + if ($conf->exists('processing-fee_on_separate_invoice')) { + $pf_seperate_bill = 'Y'; + $pf_bill_now = '1'; + } + my $pf_change_error = $self->charge({ 'amount' => $options{'processing-fee'}, 'pkg' => $processing_fee_text, 'setuptax' => 'Y', 'cust_pkg_ref' => \$pf_cust_pkg, + 'separate_bill' => $pf_seperate_bill, + 'bill_now' => $pf_bill_now, }); if($pf_change_error) { @@ -1156,17 +1171,41 @@ sub _realtime_bop_result { # but keep going... } - my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum }); - unless ( $cust_bill ) { - warn "race condition + invoice deletion just happened"; - return ''; - } + if ($conf->exists('processing-fee_on_separate_invoice')) { + my $cust_bill_pkg = qsearchs( 'cust_bill_pkg', { 'pkgnum' => $pf_cust_pkg->pkgnum } ); + + my $pf_cust_bill = qsearchs('cust_bill', { 'invnum' => $cust_bill_pkg->invnum }); + unless ( $pf_cust_bill ) { + warn "no processing fee inv found!"; + return ''; + } + + my $pf_apply_error = $pf_cust_bill->apply_payments_and_credits; + + my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum }); + unless ( $cust_bill ) { + warn "race condition + invoice deletion just happened"; + return ''; + } + + my $grand_pf_error = $cust_bill->apply_payments_and_credits; + + warn "cannot apply Processing fee to invoice #$invnum: $grand_pf_error - $pf_apply_error" + if $grand_pf_error || $pf_apply_error; + } ## processing-fee_on_separate_invoice + else { + my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum }); + unless ( $cust_bill ) { + warn "race condition + invoice deletion just happened"; + return ''; + } - my $grand_pf_error = - $cust_bill->add_cc_surcharge($pf_cust_pkg->pkgnum,$options{'processing-fee'}); + my $grand_pf_error = + $cust_bill->add_cc_surcharge($pf_cust_pkg->pkgnum,$options{'processing-fee'}); - warn "cannot add Processing fee to invoice #$invnum: $grand_pf_error" - if $grand_pf_error; + warn "cannot add Processing fee to invoice #$invnum: $grand_pf_error" + if $grand_pf_error; + } ## no processing-fee_on_separate_invoice } #end if $options{'processing-fee'} } #end if ( $options{'cc_surcharge'} > 0 || $options{'processing-fee'} > 0) diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index 915cb3303..c0a254119 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -395,6 +395,8 @@ sub insert { $dbh->commit or die $dbh->errstr if $oldAutoCommit; + $self->{'processing_fee'} = $options{'processing-fee'}; + #payment receipt my $trigger = $conf->config('payment_receipt-trigger', $self->cust_main->agentnum) || 'cust_pay'; @@ -735,6 +737,8 @@ sub send_message_receipt { my %substitutions = (); $substitutions{invnum} = $cust_bill->invnum if $cust_bill; + $substitutions{'processing_fee'} = $self->{'processing_fee'}; + my $msg_template = qsearchs('msg_template',{ msgnum => $msgnum}); unless ($msg_template) { diff --git a/httemplate/config/config-process.cgi b/httemplate/config/config-process.cgi index d84edce00..6af7d2322 100644 --- a/httemplate/config/config-process.cgi +++ b/httemplate/config/config-process.cgi @@ -155,7 +155,9 @@ foreach my $type ( ref($i->type) ? @{$i->type} : $i->type ) { } } elsif ( $type eq 'checkbox' ) { if ( defined $cgi->param($i->key.$n) ) { - push @touch, $i->key; + my $error = &{$i->validate}('', $n) if $i->validate; + push @error, $error if $error; + push @touch, $i->key if !$error; } else { push @delete, $i->key; } @@ -167,7 +169,7 @@ foreach my $type ( ref($i->type) ? @{$i->type} : $i->type ) { if ( scalar(@{[ $cgi->param($i->key.$n) ]}) && $cgi->param($i->key.$n) ne '' ) { my $error = &{$i->validate}([ $cgi->param($i->key.$n) ], $n) if $i->validate; push @error, $error if $error; - $conf->set($i->key, join("\n", @{[ $cgi->param($i->key.$n) ]} ), $agentnum); + $conf->set($i->key, join("\n", @{[ $cgi->param($i->key.$n) ]} ), $agentnum) if !$error; } else { $conf->delete($i->key, $agentnum); } diff --git a/httemplate/elements/tr-amount_fee.html b/httemplate/elements/tr-amount_fee.html index 94795de37..0ae6a0f74 100644 --- a/httemplate/elements/tr-amount_fee.html +++ b/httemplate/elements/tr-amount_fee.html @@ -44,7 +44,7 @@
    'Day Phone', night => 'Night Phone', mobile => 'Mobile Phone', + fax => 'Fax Number', ); \ No newline at end of file diff --git a/httemplate/view/cust_main/contacts_new.html b/httemplate/view/cust_main/contacts_new.html index bd213d1dd..0fdcc5371 100644 --- a/httemplate/view/cust_main/contacts_new.html +++ b/httemplate/view/cust_main/contacts_new.html @@ -13,7 +13,7 @@ <%$th%>Send messages <%$th%>Self-service % foreach my $phone_type (@phone_type) { - <%$th%><% $phone_type->typename |h %> + <%$th%><% $phone_type->typename |h %> phone % } <%$th%>Comment
    <%=$r%>City - > - ${r}City$city_html${r}State/County$county_html $state_html${r}Zip
    ' . include('/elements/progress-init.html', $part_export->exporttype, - [ $script.'_exportnum', $script.'_script' ], + [ $script.'_exportnum' ], rooturl().'view/svc_export/run_script.cgi', { 'error_url' => rooturl().$exports->{$layer}{scripts}{$script}->{error_url}."exportnum=".$part_export->{Hash}->{exportnum}, @@ -307,7 +307,6 @@ my $widget = new HTML::Widgets::SelectLayers( $script, ) . ' - '.$exports->{$layer}{scripts}{$script}->{html_label}.'
    - + A processing fee of <% $processing_fee %> is being applied to this transaction. diff --git a/httemplate/elements/tr-select-payment_options.html b/httemplate/elements/tr-select-payment_options.html index c5b84e756..27df9622e 100644 --- a/httemplate/elements/tr-select-payment_options.html +++ b/httemplate/elements/tr-select-payment_options.html @@ -60,7 +60,7 @@ Example: $('#payment_option_row').<% $payment_option_row %>(); $('#payment_amount_row').<% $payment_amount_row %>(); - $('#ajax_processingfee_cell').hide(); + $('#ajax_processingfee_cell').show(); if($('#payment_amount_row').is(':visible')) { var surcharge; -- cgit v1.2.1 From 632fe1bb8d84b2dd069197b18452cc7c131abee9 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Mon, 24 Jun 2019 15:07:04 -0400 Subject: RT# 82137 - fixed problem with form submit before selecting payment amount --- httemplate/elements/tr-amount_fee.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httemplate/elements/tr-amount_fee.html b/httemplate/elements/tr-amount_fee.html index 0ae6a0f74..42132e09a 100644 --- a/httemplate/elements/tr-amount_fee.html +++ b/httemplate/elements/tr-amount_fee.html @@ -5,7 +5,7 @@ <% $money_char %> Date: Tue, 25 Jun 2019 08:27:29 -0400 Subject: RT# 82137 - default payment amount now has processing fee in total if processing fee exists. --- httemplate/elements/tr-amount_fee.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/httemplate/elements/tr-amount_fee.html b/httemplate/elements/tr-amount_fee.html index 42132e09a..42636cfe4 100644 --- a/httemplate/elements/tr-amount_fee.html +++ b/httemplate/elements/tr-amount_fee.html @@ -140,6 +140,8 @@ if ( $amount ) { $amount += $surcharge; + $amount += $processing_fee; ## needed if processing fee is checked on default. + $amount = sprintf("%.2f", $amount); } -- cgit v1.2.1 From 2009d9cee8038aeff5b4313113fc23f546455cf5 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 25 Jun 2019 10:55:00 -0400 Subject: RT# 82137 - added processing fee template substitution. --- httemplate/edit/msg_template/email.html | 2 ++ httemplate/elements/tr-amount_fee.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/httemplate/edit/msg_template/email.html b/httemplate/edit/msg_template/email.html index 53f538b11..a400bc804 100644 --- a/httemplate/edit/msg_template/email.html +++ b/httemplate/edit/msg_template/email.html @@ -297,6 +297,7 @@ my %substitutions = ( 'cust_pay' => [ '$paynum' => 'Payment#', '$paid' => 'Amount', + '$processing_fee' => 'Processing fee', '$payby' => 'Payment method', '$date' => 'Payment date', '$payinfo' => 'Card/account# (masked)', @@ -372,6 +373,7 @@ Substitutions: ' Enclose substitutions and other Perl expressions in braces:
    { $name } = ExampleCo (Smith, John)
    { time2str("%D", time) } = '.time2str("%D", time).' +
    { "processing fee of $processing_fee" if $processing_fee; } = Will display text if there is a processing fee

    '; $sidebar .= include('/elements/template_image-dialog.html', 'callback' => 'insertHtml' diff --git a/httemplate/elements/tr-amount_fee.html b/httemplate/elements/tr-amount_fee.html index 42636cfe4..40f55e7a3 100644 --- a/httemplate/elements/tr-amount_fee.html +++ b/httemplate/elements/tr-amount_fee.html @@ -5,7 +5,7 @@ <% $money_char %>