From ddcfe66496c323f1f52fdbd00e8babd43249a609 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Fri, 18 May 2018 11:41:21 -0400 Subject: RT# 77792 - Created method to decide if batch payname should be name on card or personal name or business name. --- FS/FS/cust_main.pm | 21 +++++++++++++++++++++ FS/FS/pay_batch/RBC.pm | 4 +--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 7c9868d7a..3bffa3a59 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3922,6 +3922,27 @@ sub name { $name; } +=item batch_payment_payname + +Returns a name string for this customer, either "cust_batch_payment->payname" or "First Last" or "Company, +based on if a company name exists and is the account being used a business account. + +=cut + +sub batch_payment_payname { + my $self = shift; + my $cust_pay_batch = shift; + my $name; + + if ($cust_pay_batch->{Hash}->{payby} eq "CARD") { $name = $cust_pay_batch->payname; } + else { $name = $self->first .' '. $self->last; } + + $name = $self->company + if (($cust_pay_batch->{Hash}->{paytype} eq "Business checking" || $cust_pay_batch->{Hash}->{paytype} eq "Business savings") && $self->company); + + $name; +} + =item service_contact Returns the L object for this customer that has the 'Service' diff --git a/FS/FS/pay_batch/RBC.pm b/FS/FS/pay_batch/RBC.pm index 1577a7f85..dea89cde1 100644 --- a/FS/FS/pay_batch/RBC.pm +++ b/FS/FS/pay_batch/RBC.pm @@ -175,9 +175,7 @@ $name = 'RBC'; } ## set custname to business name if business checking or savings account is used otherwise leave as first and last name. - my $custname = $cust_pay_batch->cust_main->first . ' ' . $cust_pay_batch->cust_main->last; - $custname = $cust_pay_batch->cust_main->company - if (($cust_pay_batch->{Hash}->{paytype} eq "Business checking" || $cust_pay_batch->{Hash}->{paytype} eq "Business savings") && $cust_pay_batch->cust_main->company); + my $custname = $cust_pay_batch->cust_main->batch_payment_payname($cust_pay_batch); $i++; -- cgit v1.2.1 From fc5bf4070e3d34c6f6298fb3c61263644fa71ade Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Fri, 18 May 2018 22:40:08 +0000 Subject: RT# 78190 Fix billing event/late fee on sectioned invoices --- FS/FS/Template_Mixin.pm | 69 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index 646312502..edbb4d440 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -1346,27 +1346,36 @@ sub print_generic { #$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : ''; #$tax_section->{'sort_weight'} = $tax_weight; + my $invoice_sections_with_taxes = $conf->config_bool( + 'invoice_sections_with_taxes', $cust_main->agentnum + ); + foreach my $tax ( @items_tax ) { - $taxtotal += $tax->{'amount'}; my $description = &$escape_function( $tax->{'description'} ); my $amount = sprintf( '%.2f', $tax->{'amount'} ); if ( $multisection ) { + if ( !$invoice_sections_with_taxes ) { - push @detail_items, { - ext_description => [], - ref => '', - quantity => '', - description => $description, - amount => $money_char. $amount, - product_code => '', - section => $tax_section, - }; + $taxtotal += $tax->{'amount'}; + + push @detail_items, { + ext_description => [], + ref => '', + quantity => '', + description => $description, + amount => $money_char. $amount, + product_code => '', + section => $tax_section, + }; + } } else { + $taxtotal += $tax->{'amount'}; + push @total_items, { 'total_item' => $description, 'total_amount' => $other_money_char. $amount, @@ -1402,8 +1411,10 @@ sub print_generic { if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) { - # remove tax section if taxes are itemized within other sections - @sections = grep{ $_ ne $tax_section } @sections; + # If all tax items are displayed in location/category sections, + # remove the empty tax section + @sections = grep{ $_ ne $tax_section } @sections + unless grep{ $_->{section} eq $tax_section } @detail_items; } elsif ( !grep $tax_section, @sections ) { @@ -3134,16 +3145,30 @@ sub _items_fee { warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; next; } - if ( exists($options{section}) and exists($options{section}{category}) ) - { - my $categoryname = $options{section}{category}; - # then filter for items that have that section - if ( $part_fee->categoryname ne $categoryname ) { - warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG; - next; - } - } # otherwise include them all in the main section - # XXX what to do when sectioning by location? + + # If _items_fee is called while building a sectioned invoice, + # - invoice_sections_method: category + # Skip fee records that do not match the section category. + # - invoice_sections_method: location + # Skip fee records always for location sections. + # The fee records will be presented in the tax/fee section instead. + if ( + exists( $options{section} ) + and + ( + ( + exists( $options{section}{category} ) + and + $part_fee->categoryname ne $options{section}{category} + ) + or + exists( $options{section}{location}) + ) + ) { + warn "skipping fee '".$part_fee->itemdesc. + "'--not in section $options{section}{category}\n" if $DEBUG; + next; + } my @ext_desc; my %base_invnums; # invnum => invoice date -- cgit v1.2.1 From 9996cfd8b87a47576dbac33a04007ec42d024d23 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sat, 19 May 2018 20:05:15 +0000 Subject: RT# 78190 Fix taxes on fees for sectioned invoices Fix taxes charged on a billing-event fee, such as a late fee, displayed incorrectly on some sectioned invoices --- FS/FS/Template_Mixin.pm | 1 + FS/FS/cust_bill_pkg.pm | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index edbb4d440..f36fb9628 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -3196,6 +3196,7 @@ sub _items_fee { push @items, { feepart => $cust_bill_pkg->feepart, + billpkgnum => $cust_bill_pkg->billpkgnum, amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur), description => $desc, pkg_tax => \@pkg_tax, diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index f6b40f6b2..1262c3874 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -1881,7 +1881,29 @@ sub _pkg_tax_list { # Duplicates can be identified by billpkgtaxlocationnum column. my $self = shift; - return unless $self->pkgnum; + + my $search_selector; + if ( $self->pkgnum ) { + + # For taxes applied to normal billing items + $search_selector = + ' cust_bill_pkg_tax_location.pkgnum = ' + . dbh->quote( $self->pkgnum ); + + } elsif ( $self->feepart ) { + + # For taxes applied to fees, when the fee is not attached to a package + # i.e. late fees, billing events fees + $search_selector = + ' cust_bill_pkg_tax_location.taxable_billpkgnum = ' + . dbh->quote( $self->billpkgnum ); + + } else { + warn "_pkg_tax_list() unhandled case breaking taxes into sections"; + warn "_pkg_tax_list() $_: ".$self->$_ + for qw(pkgnum billpkgnum feepart); + return; + } map +{ billpkgtaxlocationnum => $_->billpkgtaxlocationnum, @@ -1907,7 +1929,7 @@ sub _pkg_tax_list { ' WHERE '. ' cust_bill_pkg.invnum = ' . dbh->quote( $self->invnum ) . ' AND '. - ' cust_bill_pkg_tax_location.pkgnum = ' . dbh->quote( $self->pkgnum ), + $search_selector }); } -- cgit v1.2.1 From 709a481dd9a9f29009505356603db66613bf2cb6 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sat, 19 May 2018 20:31:02 +0000 Subject: RT# 79363 Hide empty tax section, invoice_sections_with_taxes --- FS/FS/Template_Mixin.pm | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index f36fb9628..51f89f717 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -1396,6 +1396,14 @@ sub print_generic { $other_money_char. sprintf('%.2f', $self->charged - $taxtotal ); if ( $multisection ) { + + if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) { + # If all tax items are displayed in location/category sections, + # remove the empty tax section + @sections = grep{ $_ ne $tax_section } @sections + unless grep{ $_->{section} eq $tax_section } @detail_items; + } + if ( $taxtotal > 0 ) { # there are taxes, so prepare the section to be displayed. # $taxtotal already includes any line items that were already in the @@ -1409,14 +1417,7 @@ sub print_generic { $tax_section->{'description'} = $self->mt($tax_description); $tax_section->{'summarized'} = ''; - if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) { - - # If all tax items are displayed in location/category sections, - # remove the empty tax section - @sections = grep{ $_ ne $tax_section } @sections - unless grep{ $_->{section} eq $tax_section } @detail_items; - - } elsif ( !grep $tax_section, @sections ) { + if ( !grep $tax_section, @sections ) { # append it if it's not already there push @sections, $tax_section; -- cgit v1.2.1 From c34212f6b04a6796467ad1fb5d32154eae1ea40d Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sat, 19 May 2018 19:51:03 -0500 Subject: RT# 78190 Fix bill summary missing taxes or fees --- FS/FS/Template_Mixin.pm | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index 51f89f717..c90e65245 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -1417,13 +1417,12 @@ sub print_generic { $tax_section->{'description'} = $self->mt($tax_description); $tax_section->{'summarized'} = ''; - if ( !grep $tax_section, @sections ) { + # append tax section unless it's already there + push @sections, $tax_section + unless grep {$_ eq $tax_section} @sections; - # append it if it's not already there - push @sections, $tax_section; - push @summary_subtotals, $tax_section; - - } + push @summary_subtotals, $tax_section + unless grep {$_ eq $tax_section} @summary_subtotals; } } else { -- cgit v1.2.1 From b5876680bf13c72fc9ba00e0ad3b87967b69005c Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Mon, 21 May 2018 09:44:55 -0400 Subject: RT# 77964 - refined code to defer dates when waiving setup fee for prorated packages. --- FS/FS/cust_main/Billing.pm | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 51b49e4f0..9cf9b56c6 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -1052,10 +1052,8 @@ sub _make_lines { } } - if ($cust_pkg->waive_setup && $part_pkg->plan eq "prorate") { - $lineitems++; - $setup = 0 if $part_pkg->prorate_setup($cust_pkg, $time); - } + $lineitems++ + if $cust_pkg->waive_setup && $part_pkg->can('prorate_setup') && $part_pkg->prorate_setup($cust_pkg, $time); if ( $cust_pkg->get('setup') ) { # don't change it -- cgit v1.2.1 From 9b69e985d204fea71a283d32ca607c9b4be2f1c5 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Tue, 22 May 2018 23:44:21 -0500 Subject: RT# 79705 Correct UTF-8 output for generated E-Mail --- FS/FS/Template_Mixin.pm | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index c90e65245..b9514fee1 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -2273,8 +2273,7 @@ sub generate_email { warn "$me generating plain text invoice" if $DEBUG; - # 'print_text' argument is no longer used - @text = map Encode::encode_utf8($_), $self->print_text(\%args); + @text = $self->print_text(\%args); } else { @@ -2290,7 +2289,11 @@ sub generate_email { 'Encoding' => 'quoted-printable', 'Charset' => 'UTF-8', #'Encoding' => '7bit', - 'Data' => \@text, + 'Data' => [ + map + { Encode::encode('UTF-8', $_, Encode::FB_WARN | Encode::LEAVE_SRC ) } + @text + ], 'Disposition' => 'inline', ); @@ -2369,7 +2372,11 @@ sub generate_email { ' ', ' ', ' ', - Encode::encode_utf8($html), + Encode::encode( + 'UTF-8', + $html, + Encode::FB_WARN | Encode::LEAVE_SRC + ), ' ', '', ], -- cgit v1.2.1 From 83d79f6af4ddd62735263e55f2249ba80fd9f402 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sat, 26 May 2018 17:38:09 -0500 Subject: RT# 80268 Fix RADIUS usergroup UI bug --- FS/FS/svc_broadband.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm index 38594f0df..96028df9c 100755 --- a/FS/FS/svc_broadband.pm +++ b/FS/FS/svc_broadband.pm @@ -134,6 +134,7 @@ sub table_info { #select_table => 'radius_group', #select_key => 'groupnum', #select_label => 'groupname', + disable_select => 1, disable_inventory => 1, multiple => 1, }, @@ -523,4 +524,3 @@ FS::part_svc, schema.html from the base documentation. =cut 1; - -- cgit v1.2.1 From e928101f860813b7485e79ea549e736f69c50948 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Mon, 4 Jun 2018 20:10:15 -0400 Subject: RT# 77917 - Updated event option Agent to allow for selection of multiple agents. --- FS/FS/Upgrade.pm | 3 +++ FS/FS/part_event/Condition/agent.pm | 9 ++++----- FS/FS/part_event_condition_option.pm | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index 0069e207a..5be33fa59 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -501,6 +501,9 @@ sub upgrade_data { #'compliance solutions' -> 'compliance_solutions' 'tax_rate' => [], 'tax_rate_location' => [], + + #upgrade part_event_condition_option agentnum to a multiple hash value + 'part_event_condition_option' =>[], ; \%hash; diff --git a/FS/FS/part_event/Condition/agent.pm b/FS/FS/part_event/Condition/agent.pm index bdd4e12de..917cf468b 100644 --- a/FS/FS/part_event/Condition/agent.pm +++ b/FS/FS/part_event/Condition/agent.pm @@ -13,7 +13,7 @@ sub description { sub option_fields { ( - 'agentnum' => { label=>'Agent', type=>'select-agent', }, + 'agentnum' => { label=>'Agent', type=>'select-agent', multiple => '1' }, ); } @@ -22,16 +22,15 @@ sub condition { my $cust_main = $self->cust_main($object); - my $agentnum = $self->option('agentnum'); - - $cust_main->agentnum == $agentnum; + my $hashref = $self->option('agentnum') || {}; + grep $hashref->{ $_->agentnum }, $cust_main->agent; } sub condition_sql { my( $class, $table, %opt ) = @_; - "cust_main.agentnum = " . $class->condition_sql_option_integer('agentnum', $opt{'driver_name'}); + "cust_main.agentnum IN " . $class->condition_sql_option_option_integer('agentnum', $opt{'driver_name'}); } 1; diff --git a/FS/FS/part_event_condition_option.pm b/FS/FS/part_event_condition_option.pm index 3256dc0bd..15a6a5553 100644 --- a/FS/FS/part_event_condition_option.pm +++ b/FS/FS/part_event_condition_option.pm @@ -138,6 +138,30 @@ sub optionvalue { } } +use FS::upgrade_journal; +sub _upgrade_data { #class method + my ($class, %opts) = @_; + + # migrate part_event_condition_option agentnum to part_event_condition_option_option agentnum + unless ( FS::upgrade_journal->is_done('agentnum_to_hash') ) { + + foreach my $condition_option (qsearch('part_event_condition_option', { optionname => 'agentnum', })) { + my $optionvalue = $condition_option->get("optionvalue"); + if ($optionvalue eq 'HASH' ) { next; } + else { + my $options = {"$optionvalue" => '1',}; + $condition_option->optionvalue(ref($options)); + my $error = $condition_option->replace($options); + die $error if $error; + } + } + + FS::upgrade_journal->set_done('agentnum_to_hash'); + + } + +} + =back =head1 SEE ALSO -- cgit v1.2.1 From 17a3178440ad657665657896fa6ae6d94a8d4e61 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 5 Jun 2018 08:50:17 -0400 Subject: RT# 77917 - fixed upgrade to work when all agents were selected. --- FS/FS/part_event_condition_option.pm | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/FS/FS/part_event_condition_option.pm b/FS/FS/part_event_condition_option.pm index 15a6a5553..f1d1b6a15 100644 --- a/FS/FS/part_event_condition_option.pm +++ b/FS/FS/part_event_condition_option.pm @@ -146,14 +146,23 @@ sub _upgrade_data { #class method unless ( FS::upgrade_journal->is_done('agentnum_to_hash') ) { foreach my $condition_option (qsearch('part_event_condition_option', { optionname => 'agentnum', })) { + my %options; my $optionvalue = $condition_option->get("optionvalue"); if ($optionvalue eq 'HASH' ) { next; } + elsif ($optionvalue eq '') { + foreach my $agent (qsearch('agent', {})) { + $options{$agent->agentnum} = '1'; + } + + } else { - my $options = {"$optionvalue" => '1',}; - $condition_option->optionvalue(ref($options)); - my $error = $condition_option->replace($options); - die $error if $error; + $options{$optionvalue} = '1'; } + + $condition_option->optionvalue(ref(\%options)); + my $error = $condition_option->replace(\%options); + die $error if $error; + } FS::upgrade_journal->set_done('agentnum_to_hash'); -- cgit v1.2.1 From 2a557042a62c71b2bcee3d4d2b7e6c79bc52e67a Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Thu, 7 Jun 2018 21:25:10 -0500 Subject: RT# 74877 flat pkg conf opt prorate_defer_change_bill Enable this package flag to delay invoicing a customer for a package change until their next normal bill date --- FS/FS/cust_pkg.pm | 6 ++++++ FS/FS/part_pkg/flat.pm | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 07c5a4756..d00f0397b 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -2487,6 +2487,12 @@ sub change { $keep_dates = 0; $hash{'last_bill'} = ''; $hash{'bill'} = ''; + + # Optionally, carry over the next bill date from the changed cust_pkg + # so an invoice isn't generated until the customer's usual billing date + if ( $self->part_pkg->option('prorate_defer_change_bill', 1) ) { + $hash{bill} = $self->bill; + } } if ( $keep_dates ) { diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm index 6fd9c7d08..c06328b1b 100644 --- a/FS/FS/part_pkg/flat.pm +++ b/FS/FS/part_pkg/flat.pm @@ -57,6 +57,12 @@ tie my %contract_years, 'Tie::IxHash', ( 'the customer\'s next bill date', 'type' => 'checkbox', }, + 'prorate_defer_change_bill' => { + 'name' => 'When synchronizing, defer bill for '. + 'package changes until the customer\'s '. + 'next bill date', + 'type' => 'checkbox', + }, 'prorate_round_day' => { 'name' => 'When synchronizing, round the prorated '. 'period', @@ -87,7 +93,8 @@ tie my %contract_years, 'Tie::IxHash', ( }, 'fieldorder' => [ qw( recur_temporality start_1st - sync_bill_date prorate_defer_bill prorate_round_day + sync_bill_date prorate_defer_bill + prorate_defer_change_bill prorate_round_day suspend_bill unsuspend_adjust_bill bill_recur_on_cancel bill_suspend_as_cancel -- cgit v1.2.1 From 2cc8d4007576bfd2efd294fe19f5c3885c5abd8e Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sun, 10 Jun 2018 23:25:41 -0500 Subject: RT# 32233 Mask ssn and stateid in selfservice --- FS/FS/ClientAPI/MyAccount.pm | 5 ++++- fs_selfservice/FS-SelfService/cgi/selfservice.cgi | 9 +++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index e4fef9554..ae0fa614c 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -630,6 +630,10 @@ sub customer_info_short { for (@cust_main_editable_fields) { $return{$_} = $cust_main->get($_); } + + $return{ss} = $cust_main->masked('ss') if $p->{mask_ss}; + $return{stateid} = $cust_main->masked('stateid') if $p->{mask_stateid}; + #maybe a little more expensive, but it should be cached by now for (@location_editable_fields) { $return{$_} = $cust_main->bill_location->get($_) @@ -3900,4 +3904,3 @@ sub _custoragent_session_custnum { } 1; - diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi index 6cf264c08..161231555 100755 --- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi +++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi @@ -276,8 +276,11 @@ sub change_bill { } return $payment_info if ( $payment_info->{'error'} ); - my $customer_info = - customer_info( 'session_id' => $session_id ); + my $customer_info = customer_info( + mask_ss => 1, + mask_stateid => 1, + session_id => $session_id, + ); return { %$payment_info, %$customer_info, @@ -1325,5 +1328,3 @@ sub include { ); } - - -- cgit v1.2.1 From f04d95852fef6dfaf1813ceacd94f68a528796cd Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 11 Jun 2018 00:04:00 -0500 Subject: RT# 32233 Mask ssn and stateid in selfservice --- FS/FS/ClientAPI/MyAccount.pm | 4 +--- fs_selfservice/FS-SelfService/cgi/selfservice.cgi | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index ae0fa614c..a30dde568 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -630,9 +630,7 @@ sub customer_info_short { for (@cust_main_editable_fields) { $return{$_} = $cust_main->get($_); } - - $return{ss} = $cust_main->masked('ss') if $p->{mask_ss}; - $return{stateid} = $cust_main->masked('stateid') if $p->{mask_stateid}; + $return{$_} = $cust_main->masked($_) for qw/ss stateid/; #maybe a little more expensive, but it should be cached by now for (@location_editable_fields) { diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi index 161231555..6cf264c08 100755 --- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi +++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi @@ -276,11 +276,8 @@ sub change_bill { } return $payment_info if ( $payment_info->{'error'} ); - my $customer_info = customer_info( - mask_ss => 1, - mask_stateid => 1, - session_id => $session_id, - ); + my $customer_info = + customer_info( 'session_id' => $session_id ); return { %$payment_info, %$customer_info, @@ -1328,3 +1325,5 @@ sub include { ); } + + -- cgit v1.2.1 From 8b8d621a1bb4e9abaa1f0edab5190d302e6a6e99 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 11 Jun 2018 15:06:50 -0500 Subject: RT# 80514 Selfservice can update ssn/stateid payment info --- FS/FS/ClientAPI/MyAccount.pm | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index a30dde568..f82787482 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -1727,12 +1727,18 @@ sub update_payby { $p->{'payinfo'} = $payinfo1. '@'. $payinfo2; } + # Perform update within a transaction + local $FS::UID::AutoCommit = 0; + my $cust_payby = qsearchs('cust_payby', { 'custnum' => $custnum, 'custpaybynum' => $p->{'custpaybynum'}, }) or return { 'error' => 'unknown custpaybynum '. $p->{'custpaybynum'} }; + my $cust_main = qsearchs( 'cust_main', {custnum => $cust_payby->custnum} ) + or return { 'error' => 'unknown custnum '.$cust_payby->custnum }; + foreach my $field ( qw( weight payby payinfo paycvv paydate payname paystate paytype payip ) ) { @@ -1741,9 +1747,22 @@ sub update_payby { } my $error = $cust_payby->replace; + + if (!$error) { + my $is_changed = 0; + for my $field ( qw/ss stateid/ ) { + next if !exists $p->{$field} || $p->{$field} =~ /^x/i; + $cust_main->set( $field, $p->{$field} ); + $is_changed = 1; + } + $error = $cust_main->replace if $is_changed; + } + if ( $error ) { + dbh->rollback; return { 'error' => $error }; } else { + dbh->commit; return { 'custpaybynum' => $cust_payby->custpaybynum }; } -- cgit v1.2.1 From ed87c04490375c6663843f8065e69a7690939b18 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 11 Jun 2018 18:37:13 -0500 Subject: RT# 80514 Selfservice can update ssn/stateid payment info --- FS/FS/ClientAPI/MyAccount.pm | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index f82787482..60a70fddc 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -1727,9 +1727,6 @@ sub update_payby { $p->{'payinfo'} = $payinfo1. '@'. $payinfo2; } - # Perform update within a transaction - local $FS::UID::AutoCommit = 0; - my $cust_payby = qsearchs('cust_payby', { 'custnum' => $custnum, 'custpaybynum' => $p->{'custpaybynum'}, @@ -1746,26 +1743,23 @@ sub update_payby { $cust_payby->set($field,$p->{$field}); } - my $error = $cust_payby->replace; + # Update column if given a value, and the given value wasn't + # the value generated by $cust_main->masked($column); + $cust_main->set( $_, $p->{$_} ) + for grep{ $p->{$_} !~ /^x/i; } + grep{ exists $p->{$_} } + qw/ss stateid/; - if (!$error) { - my $is_changed = 0; - for my $field ( qw/ss stateid/ ) { - next if !exists $p->{$field} || $p->{$field} =~ /^x/i; - $cust_main->set( $field, $p->{$field} ); - $is_changed = 1; - } - $error = $cust_main->replace if $is_changed; - } + # Perform updates within a transaction + local $FS::UID::AutoCommit = 0; - if ( $error ) { + if ( my $error = $cust_payby->replace || $cust_main->replace ) { dbh->rollback; - return { 'error' => $error }; - } else { - dbh->commit; - return { 'custpaybynum' => $cust_payby->custpaybynum }; + return { error => $error }; } - + + dbh->commit; + return { custpaybynum => $cust_payby->custpaybynum }; } sub verify_payby { -- cgit v1.2.1 From f079061ec1005edbb9d292377bf4eb1a769fa681 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 11 Jun 2018 20:31:59 -0500 Subject: RT# 80513 Selfservice not reflecting change to payinfo --- FS/FS/ClientAPI/MyAccount.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 60a70fddc..263b3116b 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -1742,6 +1742,7 @@ sub update_payby { next unless exists($p->{$field}); $cust_payby->set($field,$p->{$field}); } + $cust_payby->set( 'paymask' => $cust_payby->mask_payinfo ); # Update column if given a value, and the given value wasn't # the value generated by $cust_main->masked($column); -- cgit v1.2.1 From 360f89789c45e1fd7cb84b1442d2f0c8353066d9 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 12 Jun 2018 09:51:58 -0400 Subject: RT# 74435 - fixed errors in posting a echeck refund when no account is listed. --- FS/FS/cust_main/Billing_Batch.pm | 4 +- httemplate/edit/cust_refund.cgi | 39 ++++- httemplate/edit/process/cust_refund.cgi | 8 +- httemplate/elements/cust_payby_new.html | 222 ++++++++++++++++++++++++++ httemplate/elements/tr-select-cust_payby.html | 2 +- httemplate/misc/payment.cgi | 183 +-------------------- httemplate/misc/process/payment.cgi | 7 +- 7 files changed, 277 insertions(+), 188 deletions(-) create mode 100644 httemplate/elements/cust_payby_new.html diff --git a/FS/FS/cust_main/Billing_Batch.pm b/FS/FS/cust_main/Billing_Batch.pm index 38d100ef6..35e2714b5 100644 --- a/FS/FS/cust_main/Billing_Batch.pm +++ b/FS/FS/cust_main/Billing_Batch.pm @@ -114,7 +114,7 @@ sub batch_card { } ); foreach (qw( address1 address2 city state zip country latitude longitude - payby payinfo paydate payname paycode )) + payby payinfo paydate payname paycode paytype )) { $options{$_} = '' unless exists($options{$_}); } @@ -142,7 +142,7 @@ sub batch_card { 'payname' => $options{payname} || $cust_payby->payname, 'paytype' => $options{paytype} || $cust_payby->paytype, 'amount' => $amount, # consolidating - 'paycode' => $options{paycode} || $cust_payby->paycode, + 'paycode' => $options{paycode} || '', } ); $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum) diff --git a/httemplate/edit/cust_refund.cgi b/httemplate/edit/cust_refund.cgi index e1975ed70..27c4b1937 100755 --- a/httemplate/edit/cust_refund.cgi +++ b/httemplate/edit/cust_refund.cgi @@ -34,7 +34,7 @@ % }
Payment - <% ntable("#cccccc", 2) %> + @@ -85,7 +85,8 @@
Refund -<% ntable("#cccccc", 2) %> + +
Amount$<% $cust_pay->paid %>
@@ -102,9 +103,23 @@ +
DateCheck #
% } % elsif ($payby eq 'CHEK') { % + % my @cust_payby = (); % if ( $payby eq 'CARD' ) { % @cust_payby = $cust_main->cust_payby('CARD','DCRD'); @@ -123,10 +138,30 @@ 'onchange' => 'cust_payby_changed(this)', &> + +

+

+> + + +<& /elements/cust_payby_new.html, + 'cust_payby' => \@cust_payby, + 'curr_value' => $custpaybynum, +&> + +
+
+ % } else { + % } +

+ <& /elements/tr-select-reason.html, 'field' => 'reasonnum', 'reason_class' => 'F', diff --git a/httemplate/edit/process/cust_refund.cgi b/httemplate/edit/process/cust_refund.cgi index 0a3d55036..77da8d5d2 100755 --- a/httemplate/edit/process/cust_refund.cgi +++ b/httemplate/edit/process/cust_refund.cgi @@ -71,6 +71,7 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) { $paycvv = $cust_payby->paycvv; # pass it if we got it, running a transaction will clear it ( $month, $year ) = $cust_payby->paydate_mon_year; $payname = $cust_payby->payname; + $cgi->param(-name=>"paytype", -value=>$cust_payby->paytype) unless $cgi->param("paytype"); } else { @@ -192,8 +193,9 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) { my $refund = "$1$2"; $cgi->param('paynum') =~ /^(\d*)$/ or die "Illegal paynum!"; my $paynum = $1; - my $paydate = $cgi->param('exp_year'). '-'. $cgi->param('exp_month'). '-01'; - $options{'paydate'} = $paydate if $paydate =~ /^\d{2,4}-\d{1,2}-01$/; + my $paydate; + if ($cust_payby->paydate) { $paydate = "$year-$month-01"; } + else { $paydate = "2037-12-01"; } if ( $cgi->param('batch') ) { @@ -201,7 +203,7 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) { 'payby' => $payby, 'amount' => $refund, 'payinfo' => $payinfo, - 'paydate' => "$year-$month-01", + 'paydate' => $paydate, 'payname' => $payname, 'paycode' => 'C', map { $_ => scalar($cgi->param($_)) } diff --git a/httemplate/elements/cust_payby_new.html b/httemplate/elements/cust_payby_new.html new file mode 100644 index 000000000..7ed049686 --- /dev/null +++ b/httemplate/elements/cust_payby_new.html @@ -0,0 +1,222 @@ +% my $auto = 0; +% if ( $payby eq 'CARD' ) { +% +% my( $payinfo, $paycvv, $month, $year ) = ( '', '', '', '' ); +% my $payname = $cust_main->first. ' '. $cust_main->getfield('last'); +% my $location = $cust_main->bill_location; + + + + + + + + + + + + + + + <& /elements/location.html, + 'object' => $location, + 'no_asterisks' => 1, + 'address1_label' => emt('Card billing address'), + &> + +% } elsif ( $payby eq 'CHEK' ) { +% +% my( $account, $aba, $branch, $payname, $ss, $paytype, $paystate, +% $stateid, $stateid_state ) +% = ( '', '', '', '', '', '', '', '', '' ); +% +% #false laziness w/{edit,view}/cust_main/billing.html +% my $routing_label = $conf->config('echeck-country') eq 'US' +% ? 'ABA/Routing number' +% : 'Routing number'; +% my $routing_size = $conf->config('echeck-country') eq 'CA' ? 4 : 10; +% my $routing_maxlength = $conf->config('echeck-country') eq 'CA' ? 3 : 9; + + + + + + + + + + + + + +% if ( $conf->config('echeck-country') eq 'CA' ) { + + + + +% } + + + + + +% if ( $conf->exists('show_bankstate') ) { + + + + +% } else { + +% } + +% if ( $conf->exists('show_ss') ) { + + + + +% } else { + +% } + +% if ( $conf->exists('show_stateid') ) { + + + + + + +% } else { + + +% } + +% } #end CARD/CHEK-specific section + + + + + + + + + + +<%once> + +my %weight = ( + 1 => 'Primary', + 2 => 'Secondary', + 3 => 'Tertiary', + 4 => 'Fourth', + 5 => 'Fifth', + 6 => 'Sixth', + 7 => 'Seventh', +); + + + +<%init> + +my %opt = @_; + +my @cust_payby = @{$opt{cust_payby}}; + +my %type = ( 'CARD' => 'credit card', + 'CHEK' => 'electronic check (ACH)', + ); + +$cgi->param('payby') =~ /^(CARD|CHEK)$/ + or die "unknown payby ". $cgi->param('payby'); +my $payby = $1; + +$cgi->param('custnum') =~ /^(\d+)$/ + or die "illegal custnum ". $cgi->param('custnum'); +my $custnum = $1; + +my $cust_main = qsearchs( 'cust_main', { 'custnum'=>$custnum } ); +die "unknown custnum $custnum" unless $cust_main; + +my $balance = $cust_main->balance; + +my $payinfo = ''; + +my $conf = new FS::Conf; + +#false laziness w/selfservice make_payment.html shortcut for one-country +my %states = map { $_->state => 1 } + qsearch('cust_main_county', { + 'country' => $conf->config('countrydefault') || 'US' + } ); +my @states = sort { $a cmp $b } keys %states; + + \ No newline at end of file diff --git a/httemplate/elements/tr-select-cust_payby.html b/httemplate/elements/tr-select-cust_payby.html index e2b2e09d1..e5ace4d39 100644 --- a/httemplate/elements/tr-select-cust_payby.html +++ b/httemplate/elements/tr-select-cust_payby.html @@ -1,4 +1,4 @@ -% if ( scalar(@{ $opt{'cust_payby'} }) == 0 ) { +% if ( scalar(@{ $opt{'cust_payby'} }) == 0 ) { diff --git a/httemplate/misc/payment.cgi b/httemplate/misc/payment.cgi index 4f6f7ef75..80cb15d79 100644 --- a/httemplate/misc/payment.cgi +++ b/httemplate/misc/payment.cgi @@ -135,178 +135,10 @@ function change_batch_checkbox () { >
<% mt('Card number') |h %> + + + + + + + + +
+ <% mt('Exp.') |h %> + + / + +
+
<% mt('CVV2') |h %> + (<% mt('help') |h %>) +
<% mt('Exact name on card') |h %>
<% mt('Account number') |h %><% mt('Type') |h %>
<% mt($routing_label) |h %> + + (<% mt('help') |h %>) +
<% mt('Branch number') |h %> + +
<% mt('Bank name') |h %>
<% mt('Bank state') |h %><& /elements/select-state.html, + 'disable_empty' => 0, + 'empty_label' => emt('(choose)'), + 'state' => $paystate, + 'country' => $cust_main->country, + 'prefix' => 'pay', + &> +
+ <% mt('Account holder') |h %>
+ <% mt('Social security or tax ID #') |h %> +
+ <% mt('Account holder') |h %>
+ <% mt("Driver's license or state ID #") |h %> +
<% mt('State') |h %><& /elements/select-state.html, + 'disable_empty' => 0, + 'empty_label' => emt('(choose)'), + 'state' => $stateid_state, + 'country' => $cust_main->country, + 'prefix' => 'stateid_', + &> +
+ + <% mt('Remember this information') |h %> +
+ NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }"> + <% mt("Charge future payments to this [_1] automatically",$type{$payby}) |h %> +% if ( @cust_payby ) { + <% mt('as') |h %> + +% } else { + +% } +
-% my $auto = 0; -% if ( $payby eq 'CARD' ) { -% -% my( $payinfo, $paycvv, $month, $year ) = ( '', '', '', '' ); -% my $payname = $cust_main->first. ' '. $cust_main->getfield('last'); -% my $location = $cust_main->bill_location; - - - - - - - - - - - - - - - <& /elements/location.html, - 'object' => $location, - 'no_asterisks' => 1, - 'address1_label' => emt('Card billing address'), - &> - -% } elsif ( $payby eq 'CHEK' ) { -% -% my( $account, $aba, $branch, $payname, $ss, $paytype, $paystate, -% $stateid, $stateid_state ) -% = ( '', '', '', '', '', '', '', '', '' ); -% -% #false laziness w/{edit,view}/cust_main/billing.html -% my $routing_label = $conf->config('echeck-country') eq 'US' -% ? 'ABA/Routing number' -% : 'Routing number'; -% my $routing_size = $conf->config('echeck-country') eq 'CA' ? 4 : 10; -% my $routing_maxlength = $conf->config('echeck-country') eq 'CA' ? 3 : 9; - - - - - - - - - - - - - -% if ( $conf->config('echeck-country') eq 'CA' ) { - - - - -% } - - - - - -% if ( $conf->exists('show_bankstate') ) { - - - - -% } else { - -% } - -% if ( $conf->exists('show_ss') ) { - - - - -% } else { - -% } - -% if ( $conf->exists('show_stateid') ) { - - - - - - -% } else { - - -% } - -% } #end CARD/CHEK-specific section - - - - - - - - - +<& /elements/cust_payby_new.html, + 'cust_payby' => \@cust_payby, + 'curr_value' => $custpaybynum, +&>
<% mt('Card number') |h %> - - - - - - - - -
- <% mt('Exp.') |h %> - - / - -
-
<% mt('CVV2') |h %> - (<% mt('help') |h %>) -
<% mt('Exact name on card') |h %>
<% mt('Account number') |h %><% mt('Type') |h %>
<% mt($routing_label) |h %> - - (<% mt('help') |h %>) -
<% mt('Branch number') |h %> - -
<% mt('Bank name') |h %>
<% mt('Bank state') |h %><& /elements/select-state.html, - 'disable_empty' => 0, - 'empty_label' => emt('(choose)'), - 'state' => $paystate, - 'country' => $cust_main->country, - 'prefix' => 'pay', - &> -
- <% mt('Account holder') |h %>
- <% mt('Social security or tax ID #') |h %> -
- <% mt('Account holder') |h %>
- <% mt("Driver's license or state ID #") |h %> -
<% mt('State') |h %><& /elements/select-state.html, - 'disable_empty' => 0, - 'empty_label' => emt('(choose)'), - 'state' => $stateid_state, - 'country' => $cust_main->country, - 'prefix' => 'stateid_', - &> -
- - <% mt('Remember this information') |h %> -
- NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }"> - <% mt("Charge future payments to this [_1] automatically",$type{$payby}) |h %> -% if ( @cust_payby ) { - <% mt('as') |h %> - -% } else { - -% } -
@@ -355,13 +187,6 @@ my $payinfo = ''; my $conf = new FS::Conf; -#false laziness w/selfservice make_payment.html shortcut for one-country -my %states = map { $_->state => 1 } - qsearch('cust_main_county', { - 'country' => $conf->config('countrydefault') || 'US' - } ); -my @states = sort { $a cmp $b } keys %states; - my $payunique = "webui-payment-". time. "-$$-". rand() * 2**32; diff --git a/httemplate/misc/process/payment.cgi b/httemplate/misc/process/payment.cgi index 717d57c85..5620b5b4b 100644 --- a/httemplate/misc/process/payment.cgi +++ b/httemplate/misc/process/payment.cgi @@ -90,6 +90,7 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) { $paycvv = $cust_payby->paycvv; # pass it if we got it, running a transaction will clear it ( $month, $year ) = $cust_payby->paydate_mon_year; $payname = $cust_payby->payname; + $cgi->param(-name=>"paytype", -value=>$cust_payby->paytype) unless $cgi->param("paytype"); } else { @@ -208,6 +209,10 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) { my $error = ''; my $paynum = ''; +my $paydate; +if ($cust_payby->paydate) { $paydate = "$year-$month-01"; } +else { $paydate = "2037-12-01"; } + if ( $cgi->param('batch') ) { $error = 'Prepayment discounts not supported with batched payments' @@ -217,7 +222,7 @@ if ( $cgi->param('batch') ) { 'payby' => $payby, 'amount' => $amount, 'payinfo' => $payinfo, - 'paydate' => "$year-$month-01", + 'paydate' => $paydate, 'payname' => $payname, map { $_ => scalar($cgi->param($_)) } @{$payby2fields{$payby}} -- cgit v1.2.1 From 381992561d7b1a88e05d49d3e474a2fad25873c7 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Wed, 13 Jun 2018 02:25:09 -0500 Subject: RT# 32234 Allow unmask of SSN/DL# --- FS/FS/AccessRight.pm | 3 +- httemplate/edit/cust_main/name.html | 19 ++++++++- httemplate/edit/cust_main/stateid.html | 7 +++- httemplate/elements/link-replace_element_text.html | 45 ++++++++++++++++++++++ httemplate/view/cust_main/contacts.html | 30 +++++++++++++-- 5 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 httemplate/elements/link-replace_element_text.html diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 471e32aff..1b581b247 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -156,6 +156,8 @@ tie my %rights, 'Tie::IxHash', 'View package definition costs', #NEWNEW 'Change package start date', 'Change package contract end date', + 'Unmask customer DL', + 'Unmask customer SSN', ], ### @@ -509,4 +511,3 @@ L, L, L =cut 1; - diff --git a/httemplate/edit/cust_main/name.html b/httemplate/edit/cust_main/name.html index 713f54cdb..c1078d46a 100644 --- a/httemplate/edit/cust_main/name.html +++ b/httemplate/edit/cust_main/name.html @@ -1,7 +1,17 @@ <%def .namepart> -% my ($field, $value, $label, $extra) = @_; +% my ($field, $value, $label, $extra, $unmask_field) = @_;

> +% if ( +% ref $unmask_field +% && !$unmask_field->{unmask_ss} +% && $FS::CurrentUser::CurrentUser->access_right( $unmask_field->{access_right} ) +% ) { + <& /elements/link-replace_element_text.html, { + target_id => $unmask_field->{target_id}, + replace_text => $unmask_field->{replace_text}, + } &> +% }
<% emt($label) %>
@@ -13,7 +23,12 @@ <& .namepart, 'first', $cust_main->first, 'First' &> % if ( $conf->exists('show_ss') ) {   - <& .namepart, 'ss', $ss, 'SS#', "SIZE=11" &> + <& .namepart, 'ss', $ss, 'SS#', "SIZE=11 ID='ss'", { + target_id => 'ss', + replace_text => $cust_main->ss, + access_right => 'Unmask customer SSN', + unmask_ss => $conf->exists('unmask_ss'), + } &> % } else { % } diff --git a/httemplate/edit/cust_main/stateid.html b/httemplate/edit/cust_main/stateid.html index 3500d631c..cc0890fe1 100644 --- a/httemplate/edit/cust_main/stateid.html +++ b/httemplate/edit/cust_main/stateid.html @@ -1,7 +1,12 @@ % if ( $conf->exists('show_stateid') ) { <% $stateid_label %> - + + +% if ( $FS::CurrentUser::CurrentUser->access_right( 'Unmask customer DL' )) { + <& /elements/link-replace_element_text.html, {target_id => 'stateid', replace_text => $cust_main->stateid} &> +% } + <& /elements/select-state.html, state => $cust_main->stateid_state, country => $cust_main->country, # how does this work on new customer? diff --git a/httemplate/elements/link-replace_element_text.html b/httemplate/elements/link-replace_element_text.html new file mode 100644 index 000000000..8e611954c --- /dev/null +++ b/httemplate/elements/link-replace_element_text.html @@ -0,0 +1,45 @@ +<%doc> + +Display a link with javascript to replace text within a element. + +Usage: + +<& /elements/link-replace_element_text.html, { + target_id => 'input_id', + replace_text => 'hello', + + element_type => 'input', # Uses jquery val() method to replace text + element_type => 'div', # Uses jquery text() method to replace text + + href => ... + style => ... + class => ... + } +&> + + + +<%init> + +die "template call requires a parameter hashref" unless ref $_[0]; + +# Defaults that can be overridden in param hashref +my %param = ( + target_id => 'SPECIFY_AN_INPUT_ELEMENT_ID', + replace_text => 'REPLACEMENT_TEXT_FOR_INPUT_ELEMENT', + element_type => 'input', + + link_text => '%#x25C1;', # ◁ + href => 'javascript:void(0)', + style => 'text-decoration:none;', + class => undef, + + %{ $_[0] }, +); +$param{jmethod} = $param{element_type} eq 'input' ? 'val' : 'text'; + diff --git a/httemplate/view/cust_main/contacts.html b/httemplate/view/cust_main/contacts.html index 1660c1c22..367659293 100644 --- a/httemplate/view/cust_main/contacts.html +++ b/httemplate/view/cust_main/contacts.html @@ -29,9 +29,20 @@ <% $cust_main->contact |h %> % if ( $conf->exists('show_ss') ) { <% mt('SS#') |h %> - <% $conf->exists('unmask_ss') - ? $cust_main->ss - : $cust_main->masked('ss') || ' ' %> + + + <% $conf->exists('unmask_ss') + ? $cust_main->ss + : $cust_main->masked('ss') || ' ' %> +% if ( !$conf->exists('unmask_ss') && $FS::CurrentUser::CurrentUser->access_right('Unmask customer SSN')) { + <& /elements/link-replace_element_text.html, { + target_id => 'ss_span', + replace_text => $cust_main->ss, + element_type => 'span' + } &> +% } + + % } % if ( $conf->exists('cust_main-enable_spouse') and @@ -172,7 +183,18 @@ <% $stateid_label %> - <% $cust_main->masked('stateid') || ' ' %> + + + <% $cust_main->masked('stateid') || ' ' %> +% if ( $FS::CurrentUser::CurrentUser->access_right('Unmask customer DL')) { + <& /elements/link-replace_element_text.html, { + target_id => 'stateid_span', + replace_text => $cust_main->stateid, + element_type => 'span' + } &> +% } + + <% $stateid_state_label %> <% $cust_main->stateid_state || ' ' %> -- cgit v1.2.1 From 965826f80175e6e09b851bdd119e955b6c3783d4 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Thu, 14 Jun 2018 14:46:38 -0500 Subject: RT# 80543 Crash creating new quotation --- FS/FS/Template_Mixin.pm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index b9514fee1..578e5a192 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -3139,7 +3139,9 @@ sub _items_fee { my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg; my $escape_function = $options{escape_function}; - my $locale = $self->cust_main->locale; + my $locale = $self->quotationnum + ? $self->prospect_main->locale + : $self->cust_main->locale; my @items; foreach my $cust_bill_pkg (@cust_bill_pkg) { -- cgit v1.2.1 From b4883ac11a0ddabd2b78a7451fe3634f9510f5f7 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Thu, 14 Jun 2018 16:31:42 -0500 Subject: RT# 31964 Display more information on prospect contacts --- httemplate/search/prospect_main.html | 1 - httemplate/view/prospect_main.html | 17 +++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/httemplate/search/prospect_main.html b/httemplate/search/prospect_main.html index d65d4d19d..0eb45f338 100644 --- a/httemplate/search/prospect_main.html +++ b/httemplate/search/prospect_main.html @@ -17,7 +17,6 @@ } $pm->prospect_contact ]; - '' }, sub { my $pr = shift->part_referral; diff --git a/httemplate/view/prospect_main.html b/httemplate/view/prospect_main.html index f4dd4146f..504a5a8ec 100644 --- a/httemplate/view/prospect_main.html +++ b/httemplate/view/prospect_main.html @@ -24,8 +24,21 @@ % foreach my $prospect_contact ( $prospect_main->prospect_contact ) { % my $contact = $prospect_contact->contact; - <% $prospect_contact->contact_classname %> Contact - <% $contact->line %> + <% $prospect_contact->contact_classname %> Contact + + <% $contact->line %>
+ +% for my $row ( $contact->contact_email ) { + +% } +% for my $row ( $contact->contact_phone ) { + +% } +% if ( $prospect_contact->comment ) { + +% } +
E-Mail:<% $row->emailaddress %>
<% $row->phone_type->typename %>:<% $row->phonenum_pretty %>
Comment:<% $prospect_contact->comment %>
+ %} -- cgit v1.2.1 From 618f8725c360f29941b9d1720eb01fea85403185 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Fri, 15 Jun 2018 16:52:39 -0400 Subject: RT# 80175 - fixed error with ACH gateway not being selected. --- FS/FS/agent.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm index 810709357..e1d9ccf1d 100644 --- a/FS/FS/agent.pm +++ b/FS/FS/agent.pm @@ -294,7 +294,7 @@ sub payment_gateway { } } - my $cardtype_search = "AND cardtype != 'ACH'"; + my $cardtype_search = "AND ( cardtype IS NULL OR cardtype <> 'ACH')"; $cardtype_search = "AND cardtype = 'ACH'" if $options{method} eq 'ECHECK'; my $override = -- cgit v1.2.1 From 7bf6dafb0da4a525388a6f145dd0a904eddcec14 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Fri, 15 Jun 2018 21:14:44 -0500 Subject: RT# 32233 Show unmask widget only if a value exists to unmask --- httemplate/edit/cust_main/name.html | 3 ++- httemplate/edit/cust_main/stateid.html | 2 +- httemplate/view/cust_main/contacts.html | 11 +++++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/httemplate/edit/cust_main/name.html b/httemplate/edit/cust_main/name.html index c1078d46a..120475b92 100644 --- a/httemplate/edit/cust_main/name.html +++ b/httemplate/edit/cust_main/name.html @@ -3,7 +3,8 @@
> % if ( -% ref $unmask_field +% $value +% && ref $unmask_field % && !$unmask_field->{unmask_ss} % && $FS::CurrentUser::CurrentUser->access_right( $unmask_field->{access_right} ) % ) { diff --git a/httemplate/edit/cust_main/stateid.html b/httemplate/edit/cust_main/stateid.html index cc0890fe1..0f288099b 100644 --- a/httemplate/edit/cust_main/stateid.html +++ b/httemplate/edit/cust_main/stateid.html @@ -3,7 +3,7 @@ <% $stateid_label %> -% if ( $FS::CurrentUser::CurrentUser->access_right( 'Unmask customer DL' )) { +% if ( $stateid && $FS::CurrentUser::CurrentUser->access_right( 'Unmask customer DL' )) { <& /elements/link-replace_element_text.html, {target_id => 'stateid', replace_text => $cust_main->stateid} &> % } diff --git a/httemplate/view/cust_main/contacts.html b/httemplate/view/cust_main/contacts.html index 367659293..11efcd568 100644 --- a/httemplate/view/cust_main/contacts.html +++ b/httemplate/view/cust_main/contacts.html @@ -34,7 +34,11 @@ <% $conf->exists('unmask_ss') ? $cust_main->ss : $cust_main->masked('ss') || ' ' %> -% if ( !$conf->exists('unmask_ss') && $FS::CurrentUser::CurrentUser->access_right('Unmask customer SSN')) { +% if ( +% $cust_main->ss +% && !$conf->exists('unmask_ss') +% && $FS::CurrentUser::CurrentUser->access_right('Unmask customer SSN') +% ) { <& /elements/link-replace_element_text.html, { target_id => 'ss_span', replace_text => $cust_main->ss, @@ -186,7 +190,10 @@ <% $cust_main->masked('stateid') || ' ' %> -% if ( $FS::CurrentUser::CurrentUser->access_right('Unmask customer DL')) { +% if ( +% $cust_main->stateid +% && $FS::CurrentUser::CurrentUser->access_right('Unmask customer DL') +% ) { <& /elements/link-replace_element_text.html, { target_id => 'stateid_span', replace_text => $cust_main->stateid, -- cgit v1.2.1 From 7b52b31205b01810da69c7e64cf8ad3806988757 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Mon, 18 Jun 2018 10:16:37 -0400 Subject: RT# 80175 - changed gateway selection to select either ACH or NULL for echeck payments --- FS/FS/agent.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm index e1d9ccf1d..8aff96a8d 100644 --- a/FS/FS/agent.pm +++ b/FS/FS/agent.pm @@ -295,7 +295,7 @@ sub payment_gateway { } my $cardtype_search = "AND ( cardtype IS NULL OR cardtype <> 'ACH')"; - $cardtype_search = "AND cardtype = 'ACH'" if $options{method} eq 'ECHECK'; + $cardtype_search = "AND ( cardtype IS NULL OR cardtype = 'ACH' )" if $options{method} eq 'ECHECK'; my $override = qsearchs({ -- cgit v1.2.1 From 7f751940088ed6dabcf6cbcd67993313296b21bc Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 19 Jun 2018 13:08:59 -0400 Subject: RT# 34134 - readded config option and move to deprecated section --- FS/FS/Conf.pm | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 5c6c411b3..35932b8bd 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2787,6 +2787,13 @@ and customer address. Include units.', 'type' => 'checkbox', }, + { + 'key' => 'manual_process-single_invoice_amount', + 'section' => 'deprecated', + 'description' => 'When entering manual credit card and ACH payments, amount will not autofill if the customer has more than one open invoice', + 'type' => 'checkbox', + }, + { 'key' => 'manual_process-pkgpart', 'section' => 'payments', -- cgit v1.2.1 From 5506c3aea73686da65f7caf2acbdca715cc6c1a5 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 25 Jun 2018 14:07:52 -0500 Subject: RT# 30783 Add network block enumerating utils --- FS/FS/IP_Mixin.pm | 28 ++++++++++++++++++--- FS/FS/addr_block.pm | 18 +++++++++++++- FS/FS/svc_IP_Mixin.pm | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/FS/FS/IP_Mixin.pm b/FS/FS/IP_Mixin.pm index 3ec769313..2d69d9cc1 100644 --- a/FS/FS/IP_Mixin.pm +++ b/FS/FS/IP_Mixin.pm @@ -266,9 +266,10 @@ sub router { =item used_addresses [ BLOCK ] -Returns a list of all addresses (in BLOCK, or in all blocks) -that are in use. If called as an instance method, excludes -that instance from the search. +Returns a list of all addresses that are in use by a service. If called as an +instance method, excludes that instance from the search. + +Does not filter by block, will return ALL used addresses. ref:f197bdbaa1 =cut @@ -283,6 +284,27 @@ sub _used_addresses { die "$class->_used_addresses not implemented"; } +=item used_addresses_in_block [ FS::addr_block ] + +Returns a list of all addresses in use within the given L + +=cut + +sub used_addresses_in_block { + my ($self, $block) = @_; + + ( + $block->ip_gateway ? $block->ip_gateway : (), + $block->NetAddr->broadcast->addr, + map { $_->_used_addresses_in_block($block, $self ) } @subclasses + ); +} + +sub _used_addresses_in_block { + my $class = shift; + die "$class->_used_addresses_in_block not implemented"; +} + =item is_used ADDRESS Returns a string describing what object is using ADDRESS, or diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm index ba0f61db1..fa0e42f62 100755 --- a/FS/FS/addr_block.pm +++ b/FS/FS/addr_block.pm @@ -207,6 +207,23 @@ sub cidr { $self->NetAddr->cidr; } +=item free_addrs + +Returns a sorted list of free addresses in the block. + +=cut + +sub free_addrs { + my $self = shift; + + my %used_addr_map = + map {$_ => 1} + FS::IP_Mixin->used_addresses_in_block($self), + FS::Conf->new()->config('exclude_ip_addr'); + + grep { !exists $used_addr_map{$_} } map { $_->addr } $self->NetAddr->hostenum; +} + =item next_free_addr Returns a NetAddr::IP object corresponding to the first unassigned address @@ -416,4 +433,3 @@ now because that's the smallest block that makes any sense at all. =cut 1; - diff --git a/FS/FS/svc_IP_Mixin.pm b/FS/FS/svc_IP_Mixin.pm index c89245fe2..4266a2a5e 100644 --- a/FS/FS/svc_IP_Mixin.pm +++ b/FS/FS/svc_IP_Mixin.pm @@ -3,7 +3,8 @@ use base 'FS::IP_Mixin'; use strict; use NEXT; -use FS::Record qw(qsearchs qsearch); +use Carp qw(croak carp); +use FS::Record qw(qsearchs qsearch dbh); use FS::Conf; use FS::router; use FS::part_svc_router; @@ -90,6 +91,9 @@ sub svc_ip_check { } sub _used_addresses { + + # Returns all addresses in use. Does not filter with $block. ref:f197bdbaa1 + my ($class, $block, $exclude) = @_; my $ip_field = $class->table_info->{'ip_field'} or return (); @@ -107,6 +111,69 @@ sub _used_addresses { }); } +sub _used_addresses_in_block { + my ($class, $block) = @_; + + croak "_used_addresses_in_block() requires an FS::addr_block parameter" + unless ref $block && $block->isa('FS::addr_block'); + + my $ip_field = $class->table_info->{'ip_field'}; + if ( !$ip_field ) { + carp "_used_addresses_in_block() skipped, no ip_field"; + return; + } + + my $block_na = $block->NetAddr; + + my $octets; + if ($block->ip_netmask >= 24) { + $octets = 3; + } elsif ($block->ip_netmask >= 16) { + $octets = 2; + } elsif ($block->ip_netmask >= 8) { + $octets = 1; + } + + # e.g. + # SELECT ip_addr + # FROM svc_broadband + # WHERE ip_addr != '' + # AND ip_addr != '0e0' + # AND ip_addr LIKE '10.0.2.%'; + # + # For /24, /16 and /8 this approach is fast, even when svc_broadband table + # contains 650,000+ ip records. For other allocations, this approach is + # not speedy, but usable. + # + # Note: A use case like this would could greatly benefit from a qsearch() + # parameter to bypass FS::Record objects creation and just + # return hashrefs from DBI. 200,000 hashrefs are many seconds faster + # than 200,000 FS::Record objects + my %qsearch = ( + table => $class->table, + select => $ip_field, + hashref => { $ip_field => { op => '!=', value => '' }}, + extra_sql => " AND $ip_field != '0e0' ", + ); + if ( $octets ) { + my $block_str = join('.', (split(/\D/, $block_na->first))[0..$octets-1]); + $qsearch{extra_sql} .= " AND $ip_field LIKE ".dbh->quote("${block_str}.%"); + } + + if ( $block->ip_netmask % 8 ) { + # Some addresses returned by qsearch may be outside the network block, + # so each ip address is tested to be in the block before it's returned. + return + grep { $block_na->contains( NetAddr::IP->new( $_ ) ) } + map { $_->$ip_field } + qsearch( \%qsearch ); + } + + return + map { $_->$ip_field } + qsearch( \%qsearch ); +} + sub _is_used { my ($class, $addr, $exclude) = @_; my $ip_field = $class->table_info->{'ip_field'} -- cgit v1.2.1 From b79b0ebca0c15ad527de3d589cda36da63b0601e Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 25 Jun 2018 14:09:42 -0500 Subject: RT# 30783 Improve speed of ip address auto-assignment --- FS/FS/addr_block.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm index fa0e42f62..eb84dafc3 100755 --- a/FS/FS/addr_block.pm +++ b/FS/FS/addr_block.pm @@ -250,7 +250,7 @@ sub next_free_addr { $selfaddr->addr, $selfaddr->network->addr, $selfaddr->broadcast->addr, - FS::IP_Mixin->used_addresses($self) + FS::IP_Mixin->used_addresses_in_block($self) ); # just do a linear search of the block -- cgit v1.2.1 From 7b56ddf363cbd15acab7e7098a7769e7a33640a3 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 26 Jun 2018 09:51:37 -0400 Subject: RT# 80175 - restored ability to set override to ACH only. --- httemplate/edit/agent_payment_gateway.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/httemplate/edit/agent_payment_gateway.html b/httemplate/edit/agent_payment_gateway.html index 6d15164ac..38411f12e 100644 --- a/httemplate/edit/agent_payment_gateway.html +++ b/httemplate/edit/agent_payment_gateway.html @@ -18,9 +18,12 @@ Use gateway -

+
+ + for ACH only. +
+
-- cgit v1.2.1 From 211865a049969b1b539988cc8558ee35e8cb5945 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Tue, 26 Jun 2018 17:34:50 -0500 Subject: RT# 30783 Selectbox of available IPs when provisioning --- FS/FS/addr_block.pm | 8 +- httemplate/elements/tr-select-router_block_ip.html | 128 +++++++++++++++++---- httemplate/json/free_addresses_in_block.json.html | 18 +++ 3 files changed, 129 insertions(+), 25 deletions(-) create mode 100644 httemplate/json/free_addresses_in_block.json.html diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm index eb84dafc3..74c372e29 100755 --- a/FS/FS/addr_block.pm +++ b/FS/FS/addr_block.pm @@ -209,7 +209,7 @@ sub cidr { =item free_addrs -Returns a sorted list of free addresses in the block. +Returns an aref sorted list of free addresses in the block. =cut @@ -221,7 +221,11 @@ sub free_addrs { FS::IP_Mixin->used_addresses_in_block($self), FS::Conf->new()->config('exclude_ip_addr'); - grep { !exists $used_addr_map{$_} } map { $_->addr } $self->NetAddr->hostenum; + [ + grep { !exists $used_addr_map{$_} } + map { $_->addr } + $self->NetAddr->hostenum + ]; } =item next_free_addr diff --git a/httemplate/elements/tr-select-router_block_ip.html b/httemplate/elements/tr-select-router_block_ip.html index 2aa715e29..535e953c4 100644 --- a/httemplate/elements/tr-select-router_block_ip.html +++ b/httemplate/elements/tr-select-router_block_ip.html @@ -2,34 +2,110 @@ var manual_addr_routernum = <% encode_json(\%manual_addr_routernum) %>; var ip_addr_curr_value = <% $opt{'ip_addr'} |js_string %>; var blocknum_curr_value = <% $opt{'blocknum'} |js_string %>; -function update_ip_addr(obj, i) { - var routernum = document.getElementById('router_select_0').value; - var select_blocknum = document.getElementById('router_select_1'); - var blocknum = select_blocknum.value; - var input_ip_addr = document.getElementById('input_ip_addr'); + +function update_ip_addr() { + var routernum = $('#router_select_0').val(); + var blocknum = $('#router_select_1').val(); + var e_input_ip_addr = $('#input_ip_addr'); + var e_router_select_1 = $('#router_select_1'); + + <% # Is block is automatically selected for this router? %> if ( manual_addr_routernum[routernum] == 'Y' ) { -%# hide block selection and default ip address to its previous value - select_blocknum.style.display = 'none'; - input_ip_addr.value = ip_addr_curr_value; - } - else { -%# the reverse - select_blocknum.style.display = ''; -%# default ip address to null, unless the router/block are set to the -%# previous value, in which case default it to current value + show_ip_input(); + hide_ip_select(); + e_router_select_1.hide(); + e_input_ip_addr.val( ip_addr_curr_value ); + } else { + e_router_select_1.show(); + e_input_ip_addr.attr('placeholder', <% mt('(automatic)') | js_string %> ); if ( routernum == router_curr_values[0] && - blocknum == router_curr_values[1] ) { - input_ip_addr.value = ip_addr_curr_value; + blocknum == router_curr_values[1] ) { + e_input_ip_addr.val( ip_addr_curr_value ); } else { - input_ip_addr.value = <% mt('(automatic)') |js_string %>; + e_input_ip_addr.val(''); } } + show_or_hide_toggle_ip(); + populate_ip_select(); +} + +function toggle_ip_input() { + if ( $('#input_ip_addr').is(':hidden') ) { + show_ip_input(); + } else { + show_ip_select(); + } +} + +function show_ip_input() { + $('#input_ip_addr').show(); + $('#select_ip_addr').hide(); + depopulate_ip_select(); +} + +function show_ip_select() { + var e_input_ip_addr = $('#input_ip_addr'); + var e_select_ip_addr = $('#select_ip_addr'); + + e_select_ip_addr.width( e_input_ip_addr.width() ); + e_input_ip_addr.hide(); + e_select_ip_addr.show(); + populate_ip_select(); +} + +function populate_ip_select() { + depopulate_ip_select(); + var e = $('#select_ip_addr'); + var blocknum = $('#router_select_1').val(); + + var opts = [ '' ]; + e.html(opts.join('')); + +% if ( $opt{ip_addr} ) { + opts = [ + '', + '' + ]; +% } else { + opts = [ '' ]; +% } + if ( blocknum && $.isNumeric(blocknum) && ! e.is(':hidden')) { + $.getJSON( + '<% $p %>json/free_addresses_in_block.json.html', + {blocknum: blocknum}, + function(ip_json) { + $.each( ip_json, function(idx, val) { + opts.push( + '' + + val + + '' + ); + }); + e.html(opts.join('')); + } + ); + } } -function clearhint_ip_addr (what) { - if ( what.value == <% mt('(automatic)') |js_string %> ) - what.value = ''; + +function depopulate_ip_select() { + $('#select_ip_addr').children().remove(); } + +function propogate_ip_select() { + $('#input_ip_addr').val( $('#select_ip_addr').val() ); +} + +function show_or_hide_toggle_ip() { + if ( $('#router_select_1').val() ) { + $('#toggle_ip').show(); + } else { + show_ip_input(); + $('#toggle_ip').hide(); + } +} + + <& /elements/tr-td-label.html, label => ($opt{'label'} || 'Router'), required => $opt{'required'} &> <& /elements/select-tiered.html, prefix => 'router_', tiers => [ @@ -58,14 +134,20 @@ function clearhint_ip_addr (what) { <& /elements/tr-td-label.html, label => ($opt{'ip_addr_label'} || 'IP address'), required => $opt{'ip_addr_required'} &> -% #warn Dumper \%fixed; % if ( exists $fixed{$ip_field} ) { <% $opt{'ip_addr'} || '' %> % } % else { - + + + % }