diff options
author | Ivan Kohler <ivan@freeside.biz> | 2018-07-16 18:43:48 -0700 |
---|---|---|
committer | Ivan Kohler <ivan@freeside.biz> | 2018-07-16 18:43:48 -0700 |
commit | 2a9d2460814b37e07d7cb83392723146a60d34c7 (patch) | |
tree | 074f234834e4389a0da90c15e4a91c72a25d68b9 | |
parent | 4facec5414f411b97c9b19772c13f125a0b748cb (diff) | |
parent | e44835f640559a6eb5bc831e08732d5ab8947dd4 (diff) |
Merge branch 'master' of git.freeside.biz:/home/git/freeside
46 files changed, 1296 insertions, 406 deletions
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<FS::access_right>, L<FS::access_group>, L<FS::access_user> =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<FS::addr_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. +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 { ' </title>', ' </head>', ' <body bgcolor="#e8e8e8">', - Encode::encode_utf8($html), + Encode::encode( + 'UTF-8', + $html, + Encode::FB_WARN | Encode::LEAVE_SRC + ), ' </body>', '</html>', ], @@ -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<FS::contact> 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<FS::Record> =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<FS::Record>, L<FS::svc_Common>, 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<FS::cust_pkg>, 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<FS::tower>, L<FS::Record>, 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', +); </%init> 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 <SELECT NAME="gatewaynum"> <OPTION VALUE="<% $payment_gateway->gatewaynum %>"><% $payment_gateway->gateway_module %> (<% $payment_gateway->gateway_username %>) % } - </SELECT> -<BR><BR> +<BR> + +<INPUT TYPE="checkbox" NAME="cardtype" VALUE="ACH"> for ACH only. +<BR> +<BR> <INPUT TYPE="submit" VALUE="Add gateway override"> </FORM> 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) = @_; <DIV STYLE="display: inline-block" ID="<% $field %>_input"> <INPUT TYPE="text" NAME="<% $field %>" VALUE="<% $value |h %>" <%$extra%>> +% 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}, + } &> +% } <BR><FONT SIZE="-1" COLOR="#333333"><% emt($label) %></FONT> </DIV> </%def> @@ -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 { <INPUT TYPE="hidden" NAME="ss" VALUE="<% $ss %>"> % } 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') ) { <TR> <TH ALIGN="right"><% $stateid_label %></TH> - <TD><INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>" SIZE=12></TD> + <TD> + <INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>" SIZE=12 ID="stateid"> +% if ( $stateid && $FS::CurrentUser::CurrentUser->access_right( 'Unmask customer DL' )) { + <& /elements/link-replace_element_text.html, {target_id => 'stateid', replace_text => $cust_main->stateid} &> +% } + </TD> <TD><& /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 @@ % } <BR>Payment - <% ntable("#cccccc", 2) %> + <TABLE class="fsinnerbox"> <TR> <TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">$<% $cust_pay->paid %></TD> @@ -85,7 +85,8 @@ <BR>Refund -<% ntable("#cccccc", 2) %> + +<TABLE class="fsinnerbox"> <TR> <TD ALIGN="right">Date</TD> @@ -102,9 +103,23 @@ <TD ALIGN="right">Check #</TD> <TD COLSPAN=2><INPUT TYPE="text" NAME="payinfo" VALUE="<% $payinfo %>" SIZE=10></TD> </TR> + </TABLE> % } -% elsif ($payby eq 'CHEK') { +% elsif ($payby eq 'CHEK' || $payby eq 'CARD') { % +<SCRIPT TYPE="text/javascript"> + function cust_payby_changed (what) { + var custpaybynum = what.options[what.selectedIndex].value + if ( custpaybynum == '' || custpaybynum == '0' ) { + //what.form.payinfo.disabled = false; + $('#cust_payby').slideDown(); + } else { + //what.form.payinfo.value = ''; + //what.form.payinfo.disabled = true; + $('#cust_payby').slideUp(); + } + } +</SCRIPT> % 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) { + <INPUT TYPE="hidden" NAME="payinfo" VALUE="<% $payinfo %>" SIZE=10> +% } +% else { + <& /elements/tr-select-cust_payby.html, 'cust_payby' => \@cust_payby, 'curr_value' => $custpaybynum, 'onchange' => 'cust_payby_changed(this)', -&> - <INPUT TYPE="hidden" NAME="batch" VALUE="1"> + &> +% } + +% if ( $conf->exists("batch-enable") +% || grep $payby eq $_, $conf->config('batch-enable_payby') +% ) { +% if ( grep $payby eq $_, $conf->config('realtime-disable_payby') ) { + <INPUT TYPE="hidden" NAME="batch" VALUE="1"> +% } else { + <TR> + <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="batch" VALUE="1" ID="batch" <% ($batchnum || $batch) ? 'checked' : '' %> ></TD> + <TH ALIGN="left"> <% mt('Add to current batch') |h %></TH> + </TR> +% } +% } + + </TABLE> +<P> + +% if ( !$cust_pay ) { +<DIV ID="cust_payby" + <% $custpaybynum ? 'STYLE="display:none"' + : '' + %> +> +<TABLE class="fsinnerbox"> + + <& /elements/cust_payby_new.html, + 'cust_payby' => \@cust_payby, + 'curr_value' => $custpaybynum, + &> + +</TABLE> +</DIV> +% } # end if cust_pay + % } else { <INPUT TYPE="hidden" NAME="payinfo" VALUE=""> + </TABLE> % } +<P> +<TABLE class="fsinnerbox"> <& /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; + + <TR> + <TH ALIGN="right"><% mt('Card number') |h %></TH> + <TD COLSPAN=7> + <TABLE> + <TR> + <TD> + <INPUT TYPE="text" NAME="payinfo" SIZE=20 MAXLENGTH=19 VALUE="<%$payinfo%>"> </TD> + <TH><% mt('Exp.') |h %></TH> + <TD> + <SELECT NAME="month"> +% for ( ( map "0$_", 1 .. 9 ), 10 .. 12 ) { + + <OPTION<% $_ == $month ? ' SELECTED' : '' %>><% $_ %> +% } + + </SELECT> + </TD> + <TD> / </TD> + <TD> + <SELECT NAME="year"> +% my @a = localtime; for ( $a[5]+1900 .. $a[5]+1915 ) { + + <OPTION<% $_ == $year ? ' SELECTED' : '' %>><% $_ %> +% } + + </SELECT> + </TD> + </TR> + </TABLE> + </TD> + </TR> + <TR> + <TH ALIGN="right"><% mt('CVV2') |h %></TH> + <TD><INPUT TYPE="text" NAME="paycvv" VALUE="<% $paycvv %>" SIZE=4 MAXLENGTH=4> + (<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/cvv2.html', 480, 352, 'cvv2_popup' ), CAPTION, 'CVV2 Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;"><% mt('help') |h %></A>) + </TD> + </TR> + <TR> + <TH ALIGN="right"><% mt('Exact name on card') |h %></TH> + <TD><INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="payname" VALUE="<%$payname%>"></TD> + </TR> + + <& /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; + + <INPUT TYPE="hidden" NAME="month" VALUE="12"> + <INPUT TYPE="hidden" NAME="year" VALUE="2037"> + <TR> + <TD ALIGN="right"><% mt('Account number') |h %></TD> + <TD><INPUT TYPE="text" SIZE=10 NAME="payinfo1" VALUE="<%$account%>"></TD> + <TD ALIGN="right"><% mt('Type') |h %></TD> + <TD><SELECT NAME="paytype"><% join('', map { qq!<OPTION VALUE="$_" !.($paytype eq $_ ? 'SELECTED' : '').">$_</OPTION>" } FS::cust_payby->paytypes) %></SELECT></TD> + </TR> + <TR> + <TD ALIGN="right"><% mt($routing_label) |h %></TD> + <TD> + <INPUT TYPE="text" SIZE="<% $routing_size %>" MAXLENGTH="<% $routing_maxlength %>" NAME="payinfo2" VALUE="<%$aba%>"> + (<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/ach.html', 380, 240, 'ach_popup' ), CAPTION, 'ACH Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;"><% mt('help') |h %></A>) + </TD> + </TR> +% if ( $conf->config('echeck-country') eq 'CA' ) { + <TR> + <TD ALIGN="right"><% mt('Branch number') |h %></TD> + <TD> + <INPUT TYPE="text" NAME="payinfo3" VALUE="<%$branch%>" SIZE=6 MAXLENGTH=5> + </TD> + </TR> +% } + <TR> + <TD ALIGN="right"><% mt('Bank name') |h %></TD> + <TD><INPUT TYPE="text" NAME="payname" VALUE="<%$payname%>"></TD> + </TR> + +% if ( $conf->exists('show_bankstate') ) { + <TR> + <TD ALIGN="right"><% mt('Bank state') |h %></TD> + <TD><& /elements/select-state.html, + 'disable_empty' => 0, + 'empty_label' => emt('(choose)'), + 'state' => $paystate, + 'country' => $cust_main->country, + 'prefix' => 'pay', + &> + </TD> + </TR> +% } else { + <INPUT TYPE="hidden" NAME="paystate" VALUE="<% $paystate %>"> +% } + +% if ( $conf->exists('show_ss') ) { + <TR> + <TD ALIGN="right"> + <% mt('Account holder') |h %><BR> + <% mt('Social security or tax ID #') |h %> + </TD> + <TD><INPUT TYPE="text" NAME="ss" VALUE="<% $ss %>"></TD> + </TR> +% } else { + <INPUT TYPE="hidden" NAME="ss" VALUE="<% $ss %>"></TD> +% } + +% if ( $conf->exists('show_stateid') ) { + <TR> + <TD ALIGN="right"> + <% mt('Account holder') |h %><BR> + <% mt("Driver's license or state ID #") |h %> + </TD> + <TD><INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>"></TD> + <TD ALIGN="right"><% mt('State') |h %></TD> + <TD><& /elements/select-state.html, + 'disable_empty' => 0, + 'empty_label' => emt('(choose)'), + 'state' => $stateid_state, + 'country' => $cust_main->country, + 'prefix' => 'stateid_', + &> + </TD> + </TR> +% } else { + <INPUT TYPE="hidden" NAME="stateid" VALUE="<% $stateid %>"> + <INPUT TYPE="hidden" NAME="stateid_state" VALUE="<% $stateid_state %>"> +% } + +% } #end CARD/CHEK-specific section + + +<TR> + <TD COLSPAN=8> + <INPUT TYPE="checkbox" CHECKED NAME="save" VALUE="1"> + <% mt('Remember this information') |h %> + </TD> +</TR> + +<TR> + <TD COLSPAN=8> + <INPUT TYPE="checkbox"<% $auto ? ' CHECKED' : '' %> 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 %> + <SELECT NAME="weight"> +% for ( 1 .. 1+scalar(grep { $_->payby =~ /^(CARD|CHEK)$/ } @cust_payby) ) { + <OPTION VALUE="<%$_%>"><% mt( $weight{$_} ) |h %> +% } + </SELECT> +% } else { + <INPUT TYPE="hidden" NAME="weight" VALUE="1"> +% } + </TD> +</TR> + +<%once> + +my %weight = ( + 1 => 'Primary', + 2 => 'Secondary', + 3 => 'Tertiary', + 4 => 'Fourth', + 5 => 'Fifth', + 6 => 'Sixth', + 7 => 'Seventh', +); + +</%once> + +<%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; + +</%init>
\ 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 => ... + } +&> + +</%doc> +<a href="<% $param{href} %>" + style="<% $param{style} %>" +% if ($param{class}) { + class="<% $param{class} %>" +% } + onClick="$('#<% $param{target_id} %>').<% $param{jmethod} %>('<% $param{replace_text} |h %>');">◁</a> +<%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'; +</%init> 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 ) { <INPUT TYPE="hidden" NAME="<% $opt{'element_name'} || $opt{'field'} || 'custpaybynum' %>" VALUE=""> 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 = [ '<option value="">loading...</option>' ]; + e.html(opts.join('')); + +% if ( $opt{ip_addr} ) { + opts = [ + '<option value="<% $opt{ip_addr} |h %>"><% $opt{ip_addr} |h %></option>', + '<option value="">-----------</option>' + ]; +% } else { + opts = [ '<option value=""><% mt('(automatic)') |h %></option>' ]; +% } + 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( + '<option' + (val == ip_addr_curr_value ? 'selected' : '') + '>' + + val + + '</option>' + ); + }); + 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(); + } +} + </script> + <& /elements/tr-td-label.html, label => ($opt{'label'} || 'Router'), required => $opt{'required'} &> <td> <& /elements/select-tiered.html, prefix => 'router_', tiers => [ @@ -58,14 +134,20 @@ function clearhint_ip_addr (what) { </td></tr> <& /elements/tr-td-label.html, label => ($opt{'ip_addr_label'} || 'IP address'), required => $opt{'ip_addr_required'} &> <td> -% #warn Dumper \%fixed; % if ( exists $fixed{$ip_field} ) { <input type="hidden" id="input_ip_addr" name="<% $ip_field %>" value="<% $opt{'ip_addr'} |h%>"><% $opt{'ip_addr'} || '' %> % } % else { - <input type="text" id="input_ip_addr" name="<% $ip_field %>" - value="<% $opt{'ip_addr'} |h%>" onfocus="clearhint_ip_addr(this)"> + <input type="text" + id="input_ip_addr" + name="<% $ip_field %>" + value="<% $opt{'ip_addr'} | h %>" + onfocus="clearhint_ip_addr(this)"> + <select id="select_ip_addr" style="display: none;" onChange='javascript:propogate_ip_select();'> + <option><% mt('loading') |h %>...</option> + </select> + <button type="button" onClick='javascript:toggle_ip_input();' id="toggle_ip" style="display: none;">▼</button> % } </td> </tr> <script type="text/javascript"> diff --git a/httemplate/misc/download-batch.cgi b/httemplate/misc/download-batch.cgi index c4bc37e93..c6a0b68c3 100644 --- a/httemplate/misc/download-batch.cgi +++ b/httemplate/misc/download-batch.cgi @@ -20,9 +20,18 @@ elsif ( $cgi->param('format') =~ /^([\w\- ]+)$/ ) { $opt{'format'} = $1; } -my $pay_batch = qsearchs('pay_batch', { batchnum => $batchnum } ); +my $credit_transactions = "EXISTS (SELECT 1 FROM cust_pay_batch WHERE batchnum = $batchnum AND paycode = 'C') AS arecredits"; +my $pay_batch = qsearchs({ 'select' => "*, $credit_transactions", + 'table' => 'pay_batch', + 'hashref' => { batchnum => $batchnum }, + }); die "Batch not found: '$batchnum'" if !$pay_batch; +if ($pay_batch->{Hash}->{arecredits}) { + my $export_format = "FS::pay_batch::".$opt{'format'}; + die "This format can not handle refunds." unless $export_format->can('can_handle_credits'); +} + my $exporttext = $pay_batch->export_batch(%opt); unless ($exporttext) { http_header('Content-Type' => 'text/html' ); 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 () { > <TABLE class="fsinnerbox"> -% 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; - - <TR> - <TH ALIGN="right"><% mt('Card number') |h %></TH> - <TD COLSPAN=7> - <TABLE> - <TR> - <TD> - <INPUT TYPE="text" NAME="payinfo" SIZE=20 MAXLENGTH=19 VALUE="<%$payinfo%>"> </TD> - <TH><% mt('Exp.') |h %></TH> - <TD> - <SELECT NAME="month"> -% for ( ( map "0$_", 1 .. 9 ), 10 .. 12 ) { - - <OPTION<% $_ == $month ? ' SELECTED' : '' %>><% $_ %> -% } - - </SELECT> - </TD> - <TD> / </TD> - <TD> - <SELECT NAME="year"> -% my @a = localtime; for ( $a[5]+1900 .. $a[5]+1915 ) { - - <OPTION<% $_ == $year ? ' SELECTED' : '' %>><% $_ %> -% } - - </SELECT> - </TD> - </TR> - </TABLE> - </TD> - </TR> - <TR> - <TH ALIGN="right"><% mt('CVV2') |h %></TH> - <TD><INPUT TYPE="text" NAME="paycvv" VALUE="<% $paycvv %>" SIZE=4 MAXLENGTH=4> - (<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/cvv2.html', 480, 352, 'cvv2_popup' ), CAPTION, 'CVV2 Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;"><% mt('help') |h %></A>) - </TD> - </TR> - <TR> - <TH ALIGN="right"><% mt('Exact name on card') |h %></TH> - <TD><INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="payname" VALUE="<%$payname%>"></TD> - </TR> - - <& /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; - - <INPUT TYPE="hidden" NAME="month" VALUE="12"> - <INPUT TYPE="hidden" NAME="year" VALUE="2037"> - <TR> - <TD ALIGN="right"><% mt('Account number') |h %></TD> - <TD><INPUT TYPE="text" SIZE=10 NAME="payinfo1" VALUE="<%$account%>"></TD> - <TD ALIGN="right"><% mt('Type') |h %></TD> - <TD><SELECT NAME="paytype"><% join('', map { qq!<OPTION VALUE="$_" !.($paytype eq $_ ? 'SELECTED' : '').">$_</OPTION>" } FS::cust_payby->paytypes) %></SELECT></TD> - </TR> - <TR> - <TD ALIGN="right"><% mt($routing_label) |h %></TD> - <TD> - <INPUT TYPE="text" SIZE="<% $routing_size %>" MAXLENGTH="<% $routing_maxlength %>" NAME="payinfo2" VALUE="<%$aba%>"> - (<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/ach.html', 380, 240, 'ach_popup' ), CAPTION, 'ACH Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;"><% mt('help') |h %></A>) - </TD> - </TR> -% if ( $conf->config('echeck-country') eq 'CA' ) { - <TR> - <TD ALIGN="right"><% mt('Branch number') |h %></TD> - <TD> - <INPUT TYPE="text" NAME="payinfo3" VALUE="<%$branch%>" SIZE=6 MAXLENGTH=5> - </TD> - </TR> -% } - <TR> - <TD ALIGN="right"><% mt('Bank name') |h %></TD> - <TD><INPUT TYPE="text" NAME="payname" VALUE="<%$payname%>"></TD> - </TR> - -% if ( $conf->exists('show_bankstate') ) { - <TR> - <TD ALIGN="right"><% mt('Bank state') |h %></TD> - <TD><& /elements/select-state.html, - 'disable_empty' => 0, - 'empty_label' => emt('(choose)'), - 'state' => $paystate, - 'country' => $cust_main->country, - 'prefix' => 'pay', - &> - </TD> - </TR> -% } else { - <INPUT TYPE="hidden" NAME="paystate" VALUE="<% $paystate %>"> -% } - -% if ( $conf->exists('show_ss') ) { - <TR> - <TD ALIGN="right"> - <% mt('Account holder') |h %><BR> - <% mt('Social security or tax ID #') |h %> - </TD> - <TD><INPUT TYPE="text" NAME="ss" VALUE="<% $ss %>"></TD> - </TR> -% } else { - <INPUT TYPE="hidden" NAME="ss" VALUE="<% $ss %>"></TD> -% } - -% if ( $conf->exists('show_stateid') ) { - <TR> - <TD ALIGN="right"> - <% mt('Account holder') |h %><BR> - <% mt("Driver's license or state ID #") |h %> - </TD> - <TD><INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>"></TD> - <TD ALIGN="right"><% mt('State') |h %></TD> - <TD><& /elements/select-state.html, - 'disable_empty' => 0, - 'empty_label' => emt('(choose)'), - 'state' => $stateid_state, - 'country' => $cust_main->country, - 'prefix' => 'stateid_', - &> - </TD> - </TR> -% } else { - <INPUT TYPE="hidden" NAME="stateid" VALUE="<% $stateid %>"> - <INPUT TYPE="hidden" NAME="stateid_state" VALUE="<% $stateid_state %>"> -% } - -% } #end CARD/CHEK-specific section - - -<TR> - <TD COLSPAN=8> - <INPUT TYPE="checkbox" CHECKED NAME="save" VALUE="1"> - <% mt('Remember this information') |h %> - </TD> -</TR> - -<TR> - <TD COLSPAN=8> - <INPUT TYPE="checkbox"<% $auto ? ' CHECKED' : '' %> 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 %> - <SELECT NAME="weight"> -% for ( 1 .. 1+scalar(grep { $_->payby =~ /^(CARD|CHEK)$/ } @cust_payby) ) { - <OPTION VALUE="<%$_%>"><% mt( $weight{$_} ) |h %> -% } - </SELECT> -% } else { - <INPUT TYPE="hidden" NAME="weight" VALUE="1"> -% } - </TD> -</TR> +<& /elements/cust_payby_new.html, + 'cust_payby' => \@cust_payby, + 'curr_value' => $custpaybynum, +&> </TABLE> </DIV> @@ -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; </%init> 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}} diff --git a/httemplate/misc/xmlhttp-free_addresses_in_block.json.html b/httemplate/misc/xmlhttp-free_addresses_in_block.json.html new file mode 100644 index 000000000..801718d35 --- /dev/null +++ b/httemplate/misc/xmlhttp-free_addresses_in_block.json.html @@ -0,0 +1,18 @@ +<%doc> + Return a json array containing all free ip addresses within a given block + Unless block is larger than /24 - Does somebody really want to populate + 65k addresses into a HTML selectbox? +</%doc> +<% encode_json($json) %>\ +<%init> + +my $json = []; + +my $blocknum = $cgi->param('blocknum'); + +my $addr_block = qsearchs( addr_block => { blocknum => $blocknum }); + +$json = $addr_block->free_addrs + if ref $addr_block && $addr_block->ip_netmask >= 24; + +</%init> diff --git a/httemplate/search/e911.html b/httemplate/search/e911.html index 75dbef7d5..6d387d563 100644 --- a/httemplate/search/e911.html +++ b/httemplate/search/e911.html @@ -1,6 +1,23 @@ +<%doc> + + E911 Fee Report + + Finds billing totals for a given pkgpart where the bill item matches + cust_pkg.pkgpart or cust_bill_pkg.pkgpart_override columns. + + Given date range, filter by when the invoice was paid. + + * E911 access lines - SUM(cust_bill_pkg.quantity) + * Total fees charged - SUM(cust_bill_pay_pkg.amount) + * Fee payments collected - SUM(cust_bill_pkg.setup) + SUM(cust_bill_pkg.recur) + + * Administrative fee (1%) - 1% of Fee Payments Collected + * Amount due - 99% of Fee Payments Collected + +</%doc> % if ( $row ) { -%# pretty minimal report <& /elements/header.html, 'E911 Fee Report' &> + <& /elements/table-grid.html &> <STYLE TYPE="text/css"> table.grid TD:first-child { font-weight: normal } @@ -8,27 +25,27 @@ table.grid TD { font-weight: bold; text-align: right; padding: 1px 2px } </STYLE> + <TR><TH COLSPAN=2><% $legend %></TH></TR> <TR> - <TD>E911 access lines:</TD> - <TD><% $row->{quantity} || 0 %></TD> + <TD><% mt('E911 access lines') %>:</TD> + <TD><% $report{e911_access_lines} %></TD> </TR> <TR> - <TD>Total fees charged: </TD> - <TD><% $money_char.sprintf('%.2f', $row->{charged_amount}) %></TD> + <TD><% mt('Total fees charged') %>: </TD> + <TD><% $money_char.$report{fees_charged} %></TD> </TD> <TR> - <TD>Fee payments collected: </TD> - <TD><% $money_char.sprintf('%.2f', $row->{paid_amount}) %></TD> + <TD><% mt('Fee payments collected') %>: </TD> + <TD><% $money_char.$report{fees_collected} %></TD> </TR> <TR> - <TD>Administrative fee (1%): </TD> - <TD><% $money_char.sprintf('%.2f', $row->{paid_amount} * $admin_fee) %></TD> + <TD><% mt('Administrative fee') %> (1%): </TD> + <TD><% $money_char.$report{admin_fee} %></TD> </TR> <TR> - <TD>Amount due: </TD> - <TD><% $money_char.sprintf('%.2f', $row->{paid_amount} * (1-$admin_fee) ) %> - </TD> + <TD><% mt('Amount due') %>: </TD> + <TD><% $money_char.$report{e911_amount_due} %></TD> </TR> </TABLE> <& /elements/footer.html &> @@ -38,6 +55,8 @@ table.grid TD { font-weight: bold; % } <%init> +our $DEBUG; + die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); @@ -56,56 +75,89 @@ my $agentnum = $1; # package classes, etc.), do NOT simply loop through this and do a # bazillion scalar_sql queries. Use a properly grouped aggregate query. -my $select = 'SELECT cust_bill_pkg.billpkgnum, cust_bill_pkg.quantity, '. -'cust_bill_pkg.setup, SUM(cust_bill_pay_pkg.amount) AS paid_amount'; - -my $from = 'FROM cust_pkg - JOIN cust_bill_pkg USING (pkgnum) - JOIN cust_bill USING (invnum) - LEFT JOIN cust_bill_pay_pkg USING (billpkgnum) - LEFT JOIN cust_bill_pay USING (billpaynum) -'; -# going by payment application date here, which should be -# max(invoice date, payment date) -my $where = "WHERE cust_pkg.pkgpart = $pkgpart -AND ( (cust_bill_pay._date >= $begin AND cust_bill_pay._date < $end) - OR cust_bill_pay.paynum IS NULL )"; +my $sql_statement = " + SELECT + sum(cust_bill_pkg.quantity) as quantity, + sum(cust_bill_pay_pkg.amount) as amount, + sum(cust_bill_pkg.setup) as setup, + sum(cust_bill_pkg.recur) as recur + FROM cust_pkg + LEFT JOIN cust_bill_pkg USING (pkgnum) + LEFT JOIN cust_bill_pay_pkg USING (billpkgnum) + LEFT JOIN cust_bill_pay USING (billpaynum) +"; if ( $agentnum ) { - $from .= ' JOIN cust_main ON (cust_pkg.custnum = cust_main.custnum)'; - $where .= "\n AND cust_main.agentnum = $agentnum"; + $sql_statement .= " + LEFT JOIN cust_main USING (custnum) + WHERE + cust_main.agentnum = ? + AND "; +} else { + $sql_statement .= " + WHERE + " } +$sql_statement .= " + ( cust_bill_pkg.pkgpart_override = ? OR cust_pkg.pkgpart = ? ) + AND ( + ( cust_bill_pay._date >= ? AND cust_bill_pay._date < ? ) + OR cust_bill_pay.paynum IS NULL + ); +"; + +# Preserving this oddball, unexplained epoch substitution +$end = '' if $end == 4294967295; -my $subquery = "$select $from $where -GROUP BY cust_bill_pkg.billpkgnum, cust_bill_pkg.quantity"; -# This has one row for each E911 line item that has any payments applied. -# Fields are the billpkgnum of the item (currently unused), the number of -# E911 charges, and the total amount paid (always > 0). +my @bind_values = ( + $agentnum ? $agentnum : (), + $pkgpart, + $pkgpart, + $begin || 0, + $end || time(), +); -# now sum those rows. -my $sql = "SELECT SUM(quantity) AS quantity, SUM(setup) AS charged_amount, -SUM(paid_amount) AS paid_amount FROM ($subquery) AS paid_fees"; # no grouping +if ( $DEBUG ) { + warn "\$sql_statement: $sql_statement\n"; + warn "\@bind_values: ".join(', ',@bind_values)."\n"; +} -my $sth = dbh->prepare($sql); -$sth->execute; +my $sth = dbh->prepare( $sql_statement ); +$sth->execute( @bind_values ) || die $sth->errstr; my $row = $sth->fetchrow_hashref; -my $admin_fee = 0.01; # 1% admin fee, allowed in Texas +my %report = ( + e911_access_lines => $row->{quantity} || 0, -$end = '' if $end == 4294967295; -my $legend = ''; -if ( $agentnum ) { - $legend = FS::agent->by_key($agentnum)->agent . ', '; -} -if ( $begin and $end ) { - $legend .= time2str('%h %o %Y', $begin) . '—' . - time2str('%h %o %Y', $end); + fees_charged => sprintf( + "%.2f", + ( $row->{setup} + $row->{recur} ) || 0, + ), + + fees_collected => sprintf( + "%.2f", + ( $row->{amount} || 0 ), + ), +); + +# Does everybody use this 1% admin fee? Should this be configurable? +$report{admin_fee} = sprintf( "%.2f", $report{fees_collected} * 0.01 ); +$report{e911_amount_due} = $report{fees_collected} - $report{admin_fee}; + +my $begin_text = + $begin + ? DateTime->from_epoch(epoch => $begin)->mdy('/') + : mt('Anytime'); + +my $end_text = DateTime->from_epoch(epoch => ( $end || time ))->mdy('/'); + +my $legend = FS::agent->by_key($agentnum)->agent . ', ' if $agentnum; +if ( $begin && $end ) { + $legend .= "$begin_text ↔ $end_text"; } elsif ( $begin ) { - $legend .= time2str('after %h %o %Y', $begin); -} elsif ( $end ) { - $legend .= time2str('before %h %o %Y', $end); + $legend .= mt('After')." $begin_text"; } else { - $legend .= 'any time'; + $legend .= mt('Through')." $end_text" } -$legend = ucfirst($legend); + </%init> 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/cust_main/contacts.html b/httemplate/view/cust_main/contacts.html index 1660c1c22..11efcd568 100644 --- a/httemplate/view/cust_main/contacts.html +++ b/httemplate/view/cust_main/contacts.html @@ -29,9 +29,24 @@ <TD COLSPAN=5><% $cust_main->contact |h %></TD> % if ( $conf->exists('show_ss') ) { <TH ALIGN="right"><% mt('SS#') |h %></TH> - <TD><% $conf->exists('unmask_ss') - ? $cust_main->ss - : $cust_main->masked('ss') || ' ' %></TD> + <TD> + <span id="ss_span" style="white-space:nowrap;"> + <% $conf->exists('unmask_ss') + ? $cust_main->ss + : $cust_main->masked('ss') || ' ' %> +% 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, + element_type => 'span' + } &> +% } + </span> + </TD> % } </TR> % if ( $conf->exists('cust_main-enable_spouse') and @@ -172,7 +187,21 @@ <TR> <TH ALIGN="right"><% $stateid_label %></TH> - <TD><% $cust_main->masked('stateid') || ' ' %></TD> + <TD> + <span id="stateid_span" style="white-space:nowrap;"> + <% $cust_main->masked('stateid') || ' ' %> +% 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, + element_type => 'span' + } &> +% } + </span> + </TD> <TH ALIGN="right"><% $stateid_state_label %></TH> <TD><% $cust_main->stateid_state || ' ' %></TD> </TR> diff --git a/httemplate/view/cust_main/menu.html b/httemplate/view/cust_main/menu.html index f3aca21e8..7ec4d07db 100644 --- a/httemplate/view/cust_main/menu.html +++ b/httemplate/view/cust_main/menu.html @@ -460,7 +460,7 @@ my @menu = ( ## condition => sub { $payby{MCHK} }, #}, { - label => 'Batch Electronic check refund', + label => 'Enter electronic check refund', popup => "edit/cust_refund.cgi?popup=1;payby=CHEK;custnum=$custnum", actionlabel => 'Enter electronic check refund', width => 440, 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; <TR> - <TH ALIGN="right"><% $prospect_contact->contact_classname %> Contact</TD> - <TD BGCOLOR="#FFFFFF"><% $contact->line %></TD> + <TH ALIGN="right" VALIGN="top"><% $prospect_contact->contact_classname %> Contact</TH> + <TD BGCOLOR="#FFFFFF"> + <% $contact->line %><br> + <table> +% for my $row ( $contact->contact_email ) { + <tr><th>E-Mail:</th><td><% $row->emailaddress %></td></tr> +% } +% for my $row ( $contact->contact_phone ) { + <tr><th><% $row->phone_type->typename %>:</th><td><% $row->phonenum_pretty %></td></tr> +% } +% if ( $prospect_contact->comment ) { + <tr><th>Comment:</th><td><% $prospect_contact->comment %></td></tr> +% } + </table> + </TD> </TR> %} |