From: Ivan Kohler Date: Tue, 17 Jul 2018 01:43:48 +0000 (-0700) Subject: Merge branch 'master' of git.freeside.biz:/home/git/freeside X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=2a9d2460814b37e07d7cb83392723146a60d34c7;hp=4facec5414f411b97c9b19772c13f125a0b748cb Merge branch 'master' of git.freeside.biz:/home/git/freeside --- 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/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index e4fef9554..263b3116b 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -630,6 +630,8 @@ sub customer_info_short { for (@cust_main_editable_fields) { $return{$_} = $cust_main->get($_); } + $return{$_} = $cust_main->masked($_) for qw/ss stateid/; + #maybe a little more expensive, but it should be cached by now for (@location_editable_fields) { $return{$_} = $cust_main->bill_location->get($_) @@ -1731,20 +1733,34 @@ sub update_payby { }) 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 ) ) { next unless exists($p->{$field}); $cust_payby->set($field,$p->{$field}); } + $cust_payby->set( 'paymask' => $cust_payby->mask_payinfo ); - my $error = $cust_payby->replace; - if ( $error ) { - return { 'error' => $error }; - } else { - return { 'custpaybynum' => $cust_payby->custpaybynum }; + # 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/; + + # Perform updates within a transaction + local $FS::UID::AutoCommit = 0; + + if ( my $error = $cust_payby->replace || $cust_main->replace ) { + dbh->rollback; + return { error => $error }; } - + + dbh->commit; + return { custpaybynum => $cust_payby->custpaybynum }; } sub verify_payby { @@ -3900,4 +3916,3 @@ sub _custoragent_session_custnum { } 1; - diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 76ba30664..bac6a764c 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2788,6 +2788,13 @@ and customer address. Include units.', }, { + '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', 'description' => 'Package to add to each manual credit card and ACH payment entered by employees from the backend. WARNING: Although recently permitted to US merchants in general, specific consumer protection laws may prohibit or restrict this practice in California, Colorado, Connecticut, Florda, Kansas, Maine, Massachusetts, New York, Oklahome, and Texas. Surcharging is also generally prohibited in most countries outside the US, AU and UK.', diff --git a/FS/FS/IP_Mixin.pm b/FS/FS/IP_Mixin.pm index 3ec769313..fc3a0146b 100644 --- a/FS/FS/IP_Mixin.pm +++ b/FS/FS/IP_Mixin.pm @@ -94,6 +94,15 @@ sub ip_check { $self->ip_addr(''); } + # strip user-entered leading 0's from IPv4 addresses + # Parsers like NetAddr::IP interpret them as octal instead of decimal + $self->ip_addr( + join( '.', ( + map{ int($_) } + split( /\./, $self->ip_addr ) + )) + ) if $self->ip_addr =~ /\./ && $self->ip_addr =~ /[\.^]0/; + if ( $self->ip_addr and !$self->router and $self->conf->exists('auto_router') ) { @@ -264,18 +273,22 @@ sub router { FS::router->by_key($self->routernum); } -=item used_addresses [ BLOCK ] +=item used_addresses [ FS::addr_block ] + +Returns a list of all addresses in use within the given L. -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. +If called as an instance method, excludes that instance from the search. =cut sub used_addresses { - my $self = shift; - my $block = shift; - return ( map { $_->_used_addresses($block, $self) } @subclasses ); + my ($self, $block) = @_; + + ( + $block->ip_gateway ? $block->ip_gateway : (), + $block->NetAddr->broadcast->addr, + map { $_->_used_addresses($block, $self ) } @subclasses + ); } sub _used_addresses { diff --git a/FS/FS/Misc/FixIPFormat.pm b/FS/FS/Misc/FixIPFormat.pm new file mode 100644 index 000000000..3f9a19bba --- /dev/null +++ b/FS/FS/Misc/FixIPFormat.pm @@ -0,0 +1,124 @@ +package FS::Misc::FixIPFormat; +use strict; +use warnings; +use FS::Record qw(dbh qsearchs); +use FS::upgrade_journal; + +=head1 NAME + +FS::Misc::FixIPFormat - Functions to repair bad IP address input + +=head1 DESCRIPTION + +Provides functions for freeside_upgrade to check IP address storage for +user-entered leading 0's in IP addresses. When read from database, NetAddr::IP +would treat the number as octal isntead of decimal. If a user entered +10.0.0.052, this may get invisibly translated to 10.0.0.42 when exported. +Base8:52 = Base0:42 + +Tied to freeside_upgrade with journal name TABLE__fixipformat + +see: RT# 80555 + +=head1 SYNOPSIS + +Usage: + + # require, not use - this module is only run once + require FS::Misc::FixIPFormat; + + my $error = FS::Misc::FixIPFormat::fix_bad_addresses_in_table( + 'svc_broadband', 'svcnum', 'ip_addr' + ); + die "oh no!" if $error; + +=head2 fix_bad_addresses_in_table TABLE, ID_COLUMN, IP_COLUMN + +$error = fix_bad_addresses_in_table( 'svc_broadband', 'svcnum', 'ip_addr' ); + +=cut + +sub fix_bad_addresses_in_table { + my ( $table ) = @_; + return if FS::upgrade_journal->is_done("${table}__fixipformat"); + for my $id ( find_bad_addresses_in_table( @_ )) { + if ( my $error = fix_ip_for_record( $id, @_ )) { + die "fix_bad_addresses_in_table(): $error"; + } + } + FS::upgrade_journal->set_done("${table}__fixipformat"); + 0; +} + +=head2 find_bad_addresses_in_table TABLE, ID_COLUMN, IP_COLUMN + +@id = find_bad_addresses_in_table( 'svc_broadband', 'svcnum', 'ip_addr' ); + +=cut + +sub find_bad_addresses_in_table { + my ( $table, $id_col, $ip_col ) = @_; + my @fix_ids; + + # using DBI directly for performance + my $sql_statement = " + SELECT $id_col, $ip_col + FROM $table + WHERE $ip_col IS NOT NULL + "; + my $sth = dbh->prepare( $sql_statement ) || die "SQL ERROR ".dbh->errstr; + $sth->execute || die "SQL ERROR ".dbh->errstr; + while ( my $row = $sth->fetchrow_hashref ) { + push @fix_ids, $row->{ $id_col } + if $row->{ $ip_col } =~ /[\.^]0\d/; + } + @fix_ids; +} + +=head2 fix_ip_for_record ID, TABLE, ID_COLUMN, IP_COLUMN + +Attempt to strip the leading 0 from a stored IP address record. If +the corrected IP address would be a duplicate of another record in the +same table, thow an exception. + +$error = fix_ip_for_record( 1001, 'svc_broadband', 'svcnum', 'ip_addr', ); + +=cut + +sub fix_ip_for_record { + my ( $id, $table, $id_col, $ip_col ) = @_; + + my $row = qsearchs($table, {$id_col => $id}) + || die "Error finding $table record for id $id"; + + my $ip = $row->getfield( $ip_col ); + my $fixed_ip = join( '.', + map{ int($_) } + split( /\./, $ip ) + ); + + return undef unless $ip ne $fixed_ip; + + if ( my $dupe_row = qsearchs( $table, {$ip_col => $fixed_ip} )) { + if ( $dupe_row->getfield( $id_col ) != $row->getfield( $id_col )) { + # Another record in the table has this IP address + # Eg one ip is provisioned as 10.0.0.51 and another is + # provisioned as 10.0.0.051. Cannot auto-correct by simply + # trimming leading 0. Die, let support decide how to fix. + + die "Invalid IP address could not be auto-corrected - ". + "($table - $id_col = $id, $ip_col = $ip) ". + "colission with another reocrd - ". + "($table - $id_col = ".$dupe_row->getfield( $id_col )." ". + "$ip_col = ",$dupe_row->getfield( $ip_col )." ) - ". + "The entry must be corrected to continue"; + } + } + + warn "Autocorrecting IP address problem for ". + "($table - $id_col = $id, $ip_col = $ip) $fixed_ip\n"; + $row->setfield( $ip_col, $fixed_ip ); + $row->replace; +} + +1; diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index 5de4ca752..cf8ec4d73 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -2881,11 +2881,9 @@ to 127.0.0.1. sub ut_ip { my( $self, $field ) = @_; $self->setfield($field, '127.0.0.1') if $self->getfield($field) eq '::1'; - $self->getfield($field) =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ - or return "Illegal (IP address) $field: ". $self->getfield($field); - for ( $1, $2, $3, $4 ) { return "Illegal (IP address) $field" if $_ > 255; } - $self->setfield($field, "$1.$2.$3.$4"); - ''; + return "Illegal (IP address) $field: ".$self->getfield($field) + unless $self->getfield($field) =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; + $self->ut_ip46($field); } =item ut_ipn COLUMN @@ -2913,7 +2911,17 @@ Check/untaint IPv4 or IPv6 address. sub ut_ip46 { my( $self, $field ) = @_; - my $ip = NetAddr::IP->new($self->getfield($field)) + my $ip_addr = $self->getfield( $field ); + + # strip user-entered leading 0's from IPv4 addresses + # Parsers like NetAddr::IP interpret them as octal instead of decimal + $ip_addr = join( '.', ( + map{ int($_) } + split( /\./, $ip_addr ) + ) + ) if $ip_addr =~ /\./ && $ip_addr =~ /[\.^]0/; + + my $ip = NetAddr::IP->new( $ip_addr ) or return "Illegal (IP address) $field: ".$self->getfield($field); $self->setfield($field, lc($ip->addr)); return ''; diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index 646312502..1b3df0066 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, @@ -1387,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 @@ -1400,18 +1417,12 @@ sub print_generic { $tax_section->{'description'} = $self->mt($tax_description); $tax_section->{'summarized'} = ''; - 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; + # append tax section unless it's already there + push @sections, $tax_section + unless grep {$_ eq $tax_section} @sections; - } elsif ( !grep $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 { @@ -2262,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 { @@ -2279,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', ); @@ -2358,7 +2372,11 @@ sub generate_email { ' ', ' ', ' ', - Encode::encode_utf8($html), + Encode::encode( + 'UTF-8', + $html, + Encode::FB_WARN | Encode::LEAVE_SRC + ), ' ', '', ], @@ -3121,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->cust_main + ? $self->cust_main->locale + : $self->prospect_main->locale; my @items; foreach my $cust_bill_pkg (@cust_bill_pkg) { @@ -3134,16 +3154,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 @@ -3171,6 +3205,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/Upgrade.pm b/FS/FS/Upgrade.pm index 92a056c02..aebfc29c7 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -504,6 +504,23 @@ 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' =>[], + + #fix ip format + 'svc_circuit' => [], + + #fix ip format + 'svc_hardware' => [], + + #fix ip format + 'svc_pbx' => [], + + #fix ip format + 'tower_sector' => [], + + ; \%hash; @@ -714,4 +731,3 @@ Sure. =cut 1; - diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm index ba0f61db1..5fd64bf7a 100755 --- a/FS/FS/addr_block.pm +++ b/FS/FS/addr_block.pm @@ -207,6 +207,27 @@ sub cidr { $self->NetAddr->cidr; } +=item free_addrs + +Returns an aref 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($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 +437,3 @@ now because that's the smallest block that makes any sense at all. =cut 1; - diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm index 810709357..8aff96a8d 100644 --- a/FS/FS/agent.pm +++ b/FS/FS/agent.pm @@ -294,8 +294,8 @@ sub payment_gateway { } } - my $cardtype_search = "AND cardtype != 'ACH'"; - $cardtype_search = "AND cardtype = 'ACH'" if $options{method} eq 'ECHECK'; + my $cardtype_search = "AND ( cardtype IS NULL OR cardtype <> 'ACH')"; + $cardtype_search = "AND ( cardtype IS NULL OR cardtype = 'ACH' )" if $options{method} eq 'ECHECK'; my $override = qsearchs({ 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 }); } 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/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 diff --git a/FS/FS/cust_main/Billing_Batch.pm b/FS/FS/cust_main/Billing_Batch.pm index 74748ec33..eb66436ff 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{$_}); } @@ -147,7 +147,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/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 78f2cbcbe..f4d87ddd1 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -1531,7 +1531,7 @@ sub realtime_refund_bop { my $payment_gateway = $self->agent->payment_gateway( 'method' => $options{method} ); - my( $processor, $login, $password, $namespace ) = + ( $processor, $login, $password, $namespace ) = map { my $method = "gateway_$_"; $payment_gateway->$method } qw( module username password namespace ); 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_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/cust_birthdate.pm b/FS/FS/part_event/Condition/cust_birthdate.pm new file mode 100644 index 000000000..874e3acd0 --- /dev/null +++ b/FS/FS/part_event/Condition/cust_birthdate.pm @@ -0,0 +1,64 @@ +package FS::part_event::Condition::cust_birthdate; +use base qw( FS::part_event::Condition ); +use strict; +use warnings; +use DateTime; + +=head2 NAME + +FS::part_event::Condition::cust_birthdate + +=head1 DESCRIPTION + +Billing event triggered by the time until the customer's next +birthday (cust_main.birthdate) + +=cut + +sub description { + 'Customer birthdate occurs within the given timeframe'; +} + +sub option_fields { + ( + timeframe => { + label => 'Timeframe', + type => 'freq', + value => '1m', + } + ); +} + +sub condition { + my( $self, $object, %opt ) = @_; + my $cust_main = $self->cust_main($object); + + my $birthdate = $cust_main->birthdate || return 0; + + my %timeframe; + if ( $self->option('timeframe') =~ /(\d+)([mwdh])/ ) { + my $k = {qw|m months w weeks d days h hours|}->{$2}; + $timeframe{ $k } = $1; + } else { + die "Unparsable timeframe given: ".$self->option('timeframe'); + } + + my $ck_dt = DateTime->from_epoch( epoch => $opt{time} ); + my $bd_dt = DateTime->from_epoch( epoch => $birthdate ); + + # Find the birthday for this calendar year. If customer birthday + # has already passed this year, find the birthday for next year. + my $next_bd_dt = DateTime->new( + month => $bd_dt->month, + day => $bd_dt->day, + year => $ck_dt->year, + ); + $next_bd_dt->add( years => 1 ) + if DateTime->compare( $next_bd_dt, $ck_dt ) == -1; + + # Does next birthday occur between now and specified duration? + $ck_dt->add( %timeframe ); + DateTime->compare( $next_bd_dt, $ck_dt ) != 1 ? 1 : 0; +} + +1; diff --git a/FS/FS/part_event_condition_option.pm b/FS/FS/part_event_condition_option.pm index 3256dc0bd..f1d1b6a15 100644 --- a/FS/FS/part_event_condition_option.pm +++ b/FS/FS/part_event_condition_option.pm @@ -138,6 +138,39 @@ 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 %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 { + $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 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 diff --git a/FS/FS/pay_batch/RBC.pm b/FS/FS/pay_batch/RBC.pm index 1577a7f85..3d1d98b17 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++; @@ -230,5 +228,10 @@ $name = 'RBC'; }, ); +## this format can handle credit transactions +sub can_handle_credits { + 1; +} + 1; diff --git a/FS/FS/svc_IP_Mixin.pm b/FS/FS/svc_IP_Mixin.pm index d28261141..4c2180ece 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,21 +91,71 @@ sub svc_ip_check { } sub _used_addresses { - my ($class, $block, $exclude) = @_; - my $ip_field = $class->table_info->{'ip_field'} - or return (); - # if the service doesn't have an ip_field, then it has no IP addresses - # in use, yes? - - my %hash = ( $ip_field => { op => '!=', value => '' } ); - #$hash{'blocknum'} = $block->blocknum if $block; - $hash{'svcnum'} = { op => '!=', value => $exclude->svcnum } if ref $exclude; - map { my $na = $_->NetAddr; $na ? $na->addr : () } - qsearch({ - table => $class->table, - hashref => \%hash, - extra_sql => " AND $ip_field != '0e0'", - }); + my ($class, $block, $exclude_svc) = @_; + + croak "_used_addresses() 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() skipped, no ip_field"; + return; + } + + my %qsearch = ( $ip_field => { op => '!=', value => '' }); + $qsearch{svcnum} = { op => '!=', value => $exclude_svc->svcnum } + if ref $exclude_svc && $exclude_svc->svcnum; + + 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_param = ( + table => $class->table, + select => $ip_field, + hashref => \%qsearch, + extra_sql => " AND $ip_field != '0e0' ", + ); + if ( $octets ) { + my $block_str = join('.', (split(/\D/, $block_na->first))[0..$octets-1]); + $qsearch_param{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_param ); } sub _is_used { diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm index 078df559a..b8b1a6eab 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, }, @@ -501,6 +502,11 @@ sub _upgrade_data { #next SVC; } + require FS::Misc::FixIPFormat; + FS::Misc::FixIPFormat::fix_bad_addresses_in_table( + 'svc_broadband', 'svcnum', 'ip_addr', + ); + ''; } @@ -523,4 +529,3 @@ FS::part_svc, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/svc_circuit.pm b/FS/FS/svc_circuit.pm index 7f49715b9..7f2ef807c 100644 --- a/FS/FS/svc_circuit.pm +++ b/FS/FS/svc_circuit.pm @@ -236,6 +236,17 @@ sub search_sql_addl_from { 'LEFT JOIN circuit_type USING ( typenum )'; } +sub _upgrade_data { + + require FS::Misc::FixIPFormat; + FS::Misc::FixIPFormat::fix_bad_addresses_in_table( + 'svc_circuit', 'svcnum', 'endpoint_ip_addr', + ); + + ''; + +} + =back =head1 SEE ALSO @@ -245,4 +256,3 @@ L =cut 1; - diff --git a/FS/FS/svc_hardware.pm b/FS/FS/svc_hardware.pm index 4bff483e1..019a5646c 100644 --- a/FS/FS/svc_hardware.pm +++ b/FS/FS/svc_hardware.pm @@ -245,6 +245,17 @@ sub display_hw_addr { join(':', $self->hw_addr =~ /../g) : $self->hw_addr) } +sub _upgrade_data { + + require FS::Misc::FixIPFormat; + FS::Misc::FixIPFormat::fix_bad_addresses_in_table( + 'svc_hardware', 'svcnum', 'ip_addr', + ); + + ''; + +} + =back =head1 SEE ALSO @@ -254,4 +265,3 @@ L, L, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/svc_pbx.pm b/FS/FS/svc_pbx.pm index a5e181d9d..b0f6e8d98 100644 --- a/FS/FS/svc_pbx.pm +++ b/FS/FS/svc_pbx.pm @@ -387,6 +387,17 @@ sub sum_cdrs { qsearchs ( $psearch->{query} ); } +sub _upgrade_data { + + require FS::Misc::FixIPFormat; + FS::Misc::FixIPFormat::fix_bad_addresses_in_table( + 'svc_pbx', 'svcnum', 'ip_addr', + ); + + ''; + +} + =back =head1 BUGS @@ -399,4 +410,3 @@ L, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/tower_sector.pm b/FS/FS/tower_sector.pm index 800d4989a..eb00d33be 100644 --- a/FS/FS/tower_sector.pm +++ b/FS/FS/tower_sector.pm @@ -247,7 +247,7 @@ sub check { $self->ut_numbern('sectornum') || $self->ut_number('towernum', 'tower', 'towernum') || $self->ut_text('sectorname') - || $self->ut_textn('ip_addr') + || $self->ut_ip46n('ip_addr') || $self->ut_floatn('height') || $self->ut_numbern('freq_mhz') || $self->ut_numbern('direction') @@ -471,6 +471,17 @@ sub process_generate_coverage { die $error if $error; } +sub _upgrade_data { + + require FS::Misc::FixIPFormat; + FS::Misc::FixIPFormat::fix_bad_addresses_in_table( + 'tower_sector', 'sectornum', 'ip_addr', + ); + + ''; + +} + =head1 BUGS =head1 SEE ALSO @@ -480,4 +491,3 @@ L, L, schema.html from the base documentation. =cut 1; - diff --git a/httemplate/browse/discount.html b/httemplate/browse/discount.html index 9b2298ae4..deb98c3c7 100644 --- a/httemplate/browse/discount.html +++ b/httemplate/browse/discount.html @@ -1,22 +1,18 @@ <% include( 'elements/browse.html', 'title' => 'Discounts', 'name' => 'discounts', - 'menubar' => [ 'Add a new discount' => - $p.'edit/discount.html', - ], - 'query' => { 'table' => 'discount', }, + 'menubar' => \@menubar, + 'query' => \%query, + 'order_by_sql' => { description => 'discountnum' }, 'count_query' => 'SELECT COUNT(*) FROM discount', 'disableable' => 1, 'disabled_statuspos' => 1, - 'header' => [ 'Name', 'Comment', 'Class', 'Discount', ], + 'header' => [ 'Name', 'Class', 'Discount', ], 'fields' => [ 'name', - 'comment', 'classname', 'description', ], - 'links' => [ $link, - $link, - ], + 'links' => \@links ) %> <%init> @@ -24,6 +20,20 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); -my $link = [ "${p}edit/discount.html?", 'discountnum' ]; +my @links = ( + [ "${p}edit/discount.html?", 'discountnum' ], + [ "${p}edit/discount_class.html?", 'classnum' ], +); + +my %query = ( + select => 'discount.*, discount_class.*', + table => 'discount', + addl_from => 'LEFT JOIN discount_class USING(classnum)', +); + +my @menubar = ( + 'Add a new discount' => $p.'edit/discount.html', + 'Discount classes' => $p.'browse/discount_class.html', +); 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. +
+
diff --git a/httemplate/edit/cust_main/name.html b/httemplate/edit/cust_main/name.html index 713f54cdb..120475b92 100644 --- a/httemplate/edit/cust_main/name.html +++ b/httemplate/edit/cust_main/name.html @@ -1,7 +1,18 @@ <%def .namepart> -% my ($field, $value, $label, $extra) = @_; +% my ($field, $value, $label, $extra, $unmask_field) = @_;
> +% if ( +% $value +% && 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 +24,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..0f288099b 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 ( $stateid && $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/edit/cust_refund.cgi b/httemplate/edit/cust_refund.cgi index e1975ed70..f3dec98e1 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') { +% elsif ($payby eq 'CHEK' || $payby eq 'CARD') { % + % my @cust_payby = (); % if ( $payby eq 'CARD' ) { % @cust_payby = $cust_main->cust_payby('CARD','DCRD'); @@ -117,16 +132,58 @@ % my $custpaybynum = length(scalar($cgi->param('custpaybynum'))) % ? scalar($cgi->param('custpaybynum')) % : scalar(@cust_payby) && $cust_payby[0]->custpaybynum; -<& /elements/tr-select-cust_payby.html, + +% if ($cust_pay) { + +% } +% else { + <& /elements/tr-select-cust_payby.html, 'cust_payby' => \@cust_payby, 'curr_value' => $custpaybynum, 'onchange' => 'cust_payby_changed(this)', -&> - + &> +% } + +% if ( $conf->exists("batch-enable") +% || grep $payby eq $_, $conf->config('batch-enable_payby') +% ) { +% if ( grep $payby eq $_, $conf->config('realtime-disable_payby') ) { + +% } else { + + > +    <% mt('Add to current batch') |h %> + +% } +% } + + +

+ +% if ( !$cust_pay ) { +

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

+ <& /elements/tr-select-reason.html, 'field' => 'reasonnum', 'reason_class' => 'F', @@ -159,16 +216,18 @@ my $payby = $cgi->param('payby'); my $payinfo = $cgi->param('payinfo'); my $reason = $cgi->param('reason'); my $link = $cgi->param('popup') ? 'popup' : ''; +my $batch = $cgi->param('batch'); die "access denied" unless $FS::CurrentUser::CurrentUser->refund_access_right($payby); -my( $paynum, $cust_pay ) = ( '', '' ); +my( $paynum, $cust_pay, $batchnum ) = ( '', '', '' ); if ( $cgi->param('paynum') =~ /^(\d+)$/ ) { $paynum = $1; $cust_pay = qsearchs('cust_pay', { paynum=>$paynum } ) or die "unknown payment # $paynum"; $refund ||= $cust_pay->unrefunded; + $batchnum = $cust_pay->batchnum; if ( $custnum ) { die "payment # $paynum is not for specified customer # $custnum" unless $custnum == $cust_pay->custnum; diff --git a/httemplate/edit/process/cust_refund.cgi b/httemplate/edit/process/cust_refund.cgi index 0a3d55036..1f96456e0 100755 --- a/httemplate/edit/process/cust_refund.cgi +++ b/httemplate/edit/process/cust_refund.cgi @@ -53,7 +53,7 @@ if ( $error ) { 'CHEK' => 'electronic check (ACH)', ); -my( $cust_payby, $payinfo, $paycvv, $month, $year, $payname ); +my( $cust_pay, $cust_payby, $payinfo, $paycvv, $month, $year, $payname ); my $paymask = ''; if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) { @@ -71,6 +71,18 @@ 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"); + +} elsif ( $cgi->param('paynum') > 0) { + + $cust_pay = qsearchs({ + 'table' => 'cust_pay', + 'hashref' => { 'paynum' => $cgi->param('paynum') }, + 'select' => 'cust_pay.*, cust_pay_batch.payname ', + 'addl_from' => "left join cust_pay_batch on cust_pay_batch.batchnum = cust_pay.batchnum and cust_pay_batch.custnum = $custnum ", + }); + $payinfo = $cust_pay->payinfo; + $payname = $cust_pay->payname; } else { @@ -192,16 +204,19 @@ 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; + unless ($paynum) { + if ($cust_payby->paydate) { $paydate = "$year-$month-01"; } + else { $paydate = "2037-12-01"; } + } if ( $cgi->param('batch') ) { - + $paydate = "2037-12-01" unless $paydate; $error ||= $cust_main->batch_card( 'payby' => $payby, 'amount' => $refund, 'payinfo' => $payinfo, - 'paydate' => "$year-$month-01", + 'paydate' => $paydate, 'payname' => $payname, 'paycode' => 'C', map { $_ => scalar($cgi->param($_)) } @@ -209,28 +224,23 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) { ); errorpage($error) if $error; -#### post refund ##### my %hash = map { $_, scalar($cgi->param($_)) } fields('cust_refund'); - $paynum = $cgi->param('paynum'); - $paynum =~ /^(\d*)$/ or die "Illegal paynum!"; - if ($paynum) { - my $cust_pay = qsearchs('cust_pay',{ 'paynum' => $paynum }); - die "Could not find paynum $paynum" unless $cust_pay; - $error = $cust_pay->refund(\%hash); - } else { - my $new = new FS::cust_refund ( \%hash ); - $error = $new->insert; - } - # if not a batch refund run realtime. + + my $new = new FS::cust_refund ( { 'paynum' => $paynum, + %hash, + } ); + $error = $new->insert; + + # if not a batch refund run realtime. } else { $error = $cust_main->realtime_refund_bop( $bop, 'amount' => $refund, 'paynum' => $paynum, 'reasonnum' => scalar($cgi->param('reasonnum')), %options ); } -} else { +} else { # run cash refund. my %hash = map { $_, scalar($cgi->param($_)) } fields('cust_refund'); 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/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/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/elements/tr-select-router_block_ip.html b/httemplate/elements/tr-select-router_block_ip.html index 2aa715e29..72640d3d5 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 %>misc/xmlhttp-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/tr-td-label.html, label => ($opt{'ip_addr_label'} || 'IP address'), required => $opt{'ip_addr_required'} &>
<% 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 { + +% } +
<& /elements/select-tiered.html, prefix => 'router_', tiers => [ @@ -58,14 +134,20 @@ function clearhint_ip_addr (what) {
-% #warn Dumper \%fixed; % if ( exists $fixed{$ip_field} ) { <% $opt{'ip_addr'} || '' %> % } % else { - + + + % }