From f2e2f27be8635833c9b7378cdef1b4b206a17955 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 23 Jul 2012 16:47:39 -0700 Subject: don't enumerate the whole subnet when looking for a free address, #18556 --- FS/FS/addr_block.pm | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm index e00f587c6..686bdbd18 100755 --- a/FS/FS/addr_block.pm +++ b/FS/FS/addr_block.pm @@ -223,43 +223,45 @@ sub cidr { $self->NetAddr->cidr; } -=item free_addrs +=item next_free_addr Returns a NetAddr::IP object corresponding to the first unassigned address in the block (other than the network, broadcast, or gateway address). If there are no free addresses, returns nothing. There are never free addresses when manual_flag is true. -=item next_free_addr - -Returns a NetAddr::IP object for the first unassigned address in the block, -or '' if there are none. +There is no longer a method to return all free addresses in a block. =cut -sub free_addrs { +sub next_free_addr { my $self = shift; + my $selfaddr = $self->NetAddr; return if $self->manual_flag; my $conf = new FS::Conf; my @excludeaddr = $conf->config('exclude_ip_addr'); - + my %used = map { $_ => 1 } ( + @excludeaddr, + $selfaddr->addr, + $selfaddr->network->addr, + $selfaddr->broadcast->addr, (map { $_->NetAddr->addr } - ($self, - qsearch('svc_broadband', { blocknum => $self->blocknum })) + qsearch('svc_broadband', { blocknum => $self->blocknum }) ), @excludeaddr ); - grep { !$used{$_->addr} } $self->NetAddr->hostenum; - -} + # just do a linear search of the block + my $freeaddr = $selfaddr->network + 1; + while ( $freeaddr < $selfaddr->broadcast ) { + return $freeaddr unless $used{ $freeaddr->addr }; + $freeaddr++; + } + return; -sub next_free_addr { - my $self = shift; - ($self->free_addrs, '')[0] } =item allocate -- deprecated -- cgit v1.2.1 From 217b71622d3898e6df98832d378312aa70d6ae7e Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 25 Jul 2012 16:15:40 -0700 Subject: typo --- FS/FS/part_pkg/prepaid.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/part_pkg/prepaid.pm b/FS/FS/part_pkg/prepaid.pm index 407343bc8..50f908c6d 100644 --- a/FS/FS/part_pkg/prepaid.pm +++ b/FS/FS/part_pkg/prepaid.pm @@ -23,7 +23,7 @@ tie my %overlimit_action, 'Tie::IxHash', 'shortname' => 'Prepaid, no automatic cycle', 'inherit_fields' => [ 'usage_Mixin', 'global_Mixin' ], 'fields' => { - 'recur_action' => { 'name' => 'Action to take upon reaching end of prepaid preiod', + 'recur_action' => { 'name' => 'Action to take upon reaching end of prepaid period', 'type' => 'select', 'select_options' => \%recur_action, }, -- cgit v1.2.1 From 74c9101d79b0dfd2f2d3a2996bd3d3575ada9252 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 25 Jul 2012 16:16:02 -0700 Subject: fix ordering packages with no unit price, #13136 --- httemplate/misc/order_pkg.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/httemplate/misc/order_pkg.html b/httemplate/misc/order_pkg.html index 7aa024a34..c5d3fa3a8 100644 --- a/httemplate/misc/order_pkg.html +++ b/httemplate/misc/order_pkg.html @@ -39,6 +39,8 @@ +% } else { + % } -- cgit v1.2.1 From bd647f16de5352722baed016b45baa4e7c695278 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 25 Jul 2012 16:16:57 -0700 Subject: EZ-Prepaid PIN export, #16172 --- FS/FS/part_export/ez_prepaid.pm | 183 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 FS/FS/part_export/ez_prepaid.pm diff --git a/FS/FS/part_export/ez_prepaid.pm b/FS/FS/part_export/ez_prepaid.pm new file mode 100644 index 000000000..d171eb135 --- /dev/null +++ b/FS/FS/part_export/ez_prepaid.pm @@ -0,0 +1,183 @@ +package FS::part_export::ez_prepaid; + +use base qw( FS::part_export ); + +use strict; +use vars qw(@ISA %info $version $replace_ok_kludge $product_info); +use Tie::IxHash; +use FS::Record qw( qsearchs ); +use FS::svc_external; +use SOAP::Lite; +use XML::Simple qw( xml_in ); +use Data::Dumper; + +$version = '01'; + +my $product_info; +my %language_id = ( English => 1, Spanish => 2 ); + +tie my %options, 'Tie::IxHash', + 'site_id' => { label => 'Site ID' }, + 'clerk_id' => { label => 'Clerk ID' }, +# 'product_id' => { label => 'Product ID' }, use the 'title' field +# 'amount' => { label => 'Purchase amount' }, + 'language' => { label => 'Language', + type => 'select', + options => [ 'English', 'Spanish' ], + }, + + 'debug' => { label => 'Debug level', + type => 'select', options => [0, 1, 2 ] }, +; + +%info = ( + 'svc' => 'svc_external', + 'desc' => 'Purchase EZ-Prepaid PIN', + 'options' => \%options, + 'notes' => <<'END' +

Export to the EZ-Prepaid PIN purchase service. If the purchase is allowed, +the PIN will be stored as svc_external.id.

+

svc_external.title must contain the product ID, and should be set as a fixed +field in the service definition. For a list of product IDs, see the +"Merchant Info" tab in the EZ Prepaid reseller portal.

+END + ); + +$replace_ok_kludge = 0; + +sub _export_insert { + my ($self, $svc_external) = @_; + + # the name on the certificate is 'debisys.com', for some reason + local $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME}=0; + + my $pin = eval { $self->ez_prepaid_PinDistSale( $svc_external->title ) }; + return $@ if $@; + + local($replace_ok_kludge) = 1; + $svc_external->set('id', $pin); + $svc_external->replace; +} + +sub _export_replace { + $replace_ok_kludge ? '' : "can't change PIN after purchase"; +} + +sub _export_delete { + "can't delete PIN after purchase"; +} + +# possibly options at some point to relate these to agentnum/usernum +sub site_id { $_[0]->option('site_id') } + +sub clerk_id { $_[0]->option('clerk_id') } + +sub ez_prepaid_PinDistSale { + my $self = shift; + my $product_id = shift; + $self->ez_prepaid_init; # populate product ID cache + my $info = $product_info->{$product_id}; + if ( $info ) { + if ( $self->option('debug') ) { + warn "Purchasing PIN product #$product_id:\n" . + $info->{Description}."\n". + $info->{CurrencyCode} . ' ' .$info->{Amount}."\n"; + } + } else { #no $info + die "Unknown PIN product #$product_id.\n"; + } + + my $response = $self->ez_prepaid_request( + 'PinDistSale', + $version, + $self->site_id, + $self->clerk_id, + $product_id, + '', # AccountID, not used for PIN sale + $product_info->{$product_id}->{Amount}, + $self->svcnum, + ($language_id{ $self->option('language') } || 1), + ); + if ( $self->option('debug') ) { + warn Dumper($response); + # includes serial number and transaction ID, possibly useful + # (but we don't have a structured place to store it--maybe in + # a customer note?) + } + $response->{Pin}; +} + +sub ez_prepaid_init { + # returns the SOAP client object + my $self = shift; + my $wsdl = 'https://webservice.ez-prepaid.com/soap/webServices.wsdl'; + + if ( $self->option('debug') >= 2 ) { + SOAP::Lite->import(+trace => [transport => \&log_transport ]); + } + + if ( !$self->client ) { + $self->set(client => SOAP::Lite->new->service($wsdl)); + # I don't know if this can happen, but better to bail out here + # than go into recursion. + die "Error creating SOAP client\n" if !$self->client; + } + + if ( !defined($product_info) ) { + # for now we only support the 'PIN' type + my $response = $self->ez_prepaid_request( + 'GetTransTypeList', $version, $self->site_id, '', '', '', '' + ); + my %transtype = map { $_->{Description} => $_->{TransTypeId} } + @{ $response->{TransType} }; + + if ( !exists $transtype{PIN} ) { + warn "'PIN' transaction type not available.\n"; + # or else your site ID is wrong + return; + } + + $response = $self->ez_prepaid_request( + 'GetProductList', + $version, + $self->option('site_id'), + $transtype{PIN}, + '', #CarrierId + '', #CategoryId + '', #ProductId + ); + $product_info = +{ + map { $_->{ProductId} => $_ } + @{ $response->{Product} } + }; + } #!defined $product_info +} + +sub log_transport { + my $in = shift; + if ( UNIVERSAL::can($in, 'content') ) { + warn $in->content."\n"; + } +} + +my @ForceArray = qw(TransType Product); # add others as needed +sub ez_prepaid_request { + my $self = shift; + # takes a method name and param list, + # returns a hashref containing the unpacked response + # or dies on error + + $self->ez_prepaid_init if !$self->client; + + my $method = shift; + my $xml = $self->client->$method(@_); + # All of their response data types are one part, a string, containing + # an encoded XML structure, containing the fields described in the docs. + my $response = xml_in($xml, ForceArray => \@ForceArray); + if ( exists($response->{ResponseCode}) && $response->{ResponseCode} > 0 ) { + die "[$method] ".$response->{ResponseMessage}; + } + $response; +} + +1; -- cgit v1.2.1 From c1cabbc4cea2e0cfd9e1db668100b97069140cf5 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 26 Jul 2012 14:02:36 -0700 Subject: add anniversary date, RT#18631 --- FS/FS/Conf.pm | 7 +++++++ FS/FS/Schema.pm | 1 + FS/FS/cust_main.pm | 4 +++- FS/FS/cust_main/Import.pm | 3 ++- FS/FS/cust_main/Search.pm | 6 +++++- httemplate/edit/cust_main.cgi | 1 + httemplate/edit/cust_main/birthdate.html | 16 ++++++++++++++++ httemplate/edit/process/cust_main.cgi | 5 ++++- httemplate/search/cust_main.html | 2 +- httemplate/search/report_cust_main.html | 14 ++++++++++++++ httemplate/view/cust_main/misc.html | 14 ++++++++++++++ 11 files changed, 68 insertions(+), 5 deletions(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 7e641308b..043594536 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -3652,6 +3652,13 @@ and customer address. Include units.', 'type' => 'checkbox', }, + { + 'key' => 'cust_main-enable_anniversary_date', + 'section' => 'UI', + 'description' => 'Enable tracking of an anniversary date with each customer record', + 'type' => 'checkbox', + }, + { 'key' => 'cust_main-edit_calling_list_exempt', 'section' => 'UI', diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 797b70549..3fc26b071 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -858,6 +858,7 @@ sub tables_hashref { 'stateid_state', 'varchar', 'NULL', $char_d, '', '', 'birthdate' ,@date_type, '', '', 'spouse_birthdate' ,@date_type, '', '', + 'anniversary_date' ,@date_type, '', '', 'signupdate',@date_type, '', '', 'dundate', @date_type, '', '', 'company', 'varchar', 'NULL', $char_d, '', '', diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 82b09b61f..78791dd57 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -1779,8 +1779,10 @@ sub check { || $self->ut_textn('custbatch') || $self->ut_name('last') || $self->ut_name('first') - || $self->ut_snumbern('birthdate') || $self->ut_snumbern('signupdate') + || $self->ut_snumbern('birthdate') + || $self->ut_snumbern('spouse_birthdate') + || $self->ut_snumbern('anniversary_date') || $self->ut_textn('company') || $self->ut_anything('comments') || $self->ut_numbern('referral_custnum') diff --git a/FS/FS/cust_main/Import.pm b/FS/FS/cust_main/Import.pm index 6681f9ec2..ee14cbaed 100644 --- a/FS/FS/cust_main/Import.pm +++ b/FS/FS/cust_main/Import.pm @@ -375,7 +375,8 @@ sub batch_import { } $cust_main{$_} = parse_datetime($cust_main{$_}) - foreach grep $cust_main{$_}, qw( birthdate spouse_birthdate ); + foreach grep $cust_main{$_}, + qw( birthdate spouse_birthdate anniversary_date ); my $invoicing_list = $cust_main{'invoicing_list'} ? [ delete $cust_main{'invoicing_list'} ] diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index b528a689c..b213bbcbe 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -475,6 +475,10 @@ listref of start date, end date listref of start date, end date +=item anniversary_date + +listref of start date, end date + =item payby listref @@ -617,7 +621,7 @@ sub search { # dates ## - foreach my $field (qw( signupdate birthdate spouse_birthdate )) { + foreach my $field (qw( signupdate birthdate spouse_birthdate anniversary_date )) { next unless exists($params->{$field}); diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi index ef81ebab1..1ad031d81 100755 --- a/httemplate/edit/cust_main.cgi +++ b/httemplate/edit/cust_main.cgi @@ -30,6 +30,7 @@ %# birthdate % if ( $conf->exists('cust_main-enable_birthdate') % || $conf->exists('cust_main-enable_spouse_birthdate') +% || $conf->exists('cust_main-enable_anniversary_date') % ) % {
diff --git a/httemplate/edit/cust_main/birthdate.html b/httemplate/edit/cust_main/birthdate.html index 5d6a123b1..5447a3ad0 100644 --- a/httemplate/edit/cust_main/birthdate.html +++ b/httemplate/edit/cust_main/birthdate.html @@ -1,5 +1,7 @@ <% ntable("#cccccc", 2) %> + % # maybe put after the contact names? + % if ( $conf->exists('cust_main-enable_birthdate') ) { <% include( '/elements/tr-input-date-field.html', { 'name' => 'birthdate', @@ -11,6 +13,7 @@ }) %> % } + % if ( $conf->exists('cust_main-enable_spouse_birthdate') ) { <% include( '/elements/tr-input-date-field.html', { 'name' => 'spouse_birthdate', @@ -22,6 +25,19 @@ }) %> % } + +% if ( $conf->exists('cust_main-enable_anniversary_date') ) { + <% include( '/elements/tr-input-date-field.html', { + 'name' => 'anniversary_date', + 'value' => $cust_main->anniversary_date, + 'label' => 'Anniversary Date', + 'format' => ( $conf->config('date_format') || "%m/%d/%Y" ), + 'usedatetime' => 1, + 'noinit' => $noinit++, + }) + %> +% } + <%init> diff --git a/httemplate/edit/process/cust_main.cgi b/httemplate/edit/process/cust_main.cgi index 5ee553b32..866452de1 100755 --- a/httemplate/edit/process/cust_main.cgi +++ b/httemplate/edit/process/cust_main.cgi @@ -112,9 +112,12 @@ $new->tagnum( [ $cgi->param('tagnum') ] ); my %usedatetime = ( 'birthdate' => 1, 'spouse_birthdate' => 1, + 'anniversary_date' => 1, ); -foreach my $dfield (qw( birthdate spouse_birthdate signupdate )) { +foreach my $dfield (qw( + signupdate birthdate spouse_birthdate anniversary_date +)) { if ( $cgi->param($dfield) && $cgi->param($dfield) =~ /^([ 0-9\-\/]{0,10})$/) { diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html index e164b98f4..f75b45197 100755 --- a/httemplate/search/cust_main.html +++ b/httemplate/search/cust_main.html @@ -61,7 +61,7 @@ for my $param (qw( classnum refnum payby tagnum )) { # parse dates ### -foreach my $field (qw( signupdate birthdate spouse_birthdate )) { +foreach my $field (qw( signupdate birthdate spouse_birthdate anniversary_date )) { my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field); diff --git a/httemplate/search/report_cust_main.html b/httemplate/search/report_cust_main.html index 39cf695d8..526bce26c 100755 --- a/httemplate/search/report_cust_main.html +++ b/httemplate/search/report_cust_main.html @@ -76,6 +76,20 @@ % } +% if ( $conf->exists('cust_main-enable_anniversary_date') ) { + + <% mt('Anniversary Date') |h %> + + + <& /elements/tr-input-beginning_ending.html, + prefix => 'anniversary_date', + layout => 'horiz', + &> +
+ + +% } + <& /elements/tr-select-cust_tag.html, 'cgi' => $cgi, 'is_report' => 1, diff --git a/httemplate/view/cust_main/misc.html b/httemplate/view/cust_main/misc.html index a0ab403e8..dc6da53f1 100644 --- a/httemplate/view/cust_main/misc.html +++ b/httemplate/view/cust_main/misc.html @@ -130,6 +130,20 @@ % } +% if ( $conf->exists('cust_main-enable_anniversary_date') ) { +% my $dt = $cust_main->anniversary_date ne '' +% ? DateTime->from_epoch( 'epoch' => $cust_main->anniversary_date, +% 'time_zone' =>'floating', +% ) +% : ''; + + + <% mt('Anniversary Date') |h %> + <% $dt ? $dt->strftime($date_format) : '' %> + + +% } + % if ( $conf->exists('cust_main-require_censustract') ) { -- cgit v1.2.1 From 0397a2246116fbb87961200865983760fea6a82c Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 26 Jul 2012 14:03:23 -0700 Subject: 23commit working from top level now i guess --- bin/23commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/23commit b/bin/23commit index b7b0c1e57..d64459cf0 100755 --- a/bin/23commit +++ b/bin/23commit @@ -19,7 +19,7 @@ die "no files!" unless @ARGV; #print < Date: Thu, 26 Jul 2012 14:05:05 -0700 Subject: quotations, RT#16996 --- httemplate/misc/order_pkg.html | 62 ++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/httemplate/misc/order_pkg.html b/httemplate/misc/order_pkg.html index 7aa024a34..57fdd64ee 100644 --- a/httemplate/misc/order_pkg.html +++ b/httemplate/misc/order_pkg.html @@ -1,4 +1,6 @@ -<& /elements/header-popup.html, mt('Order new package') &> +<& /elements/header-popup.html, $quotationnum ? mt('Add package to quotation') + : mt('Order new package') +&> @@ -11,8 +13,10 @@
- + + + % if ( $svcpart ) { % } @@ -26,9 +30,10 @@ % } else { <& /elements/tr-select-cust-part_pkg.html, - 'curr_value' => $pkgpart, - 'classnum' => -1, - 'cust_main' => $cust_main, + 'curr_value' => $pkgpart, + 'classnum' => -1, + 'cust_main' => $cust_main, + 'prospect_main' => $prospect_main, &> % } @@ -54,7 +59,7 @@ -% if ( $cust_main->payby =~ /^(CARD|CHEK)$/ ) { +% if ( $cust_main && $cust_main->payby =~ /^(CARD|CHEK)$/ ) { % my $what = lc(FS::payby->shortname($cust_main->payby)); <% mt("Disable automatic $what charge") |h %> @@ -97,8 +102,9 @@ % } else { <& /elements/tr-select-cust_location.html, - 'cgi' => $cgi, - 'cust_main' => $cust_main, + 'cgi' => $cgi, + 'cust_main' => $cust_main, + 'prospect_main' => $prospect_main, &> % } @@ -152,20 +158,42 @@ die "access denied" my $conf = new FS::Conf; my $date_format = $conf->config('date_format') || '%m/%d/%Y'; -$cgi->param('custnum') =~ /^(\d+)$/ or die "no custnum"; -my $custnum = $1; -my $cust_main = qsearchs({ - 'table' => 'cust_main', - 'hashref' => { 'custnum' => $custnum }, - 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, -}); +my $cust_main = ''; +if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { + my $custnum = $1; + $cust_main = qsearchs({ + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $custnum }, + 'extra_sql' => ' AND '. $curuser->agentnums_sql, + }); +} + +my $prospect_main = ''; +if ( $cgi->param('prospectnum') =~ /^(\d+)$/ ) { + my $prospectnum = $1; + $prospect_main = qsearchs({ + 'table' => 'prospect_main', + 'hashref' => { 'prospectnum' => $prospectnum }, + 'extra_sql' => ' AND '. $curuser->agentnums_sql, + }); +} + +my $quotationnum = ''; +if ( $cgi->param('quotationnum') =~ /^(\d+)$/ ) { + $quotationnum = $1; +} + +die 'no custnum or prospectnum' unless $cust_main || $prospect_main; my $part_pkg = ''; if ( $cgi->param('lock_pkgpart') ) { $part_pkg = qsearchs({ 'table' => 'part_pkg', 'hashref' => { 'pkgpart' => scalar($cgi->param('lock_pkgpart')) }, - 'extra_sql' => ' AND '. FS::part_pkg->agent_pkgs_sql( $cust_main->agent ), + 'extra_sql' => ' AND '. FS::part_pkg->agent_pkgs_sql( + $cust_main ? $cust_main->agent + : $prospect_main->agent + ), }) or die "unknown pkgpart ". $cgi->param('lock_pkgpart'); } @@ -179,7 +207,7 @@ if ( $cgi->param('quantity') =~ /^\s*(\d+)\s*$/ ) { my $format = $date_format. ' %T %z (%Z)'; #false laziness w/REAL_cust_pkg.cgi? my $start_date = ''; -if( ! $conf->exists('order_pkg-no_start_date') ) { +if( ! $conf->exists('order_pkg-no_start_date') && $cust_main ) { $start_date = $cust_main->next_bill_date; $start_date = $start_date ? time2str($format, $start_date) : ''; } -- cgit v1.2.1 From b0651fcfbe21c6f97c164ce0996fa281db5100b0 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 26 Jul 2012 18:18:04 -0700 Subject: when Email invoices is set to no, don't send payment receipt Statement invoice either, RT#17676 --- FS/FS/cust_pay.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index c11738665..d28997ccd 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -662,7 +662,7 @@ sub send_receipt { } - } else { #not manual + } elsif ( ! $cust_main->invoice_noemail ) { #not manual my $queue = new FS::queue { 'paynum' => $self->paynum, -- cgit v1.2.1 From 437fb0a25140cd5a181f8d26204f4949874a00db Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sun, 29 Jul 2012 16:11:01 -0700 Subject: more efficient ticket counting on dashboard page, #17067 --- httemplate/elements/dashboard-toplist.html | 81 ++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/httemplate/elements/dashboard-toplist.html b/httemplate/elements/dashboard-toplist.html index 72f596f4a..f6ebb60fe 100644 --- a/httemplate/elements/dashboard-toplist.html +++ b/httemplate/elements/dashboard-toplist.html @@ -32,18 +32,21 @@ -% foreach my $priority ( @custom_priorities, '' ) { -% my $num = -% FS::TicketSystem->num_customer_tickets($custnum,$priority); -% my $ahref = ''; -% $ahref= '' -% if $num; - +% foreach my $priority ( @custom_priorities ) { - <% $ahref.$num %> - +% my $num = $num_tickets_by_priority{$priority}->{$custnum}; +% if ( $num ) { + <% $num %> +% if ( $priority && +% exists($num_tickets_by_priority{''}{$custnum}) ) { +% # decrement the customer's total by the number in +% # this priority bin +% $num_tickets_by_priority{''}{$custnum} -= $num; +% } +% } + % } @@ -77,7 +80,7 @@ <% $line %> <% mt('Lint') |h %> -% foreach my $priority ( @custom_priorities, '' ) { +% foreach my $priority ( @custom_priorities ) { <% $priority || '(none)'%> @@ -105,11 +108,61 @@ my $conf = new FS::Conf; #false laziness w/httemplate/search/cust_main.cgi... care if # custom_priority_field becomes anything but a local hack... + my @custom_priorities = (); -if ( $conf->config('ticket_system-custom_priority_field') +my $custom_priority_field = $conf->config('ticket_system-custom_priority_field'); +if ( $custom_priority_field && @{[ $conf->config('ticket_system-custom_priority_field-values') ]} ) { @custom_priorities = $conf->config('ticket_system-custom_priority_field-values'); } - +push @custom_priorities, ''; + +my %num_tickets_by_priority = map { $_ => {} } @custom_priorities; +# "optimization" (i.e. "terrible hack") to avoid constructing +# (@custom_priorities) x (cust_main) queries with a bazillion +# joins each just to count tickets +if ( $FS::TicketSystem::system eq 'RT_Internal' ) { + my $text = (driver_name =~ /^Pg/) ? 'text' : 'char'; + # The RT API does not play nicely with aggregate queries, + # so we're going to go around it. + my $sql = + "SELECT cust_main.custnum AS custnum, + ObjectCustomFieldValues.Content as priority, + COUNT(DISTINCT Tickets.Id) AS num_tickets + FROM cust_main + LEFT JOIN cust_pkg USING (custnum) + LEFT JOIN cust_svc USING (pkgnum) + JOIN Links ON ( + ( Links.Target = 'freeside://freeside/cust_main/' || CAST(cust_main.custnum AS $text) OR + Links.Target = 'freeside://freeside/cust_svc/' || CAST(cust_svc.svcnum AS $text) + ) AND + Links.Base LIKE '%rt://%/ticket/%' AND + Links.Type = 'MemberOf' + ) + JOIN Tickets ON (Links.LocalBase = Tickets.Id) + LEFT JOIN ObjectCustomFields ON ( + ObjectCustomFields.ObjectId = '0' OR + ObjectCustomFields.ObjectId = Tickets.Queue + ) + LEFT JOIN CustomFields ON ( + ObjectCustomFields.CustomField = CustomFields.Id AND + CustomFields.Name = '$custom_priority_field' + ) + LEFT JOIN ObjectCustomFieldValues ON ( + ObjectCustomFieldValues.CustomField = CustomFields.Id AND + ObjectCustomFieldValues.ObjectType = 'RT::Ticket' AND + ObjectCustomFieldValues.Disabled = '0' AND + ObjectCustomFieldValues.ObjectId = Tickets.Id + ) + GROUP BY cust_main.custnum, ObjectCustomFieldValues.Content"; + #warn $sql."\n"; + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute or die $sth->errstr; + while ( my $row = $sth->fetchrow_hashref ) { + #warn to_json($row)."\n"; + $num_tickets_by_priority{ $row->{priority} }->{ $row->{custnum} } = + $row->{num_tickets}; + } +} -- cgit v1.2.1 From 7b6434395ecdbf8013309d22564b146736d6e927 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sun, 29 Jul 2012 16:42:43 -0700 Subject: remove unearned junk from cust_bill_pkg.cgi now that it's a separate report --- httemplate/search/cust_bill_pkg.cgi | 203 ++++++++---------------------------- 1 file changed, 43 insertions(+), 160 deletions(-) diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index 1a46b0097..5032542d0 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -3,25 +3,14 @@ 'name' => emt('line items'), 'query' => $query, 'count_query' => $count_query, - 'count_addl' => [ $money_char. '%.2f total', - $unearned ? ( $money_char. '%.2f unearned revenue' ) : (), - ], + 'count_addl' => [ $money_char. '%.2f total', ], 'header' => [ emt('Description'), - ( $unearned - ? ( emt('Unearned'), - emt('Owed'), # useful in 'paid' mode? - emt('Payment date') ) - : ( emt('Setup charge') ) - ), + emt('Setup charge'), ( $use_usage eq 'usage' ? emt('Usage charge') : emt('Recurring charge') ), - ( $unearned - ? ( emt('Charge start'), emt('Charge end') ) - : () - ), emt('Invoice'), emt('Date'), FS::UI::Web::cust_header(), @@ -34,22 +23,11 @@ #strikethrough or "N/A ($amount)" or something these when # they're not applicable to pkg_tax search sub { my $cust_bill_pkg = shift; - if ( $unearned ) { - - sprintf($money_char.'%.2f', - $cust_bill_pkg->unearned_revenue) - - } else { - sprintf($money_char.'%.2f', $cust_bill_pkg->setup ); - } + sprintf($money_char.'%.2f', $cust_bill_pkg->setup ); }, - ( $unearned - ? ( $owed_sub, $payment_date_sub, ) - : () - ), sub { my $row = shift; my $value = 0; - if ( $use_usage eq 'recurring' or $unearned ) { + if ( $use_usage eq 'recurring' ) { $value = $row->recur - $row->usage; } elsif ( $use_usage eq 'usage' ) { $value = $row->usage; @@ -58,30 +36,19 @@ } sprintf($money_char.'%.2f', $value ); }, - ( $unearned - ? ( sub { time2str('%b %d %Y', shift->sdate ) }, - # shift edate back a day - # 82799 = 3600*23 - 1 - # (to avoid skipping a day during DST) - sub { time2str('%b %d %Y', shift->edate - 82799 ) }, - ) - : () - ), 'invnum', sub { time2str('%b %d %Y', shift->_date ) }, \&FS::UI::Web::cust_fields, ], 'sort_fields' => [ '', - 'setup', #broken in $unearned case i guess - ( $unearned ? ('', '') : () ), - ( $use_usage eq 'recurring' or $unearned + 'setup', + ( $use_usage eq 'recurring' ? 'recur - usage' : $use_usage eq 'usage' ? 'usage' : 'recur' ), - ( $unearned ? ('sdate', 'edate') : () ), 'invnum', '_date', ], @@ -89,9 +56,7 @@ #'', '', '', - ( $unearned ? ( '', '' ) : () ), '', - ( $unearned ? ( '', '' ) : () ), $ilink, $ilink, ( map { $_ ne 'Cust. Status' ? $clink : '' } @@ -100,18 +65,14 @@ ], #'align' => 'rlrrrc'.FS::UI::Web::cust_aligns(), 'align' => 'lr'. - ( $unearned ? 'rc' : '' ). 'r'. - ( $unearned ? 'cc' : '' ). 'rc'. FS::UI::Web::cust_aligns(), 'color' => [ #'', '', '', - ( $unearned ? ( '', '' ) : () ), '', - ( $unearned ? ( '', '' ) : () ), '', '', FS::UI::Web::cust_colors(), @@ -120,9 +81,7 @@ #'', '', '', - ( $unearned ? ( '', '' ) : () ), '', - ( $unearned ? ( '', '' ) : () ), '', '', FS::UI::Web::cust_styles(), @@ -137,11 +96,6 @@ die "access denied" my $conf = new FS::Conf; -my $unearned = ''; -my $unearned_mode = ''; -my $unearned_base = ''; -my $unearned_sql = ''; - my @select = ( 'cust_bill_pkg.*', 'cust_bill._date' ); my ($join_cust, $join_pkg ) = ('', ''); @@ -207,44 +161,23 @@ if ( $cgi->param('taxclass') { #override taxclass when use_override is specified? probably - #if ( $use_override ) { - # - # push @where, - # ' ( '. join(' OR ', - # map { - # ' ( part_pkg.taxclass = '. dbh->quote($_). - # ' AND pkgpart_override IS NULL '. - # ' OR '. - # ' override.taxclass = '. dbh->quote($_). - # ' AND pkgpart_override IS NOT NULL '. - # ' ) ' - # } - # $cgi->param('taxclass') - # ). - # ' ) '; - # - #} else { push @where, ' part_pkg.taxclass IN ( '. join(', ', map dbh->quote($_), $cgi->param('taxclass') ). ' ) '; - #} - } my @loc_param = qw( district city county state country ); if ( $cgi->param('out') ) { - my ( $loc_sql, @param ) = FS::cust_pkg->location_sql( 'ornull' => 1 ); - while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution - $loc_sql =~ s/\?/'cust_main_county.'.shift(@param)/e; - } - - $loc_sql =~ s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g - if $cgi->param('istax'); + my ( $loc_sql, @param ) = FS::cust_location->in_county_sql( 'ornull' => 1 ); +# while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution +# $loc_sql =~ s/\?/'cust_main_county.'.shift(@param)/e; +# } + warn "\nLOC_SQL:\n$loc_sql\n"; push @where, " 0 = ( SELECT COUNT(*) FROM cust_main_county @@ -258,16 +191,17 @@ if ( $cgi->param('out') ) { my %ph = map { $_ => dbh->quote( scalar($cgi->param($_)) ) } @loc_param; - my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; + my ( $loc_sql, @param ) = FS::cust_location->in_county_sql(param => 1); while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution $loc_sql =~ s/\?/$ph{shift(@param)}/e; } + warn "\nLOC_SQL:\n$loc_sql\n"; push @where, $loc_sql; } -} elsif ( $cgi->param('country') ) { +} elsif ( $cgi->param('country') ) { # and not $cgi->param('out') my @counties = $cgi->param('county'); @@ -284,7 +218,7 @@ if ( $cgi->param('out') ) { qw( district city state country ) ); - my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; + my ( $loc_sql, @param ) = FS::cust_location->in_county_sql(param => 1); while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution $loc_sql =~ s/\?/$ph{shift(@param)}/e; } @@ -295,17 +229,20 @@ if ( $cgi->param('out') ) { ). ' ) '; + warn "\nLOC_SQL:\n$locs_sql\n"; push @where, $locs_sql; - } else { + } else { #scalar(@counties) <= 1 my %ph = map { $_ => dbh->quote( scalar($cgi->param($_)) ) } @loc_param; - my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; + + my ( $loc_sql, @param ) = FS::cust_location->in_county_sql(param => 1); while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution $loc_sql =~ s/\?/$ph{shift(@param)}/e; } + warn "\nLOC_SQL:\n$loc_sql\n"; push @where, $loc_sql; } @@ -342,6 +279,7 @@ if ( $cgi->param('out') ) { } } elsif ( scalar( grep( /locationtaxid/, $cgi->param ) ) ) { +# and not $cgi->param('out' or 'country') push @where, FS::tax_rate_location->location_sql( map { $_ => (scalar($cgi->param($_)) || '') } @@ -350,69 +288,6 @@ if ( $cgi->param('out') ) { } -# unearned revenue mode -if ( $cgi->param('unearned_now') =~ /^(\d+)$/ ) { - - $unearned = $1; - $unearned_mode = $cgi->param('mode'); - - push @where, "cust_bill_pkg.sdate < $unearned", - "cust_bill_pkg.edate > $unearned", - "cust_bill_pkg.recur != 0", - "part_pkg.freq != '0'"; - - if ( !$cgi->param('include_monthly') ) { - push @where, - "part_pkg.freq != '1'", - "part_pkg.freq NOT LIKE '%h'", - "part_pkg.freq NOT LIKE '%d'", - "part_pkg.freq NOT LIKE '%w'"; - } - - my $usage_sql = FS::cust_bill_pkg->usage_sql; - push @select, "($usage_sql) AS usage"; # we need this - my $paid_sql = 'GREATEST(' . - FS::cust_bill_pkg->paid_sql($unearned, '', setuprecur => 'recur') . - " - $usage_sql, 0)"; - - push @select, "$paid_sql AS paid_no_usage"; # need this either way - - if ( $unearned_mode eq 'paid' ) { - # then use the amount paid, minus usage charges - $unearned_base = $paid_sql; - } - else { - # use the amount billed, minus usage charges and credits - $unearned_base = "GREATEST( cust_bill_pkg.recur - ". - FS::cust_bill_pkg->credited_sql($unearned, '', setuprecur => 'recur') . - " - $usage_sql, 0)"; - # include only rows that have some non-usage, non-credited portion - } - # whatever we're using as the base, only show rows where it's positive - push @where, "$unearned_base > 0"; - - my $period = "CAST(cust_bill_pkg.edate - cust_bill_pkg.sdate AS REAL)"; - my $elapsed = "GREATEST( $unearned - cust_bill_pkg.sdate, 0 )"; - my $remaining = "(1 - $elapsed/$period)"; - - $unearned_sql = "CAST( $unearned_base * $remaining AS DECIMAL(10,2) )"; - push @select, "$unearned_sql AS unearned_revenue"; - - # last payment/credit date - my %t = (pay => 'cust_bill_pay', credit => 'cust_credit_bill'); - foreach my $x (qw(pay credit)) { - my $table = $t{$x}; - my $link = $table.'_pkg'; - my $pkey = dbdef->table($table)->primary_key; - my $last_date_sql = "SELECT MAX(_date) - FROM $table JOIN $link USING ($pkey) - WHERE $link.billpkgnum = cust_bill_pkg.billpkgnum - AND $table._date <= $unearned"; - push @select, "($last_date_sql) AS last_$x"; - } - -} - if ( $cgi->param('itemdesc') ) { if ( $cgi->param('itemdesc') eq 'Tax' ) { push @where, "(itemdesc='Tax' OR itemdesc is null)"; @@ -516,9 +391,9 @@ if ( $cgi->param('pkg_tax') ) { AND cust_bill_pkg.recur > 0 ) )", #not a tax_exempt customer - "( tax != 'Y' OR tax IS NULL )"; + "( tax != 'Y' OR tax IS NULL )", # assume this was intended? #not covered in full by a monthly tax exemption (texas tax) - "0 < ( $setup_taxable + $recur_taxable - $exempt )", + "0 < ( $setup_taxable + $recur_taxable - $exempt )"; } else { @@ -528,9 +403,7 @@ if ( $cgi->param('pkg_tax') ) { $count_query = "SELECT COUNT(DISTINCT billpkgnum), "; } - if ( $unearned ) { - $count_query .= "SUM( $unearned_base ), SUM( $unearned_sql )"; - } elsif ( $use_usage eq 'recurring' ) { + if ( $use_usage eq 'recurring' ) { $count_query .= "SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - usage)"; } elsif ( $use_usage eq 'usage' ) { $count_query .= "SUM(usage)"; @@ -552,9 +425,10 @@ if ( $cgi->param('nottax') ) { $join_pkg .= ' LEFT JOIN cust_pkg USING ( pkgnum ) LEFT JOIN part_pkg USING ( pkgpart ) LEFT JOIN part_pkg AS override - ON pkgpart_override = override.pkgpart '; - $join_pkg .= ' LEFT JOIN cust_location USING ( locationnum ) ' - if $conf->exists('tax-pkg_address'); + ON pkgpart_override = override.pkgpart + LEFT JOIN cust_location + ON cust_location.locationnum = '. + FS::cust_pkg->tax_locationnum_sql; } elsif ( $cgi->param('istax') ) { @@ -562,17 +436,26 @@ if ( $cgi->param('nottax') ) { if ( scalar( grep( /locationtaxid/, $cgi->param ) ) || $cgi->param('iscredit') eq 'rate') { + # using tax_rate_location and friends $join_pkg .= ' LEFT JOIN cust_bill_pkg_tax_rate_location USING ( billpkgnum ) '. ' LEFT JOIN tax_rate_location USING ( taxratelocationnum ) '; - } elsif ( $conf->exists('tax-pkg_address') ) { + #} elsif ( $conf->exists('tax-pkg_address') ) { + } else { + + # using cust_bill_pkg_tax_location to relate tax items to locations + # ...but for consolidated taxes we don't want to duplicate this + my $tax_item_location = '(SELECT DISTINCT billpkgnum, locationnum + FROM cust_bill_pkg_tax_location) AS tax_item_location'; - $join_pkg .= ' LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum ) - LEFT JOIN cust_location USING ( locationnum ) '; + $join_pkg .= " LEFT JOIN $tax_item_location USING ( billpkgnum ) + LEFT JOIN cust_location + ON tax_item_location.locationnum = + cust_location.locationnum "; #quelle kludge, somewhat false laziness w/report_tax.cgi - s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g for @where; + s/cust_pkg\.locationnum/tax_item_location.locationnum/g for @where; } if ( $cgi->param('iscredit') ) { @@ -591,7 +474,7 @@ if ( $cgi->param('nottax') ) { } else { #die? - warn "neiether nottax nor istax parameters specified"; + warn "neither nottax nor istax parameters specified"; #same as before? $join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum ) LEFT JOIN part_pkg USING ( pkgpart ) '; @@ -644,5 +527,5 @@ my $payment_date_sub = sub { or return ''; time2str('%b %d %Y', $cust_pay[-1]->_date ); }; - +warn $count_query; -- cgit v1.2.1 From f578e57b593adb9254b2e465c73116a0e0e617b5 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sun, 29 Jul 2012 17:57:24 -0700 Subject: update customer list and zip code report to use cust_location, #940 --- FS/FS/cust_main/Search.pm | 15 +++++++++ httemplate/search/cust_main-zip.html | 48 +++++++++++------------------ httemplate/search/cust_main.cgi | 28 ++--------------- httemplate/search/cust_main.html | 2 +- httemplate/search/elements/search-html.html | 5 +++ httemplate/search/report_cust_main-zip.html | 4 +-- httemplate/search/report_cust_main.html | 5 +++ 7 files changed, 48 insertions(+), 59 deletions(-) diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index b213bbcbe..2d347e140 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -457,6 +457,8 @@ HASHREF. Valid parameters are =item address +=item zip + =item refnum =item cancelled_pkgs @@ -516,6 +518,7 @@ sub search { 'usernum' => '', 'status' => '', 'address' => '', + 'zip' => '', 'paydate_year' => '', 'invoice_terms' => '', 'custbatch' => '', @@ -578,6 +581,18 @@ sub search { )"; } + ## + # zipcode + ## + if ( $params->{'zip'} =~ /\S/ ) { + my $zip = dbh->quote($params->{'zip'} . '%'); + push @where, "EXISTS( + SELECT 1 FROM cust_location + WHERE cust_location.custnum = cust_main.custnum + AND cust_location.zip LIKE $zip + )"; + } + ### # refnum ### diff --git a/httemplate/search/cust_main-zip.html b/httemplate/search/cust_main-zip.html index c317dc36f..08800d431 100644 --- a/httemplate/search/cust_main-zip.html +++ b/httemplate/search/cust_main-zip.html @@ -4,8 +4,8 @@ 'query' => $sql_query, 'count_query' => $count_sql, 'header' => [ 'Zip code', 'Customers', ], - #'fields' => [ 'zip', 'num_cust', ], - #'links' => [ '', sub { 'somewhere'; } ], + 'fields' => [ 0, 1 ], + 'links' => [ '', $link ], ) %> <%init> @@ -63,48 +63,36 @@ sub strip_plus4 { END"; } -my( $zip, $czip); -if ( $cgi->param('column') eq 'ship_zip' ) { - - my $casewhen_noship = - "CASE WHEN ( ship_last IS NULL OR ship_last = '' ) THEN "; - - $czip = "$casewhen_noship zip ELSE ship_zip END"; - - if ( $cgi->param('ignore_plus4') ) { - $zip = $casewhen_noship. strip_plus4('zip'). - " ELSE ". strip_plus4('ship_zip'). ' END'; - - } else { - $zip = $casewhen_noship. fieldorempty('zip'). - " ELSE ". fieldorempty('ship_zip'). ' END'; - } +$cgi->param('column') =~ /^(bill|ship)$/; +my $location = $1 || 'bill'; +$location .= '_locationnum'; +my $zip; +if ( $cgi->param('ignore_plus4') ) { + $zip = strip_plus4('cust_location.zip'); } else { - - $czip = 'zip'; - - if ( $cgi->param('ignore_plus4') ) { - $zip = strip_plus4('zip'); - } else { - $zip = fieldorempty('zip'); - } - + $zip = fieldorempty('cust_location.zip'); } # construct the queries and send 'em off +my $join = "JOIN cust_location ON (cust_main.$location = cust_location.locationnum)"; + my $sql_query = "SELECT $zip AS zipcode, COUNT(*) AS num_cust FROM cust_main + $join $where GROUP BY zipcode - ORDER BY num_cust DESC + ORDER BY num_cust DESC, $zip ASC "; -my $count_sql = "select count(distinct $czip) from cust_main $where"; +my $count_sql = + "SELECT COUNT(DISTINCT cust_location.zip) + FROM cust_main $join $where"; -# XXX should link... +my $link = [ $p.'search/cust_main.html?zip=', + sub { $_[0]->[0] } ]; diff --git a/httemplate/search/cust_main.cgi b/httemplate/search/cust_main.cgi index 859ef04e6..a81958e20 100755 --- a/httemplate/search/cust_main.cgi +++ b/httemplate/search/cust_main.cgi @@ -81,13 +81,8 @@ <% mt('#') |h %> <% mt('Status') |h %> - <% mt('(bill) name') |h %> - <% mt('company') |h %> - -%if ( defined dbdef->table('cust_main')->column('ship_last') ) { - <% mt('(service) name') |h %> - <% mt('company') |h %> -%} + <% mt('Name') |h %> + <% mt('Company') |h %> %foreach my $addl_header ( @addl_headers ) { <% $addl_header %> @@ -172,25 +167,6 @@ <% $pcompany %> -% if ( defined dbdef->table('cust_main')->column('ship_last') ) { -% my($ship_last,$ship_first,$ship_company)=( -% $cust_main->ship_last || $cust_main->getfield('last'), -% $cust_main->ship_last ? $cust_main->ship_first : $cust_main->first, -% $cust_main->ship_last ? $cust_main->ship_company : $cust_main->company, -% ); -% my $pship_company = $ship_company -% ? qq!$ship_company! -% : ' '; -% - - > - <% "$ship_last, $ship_first" %> - - > - <% $pship_company %> - -% } -% % foreach my $addl_col ( @addl_cols ) { % if ( $addl_col eq 'tickets' ) { % if ( @custom_priorities ) { diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html index f75b45197..fa79b4dfb 100755 --- a/httemplate/search/cust_main.html +++ b/httemplate/search/cust_main.html @@ -41,7 +41,7 @@ my %search_hash = (); #scalars my @scalars = qw ( - agentnum status address paydate_year paydate_month invoice_terms + agentnum status address zip paydate_year paydate_month invoice_terms no_censustract with_geocode custbatch usernum cancelled_pkgs cust_fields flattened_pkgs diff --git a/httemplate/search/elements/search-html.html b/httemplate/search/elements/search-html.html index 53167c26e..c27771494 100644 --- a/httemplate/search/elements/search-html.html +++ b/httemplate/search/elements/search-html.html @@ -337,6 +337,11 @@ % map { % if ( ref($_) eq 'CODE' ) { % &{$_}($row); +% } elsif ( ref($row) eq 'ARRAY' and +% $_ =~ /^\d+$/ ) { +% # for the 'straight SQL' case: specify fields +% # by position +% $row->[$_]; % } else { % $row->$_(); % } diff --git a/httemplate/search/report_cust_main-zip.html b/httemplate/search/report_cust_main-zip.html index 00cb9ed2c..8bad332a9 100644 --- a/httemplate/search/report_cust_main-zip.html +++ b/httemplate/search/report_cust_main-zip.html @@ -8,8 +8,8 @@ Billing or service zip diff --git a/httemplate/search/report_cust_main.html b/httemplate/search/report_cust_main.html index 526bce26c..a2c6f1c4c 100755 --- a/httemplate/search/report_cust_main.html +++ b/httemplate/search/report_cust_main.html @@ -35,6 +35,11 @@ <% mt('Address') |h %> + + + <% mt('Zip') |h %> + + <% mt('Signup date') |h %> -- cgit v1.2.1 From 98a2c1d79af4c475e8245651a79ded35ce3391dd Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 30 Jul 2012 14:50:23 -0700 Subject: fix package changes/cancels with credits turned on, for delayed packages, RT#18576 --- FS/FS/part_pkg/delayed_Mixin.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FS/FS/part_pkg/delayed_Mixin.pm b/FS/FS/part_pkg/delayed_Mixin.pm index d28480db2..83e543a4f 100644 --- a/FS/FS/part_pkg/delayed_Mixin.pm +++ b/FS/FS/part_pkg/delayed_Mixin.pm @@ -2,6 +2,7 @@ package FS::part_pkg::delayed_Mixin; use strict; use vars qw(%info); +use NEXT; %info = ( 'disabled' => 1, @@ -45,7 +46,7 @@ sub calc_remain { && $last_bill == $cust_pkg->setup; } - return $self->SUPER::calc_remain($cust_pkg, %options); + return $self->NEXT::calc_remain($cust_pkg, %options); } sub can_start_date { ! shift->option('delay_setup', 1) } -- cgit v1.2.1 From 66e35f4c5ce2125f252d63f024f2d72eb3ec5c5d Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 30 Jul 2012 15:23:13 -0700 Subject: remove debug cruft --- httemplate/search/cust_bill_pkg.cgi | 4 ---- 1 file changed, 4 deletions(-) diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index 5032542d0..b6b70a080 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -177,7 +177,6 @@ if ( $cgi->param('out') ) { # $loc_sql =~ s/\?/'cust_main_county.'.shift(@param)/e; # } - warn "\nLOC_SQL:\n$loc_sql\n"; push @where, " 0 = ( SELECT COUNT(*) FROM cust_main_county @@ -196,7 +195,6 @@ if ( $cgi->param('out') ) { $loc_sql =~ s/\?/$ph{shift(@param)}/e; } - warn "\nLOC_SQL:\n$loc_sql\n"; push @where, $loc_sql; } @@ -229,7 +227,6 @@ if ( $cgi->param('out') ) { ). ' ) '; - warn "\nLOC_SQL:\n$locs_sql\n"; push @where, $locs_sql; } else { #scalar(@counties) <= 1 @@ -242,7 +239,6 @@ if ( $cgi->param('out') ) { $loc_sql =~ s/\?/$ph{shift(@param)}/e; } - warn "\nLOC_SQL:\n$loc_sql\n"; push @where, $loc_sql; } -- cgit v1.2.1 From dddedb2754d6cd24edfde9a6d25bc687f488e5ec Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 30 Jul 2012 16:33:32 -0700 Subject: fix multiple RADIUS group attributes with same name, RT#18580 --- FS/FS/Schema.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index ff40cd673..cfb806007 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -2518,8 +2518,8 @@ sub tables_hashref { 'op', 'char', '', 2, '', '', ], 'primary_key' => 'attrnum', - 'unique' => [ ['groupnum','attrname'] ], #? - 'index' => [], + 'unique' => [], + 'index' => [ ['groupnum'], ], }, 'msgcat' => { -- cgit v1.2.1 From 271ebf1222eabd8180eb318ea0e1a65f1f3103ff Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 30 Jul 2012 17:21:23 -0700 Subject: fix NAS association with sqlradius_withdomain exports, RT#18540 --- httemplate/edit/nas.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httemplate/edit/nas.html b/httemplate/edit/nas.html index 2e66fc3be..8e6232cdb 100644 --- a/httemplate/edit/nas.html +++ b/httemplate/edit/nas.html @@ -49,7 +49,7 @@ sub html_bottom { 'link_table' => 'export_nas', 'target_table' => 'part_export', 'hashref' => { 'exporttype' => - { op => 'LIKE', value => '%sqlradius' } + { op => 'LIKE', value => '%sqlradius%' } }, 'name_callback' => sub { $_[0]->label }, 'default' => 'yes', -- cgit v1.2.1 From ee8a023fff0a259b0c62b46b260a396805ad2f00 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 30 Jul 2012 21:08:02 -0700 Subject: correctly export RADIUS attributes with the same name, #18695 --- FS/FS/part_export/sqlradius.pm | 103 ++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 64 deletions(-) diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm index c360c9ef0..d7cd459d8 100644 --- a/FS/FS/part_export/sqlradius.pm +++ b/FS/FS/part_export/sqlradius.pm @@ -347,6 +347,7 @@ sub _export_delete { sub sqlradius_queue { my( $self, $svcnum, $method ) = (shift, shift, shift); + my %args = @_; my $queue = new FS::queue { 'svcnum' => $svcnum, 'job' => "FS::part_export::sqlradius::sqlradius_$method", @@ -966,8 +967,7 @@ are identified by the combination of group name and attribute name. In the special case where attributes are being replaced because a group name (L->groupname) is changing, the pseudo-field -'groupname' must be set in OLD_RADIUS_ATTR. It's probably best to do this - +'groupname' must be set in OLD_RADIUS_ATTR. =cut @@ -982,41 +982,43 @@ sub export_attr_replace { shift->export_attr_action('replace', @_); } sub export_attr_action { my $self = shift; my ($action, $new, $old) = @_; - my ($attrname, $attrtype, $groupname) = - ($new->attrname, $new->attrtype, $new->radius_group->groupname); - if ( $action eq 'replace' ) { - - if ( $new->attrtype ne $old->attrtype ) { - # they're in separate tables in the target - return $self->export_attr_action('delete', $old) - || $self->export_attr_action('insert', $new) - ; - } + my $err_or_queue; - # otherwise, just make sure we know the old attribute/group names - # so we can find the existing record - $attrname = $old->attrname; - $groupname = $old->groupname || $old->radius_group->groupname; - # maybe this should be enforced more strictly - warn "WARNING: attribute replace without 'groupname' set; assuming '$groupname'\n" - if !defined($old->groupname); + if ( $action eq 'delete' ) { + $old = $new; + } + if ( $action eq 'delete' or $action eq 'replace' ) { + # delete based on an exact match + my %opt = ( + attrname => $old->attrname, + attrtype => $old->attrtype, + groupname => $old->groupname || $old->radius_group->groupname, + op => $old->op, + value => $old->value, + ); + $err_or_queue = $self->sqlradius_queue('', 'attr_delete', %opt); + return $err_or_queue unless ref $err_or_queue; + } + # this probably doesn't matter, but just to be safe... + my $jobnum = $err_or_queue->jobnum if $action eq 'replace'; + if ( $action eq 'replace' or $action eq 'insert' ) { + my %opt = ( + attrname => $new->attrname, + attrtype => $new->attrtype, + groupname => $new->radius_group->groupname, + op => $new->op, + value => $new->value, + ); + $err_or_queue = $self->sqlradius_queue('', 'attr_insert', %opt); + $err_or_queue->depend_insert($jobnum) if $jobnum; + return $err_or_queue unless ref $err_or_queue; } - - my $err_or_queue = $self->sqlradius_queue('', "attr_$action", - attrnum => $new->attrnum, - attrname => $attrname, - attrtype => $attrtype, - groupname => $groupname, - ); - return $err_or_queue unless ref $err_or_queue; ''; } sub sqlradius_attr_insert { my $dbh = sqlradius_connect(shift, shift, shift); my %opt = @_; - my $radius_attr = qsearchs('radius_attr', { attrnum => $opt{'attrnum'} }) - or die 'attrnum '.$opt{'attrnum'}.' not found'; my $table; # make sure $table is completely safe @@ -1027,12 +1029,10 @@ sub sqlradius_attr_insert { $table = 'radgroupreply'; } else { - die "unknown attribute type '".$radius_attr->attrtype."'"; + die "unknown attribute type '$opt{attrtype}'"; } - my @values = ( - $opt{'groupname'}, map { $radius_attr->$_ } qw(attrname op value) - ); + my @values = @opt{ qw(groupname attrname op value) }; my $sth = $dbh->prepare( 'INSERT INTO '.$table.' (groupname, attribute, op, value) VALUES (?,?,?,?)' ); @@ -1054,41 +1054,16 @@ sub sqlradius_attr_delete { die "unknown attribute type '".$opt{'attrtype'}."'"; } + my @values = @opt{ qw(groupname attrname op value) }; my $sth = $dbh->prepare( - 'DELETE FROM '.$table.' WHERE groupname = ? AND attribute = ?' + 'DELETE FROM '.$table. + ' WHERE groupname = ? AND attribute = ? AND op = ? AND value = ?'. + ' LIMIT 1' ); - $sth->execute( @opt{'groupname', 'attrname'} ) or die $dbh->errstr; + $sth->execute(@values) or die $dbh->errstr; } -sub sqlradius_attr_replace { - my $dbh = sqlradius_connect(shift, shift, shift); - my %opt = @_; - my $radius_attr = qsearchs('radius_attr', { attrnum => $opt{'attrnum'} }) - or die 'attrnum '.$opt{'attrnum'}.' not found'; - - my $table; - if ( $opt{'attrtype'} eq 'C' ) { - $table = 'radgroupcheck'; - } - elsif ( $opt{'attrtype'} eq 'R' ) { - $table = 'radgroupreply'; - } - else { - die "unknown attribute type '".$opt{'attrtype'}."'"; - } - - my $sth = $dbh->prepare( - 'UPDATE '.$table.' SET groupname = ?, attribute = ?, op = ?, value = ? - WHERE groupname = ? AND attribute = ?' - ); - - my $new_groupname = $radius_attr->radius_group->groupname; - my @new_values = ( - $new_groupname, map { $radius_attr->$_ } qw(attrname op value) - ); - $sth->execute( @new_values, @opt{'groupname', 'attrname'} ) - or die $dbh->errstr; -} +#sub sqlradius_attr_replace { no longer needed =item export_group_replace NEW OLD -- cgit v1.2.1 From 6cce5ada4fbf1e9ad7debd0451336e8005c12195 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 31 Jul 2012 14:00:14 -0700 Subject: add strip_tld option to sqlradius_withdomain export, RT#18718 --- FS/FS/part_export/sqlradius_withdomain.pm | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/FS/FS/part_export/sqlradius_withdomain.pm b/FS/FS/part_export/sqlradius_withdomain.pm index e5a7151a2..2af9e8d76 100644 --- a/FS/FS/part_export/sqlradius_withdomain.pm +++ b/FS/FS/part_export/sqlradius_withdomain.pm @@ -6,11 +6,16 @@ use FS::part_export::sqlradius; tie my %options, 'Tie::IxHash', %FS::part_export::sqlradius::options; +$options{'strip_tld'} = { type => 'checkbox', + label => 'Strip TLD from realm names', + }; + %info = ( 'svc' => 'svc_acct', 'desc' => 'Real-time export to SQL-backed RADIUS (FreeRADIUS, ICRADIUS) with realms', 'options' => \%options, 'nodomain' => '', + 'default_svc_class' => 'Internet', 'notes' => $FS::part_export::sqlradius::notes1. 'This export exports domains to RADIUS realms (see also '. 'sqlradius). '. @@ -21,7 +26,11 @@ tie my %options, 'Tie::IxHash', %FS::part_export::sqlradius::options; sub export_username { my($self, $svc_acct) = (shift, shift); - $svc_acct->email; + my $email = $svc_acct->email; + if ( $self->option('strip_tld') ) { + $email =~ s/\.\w+$//; + } + $email; } 1; -- cgit v1.2.1 From f24c4bebce257bfcc61ba07fd3d16c5c0d730071 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 31 Jul 2012 23:02:14 -0700 Subject: invoice voiding, RT#18677 --- FS/FS/AccessRight.pm | 3 +- FS/FS/Schema.pm | 159 ++++++++++++++- FS/FS/access_right.pm | 1 + FS/FS/cust_bill.pm | 59 +++++- FS/FS/cust_bill_pkg.pm | 77 +++++++- FS/FS/cust_bill_pkg_detail_void.pm | 168 ++++++++++++++++ FS/FS/cust_bill_pkg_display_void.pm | 132 +++++++++++++ FS/FS/cust_bill_pkg_tax_location_void.pm | 139 +++++++++++++ FS/FS/cust_bill_pkg_tax_rate_location_void.pm | 139 +++++++++++++ FS/FS/cust_bill_pkg_void.pm | 181 +++++++++++++++++ FS/FS/cust_bill_void.pm | 217 +++++++++++++++++++++ FS/FS/cust_main.pm | 18 +- FS/FS/cust_tax_exempt_pkg_void.pm | 138 +++++++++++++ httemplate/misc/process/void-cust_bill.html | 22 +++ httemplate/misc/void-cust_bill.html | 45 +++++ httemplate/view/cust_bill_void.html | 56 ++++++ httemplate/view/cust_main/payment_history.html | 13 +- .../view/cust_main/payment_history/invoice.html | 14 +- .../cust_main/payment_history/voided_invoice.html | 52 +++++ 19 files changed, 1621 insertions(+), 12 deletions(-) create mode 100644 FS/FS/cust_bill_pkg_detail_void.pm create mode 100644 FS/FS/cust_bill_pkg_display_void.pm create mode 100644 FS/FS/cust_bill_pkg_tax_location_void.pm create mode 100644 FS/FS/cust_bill_pkg_tax_rate_location_void.pm create mode 100644 FS/FS/cust_bill_pkg_void.pm create mode 100644 FS/FS/cust_bill_void.pm create mode 100644 FS/FS/cust_tax_exempt_pkg_void.pm create mode 100755 httemplate/misc/process/void-cust_bill.html create mode 100644 httemplate/misc/void-cust_bill.html create mode 100755 httemplate/view/cust_bill_void.html create mode 100644 httemplate/view/cust_main/payment_history/voided_invoice.html diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 4de29481d..ebf66e64c 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -177,7 +177,8 @@ tie my %rights, 'Tie::IxHash', 'Customer invoice / financial info rights' => [ 'View invoices', 'Resend invoices', #NEWNEW - 'Delete invoices', #new, but no need to phase in + 'Void invoices', + 'Delete invoices', 'View customer tax exemptions', #yow 'Add customer tax adjustment', #new, but no need to phase in 'View customer batched payments', #NEW diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index cfb806007..e59268b2a 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -551,6 +551,35 @@ sub tables_hashref { 'index' => [ ['custnum'], ['_date'], ['statementnum'], ['agent_invid'] ], }, + 'cust_bill_void' => { + 'columns' => [ + #regular fields + 'invnum', 'int', '', '', '', '', + 'custnum', 'int', '', '', '', '', + '_date', @date_type, '', '', + 'charged', @money_type, '', '', + 'invoice_terms', 'varchar', 'NULL', $char_d, '', '', + + #customer balance info at invoice generation time + 'previous_balance', @money_typen, '', '', #eventually not nullable + 'billing_balance', @money_typen, '', '', #eventually not nullable + + #specific use cases + 'closed', 'char', 'NULL', 1, '', '', #not yet used much + 'statementnum', 'int', 'NULL', '', '', '', #invoice aggregate statements + 'agent_invid', 'int', 'NULL', '', '', '', #(varchar?) importing legacy + 'promised_date', @date_type, '', '', + + #void fields + 'void_date', @date_type, '', '', + 'reason', 'varchar', 'NULL', $char_d, '', '', + 'void_usernum', 'int', 'NULL', '', '', '', + ], + 'primary_key' => 'invnum', + 'unique' => [ [ 'custnum', 'agent_invid' ] ], #agentnum? huh + 'index' => [ ['custnum'], ['_date'], ['statementnum'], ['agent_invid'], [ 'void_usernum' ] ], + }, + #for importing invoices from a legacy system for display purposes only # no effect upon balance 'legacy_cust_bill' => { @@ -787,6 +816,101 @@ sub tables_hashref { 'index' => [ [ 'billpkgnum' ], [ 'taxnum' ], [ 'taxratelocationnum' ] ], }, + 'cust_bill_pkg_void' => { + 'columns' => [ + 'billpkgnum', 'int', '', '', '', '', + 'invnum', 'int', '', '', '', '', + 'pkgnum', 'int', '', '', '', '', + 'pkgpart_override', 'int', 'NULL', '', '', '', + 'setup', @money_type, '', '', + 'recur', @money_type, '', '', + 'sdate', @date_type, '', '', + 'edate', @date_type, '', '', + 'itemdesc', 'varchar', 'NULL', $char_d, '', '', + 'itemcomment', 'varchar', 'NULL', $char_d, '', '', + 'section', 'varchar', 'NULL', $char_d, '', '', + 'freq', 'varchar', 'NULL', $char_d, '', '', + 'quantity', 'int', 'NULL', '', '', '', + 'unitsetup', @money_typen, '', '', + 'unitrecur', @money_typen, '', '', + 'hidden', 'char', 'NULL', 1, '', '', + #void fields + 'void_date', @date_type, '', '', + 'reason', 'varchar', 'NULL', $char_d, '', '', + 'void_usernum', 'int', 'NULL', '', '', '', + ], + 'primary_key' => 'billpkgnum', + 'unique' => [], + 'index' => [ ['invnum'], [ 'pkgnum' ], [ 'itemdesc' ], [ 'void_usernum' ], ], + }, + + 'cust_bill_pkg_detail_void' => { + 'columns' => [ + 'detailnum', 'int', '', '', '', '', + 'billpkgnum', 'int', 'NULL', '', '', '', # should not be nullable + 'pkgnum', 'int', 'NULL', '', '', '', # deprecated + 'invnum', 'int', 'NULL', '', '', '', # deprecated + 'amount', 'decimal', 'NULL', '10,4', '', '', + 'format', 'char', 'NULL', 1, '', '', + 'classnum', 'int', 'NULL', '', '', '', + 'duration', 'int', 'NULL', '', 0, '', + 'phonenum', 'varchar', 'NULL', 15, '', '', + 'accountcode', 'varchar', 'NULL', 20, '', '', + 'startdate', @date_type, '', '', + 'regionname', 'varchar', 'NULL', $char_d, '', '', + 'detail', 'varchar', '', 255, '', '', + ], + 'primary_key' => 'detailnum', + 'unique' => [], + 'index' => [ [ 'billpkgnum' ], [ 'classnum' ], [ 'pkgnum', 'invnum' ] ], + }, + + 'cust_bill_pkg_display_void' => { + 'columns' => [ + 'billpkgdisplaynum', 'int', '', '', '', '', + 'billpkgnum', 'int', '', '', '', '', + 'section', 'varchar', 'NULL', $char_d, '', '', + #'unitsetup', @money_typen, '', '', #override the linked real one? + #'unitrecur', @money_typen, '', '', #this too? + 'post_total', 'char', 'NULL', 1, '', '', + 'type', 'char', 'NULL', 1, '', '', + 'summary', 'char', 'NULL', 1, '', '', + ], + 'primary_key' => 'billpkgdisplaynum', + 'unique' => [], + 'index' => [ ['billpkgnum'], ], + }, + + 'cust_bill_pkg_tax_location_void' => { + 'columns' => [ + 'billpkgtaxlocationnum', 'int', '', '', '', '', + 'billpkgnum', 'int', '', '', '', '', + 'taxnum', 'int', '', '', '', '', + 'taxtype', 'varchar', '', $char_d, '', '', + 'pkgnum', 'int', '', '', '', '', + 'locationnum', 'int', '', '', '', '', #redundant? + 'amount', @money_type, '', '', + ], + 'primary_key' => 'billpkgtaxlocationnum', + 'unique' => [], + 'index' => [ [ 'billpkgnum' ], [ 'taxnum' ], [ 'pkgnum' ], [ 'locationnum' ] ], + }, + + 'cust_bill_pkg_tax_rate_location_void' => { + 'columns' => [ + 'billpkgtaxratelocationnum', 'int', '', '', '', '', + 'billpkgnum', 'int', '', '', '', '', + 'taxnum', 'int', '', '', '', '', + 'taxtype', 'varchar', '', $char_d, '', '', + 'locationtaxid', 'varchar', 'NULL', $char_d, '', '', + 'taxratelocationnum', 'int', '', '', '', '', + 'amount', @money_type, '', '', + ], + 'primary_key' => 'billpkgtaxratelocationnum', + 'unique' => [], + 'index' => [ [ 'billpkgnum' ], [ 'taxnum' ], [ 'taxratelocationnum' ] ], + }, + 'cust_credit' => { 'columns' => [ 'crednum', 'serial', '', '', '', '', @@ -1419,20 +1543,29 @@ sub tables_hashref { 'columns' => [ 'paynum', 'int', '', '', '', '', 'custnum', 'int', '', '', '', '', - 'paid', @money_type, '', '', '_date', @date_type, '', '', + 'paid', @money_type, '', '', + 'otaker', 'varchar', 'NULL', 32, '', '', + 'usernum', 'int', 'NULL', '', '', '', 'payby', 'char', '', 4, '', '', # CARD/BILL/COMP, should be # index into payby table # eventually 'payinfo', 'varchar', 'NULL', 512, '', '', #see cust_main above 'paymask', 'varchar', 'NULL', $char_d, '', '', + #'paydate' ? 'paybatch', 'varchar', 'NULL', $char_d, '', '', #for auditing purposes. 'closed', 'char', 'NULL', 1, '', '', 'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances + # cash/check deposit info fields + 'bank', 'varchar', 'NULL', $char_d, '', '', + 'depositor', 'varchar', 'NULL', $char_d, '', '', + 'account', 'varchar', 'NULL', 20, '', '', + 'teller', 'varchar', 'NULL', 20, '', '', + 'batchnum', 'int', 'NULL', '', '', '', #pay_batch foreign key + + #void fields 'void_date', @date_type, '', '', 'reason', 'varchar', 'NULL', $char_d, '', '', - 'otaker', 'varchar', 'NULL', 32, '', '', - 'usernum', 'int', 'NULL', '', '', '', 'void_usernum', 'int', 'NULL', '', '', '', ], 'primary_key' => 'paynum', @@ -2568,6 +2701,26 @@ sub tables_hashref { ], }, + 'cust_tax_exempt_pkg_void' => { + 'columns' => [ + 'exemptpkgnum', 'int', '', '', '', '', + #'custnum', 'int', '', '', '', '' + 'billpkgnum', 'int', '', '', '', '', + 'taxnum', 'int', '', '', '', '', + 'year', 'int', '', '', '', '', + 'month', 'int', '', '', '', '', + 'creditbillpkgnum', 'int', 'NULL', '', '', '', + 'amount', @money_type, '', '', + ], + 'primary_key' => 'exemptpkgnum', + 'unique' => [], + 'index' => [ [ 'taxnum', 'year', 'month' ], + [ 'billpkgnum' ], + [ 'taxnum' ], + [ 'creditbillpkgnum' ], + ], + }, + 'router' => { 'columns' => [ 'routernum', 'serial', '', '', '', '', diff --git a/FS/FS/access_right.pm b/FS/FS/access_right.pm index e6266b49b..bc57364d2 100644 --- a/FS/FS/access_right.pm +++ b/FS/FS/access_right.pm @@ -193,6 +193,7 @@ sub _upgrade_data { # class method 'Suspend customer package' => 'Suspend customer', 'Unsuspend customer package' => 'Unsuspend customer', 'New prospect' => 'Generate quotation', + 'Delete invoices' => 'Void invoices', 'List services' => [ 'Services: Accounts', 'Services: Domains', diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index c3d48a61c..c5b707bb1 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -38,6 +38,7 @@ use FS::cust_bill_batch; use FS::cust_bill_pay_pkg; use FS::cust_credit_bill_pkg; use FS::discount_plan; +use FS::cust_bill_void; use FS::L10N; $DEBUG = 0; @@ -203,10 +204,63 @@ sub insert { } +=item void + +Voids this invoice: deletes the invoice and adds a record of the voided invoice +to the FS::cust_bill_void table (and related tables starting from +FS::cust_bill_pkg_void). + +=cut + +sub void { + my $self = shift; + my $reason = scalar(@_) ? shift : ''; + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $cust_bill_void = new FS::cust_bill_void ( { + map { $_ => $self->get($_) } $self->fields + } ); + $cust_bill_void->reason($reason); + my $error = $cust_bill_void->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { + my $error = $cust_bill_pkg->void($reason); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + $error = $self->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; + +} + =item delete This method now works but you probably shouldn't use it. Instead, apply a -credit against the invoice. +credit against the invoice, or use the new void method. Using this method to delete invoices outright is really, really bad. There would be no record you ever posted this invoice, and there are no check to @@ -236,11 +290,10 @@ sub delete { cust_event cust_credit_bill cust_bill_pay - cust_credit_bill cust_pay_batch cust_bill_pay_batch - cust_bill_pkg cust_bill_batch + cust_bill_pkg )) { foreach my $linked ( $self->$table() ) { diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 4220d3c06..2ceef0474 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -3,6 +3,7 @@ package FS::cust_bill_pkg; use strict; use vars qw( @ISA $DEBUG $me ); use Carp; +use List::Util qw( sum ); use Text::CSV_XS; use FS::Record qw( qsearch qsearchs dbdef dbh ); use FS::cust_main_Mixin; @@ -18,8 +19,12 @@ use FS::cust_tax_exempt_pkg; use FS::cust_bill_pkg_tax_location; use FS::cust_bill_pkg_tax_rate_location; use FS::cust_tax_adjustment; - -use List::Util qw(sum); +use FS::cust_bill_pkg_void; +use FS::cust_bill_pkg_detail_void; +use FS::cust_bill_pkg_display_void; +use FS::cust_bill_pkg_tax_location_void; +use FS::cust_bill_pkg_tax_rate_location_void; +use FS::cust_tax_exempt_pkg_void; @ISA = qw( FS::cust_main_Mixin FS::Record ); @@ -230,6 +235,74 @@ sub insert { } +=item void + +Voids this line item: deletes the line item and adds a record of the voided +line item to the FS::cust_bill_pkg_void table (and related tables). + +=cut + +sub void { + my $self = shift; + my $reason = scalar(@_) ? shift : ''; + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( { + map { $_ => $self->get($_) } $self->fields + } ); + $cust_bill_pkg_void->reason($reason); + my $error = $cust_bill_pkg_void->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + foreach my $table (qw( + cust_bill_pkg_detail + cust_bill_pkg_display + cust_bill_pkg_tax_location + cust_bill_pkg_tax_rate_location + cust_tax_exempt_pkg + )) { + + foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) { + + my $vclass = 'FS::'.$table.'_void'; + my $void = $vclass->new( { + map { $_ => $linked->get($_) } $linked->fields + }); + my $error = $void->insert || $linked->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + } + + } + + $error = $self->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; + +} + =item delete Not recommended. diff --git a/FS/FS/cust_bill_pkg_detail_void.pm b/FS/FS/cust_bill_pkg_detail_void.pm new file mode 100644 index 000000000..cebe7c1f8 --- /dev/null +++ b/FS/FS/cust_bill_pkg_detail_void.pm @@ -0,0 +1,168 @@ +package FS::cust_bill_pkg_detail_void; + +use strict; +use base qw( FS::Record ); +use FS::Record; # qw( qsearch qsearchs ); +use FS::cust_bill_pkg_void; +use FS::usage_class; + +=head1 NAME + +FS::cust_bill_pkg_detail_void - Object methods for cust_bill_pkg_detail_void records + +=head1 SYNOPSIS + + use FS::cust_bill_pkg_detail_void; + + $record = new FS::cust_bill_pkg_detail_void \%hash; + $record = new FS::cust_bill_pkg_detail_void { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_bill_pkg_detail_void object represents additional detail +information for a voided invoice line item. FS::cust_bill_pkg_detail_void +inherits from FS::Record. The following fields are currently supported: + +=over 4 + +=item detailnum + +primary key + +=item billpkgnum + +billpkgnum + +=item pkgnum + +pkgnum + +=item invnum + +invnum + +=item amount + +amount + +=item format + +format + +=item classnum + +classnum + +=item duration + +duration + +=item phonenum + +phonenum + +=item accountcode + +accountcode + +=item startdate + +startdate + +=item regionname + +regionname + +=item detail + +detail + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record. To add the record to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'cust_bill_pkg_detail_void'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +=item delete + +Delete this record from the database. + +=cut + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_number('detailnum') + || $self->ut_foreign_keyn('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum') + || $self->ut_numbern('pkgnum') + || $self->ut_numbern('invnum') + || $self->ut_floatn('amount') + || $self->ut_enum('format', [ '', 'C' ] ) + || $self->ut_foreign_keyn('classnum', 'usage_class', 'classnum') + || $self->ut_numbern('duration') + || $self->ut_textn('phonenum') + || $self->ut_textn('accountcode') + || $self->ut_numbern('startdate') + || $self->ut_textn('regionname') + || $self->ut_text('detail') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_bill_pkg_display_void.pm b/FS/FS/cust_bill_pkg_display_void.pm new file mode 100644 index 000000000..e78801a36 --- /dev/null +++ b/FS/FS/cust_bill_pkg_display_void.pm @@ -0,0 +1,132 @@ +package FS::cust_bill_pkg_display_void; + +use strict; +use base qw( FS::Record ); +use FS::Record; # qw( qsearch qsearchs ); +use FS::cust_bill_pkg_void; + +=head1 NAME + +FS::cust_bill_pkg_display_void - Object methods for cust_bill_pkg_display_void records + +=head1 SYNOPSIS + + use FS::cust_bill_pkg_display_void; + + $record = new FS::cust_bill_pkg_display_void \%hash; + $record = new FS::cust_bill_pkg_display_void { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_bill_pkg_display_void object represents voided line item display +information. FS::cust_bill_pkg_display_void inherits from FS::Record. The +following fields are currently supported: + +=over 4 + +=item billpkgdisplaynum + +primary key + +=item billpkgnum + +billpkgnum + +=item section + +section + +=item post_total + +post_total + +=item type + +type + +=item summary + +summary + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record. To add the record to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'cust_bill_pkg_display_void'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +=item delete + +Delete this record from the database. + +=cut + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_number('billpkgdisplaynum') + || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum') + || $self->ut_textn('section') + || $self->ut_enum('post_total', [ '', 'Y' ]) + || $self->ut_enum('type', [ '', 'S', 'R', 'U' ]) + || $self->ut_enum('summary', [ '', 'Y' ]) + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_bill_pkg_tax_location_void.pm b/FS/FS/cust_bill_pkg_tax_location_void.pm new file mode 100644 index 000000000..9e0794bad --- /dev/null +++ b/FS/FS/cust_bill_pkg_tax_location_void.pm @@ -0,0 +1,139 @@ +package FS::cust_bill_pkg_tax_location_void; + +use strict; +use base qw( FS::Record ); +use FS::Record; # qw( qsearch qsearchs ); +use FS::cust_bill_pkg_void; +use FS::cust_pkg; +use FS::cust_location; + +=head1 NAME + +FS::cust_bill_pkg_tax_location_void - Object methods for cust_bill_pkg_tax_location_void records + +=head1 SYNOPSIS + + use FS::cust_bill_pkg_tax_location_void; + + $record = new FS::cust_bill_pkg_tax_location_void \%hash; + $record = new FS::cust_bill_pkg_tax_location_void { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_bill_pkg_tax_location_void object represents a voided record +of taxation based on package location. FS::cust_bill_pkg_tax_location_void +inherits from FS::Record. The following fields are currently supported: + +=over 4 + +=item billpkgtaxlocationnum + +primary key + +=item billpkgnum + +billpkgnum + +=item taxnum + +taxnum + +=item taxtype + +taxtype + +=item pkgnum + +pkgnum + +=item locationnum + +locationnum + +=item amount + +amount + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record. To add the record to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'cust_bill_pkg_tax_location_void'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +=item delete + +Delete this record from the database. + +=cut + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_number('billpkgtaxlocationnum') + || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum' ) + || $self->ut_number('taxnum') #cust_bill_pkg/tax_rate key, based on taxtype + || $self->ut_enum('taxtype', [ qw( FS::cust_main_county FS::tax_rate ) ] ) + || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum' ) + || $self->ut_foreign_key('locationnum', 'cust_location', 'locationnum' ) + || $self->ut_money('amount') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_bill_pkg_tax_rate_location_void.pm b/FS/FS/cust_bill_pkg_tax_rate_location_void.pm new file mode 100644 index 000000000..f2e85c085 --- /dev/null +++ b/FS/FS/cust_bill_pkg_tax_rate_location_void.pm @@ -0,0 +1,139 @@ +package FS::cust_bill_pkg_tax_rate_location_void; + +use strict; +use base qw( FS::Record ); +use FS::Record; # qw( qsearch qsearchs ); +use FS::cust_bill_pkg_void; +use FS::tax_rate_location; + +=head1 NAME + +FS::cust_bill_pkg_tax_rate_location_void - Object methods for cust_bill_pkg_tax_rate_location_void records + +=head1 SYNOPSIS + + use FS::cust_bill_pkg_tax_rate_location_void; + + $record = new FS::cust_bill_pkg_tax_rate_location_void \%hash; + $record = new FS::cust_bill_pkg_tax_rate_location_void { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_bill_pkg_tax_rate_location_void object represents a voided record +of taxation based on package location. +FS::cust_bill_pkg_tax_rate_location_void inherits from FS::Record. The +following fields are currently supported: + +=over 4 + +=item billpkgtaxratelocationnum + +primary key + +=item billpkgnum + +billpkgnum + +=item taxnum + +taxnum + +=item taxtype + +taxtype + +=item locationtaxid + +locationtaxid + +=item taxratelocationnum + +taxratelocationnum + +=item amount + +amount + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record. To add the record to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'cust_bill_pkg_tax_rate_location_void'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +=item delete + +Delete this record from the database. + +=cut + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_number('billpkgtaxratelocationnum') + || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum' ) + || $self->ut_number('taxnum') #cust_bill_pkg/tax_rate key, based on taxtype + || $self->ut_text('taxtype', [ qw( FS::cust_main_county FS::tax_rate ) ] ) + || $self->ut_textn('locationtaxid') + || $self->ut_foreign_key('taxratelocationnum', 'tax_rate_location', 'taxratelocationnum' ) + || $self->ut_money('amount') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_bill_pkg_void.pm b/FS/FS/cust_bill_pkg_void.pm new file mode 100644 index 000000000..198283955 --- /dev/null +++ b/FS/FS/cust_bill_pkg_void.pm @@ -0,0 +1,181 @@ +package FS::cust_bill_pkg_void; + +use strict; +use base qw( FS::Record ); +use FS::Record; # qw( qsearch qsearchs ); + +=head1 NAME + +FS::cust_bill_pkg_void - Object methods for cust_bill_pkg_void records + +=head1 SYNOPSIS + + use FS::cust_bill_pkg_void; + + $record = new FS::cust_bill_pkg_void \%hash; + $record = new FS::cust_bill_pkg_void { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_bill_pkg_void object represents a voided invoice line item. +FS::cust_bill_pkg_void inherits from FS::Record. The following fields are +currently supported: + +=over 4 + +=item billpkgnum + +primary key + +=item invnum + +invnum + +=item pkgnum + +pkgnum + +=item pkgpart_override + +pkgpart_override + +=item setup + +setup + +=item recur + +recur + +=item sdate + +sdate + +=item edate + +edate + +=item itemdesc + +itemdesc + +=item itemcomment + +itemcomment + +=item section + +section + +=item freq + +freq + +=item quantity + +quantity + +=item unitsetup + +unitsetup + +=item unitrecur + +unitrecur + +=item hidden + +hidden + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record. To add the record to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'cust_bill_pkg_void'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +=item delete + +Delete this record from the database. + +=cut + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_number('billpkgnum') + || $self->ut_snumber('pkgnum') + || $self->ut_number('invnum') #cust_bill or cust_bill_void ? + || $self->ut_numbern('pkgpart_override') + || $self->ut_money('setup') + || $self->ut_money('recur') + || $self->ut_numbern('sdate') + || $self->ut_numbern('edate') + || $self->ut_textn('itemdesc') + || $self->ut_textn('itemcomment') + || $self->ut_textn('section') + || $self->ut_textn('freq') + || $self->ut_numbern('quantity') + || $self->ut_moneyn('unitsetup') + || $self->ut_moneyn('unitrecur') + || $self->ut_enum('hidden', [ '', 'Y' ]) + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_bill_void.pm b/FS/FS/cust_bill_void.pm new file mode 100644 index 000000000..c782172b5 --- /dev/null +++ b/FS/FS/cust_bill_void.pm @@ -0,0 +1,217 @@ +package FS::cust_bill_void; +use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record ); + +use strict; +use FS::Record qw( qsearchs ); #qsearch ); +use FS::cust_main; +use FS::cust_statement; +use FS::access_user; + +=head1 NAME + +FS::cust_bill_void - Object methods for cust_bill_void records + +=head1 SYNOPSIS + + use FS::cust_bill_void; + + $record = new FS::cust_bill_void \%hash; + $record = new FS::cust_bill_void { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_bill_void object represents a voided invoice. FS::cust_bill_void +inherits from FS::Record. The following fields are currently supported: + +=over 4 + +=item invnum + +primary key + +=item custnum + +custnum + +=item _date + +_date + +=item charged + +charged + +=item invoice_terms + +invoice_terms + +=item previous_balance + +previous_balance + +=item billing_balance + +billing_balance + +=item closed + +closed + +=item statementnum + +statementnum + +=item agent_invid + +agent_invid + +=item promised_date + +promised_date + +=item void_date + +void_date + +=item reason + +reason + +=item void_usernum + +void_usernum + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new voided invoice. To add the voided invoice to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'cust_bill_void'; } +sub notice_name { 'VOIDED Invoice'; } +#XXXsub template_conf { 'quotation_'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# the insert method can be inherited from FS::Record + +=item delete + +Delete this record from the database. + +=cut + +# the delete method can be inherited from FS::Record + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +# the replace method can be inherited from FS::Record + +=item check + +Checks all fields to make sure this is a valid voided invoice. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { + my $self = shift; + + my $error = + $self->ut_number('invnum') + || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' ) + || $self->ut_numbern('_date') + || $self->ut_money('charged') + || $self->ut_textn('invoice_terms') + || $self->ut_moneyn('previous_balance') + || $self->ut_moneyn('billing_balance') + || $self->ut_enum('closed', [ '', 'Y' ]) + || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum') + || $self->ut_numbern('agent_invid') + || $self->ut_numbern('promised_date') + || $self->ut_numbern('void_date') + || $self->ut_textn('reason') + || $self->ut_numbern('void_usernum') + ; + return $error if $error; + + $self->void_date(time) unless $self->void_date; + + $self->void_usernum($FS::CurrentUser::CurrentUser->usernum) + unless $self->void_usernum; + + $self->SUPER::check; +} + +=item display_invnum + +Returns the displayed invoice number for this invoice: agent_invid if +cust_bill-default_agent_invid is set and it has a value, invnum otherwise. + +=cut + +sub display_invnum { + my $self = shift; + my $conf = $self->conf; + if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){ + return $self->agent_invid; + } else { + return $self->invnum; + } +} + +=item void_access_user + +Returns the voiding employee object (see L). + +=cut + +sub void_access_user { + my $self = shift; + qsearchs('access_user', { 'usernum' => $self->void_usernum } ); +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 96029415a..36c62808c 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -42,6 +42,7 @@ use FS::payby; use FS::cust_pkg; use FS::cust_svc; use FS::cust_bill; +use FS::cust_bill_void; use FS::legacy_cust_bill; use FS::cust_pay; use FS::cust_pay_pending; @@ -1279,6 +1280,7 @@ sub merge { tie my %financial_tables, 'Tie::IxHash', 'cust_bill' => 'invoices', + 'cust_bill_void' => 'voided invoices', 'cust_statement' => 'statements', 'cust_credit' => 'credits', 'cust_pay' => 'payments', @@ -3646,6 +3648,20 @@ be passed. =cut +=item cust_bill_void + +Returns all the voided invoices (see L) for this customer. + +=cut + +sub cust_bill_void { + my $self = shift; + + map { $_ } #return $self->num_cust_bill_void unless wantarray; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_bill_void', { 'custnum' => $self->custnum } ) +} + sub cust_statement { my $self = shift; my $opt = ref($_[0]) ? shift : { @_ }; @@ -3802,7 +3818,7 @@ sub cust_pay_void { =item cust_pay_batch [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ] -Returns all batched payments (see L) for this customer. +Returns all batched payments (see L) for this customer. Optionally, a list or hashref of additional arguments to the qsearch call can be passed. diff --git a/FS/FS/cust_tax_exempt_pkg_void.pm b/FS/FS/cust_tax_exempt_pkg_void.pm new file mode 100644 index 000000000..51c85b463 --- /dev/null +++ b/FS/FS/cust_tax_exempt_pkg_void.pm @@ -0,0 +1,138 @@ +package FS::cust_tax_exempt_pkg_void; + +use strict; +use base qw( FS::Record ); +use FS::Record; # qw( qsearch qsearchs ); +use FS::cust_bill_pkg_void; +use FS::cust_main_county; + +=head1 NAME + +FS::cust_tax_exempt_pkg_void - Object methods for cust_tax_exempt_pkg_void records + +=head1 SYNOPSIS + + use FS::cust_tax_exempt_pkg_void; + + $record = new FS::cust_tax_exempt_pkg_void \%hash; + $record = new FS::cust_tax_exempt_pkg_void { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_tax_exempt_pkg_void object represents a voided record of a customer +tax exemption. FS::cust_tax_exempt_pkg_void inherits from FS::Record. The +following fields are currently supported: + +=over 4 + +=item exemptpkgnum + +primary key + +=item billpkgnum + +billpkgnum + +=item taxnum + +taxnum + +=item year + +year + +=item month + +month + +=item creditbillpkgnum + +creditbillpkgnum + +=item amount + +amount + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record. To add the record to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'cust_tax_exempt_pkg_void'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +=item delete + +Delete this record from the database. + +=cut + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_number('exemptpkgnum') + || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum' ) + || $self->ut_foreign_key('taxnum', 'cust_main_county', 'taxnum') + || $self->ut_number('year') + || $self->ut_number('month') + || $self->ut_numbern('creditbillpkgnum') #no FK check, will have been del'ed + || $self->ut_money('amount') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/httemplate/misc/process/void-cust_bill.html b/httemplate/misc/process/void-cust_bill.html new file mode 100755 index 000000000..f2930ec01 --- /dev/null +++ b/httemplate/misc/process/void-cust_bill.html @@ -0,0 +1,22 @@ +%if ( $error ) { +% $cgi->param('error', $error); +<% $cgi->redirect(popurl(1). "void-cust_bill.html?". $cgi->query_string ) %> +%} else { +<% $cgi->redirect(popurl(3). "view/cust_main.cgi?". $custnum) %> +%} +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Void invoices'); + +#untaint invnum +$cgi->param('invnum') =~ /^(\d+)$/ || die "Illegal invnum"; +my $invnum = $1; + +my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum}); + +my $custnum = $cust_bill->custnum; + +my $error = $cust_bill->void( $cgi->param('reason') ); + + diff --git a/httemplate/misc/void-cust_bill.html b/httemplate/misc/void-cust_bill.html new file mode 100644 index 000000000..1608fd051 --- /dev/null +++ b/httemplate/misc/void-cust_bill.html @@ -0,0 +1,45 @@ +<& /elements/header-popup.html, mt('Void invoice') &> + +<% include('/elements/error.html') %> + +<% emt('Are you sure you want to void this invoice?') %> +

+ +<% emt("Invoice #[_1] ([_2])",$cust_bill->display_invnum, $money_char. $cust_bill->owed) %> +

+ + + + +<% ntable("#cccccc", 2) %> + + Reason + + + + + +
+
+   \ + +
+ + + + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Void invoices'); + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +#untaint invnum +$cgi->param('invnum') =~ /^(\d+)$/ || die "Illegal invnum"; +my $invnum = $1; + +my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum}); + + diff --git a/httemplate/view/cust_bill_void.html b/httemplate/view/cust_bill_void.html new file mode 100755 index 000000000..c7c5da146 --- /dev/null +++ b/httemplate/view/cust_bill_void.html @@ -0,0 +1,56 @@ +<& /elements/header.html, mt('Voided Invoice'), menubar( + emt("View this customer (#[_1])",$display_custnum) => "${p}view/cust_main.cgi?$custnum", +) &> + +%#XXX something very big and obvious showing its voided... + +% #voided PDFs? +% #if ( $conf->exists('invoice_latex') ) { +%# +%# <% mt('View typeset invoice PDF') |h %> +%#

+% #} + +% if ( $conf->exists('invoice_html') ) { + <% join('', $cust_bill_void->print_html(\%opt) ) %> +% } else { +
<% join('', $cust_bill_void->print_text(\%opt) ) %>
+% } + +<& /elements/footer.html &> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('View invoices'); + +my $invnum; +my($query) = $cgi->keywords; +if ( $query =~ /^(\d+)$/ ) { + $invnum = $1; +} else { + $invnum = $cgi->param('invnum'); +} + +my $conf = new FS::Conf; + +my %opt = ( + 'unsquelch_cdr' => $conf->exists('voip-cdr_email'), +); + +my $cust_bill_void = qsearchs({ + 'select' => 'cust_bill_void.*', + 'table' => 'cust_bill_void', + #'addl_from' => 'LEFT JOIN cust_main USING ( custnum )', + 'hashref' => { 'quotationnum' => $quotationnum }, + #'extra_sql' => ' AND '. $curuser->agentnums_sql, +}); +die "Quotation #$quotationnum not found!" unless $quotation; + +my $custnum = $cust_bill->custnum; +my $display_custnum = $cust_bill->cust_main->display_custnum; + +#my $link = "invnum=$invnum"; + + diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html index 9e08c0c5d..166addbf4 100644 --- a/httemplate/view/cust_main/payment_history.html +++ b/httemplate/view/cust_main/payment_history.html @@ -277,7 +277,9 @@ % ? sprintf("$money_char\%.2f", $item->{'charge'}) % : exists($item->{'charge_nobal'}) % ? sprintf("$money_char\%.2f", $item->{'charge_nobal'}) -% : ''; +% : exists($item->{'void_charge'}) +% ? sprintf("$money_char\%.2f", $item->{'void_charge'}) +% : ''; % % my $payment = exists($item->{'payment'}) % ? sprintf("- $money_char\%.2f", $item->{'payment'}) @@ -428,6 +430,15 @@ foreach my $cust_bill ($cust_main->cust_bill) { $num_cust_bill++; } +#voided invoices +foreach my $cust_bill_void ($cust_main->cust_bill_void) { + push @history, { + 'date' => $cust_bill_void->_date, + 'desc' => include('payment_history/voided_invoice.html', $cust_bill_void, %opt ), + 'void_charge' => $cust_bill_void->charged, + }; +} + #statements foreach my $cust_statement ($cust_main->cust_statement) { push @history, { diff --git a/httemplate/view/cust_main/payment_history/invoice.html b/httemplate/view/cust_main/payment_history/invoice.html index 3028f0f69..96a9f5456 100644 --- a/httemplate/view/cust_main/payment_history/invoice.html +++ b/httemplate/view/cust_main/payment_history/invoice.html @@ -1,4 +1,4 @@ -<% $link %><% $invoice %><% $link ? '' : '' %><% $delete %><% $under %> +<% $link %><% $invoice %><% $link ? '' : '' %><% "$void$delete$under" %> <%init> my( $cust_bill, %opt ) = @_; @@ -26,6 +26,18 @@ my $link = $curuser->access_right('View invoices') ? qq!! : ''; +my $void = ''; +if ( $cust_bill->closed !~ /^Y/i && $curuser->access_right('Void invoices') ) { + $void = + ' ('. include('/elements/popup_link.html', + 'label' => emt('void'), + 'action' => "${p}misc/void-cust_bill.html?;invnum=". + $cust_bill->invnum, + 'actionlabel' => emt('Void Invoice'), + ). + ')'; +} + my $delete = ''; $delete = areyousure_link("${p}misc/delete-cust_bill.html?$invnum", emt('Are you sure you want to delete this invoice?'), diff --git a/httemplate/view/cust_main/payment_history/voided_invoice.html b/httemplate/view/cust_main/payment_history/voided_invoice.html new file mode 100644 index 000000000..422edb2f6 --- /dev/null +++ b/httemplate/view/cust_main/payment_history/voided_invoice.html @@ -0,0 +1,52 @@ +<% $link %><% $invoice %><% $link ? '' : '' %> +<% mt("voided [_1]", time2str($date_format, $cust_bill_void->void_date) ) |h %> +% my $void_user = $cust_bill_void->void_access_user; +% if ($void_user) { + by <% $void_user->username %> +% } +<% "$unvoid$delete$under" %> +<%init> + +my( $cust_bill_void, %opt ) = @_; + +my $date_format = $opt{'date_format'} || '%m/%d/%Y'; + +my $conf = new FS::Conf; + +my $curuser = $FS::CurrentUser::CurrentUser; + +my $invoice = emt("Invoice #[_1] (Balance [_2])",$cust_bill_void->display_invnum, $cust_bill_void->charged); + +my $under = ''; + +my $invnum = $cust_bill_void->invnum; + +#XXX use cust_bill.cgi or? +my $link = $curuser->access_right('View invoices') + ? qq!! + : ''; + +my $unvoid = ''; #XXX unvoid + +my $delete = ''; +$delete = areyousure_link("${p}misc/delete-cust_bill.html?$invnum", + emt('Are you sure you want to delete this invoice?'), + emt('Delete this invoice from the database completely'), + emt('delete') + ) + if ( $opt{'deleteinvoices'} && $curuser->access_right('Delete invoices') ); + +my $events = ''; +#1.9 +if ( $cust_bill_void->num_cust_event + && ( $curuser->access_right('Billing event reports') + || $curuser->access_right('View customer billing events') + ) + ) { + $under .= + qq!
( !. + emt('View invoice events').' )'; +} +$under = ''.$under.'' if length($under); + + -- cgit v1.2.1 From 5c48396fab6b19e33dbeac6f807860441465fa3b Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 31 Jul 2012 23:05:06 -0700 Subject: invoice voiding, RT#18677 --- FS/FS/Mason.pm | 7 +++++++ FS/MANIFEST | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 51edd97cc..c45e783db 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -315,6 +315,13 @@ if ( -e $addl_handler_use_file ) { use FS::quotation; use FS::quotation_pkg; use FS::quotation_pkg_discount; + use FS::cust_bill_void; + use FS::cust_bill_pkg_void; + use FS::cust_bill_pkg_detail_void; + use FS::cust_bill_pkg_display_void; + use FS::cust_bill_pkg_tax_location_void; + use FS::cust_bill_pkg_tax_rate_location_void; + use FS::cust_tax_exempt_pkg_void; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/MANIFEST b/FS/MANIFEST index 590874d46..01dab2092 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -649,3 +649,17 @@ FS/quotation_pkg_discount.pm t/quotation_pkg_discount.t FS/Quotable_Mixin.pm t/Quotable_Mixin.t +FS/cust_bill_void.pm +t/cust_bill_void.t +FS/cust_bill_pkg_void.pm +t/cust_bill_pkg_void.t +FS/cust_bill_pkg_detail_void.pm +t/cust_bill_pkg_detail_void.t +FS/cust_bill_pkg_display_void.pm +t/cust_bill_pkg_display_void.t +FS/cust_bill_pkg_tax_location_void.pm +t/cust_bill_pkg_tax_location_void.t +FS/cust_bill_pkg_tax_rate_location_void.pm +t/cust_bill_pkg_tax_rate_location_void.t +FS/cust_tax_exempt_pkg_void.pm +t/cust_tax_exempt_pkg_void.t -- cgit v1.2.1 From 338eca0837fdaddbda2a34ddf8af3f815de13d26 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 31 Jul 2012 23:05:22 -0700 Subject: invoice voiding, RT#18677 --- FS/t/cust_bill_pkg_detail_void.t | 5 +++++ FS/t/cust_bill_pkg_display_void.t | 5 +++++ FS/t/cust_bill_pkg_tax_location_void.t | 5 +++++ FS/t/cust_bill_pkg_tax_rate_location_void.t | 5 +++++ FS/t/cust_bill_pkg_void.t | 5 +++++ FS/t/cust_bill_void.t | 5 +++++ 6 files changed, 30 insertions(+) create mode 100644 FS/t/cust_bill_pkg_detail_void.t create mode 100644 FS/t/cust_bill_pkg_display_void.t create mode 100644 FS/t/cust_bill_pkg_tax_location_void.t create mode 100644 FS/t/cust_bill_pkg_tax_rate_location_void.t create mode 100644 FS/t/cust_bill_pkg_void.t create mode 100644 FS/t/cust_bill_void.t diff --git a/FS/t/cust_bill_pkg_detail_void.t b/FS/t/cust_bill_pkg_detail_void.t new file mode 100644 index 000000000..bd58c4eab --- /dev/null +++ b/FS/t/cust_bill_pkg_detail_void.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_bill_pkg_detail_void; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/cust_bill_pkg_display_void.t b/FS/t/cust_bill_pkg_display_void.t new file mode 100644 index 000000000..87403e12e --- /dev/null +++ b/FS/t/cust_bill_pkg_display_void.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_bill_pkg_display_void; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/cust_bill_pkg_tax_location_void.t b/FS/t/cust_bill_pkg_tax_location_void.t new file mode 100644 index 000000000..dbfea5131 --- /dev/null +++ b/FS/t/cust_bill_pkg_tax_location_void.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_bill_pkg_tax_location_void; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/cust_bill_pkg_tax_rate_location_void.t b/FS/t/cust_bill_pkg_tax_rate_location_void.t new file mode 100644 index 000000000..8ebda6528 --- /dev/null +++ b/FS/t/cust_bill_pkg_tax_rate_location_void.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_bill_pkg_tax_rate_location_void; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/cust_bill_pkg_void.t b/FS/t/cust_bill_pkg_void.t new file mode 100644 index 000000000..9256b469f --- /dev/null +++ b/FS/t/cust_bill_pkg_void.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_bill_pkg_void; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/cust_bill_void.t b/FS/t/cust_bill_void.t new file mode 100644 index 000000000..95ff4a45c --- /dev/null +++ b/FS/t/cust_bill_void.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_bill_void; +$loaded=1; +print "ok 1\n"; -- cgit v1.2.1 From 55675d6cdd93f00b7c0ac93403e8c4d66567a729 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Wed, 1 Aug 2012 13:16:42 -0700 Subject: invoice voiding, RT#18677 --- FS/FS/TemplateItem_Mixin.pm | 317 +++++++++++++++++++++ FS/FS/cust_bill_pkg.pm | 317 +-------------------- FS/FS/cust_bill_pkg_discount_void.pm | 129 +++++++++ FS/FS/cust_bill_pkg_void.pm | 30 +- FS/FS/cust_bill_void.pm | 30 +- FS/t/cust_bill_pkg_discount_void.t | 5 + httemplate/view/cust_bill_void.html | 13 +- .../cust_main/payment_history/voided_invoice.html | 3 +- 8 files changed, 528 insertions(+), 316 deletions(-) create mode 100644 FS/FS/TemplateItem_Mixin.pm create mode 100644 FS/FS/cust_bill_pkg_discount_void.pm create mode 100644 FS/t/cust_bill_pkg_discount_void.t diff --git a/FS/FS/TemplateItem_Mixin.pm b/FS/FS/TemplateItem_Mixin.pm new file mode 100644 index 000000000..6d7ea26bc --- /dev/null +++ b/FS/FS/TemplateItem_Mixin.pm @@ -0,0 +1,317 @@ +package FS::TemplateItem_Mixin; + +use strict; +use vars qw( $DEBUG $me ); # but NOT $conf +use Carp; +use FS::UID; +use FS::Record qw( qsearch qsearchs dbh ); +use FS::part_pkg; +use FS::cust_pkg; + +$DEBUG = 0; +$me = '[FS::TemplateItem_Mixin]'; + +=item cust_pkg + +Returns the package (see L) for this invoice line item. + +=cut + +sub cust_pkg { + my $self = shift; + carp "$me $self -> cust_pkg" if $DEBUG; + qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } ); +} + +=item part_pkg + +Returns the package definition for this invoice line item. + +=cut + +sub part_pkg { + my $self = shift; + if ( $self->pkgpart_override ) { + qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } ); + } else { + my $part_pkg; + my $cust_pkg = $self->cust_pkg; + $part_pkg = $cust_pkg->part_pkg if $cust_pkg; + $part_pkg; + } + +} + +=item desc + +Returns a description for this line item. For typical line items, this is the +I field of the corresponding B object (see L). +For one-shot line items and named taxes, it is the I field of this +line item, and for generic taxes, simply returns "Tax". + +=cut + +sub desc { + my $self = shift; + + if ( $self->pkgnum > 0 ) { + $self->itemdesc || $self->part_pkg->pkg; + } else { + my $desc = $self->itemdesc || 'Tax'; + $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/; + $desc; + } +} + +=item details [ OPTION => VALUE ... ] + +Returns an array of detail information for the invoice line item. + +Currently available options are: I, I and +I. + +If I is set to html or latex then the array members are improved +for tabular appearance in those environments if possible. + +If I is set then the array members are processed by this +function before being returned. + +I overrides the normal HTML or LaTeX function for returning +formatted CDRs. It can be set to a subroutine which returns an empty list +to skip usage detail: + + 'format_function' => sub { () }, + +=cut + +sub details { + my ( $self, %opt ) = @_; + my $escape_function = $opt{escape_function} || sub { shift }; + + my $csv = new Text::CSV_XS; + + if ( $opt{format_function} ) { + + #this still expects to be passed a cust_bill_pkg_detail object as the + #second argument, which is expensive + carp "deprecated format_function passed to cust_bill_pkg->details"; + my $format_sub = $opt{format_function} if $opt{format_function}; + + map { ( $_->format eq 'C' + ? &{$format_sub}( $_->detail, $_ ) + : &{$escape_function}( $_->detail ) + ) + } + qsearch ({ 'table' => $self->detail_table, + 'hashref' => { 'billpkgnum' => $self->billpkgnum }, + 'order_by' => 'ORDER BY detailnum', + }); + + } elsif ( $opt{'no_usage'} ) { + + my $sql = "SELECT detail FROM ". $self->detail_table. + " WHERE billpkgnum = ". $self->billpkgnum. + " AND ( format IS NULL OR format != 'C' ) ". + " ORDER BY detailnum"; + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute or die $sth->errstr; + + map &{$escape_function}( $_->[0] ), @{ $sth->fetchall_arrayref }; + + } else { + + my $format_sub; + my $format = $opt{format} || ''; + if ( $format eq 'html' ) { + + $format_sub = sub { my $detail = shift; + $csv->parse($detail) or return "can't parse $detail"; + join('', map { &$escape_function($_) } + $csv->fields + ); + }; + + } elsif ( $format eq 'latex' ) { + + $format_sub = sub { + my $detail = shift; + $csv->parse($detail) or return "can't parse $detail"; + #join(' & ', map { '\small{'. &$escape_function($_). '}' } + # $csv->fields ); + my $result = ''; + my $column = 1; + foreach ($csv->fields) { + $result .= ' & ' if $column > 1; + if ($column > 6) { # KLUDGE ALERT! + $result .= '\multicolumn{1}{l}{\scriptsize{'. + &$escape_function($_). '}}'; + }else{ + $result .= '\scriptsize{'. &$escape_function($_). '}'; + } + $column++; + } + $result; + }; + + } else { + + $format_sub = sub { my $detail = shift; + $csv->parse($detail) or return "can't parse $detail"; + join(' - ', map { &$escape_function($_) } + $csv->fields + ); + }; + + } + + my $sql = "SELECT format, detail FROM ". $self->detail_table. + " WHERE billpkgnum = ". $self->billpkgnum. + " ORDER BY detailnum"; + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute or die $sth->errstr; + + #avoid the fetchall_arrayref and loop for less memory usage? + + map { (defined($_->[0]) && $_->[0] eq 'C') + ? &{$format_sub}( $_->[1] ) + : &{$escape_function}( $_->[1] ); + } + @{ $sth->fetchall_arrayref }; + + } + +} + +=item details_header [ OPTION => VALUE ... ] + +Returns a list representing an invoice line item detail header, if any. +This relies on the behavior of voip_cdr in that it expects the header +to be the first CSV formatted detail (as is expected by invoice generation +routines). Returns the empty list otherwise. + +=cut + +sub details_header { + my $self = shift; + + my $csv = new Text::CSV_XS; + + my @detail = + qsearch ({ 'table' => $self->detail_table, + 'hashref' => { 'billpkgnum' => $self->billpkgnum, + 'format' => 'C', + }, + 'order_by' => 'ORDER BY detailnum LIMIT 1', + }); + return() unless scalar(@detail); + $csv->parse($detail[0]->detail) or return (); + $csv->fields; +} + +=item quantity + +=cut + +sub quantity { + my( $self, $value ) = @_; + if ( defined($value) ) { + $self->setfield('quantity', $value); + } + $self->getfield('quantity') || 1; +} + +=item unitsetup + +=cut + +sub unitsetup { + my( $self, $value ) = @_; + if ( defined($value) ) { + $self->setfield('unitsetup', $value); + } + $self->getfield('unitsetup') eq '' + ? $self->getfield('setup') + : $self->getfield('unitsetup'); +} + +=item unitrecur + +=cut + +sub unitrecur { + my( $self, $value ) = @_; + if ( defined($value) ) { + $self->setfield('unitrecur', $value); + } + $self->getfield('unitrecur') eq '' + ? $self->getfield('recur') + : $self->getfield('unitrecur'); +} + +=item cust_bill_pkg_display [ type => TYPE ] + +Returns an array of display information for the invoice line item optionally +limited to 'TYPE'. + +=cut + +sub cust_bill_pkg_display { + my ( $self, %opt ) = @_; + + my $class = 'FS::'. $self->display_table; + + my $default = $class->new( { billpkgnum =>$self->billpkgnum } ); + + my $type = $opt{type} if exists $opt{type}; + my @result; + + if ( $self->get('display') ) { + @result = grep { defined($type) ? ($type eq $_->type) : 1 } + @{ $self->get('display') }; + } else { + my $hashref = { 'billpkgnum' => $self->billpkgnum }; + $hashref->{type} = $type if defined($type); + + @result = qsearch ({ 'table' => $self->display_table, + 'hashref' => { 'billpkgnum' => $self->billpkgnum }, + 'order_by' => 'ORDER BY billpkgdisplaynum', + }); + } + + push @result, $default unless ( scalar(@result) || $type ); + + @result; + +} + +=item cust_bill_pkg_detail [ CLASSNUM ] + +Returns the list of associated cust_bill_pkg_detail objects +The optional CLASSNUM argument will limit the details to the specified usage +class. + +=cut + +sub cust_bill_pkg_detail { + my $self = shift; + my $classnum = shift || ''; + + my %hash = ( 'billpkgnum' => $self->billpkgnum ); + $hash{classnum} = $classnum if $classnum; + + qsearch( $self->detail_table, \%hash ), + +} + +=item cust_bill_pkg_discount + +Returns the list of associated cust_bill_pkg_discount objects. + +=cut + +sub cust_bill_pkg_discount { + my $self = shift; + qsearch( $self->discount_table, { 'billpkgnum' => $self->billpkgnum } ); +} + +1; diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 2ceef0474..304d51d6a 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -1,14 +1,13 @@ package FS::cust_bill_pkg; +use base qw( FS::TemplateItem_Mixin FS::cust_main_Mixin FS::Record ); use strict; use vars qw( @ISA $DEBUG $me ); use Carp; use List::Util qw( sum ); use Text::CSV_XS; -use FS::Record qw( qsearch qsearchs dbdef dbh ); -use FS::cust_main_Mixin; +use FS::Record qw( qsearch qsearchs dbh ); use FS::cust_pkg; -use FS::part_pkg; use FS::cust_bill; use FS::cust_bill_pkg_detail; use FS::cust_bill_pkg_display; @@ -26,7 +25,6 @@ use FS::cust_bill_pkg_tax_location_void; use FS::cust_bill_pkg_tax_rate_location_void; use FS::cust_tax_exempt_pkg_void; -@ISA = qw( FS::cust_main_Mixin FS::Record ); $DEBUG = 0; $me = '[FS::cust_bill_pkg]'; @@ -125,6 +123,13 @@ customer object (see L). sub table { 'cust_bill_pkg'; } +sub detail_table { 'cust_bill_pkg_detail'; } +sub display_table { 'cust_bill_pkg_display'; } +sub discount_table { 'cust_bill_pkg_discount'; } +#sub tax_location_table { 'cust_bill_pkg_tax_location'; } +#sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; } +#sub tax_exempt_pkg_table { 'cust_tax_exempt_pkg'; } + =item insert Adds this line item to the database. If there is an error, returns the error, @@ -270,6 +275,7 @@ sub void { foreach my $table (qw( cust_bill_pkg_detail cust_bill_pkg_display + cust_bill_pkg_discount cust_bill_pkg_tax_location cust_bill_pkg_tax_rate_location cust_tax_exempt_pkg @@ -326,6 +332,7 @@ sub delete { foreach my $table (qw( cust_bill_pkg_detail cust_bill_pkg_display + cust_bill_pkg_discount cust_bill_pkg_tax_location cust_bill_pkg_tax_rate_location cust_tax_exempt_pkg @@ -462,36 +469,6 @@ sub regularize_details { return; } -=item cust_pkg - -Returns the package (see L) for this invoice line item. - -=cut - -sub cust_pkg { - my $self = shift; - carp "$me $self -> cust_pkg" if $DEBUG; - qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } ); -} - -=item part_pkg - -Returns the package definition for this invoice line item. - -=cut - -sub part_pkg { - my $self = shift; - if ( $self->pkgpart_override ) { - qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } ); - } else { - my $part_pkg; - my $cust_pkg = $self->cust_pkg; - $part_pkg = $cust_pkg->part_pkg if $cust_pkg; - $part_pkg; - } -} - =item cust_bill Returns the invoice (see L) for this invoice line item. @@ -521,173 +498,6 @@ sub previous_cust_bill_pkg { }); } -=item details [ OPTION => VALUE ... ] - -Returns an array of detail information for the invoice line item. - -Currently available options are: I, I and -I. - -If I is set to html or latex then the array members are improved -for tabular appearance in those environments if possible. - -If I is set then the array members are processed by this -function before being returned. - -I overrides the normal HTML or LaTeX function for returning -formatted CDRs. It can be set to a subroutine which returns an empty list -to skip usage detail: - - 'format_function' => sub { () }, - -=cut - -sub details { - my ( $self, %opt ) = @_; - my $escape_function = $opt{escape_function} || sub { shift }; - - my $csv = new Text::CSV_XS; - - if ( $opt{format_function} ) { - - #this still expects to be passed a cust_bill_pkg_detail object as the - #second argument, which is expensive - carp "deprecated format_function passed to cust_bill_pkg->details"; - my $format_sub = $opt{format_function} if $opt{format_function}; - - map { ( $_->format eq 'C' - ? &{$format_sub}( $_->detail, $_ ) - : &{$escape_function}( $_->detail ) - ) - } - qsearch ({ 'table' => 'cust_bill_pkg_detail', - 'hashref' => { 'billpkgnum' => $self->billpkgnum }, - 'order_by' => 'ORDER BY detailnum', - }); - - } elsif ( $opt{'no_usage'} ) { - - my $sql = "SELECT detail FROM cust_bill_pkg_detail ". - " WHERE billpkgnum = ". $self->billpkgnum. - " AND ( format IS NULL OR format != 'C' ) ". - " ORDER BY detailnum"; - my $sth = dbh->prepare($sql) or die dbh->errstr; - $sth->execute or die $sth->errstr; - - map &{$escape_function}( $_->[0] ), @{ $sth->fetchall_arrayref }; - - } else { - - my $format_sub; - my $format = $opt{format} || ''; - if ( $format eq 'html' ) { - - $format_sub = sub { my $detail = shift; - $csv->parse($detail) or return "can't parse $detail"; - join('', map { &$escape_function($_) } - $csv->fields - ); - }; - - } elsif ( $format eq 'latex' ) { - - $format_sub = sub { - my $detail = shift; - $csv->parse($detail) or return "can't parse $detail"; - #join(' & ', map { '\small{'. &$escape_function($_). '}' } - # $csv->fields ); - my $result = ''; - my $column = 1; - foreach ($csv->fields) { - $result .= ' & ' if $column > 1; - if ($column > 6) { # KLUDGE ALERT! - $result .= '\multicolumn{1}{l}{\scriptsize{'. - &$escape_function($_). '}}'; - }else{ - $result .= '\scriptsize{'. &$escape_function($_). '}'; - } - $column++; - } - $result; - }; - - } else { - - $format_sub = sub { my $detail = shift; - $csv->parse($detail) or return "can't parse $detail"; - join(' - ', map { &$escape_function($_) } - $csv->fields - ); - }; - - } - - my $sql = "SELECT format, detail FROM cust_bill_pkg_detail ". - " WHERE billpkgnum = ". $self->billpkgnum. - " ORDER BY detailnum"; - my $sth = dbh->prepare($sql) or die dbh->errstr; - $sth->execute or die $sth->errstr; - - #avoid the fetchall_arrayref and loop for less memory usage? - - map { (defined($_->[0]) && $_->[0] eq 'C') - ? &{$format_sub}( $_->[1] ) - : &{$escape_function}( $_->[1] ); - } - @{ $sth->fetchall_arrayref }; - - } - -} - -=item details_header [ OPTION => VALUE ... ] - -Returns a list representing an invoice line item detail header, if any. -This relies on the behavior of voip_cdr in that it expects the header -to be the first CSV formatted detail (as is expected by invoice generation -routines). Returns the empty list otherwise. - -=cut - -sub details_header { - my $self = shift; - return '' unless defined dbdef->table('cust_bill_pkg_detail'); - - my $csv = new Text::CSV_XS; - - my @detail = - qsearch ({ 'table' => 'cust_bill_pkg_detail', - 'hashref' => { 'billpkgnum' => $self->billpkgnum, - 'format' => 'C', - }, - 'order_by' => 'ORDER BY detailnum LIMIT 1', - }); - return() unless scalar(@detail); - $csv->parse($detail[0]->detail) or return (); - $csv->fields; -} - -=item desc - -Returns a description for this line item. For typical line items, this is the -I field of the corresponding B object (see L). -For one-shot line items and named taxes, it is the I field of this -line item, and for generic taxes, simply returns "Tax". - -=cut - -sub desc { - my $self = shift; - - if ( $self->pkgnum > 0 ) { - $self->itemdesc || $self->part_pkg->pkg; - } else { - my $desc = $self->itemdesc || 'Tax'; - $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/; - $desc; - } -} - =item owed_setup Returns the amount owed (still outstanding) on this line item's setup fee, @@ -765,45 +575,6 @@ sub units { $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1? } -=item quantity - -=cut - -sub quantity { - my( $self, $value ) = @_; - if ( defined($value) ) { - $self->setfield('quantity', $value); - } - $self->getfield('quantity') || 1; -} - -=item unitsetup - -=cut - -sub unitsetup { - my( $self, $value ) = @_; - if ( defined($value) ) { - $self->setfield('unitsetup', $value); - } - $self->getfield('unitsetup') eq '' - ? $self->getfield('setup') - : $self->getfield('unitsetup'); -} - -=item unitrecur - -=cut - -sub unitrecur { - my( $self, $value ) = @_; - if ( defined($value) ) { - $self->setfield('unitrecur', $value); - } - $self->getfield('unitrecur') eq '' - ? $self->getfield('recur') - : $self->getfield('unitrecur'); -} =item set_display OPTION => VALUE ... @@ -1015,44 +786,8 @@ sub usage_classes { } -=item cust_bill_pkg_display [ type => TYPE ] - -Returns an array of display information for the invoice line item optionally -limited to 'TYPE'. - -=cut - -sub cust_bill_pkg_display { - my ( $self, %opt ) = @_; - - my $default = - new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum }; - - my $type = $opt{type} if exists $opt{type}; - my @result; - - if ( $self->get('display') ) { - @result = grep { defined($type) ? ($type eq $_->type) : 1 } - @{ $self->get('display') }; - } else { - my $hashref = { 'billpkgnum' => $self->billpkgnum }; - $hashref->{type} = $type if defined($type); - - @result = qsearch ({ 'table' => 'cust_bill_pkg_display', - 'hashref' => { 'billpkgnum' => $self->billpkgnum }, - 'order_by' => 'ORDER BY billpkgdisplaynum', - }); - } - - push @result, $default unless ( scalar(@result) || $type ); - - @result; - -} - # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline # and FS::cust_main::bill - sub _cust_tax_exempt_pkg { my ( $self ) = @_; @@ -1080,36 +815,6 @@ sub cust_bill_pkg_tax_Xlocation { } -=item cust_bill_pkg_detail [ CLASSNUM ] - -Returns the list of associated cust_bill_pkg_detail objects -The optional CLASSNUM argument will limit the details to the specified usage -class. - -=cut - -sub cust_bill_pkg_detail { - my $self = shift; - my $classnum = shift || ''; - - my %hash = ( 'billpkgnum' => $self->billpkgnum ); - $hash{classnum} = $classnum if $classnum; - - qsearch( 'cust_bill_pkg_detail', \%hash ), - -} - -=item cust_bill_pkg_discount - -Returns the list of associated cust_bill_pkg_discount objects. - -=cut - -sub cust_bill_pkg_discount { - my $self = shift; - qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } ); -} - =item recur_show_zero =cut diff --git a/FS/FS/cust_bill_pkg_discount_void.pm b/FS/FS/cust_bill_pkg_discount_void.pm new file mode 100644 index 000000000..859ef3cf2 --- /dev/null +++ b/FS/FS/cust_bill_pkg_discount_void.pm @@ -0,0 +1,129 @@ +package FS::cust_bill_pkg_discount_void; + +use strict; +use base qw( FS::Record ); +use FS::Record; # qw( qsearch qsearchs ); +use FS::cust_bill_pkg_void; +use FS::cust_pkg_discount; + +=head1 NAME + +FS::cust_bill_pkg_discount_void - Object methods for cust_bill_pkg_discount_void records + +=head1 SYNOPSIS + + use FS::cust_bill_pkg_discount_void; + + $record = new FS::cust_bill_pkg_discount_void \%hash; + $record = new FS::cust_bill_pkg_discount_void { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_bill_pkg_discount_void object represents the slice of a customer +discount applied to a specific voided line item. +FS::cust_bill_pkg_discount_void inherits from FS::Record. The following fields +are currently supported: + +=over 4 + +=item billpkgdiscountnum + +primary key + +=item billpkgnum + +billpkgnum + +=item pkgdiscountnum + +pkgdiscountnum + +=item amount + +amount + +=item months + +months + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new example. To add the example to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'cust_bill_pkg_discount_void'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +=item delete + +Delete this record from the database. + +=cut + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +=item check + +Checks all fields to make sure this is a valid example. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_number('billpkgdiscountnum') + || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum' ) + || $self->ut_foreign_key('pkgdiscountnum', 'cust_pkg_discount', 'pkgdiscountnum' ) + || $self->ut_money('amount') + || $self->ut_float('months') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_bill_pkg_void.pm b/FS/FS/cust_bill_pkg_void.pm index 198283955..7855d58c6 100644 --- a/FS/FS/cust_bill_pkg_void.pm +++ b/FS/FS/cust_bill_pkg_void.pm @@ -1,8 +1,12 @@ package FS::cust_bill_pkg_void; +use base qw( FS::TemplateItem_Mixin FS::Record ); use strict; -use base qw( FS::Record ); -use FS::Record; # qw( qsearch qsearchs ); +use FS::Record qw( qsearch qsearchs ); +use FS::cust_bill_void; +use FS::cust_bill_pkg_detail_void; +use FS::cust_bill_pkg_display_void; +use FS::cust_bill_pkg_discount_void; =head1 NAME @@ -113,6 +117,13 @@ points to. You can ask the object for a copy with the I method. sub table { 'cust_bill_pkg_void'; } +sub detail_table { 'cust_bill_pkg_detail_void'; } +sub display_table { 'cust_bill_pkg_display_void'; } +sub discount_table { 'cust_bill_pkg_discount_void'; } +#sub tax_location_table { 'cust_bill_pkg_tax_location'; } +#sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; } +#sub tax_exempt_pkg_table { 'cust_tax_exempt_pkg'; } + =item insert Adds this record to the database. If there is an error, returns the error, @@ -147,7 +158,7 @@ sub check { my $error = $self->ut_number('billpkgnum') || $self->ut_snumber('pkgnum') - || $self->ut_number('invnum') #cust_bill or cust_bill_void ? + || $self->ut_number('invnum') #cust_bill or cust_bill_void, if we ever support line item voiding || $self->ut_numbern('pkgpart_override') || $self->ut_money('setup') || $self->ut_money('recur') @@ -167,6 +178,19 @@ sub check { $self->SUPER::check; } +=item cust_bill + +Returns the voided invoice (see L) for this voided line +item. + +=cut + +sub cust_bill { + my $self = shift; + #cust_bill or cust_bill_void, if we ever support line item voiding + qsearchs( 'cust_bill_void', { 'invnum' => $self->invnum } ); +} + =back =head1 BUGS diff --git a/FS/FS/cust_bill_void.pm b/FS/FS/cust_bill_void.pm index c782172b5..cd6a9e13b 100644 --- a/FS/FS/cust_bill_void.pm +++ b/FS/FS/cust_bill_void.pm @@ -2,10 +2,11 @@ package FS::cust_bill_void; use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record ); use strict; -use FS::Record qw( qsearchs ); #qsearch ); +use FS::Record qw( qsearch qsearchs ); use FS::cust_main; use FS::cust_statement; use FS::access_user; +use FS::cust_bill_pkg_void; =head1 NAME @@ -203,6 +204,33 @@ sub void_access_user { qsearchs('access_user', { 'usernum' => $self->void_usernum } ); } +=item cust_main + +=cut + +sub cust_main { + my $self = shift; + qsearchs('cust_main', { 'custnum' => $self->custnum } ); +} + +=item cust_bill_pkg + +=cut + +sub cust_bill_pkg { #actually cust_bill_pkg_void objects + my $self = shift; + qsearch('cust_bill_pkg_void', { invnum=>$self->invnum }); +} + +=back + +=item enable_previous + +=cut + +sub enable_previous { 0 } + + =back =head1 BUGS diff --git a/FS/t/cust_bill_pkg_discount_void.t b/FS/t/cust_bill_pkg_discount_void.t new file mode 100644 index 000000000..e591eb03d --- /dev/null +++ b/FS/t/cust_bill_pkg_discount_void.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_bill_pkg_discount_void; +$loaded=1; +print "ok 1\n"; diff --git a/httemplate/view/cust_bill_void.html b/httemplate/view/cust_bill_void.html index c7c5da146..148c0ed7e 100755 --- a/httemplate/view/cust_bill_void.html +++ b/httemplate/view/cust_bill_void.html @@ -11,6 +11,11 @@ %#

% #} +
VOID
+ % if ( $conf->exists('invoice_html') ) { <% join('', $cust_bill_void->print_html(\%opt) ) %> % } else { @@ -43,13 +48,13 @@ my $cust_bill_void = qsearchs({ 'select' => 'cust_bill_void.*', 'table' => 'cust_bill_void', #'addl_from' => 'LEFT JOIN cust_main USING ( custnum )', - 'hashref' => { 'quotationnum' => $quotationnum }, + 'hashref' => { 'invnum' => $invnum }, #'extra_sql' => ' AND '. $curuser->agentnums_sql, }); -die "Quotation #$quotationnum not found!" unless $quotation; +die "Voided invoice #$invnum not found!" unless $cust_bill_void; -my $custnum = $cust_bill->custnum; -my $display_custnum = $cust_bill->cust_main->display_custnum; +my $custnum = $cust_bill_void->custnum; +my $display_custnum = $cust_bill_void->cust_main->display_custnum; #my $link = "invnum=$invnum"; diff --git a/httemplate/view/cust_main/payment_history/voided_invoice.html b/httemplate/view/cust_main/payment_history/voided_invoice.html index 422edb2f6..7bf206352 100644 --- a/httemplate/view/cust_main/payment_history/voided_invoice.html +++ b/httemplate/view/cust_main/payment_history/voided_invoice.html @@ -21,9 +21,8 @@ my $under = ''; my $invnum = $cust_bill_void->invnum; -#XXX use cust_bill.cgi or? my $link = $curuser->access_right('View invoices') - ? qq!! + ? qq!! : ''; my $unvoid = ''; #XXX unvoid -- cgit v1.2.1 From bec3b6c2bf97d66b992866d7ee7295f1f05452e6 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Wed, 1 Aug 2012 14:01:14 -0700 Subject: invoice voiding, RT#18677 --- FS/FS/AccessRight.pm | 7 +- FS/FS/access_right.pm | 9 ++- FS/FS/cust_bill_pkg_void.pm | 74 ++++++++++++++++++++-- FS/FS/cust_bill_void.pm | 61 +++++++++++++++--- httemplate/misc/process/void-cust_bill.html | 6 +- httemplate/misc/unvoid-cust_bill_void.html | 25 ++++++++ httemplate/misc/unvoid-cust_pay_void.cgi | 2 +- httemplate/misc/void-cust_pay.cgi | 2 +- .../view/cust_main/payment_history/payment.html | 2 +- .../cust_main/payment_history/voided_invoice.html | 8 ++- .../cust_main/payment_history/voided_payment.html | 2 +- 11 files changed, 171 insertions(+), 27 deletions(-) create mode 100755 httemplate/misc/unvoid-cust_bill_void.html diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index ebf66e64c..b41ec2fe2 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -178,6 +178,7 @@ tie my %rights, 'Tie::IxHash', 'View invoices', 'Resend invoices', #NEWNEW 'Void invoices', + 'Unvoid invoices', 'Delete invoices', 'View customer tax exemptions', #yow 'Add customer tax adjustment', #new, but no need to phase in @@ -228,11 +229,11 @@ tie my %rights, 'Tie::IxHash', ### # customer voiding rights.. ### - 'Customer void rights' => [ + 'Customer payment void rights' => [ { rightname=>'Credit card void', desc=>'Enable local-only voiding of echeck payments in addition to refunds against the payment gateway.' }, #aka. cc-void { rightname=>'Echeck void', desc=>'Enable local-only voiding of echeck payments in addition to refunds against the payment gateway.' }, #aka. echeck-void - 'Regular void', - { rightname=>'Unvoid', desc=>'Enable unvoiding of voided payments' }, #aka. unvoid + 'Void payments', + { rightname=>'Unvoid payments', desc=>'Enable unvoiding of voided payments' }, #aka. unvoid ], diff --git a/FS/FS/access_right.pm b/FS/FS/access_right.pm index bc57364d2..dc9f9978d 100644 --- a/FS/FS/access_right.pm +++ b/FS/FS/access_right.pm @@ -152,6 +152,8 @@ sub _upgrade_data { # class method 'Process payment' => [ 'Process credit card payment', 'Process Echeck payment' ], 'Post refund' => [ 'Post check refund', 'Post cash refund' ], 'Refund payment' => [ 'Refund credit card payment', 'Refund Echeck payment' ], + 'Regular void' => [ 'Void payments' ], + 'Unvoid' => [ 'Unvoid payments', 'Unvoid invoices' ], ); foreach my $oldright (keys %migrate) { @@ -174,9 +176,10 @@ sub _upgrade_data { # class method die $error if $error; } - #after the WEST stuff is sorted, etc. - #my $error = $old->delete; - #die $error if $error; + unless ( $oldright =~ / (payment|refund)$/ ) { #after the WEST stuff is sorted + my $error = $old->delete; + die $error if $error; + } } diff --git a/FS/FS/cust_bill_pkg_void.pm b/FS/FS/cust_bill_pkg_void.pm index 7855d58c6..b7c6feed5 100644 --- a/FS/FS/cust_bill_pkg_void.pm +++ b/FS/FS/cust_bill_pkg_void.pm @@ -2,11 +2,12 @@ package FS::cust_bill_pkg_void; use base qw( FS::TemplateItem_Mixin FS::Record ); use strict; -use FS::Record qw( qsearch qsearchs ); +use FS::Record qw( qsearch qsearchs dbh fields ); use FS::cust_bill_void; use FS::cust_bill_pkg_detail_void; use FS::cust_bill_pkg_display_void; use FS::cust_bill_pkg_discount_void; +use FS::cust_bill_pkg; =head1 NAME @@ -129,21 +130,84 @@ sub discount_table { 'cust_bill_pkg_discount_void'; } Adds this record to the database. If there is an error, returns the error, otherwise returns false. +=item unvoid + +"Un-void"s this line item: Deletes the voided line item from the database and +adds back a normal line item (and related tables). + =cut +sub unvoid { + my $self = shift; + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $cust_bill_pkg = new FS::cust_bill_pkg ( { + map { $_ => $self->get($_) } fields('cust_bill_pkg') + } ); + my $error = $cust_bill_pkg->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + foreach my $table (qw( + cust_bill_pkg_detail + cust_bill_pkg_display + cust_bill_pkg_discount + cust_bill_pkg_tax_location + cust_bill_pkg_tax_rate_location + cust_tax_exempt_pkg + )) { + + foreach my $voided ( + qsearch($table.'_void', { billpkgnum=>$self->billpkgnum }) + ) { + + my $class = 'FS::'.$table; + my $unvoid = $class->new( { + map { $_ => $voided->get($_) } fields($table) + }); + my $error = $unvoid->insert || $voided->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + } + + } + + $error = $self->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; + +} + =item delete Delete this record from the database. -=cut - =item replace OLD_RECORD Replaces the OLD_RECORD with this one in the database. If there is an error, returns the error, otherwise returns false. -=cut - =item check Checks all fields to make sure this is a valid record. If there is diff --git a/FS/FS/cust_bill_void.pm b/FS/FS/cust_bill_void.pm index cd6a9e13b..cce77b3aa 100644 --- a/FS/FS/cust_bill_void.pm +++ b/FS/FS/cust_bill_void.pm @@ -2,11 +2,12 @@ package FS::cust_bill_void; use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record ); use strict; -use FS::Record qw( qsearch qsearchs ); +use FS::Record qw( qsearch qsearchs dbh fields ); use FS::cust_main; use FS::cust_statement; use FS::access_user; use FS::cust_bill_pkg_void; +use FS::cust_bill; =head1 NAME @@ -117,7 +118,55 @@ otherwise returns false. =cut -# the insert method can be inherited from FS::Record +=item unvoid + +"Un-void"s this invoice: Deletes the voided invoice from the database and adds +back a normal invoice (and related tables). + +=cut + +sub unvoid { + my $self = shift; + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $cust_bill = new FS::cust_bill ( { + map { $_ => $self->get($_) } fields('cust_bill') + } ); + my $error = $cust_bill->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + foreach my $cust_bill_pkg_void ( $self->cust_bill_pkg ) { + my $error = $cust_bill_pkg_void->unvoid; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + $error = $self->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; + +} =item delete @@ -125,8 +174,6 @@ Delete this record from the database. =cut -# the delete method can be inherited from FS::Record - =item replace OLD_RECORD Replaces the OLD_RECORD with this one in the database. If there is an error, @@ -134,8 +181,6 @@ returns the error, otherwise returns false. =cut -# the replace method can be inherited from FS::Record - =item check Checks all fields to make sure this is a valid voided invoice. If there is @@ -144,9 +189,6 @@ and replace methods. =cut -# the check method should currently be supplied - FS::Record contains some -# data checking routines - sub check { my $self = shift; @@ -230,7 +272,6 @@ sub cust_bill_pkg { #actually cust_bill_pkg_void objects sub enable_previous { 0 } - =back =head1 BUGS diff --git a/httemplate/misc/process/void-cust_bill.html b/httemplate/misc/process/void-cust_bill.html index f2930ec01..899901a50 100755 --- a/httemplate/misc/process/void-cust_bill.html +++ b/httemplate/misc/process/void-cust_bill.html @@ -2,7 +2,11 @@ % $cgi->param('error', $error); <% $cgi->redirect(popurl(1). "void-cust_bill.html?". $cgi->query_string ) %> %} else { -<% $cgi->redirect(popurl(3). "view/cust_main.cgi?". $custnum) %> +<& /elements/header-popup.html, 'Invoice voided' &> + + %} <%init> diff --git a/httemplate/misc/unvoid-cust_bill_void.html b/httemplate/misc/unvoid-cust_bill_void.html new file mode 100755 index 000000000..f61416549 --- /dev/null +++ b/httemplate/misc/unvoid-cust_bill_void.html @@ -0,0 +1,25 @@ +%if ( $error ) { +% errorpage($error); +%} else { +% my $show = $curuser->default_customer_view =~ /^(jumbo|payment_history)$/ +% ? '' +% : ';show=payment_history'; +<% $cgi->redirect($p. "view/cust_main.cgi?custnum=$custnum$show" ) %> +%} +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('Unvoid invoices'); + +#untaint invnum +$cgi->param('invnum') =~ /^(\d+)$/ || die "Illegal invnum"; +my $invnum = $1; + +my $cust_bill_void = qsearchs('cust_bill_void', { 'invnum' => $invnum } ); +my $custnum = $cust_bill_void->custnum; + +my $error = $cust_bill_void->unvoid; + + diff --git a/httemplate/misc/unvoid-cust_pay_void.cgi b/httemplate/misc/unvoid-cust_pay_void.cgi index 91fe1c223..4726ee576 100755 --- a/httemplate/misc/unvoid-cust_pay_void.cgi +++ b/httemplate/misc/unvoid-cust_pay_void.cgi @@ -6,7 +6,7 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Unvoid'); + unless $FS::CurrentUser::CurrentUser->access_right('Unvoid payments'); #untaint paynum my($query) = $cgi->keywords; diff --git a/httemplate/misc/void-cust_pay.cgi b/httemplate/misc/void-cust_pay.cgi index 7b484e93e..31b7a6201 100755 --- a/httemplate/misc/void-cust_pay.cgi +++ b/httemplate/misc/void-cust_pay.cgi @@ -12,7 +12,7 @@ my $paynum = $1; my $cust_pay = qsearchs('cust_pay',{'paynum'=>$paynum}); -my $right = 'Regular void'; +my $right = 'Void payments'; $right = 'Credit card void' if $cust_pay->payby eq 'CARD'; $right = 'Echeck void' if $cust_pay->payby eq 'CHEK'; diff --git a/httemplate/view/cust_main/payment_history/payment.html b/httemplate/view/cust_main/payment_history/payment.html index d7322a2d6..ff269bfaf 100644 --- a/httemplate/view/cust_main/payment_history/payment.html +++ b/httemplate/view/cust_main/payment_history/payment.html @@ -181,7 +181,7 @@ $void = areyousure_link("${p}misc/void-cust_pay.cgi?".$cust_pay->paynum, && $curuser->access_right('Echeck void') ) || ( $cust_pay->payby !~ /^(CARD|CHEK)$/ - && $curuser->access_right('Regular void') + && $curuser->access_right('Void payments') ) ) ); diff --git a/httemplate/view/cust_main/payment_history/voided_invoice.html b/httemplate/view/cust_main/payment_history/voided_invoice.html index 7bf206352..15393cbf5 100644 --- a/httemplate/view/cust_main/payment_history/voided_invoice.html +++ b/httemplate/view/cust_main/payment_history/voided_invoice.html @@ -25,7 +25,13 @@ my $link = $curuser->access_right('View invoices') ? qq!! : ''; -my $unvoid = ''; #XXX unvoid +my $unvoid = ''; +$unvoid = areyousure_link("${p}misc/unvoid-cust_bill_void.html?invnum=". $cust_bill_void->invnum, + emt('Are you sure you want to unvoid this invoice?'), + emt('Unvoid this invoice'), + emt('unvoid') + ) + if $cust_bill_void->closed !~ /^Y/ && $curuser->access_right('Unvoid invoices'); my $delete = ''; $delete = areyousure_link("${p}misc/delete-cust_bill.html?$invnum", diff --git a/httemplate/view/cust_main/payment_history/voided_payment.html b/httemplate/view/cust_main/payment_history/voided_payment.html index 2f038be41..88b5e0a84 100644 --- a/httemplate/view/cust_main/payment_history/voided_payment.html +++ b/httemplate/view/cust_main/payment_history/voided_payment.html @@ -31,6 +31,6 @@ $unvoid = areyousure_link("${p}misc/unvoid-cust_pay_void.cgi?".$cust_pay_void->p emt('Unvoid this payment from the database') . $unvoidmsg, emt('unvoid') ) - if ( $cust_pay_void->closed !~ /^Y/i && $curuser->access_right('Unvoid') ); + if ( $cust_pay_void->closed !~ /^Y/i && $curuser->access_right('Unvoid payments') ); -- cgit v1.2.1 From 4d6c465f4b32a49f8bce091f6cb5abb209123ec2 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Wed, 1 Aug 2012 14:04:50 -0700 Subject: invoice voiding, RT#18677 --- FS/FS/Mason.pm | 1 + FS/FS/Schema.pm | 13 +++++++++++++ FS/FS/cust_bill_pkg_discount.pm | 4 ++-- FS/FS/quotation.pm | 3 +-- FS/MANIFEST | 2 ++ httemplate/view/cust_bill.cgi | 2 -- httemplate/view/cust_bill_void.html | 20 +++++++++++++++++++- httemplate/view/quotation.html | 2 -- 8 files changed, 38 insertions(+), 9 deletions(-) diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index c45e783db..663a48e72 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -322,6 +322,7 @@ if ( -e $addl_handler_use_file ) { use FS::cust_bill_pkg_tax_location_void; use FS::cust_bill_pkg_tax_rate_location_void; use FS::cust_tax_exempt_pkg_void; + use FS::cust_bill_pkg_discount_void; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index e59268b2a..cff0afd6c 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -1784,6 +1784,19 @@ sub tables_hashref { 'index' => [ [ 'billpkgnum' ], [ 'pkgdiscountnum' ] ], }, + 'cust_bill_pkg_discount_void' => { + 'columns' => [ + 'billpkgdiscountnum', 'int', '', '', '', '', + 'billpkgnum', 'int', '', '', '', '', + 'pkgdiscountnum', 'int', '', '', '', '', + 'amount', @money_type, '', '', + 'months', 'decimal', 'NULL', '7,4', '', '', + ], + 'primary_key' => 'billpkgdiscountnum', + 'unique' => [], + 'index' => [ [ 'billpkgnum' ], [ 'pkgdiscountnum' ] ], + }, + 'discount' => { 'columns' => [ 'discountnum', 'serial', '', '', '', '', diff --git a/FS/FS/cust_bill_pkg_discount.pm b/FS/FS/cust_bill_pkg_discount.pm index e7dd5f22f..dfa83d393 100644 --- a/FS/FS/cust_bill_pkg_discount.pm +++ b/FS/FS/cust_bill_pkg_discount.pm @@ -28,8 +28,8 @@ FS::cust_bill_pkg_discount - Object methods for cust_bill_pkg_discount records =head1 DESCRIPTION An FS::cust_bill_pkg_discount object represents the slice of a customer -applied to a line item. FS::cust_bill_pkg_discount inherits from -FS::Record. The following fields are currently supported: +discount applied to a specific line item. FS::cust_bill_pkg_discount inherits +from FS::Record. The following fields are currently supported: =over 4 diff --git a/FS/FS/quotation.pm b/FS/FS/quotation.pm index ccaa1c34b..9e7723c77 100644 --- a/FS/FS/quotation.pm +++ b/FS/FS/quotation.pm @@ -142,9 +142,8 @@ sub cust_main { =cut -sub cust_bill_pkg { +sub cust_bill_pkg { #actually quotation_pkg objects my $self = shift; - #actually quotation_pkg objects qsearch('quotation_pkg', { quotationnum=>$self->quotationnum }); } diff --git a/FS/MANIFEST b/FS/MANIFEST index 01dab2092..2163a2324 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -663,3 +663,5 @@ FS/cust_bill_pkg_tax_rate_location_void.pm t/cust_bill_pkg_tax_rate_location_void.t FS/cust_tax_exempt_pkg_void.pm t/cust_tax_exempt_pkg_void.t +FS/cust_bill_pkg_discount_void.pm +t/cust_bill_pkg_discount_void.t diff --git a/httemplate/view/cust_bill.cgi b/httemplate/view/cust_bill.cgi index a8b4ac15c..95ce60b1d 100755 --- a/httemplate/view/cust_bill.cgi +++ b/httemplate/view/cust_bill.cgi @@ -166,8 +166,6 @@ die "Invoice #$invnum not found!" unless $cust_bill; my $custnum = $cust_bill->custnum; my $display_custnum = $cust_bill->cust_main->display_custnum; -#my $printed = $cust_bill->printed; - my $link = "invnum=$invnum"; $link .= ';template='. uri_escape($template) if $template; $link .= ';notice_name='. $notice_name if $notice_name; diff --git a/httemplate/view/cust_bill_void.html b/httemplate/view/cust_bill_void.html index 148c0ed7e..2c526747b 100755 --- a/httemplate/view/cust_bill_void.html +++ b/httemplate/view/cust_bill_void.html @@ -2,7 +2,19 @@ emt("View this customer (#[_1])",$display_custnum) => "${p}view/cust_main.cgi?$custnum", ) &> -%#XXX something very big and obvious showing its voided... + +<% areyousure_link("${p}misc/unvoid-cust_bill_void.html?invnum=". $cust_bill_void->invnum, + emt('Are you sure you want to unvoid this invoice?'), + emt('Unvoid this invoice'), #tooltip + emt('Unvoid this invoice') #link + ) +%> +

% #voided PDFs? % #if ( $conf->exists('invoice_latex') ) { @@ -11,6 +23,7 @@ %#

% #} +%#something very big and obvious showing its voided...
'.$label.''; +} + diff --git a/httemplate/view/quotation.html b/httemplate/view/quotation.html index 461b5dfb6..a88acf82b 100755 --- a/httemplate/view/quotation.html +++ b/httemplate/view/quotation.html @@ -44,8 +44,6 @@ XXX resending quotations % } % #plaintext quotations?
<% join('', $quotation->print_text() ) %>
- - <& /elements/footer.html &> <%init> -- cgit v1.2.1 From 7404ff9d5df7679054dd73f17b3737f5474e5512 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 2 Aug 2012 14:59:53 -0700 Subject: fix some illegal RADIUS attributes during import, #18751 --- FS/FS/part_export/sqlradius.pm | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm index d7cd459d8..721396671 100644 --- a/FS/FS/part_export/sqlradius.pm +++ b/FS/FS/part_export/sqlradius.pm @@ -1160,6 +1160,7 @@ sub import_attrs { SELECT groupname, attribute, op, value, \'C\' FROM radgroupcheck UNION SELECT groupname, attribute, op, value, \'R\' FROM radgroupreply'; + my @fixes; # things that need to be changed on the radius db foreach my $row ( @{ $dbh->selectall_arrayref($sql) } ) { my ($groupname, $attrname, $op, $value, $attrtype) = @$row; warn "$groupname.$attrname\n"; @@ -1181,6 +1182,20 @@ SELECT groupname, attribute, op, value, \'R\' FROM radgroupreply'; my $old = $a->{$attrname}; my $new; + if ( $attrtype eq 'R' ) { + # Freeradius tolerates illegal operators in reply attributes. We don't. + if ( !grep ($_ eq $op, FS::radius_attr->ops('R')) ) { + warn "$groupname.$attrname: changing $op to +=\n"; + # Make a note to change it in the db + push @fixes, [ + 'UPDATE radgroupreply SET op = \'+=\' WHERE groupname = ? AND attribute = ? AND op = ? AND VALUE = ?', + $groupname, $attrname, $op, $value + ]; + # and import it correctly. + $op = '+='; + } + } + if ( defined $old ) { # replace $new = new FS::radius_attr { @@ -1210,6 +1225,13 @@ SELECT groupname, attribute, op, value, \'R\' FROM radgroupreply'; } $attrs_of{$groupname}->{$attrname} = $new; } #foreach $row + + foreach (@fixes) { + my ($sql, @args) = @$_; + my $sth = $dbh->prepare($sql); + $sth->execute(@args) or warn $sth->errstr; + } + return; } -- cgit v1.2.1 From 0f2d1070bc6f1521ab50dd07e475587f1117eec6 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 2 Aug 2012 15:54:31 -0700 Subject: sales by ad source report: filter by ad source, #17971 --- httemplate/elements/tr-select-part_referral.html | 8 +------- httemplate/search/cust_bill_pkg_referral.html | 10 ++++++++++ httemplate/search/report_cust_bill_pkg_referral.html | 5 +++++ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/httemplate/elements/tr-select-part_referral.html b/httemplate/elements/tr-select-part_referral.html index 765aa8400..5041f7f73 100644 --- a/httemplate/elements/tr-select-part_referral.html +++ b/httemplate/elements/tr-select-part_referral.html @@ -14,13 +14,7 @@ % } else { - - -% if ( $opt{'label'} ) { - <% $opt{'label'} %> -% } else { - <%$r%><% mt('Advertising source') |h %> -% } + <& /elements/tr-td-label.html, label => 'Advertising source', %opt &> <& /elements/select-part_referral.html, 'curr_value' => $refnum, diff --git a/httemplate/search/cust_bill_pkg_referral.html b/httemplate/search/cust_bill_pkg_referral.html index 3cb434caa..77b486021 100644 --- a/httemplate/search/cust_bill_pkg_referral.html +++ b/httemplate/search/cust_bill_pkg_referral.html @@ -146,6 +146,16 @@ if ( @status_where ) { ') IN (' . join(',', @status_where) .')'; } +my @refnum; +foreach my $refnum ($cgi->param('refnum')) { + if ( $refnum =~ /^\d+$/ ) { + push @refnum, $refnum; + } +} +if ( @refnum ) { + push @where, 'cust_main.refnum IN ('.join(',', @refnum).')'; +} + if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { push @where, "cust_main.agentnum = $1"; } diff --git a/httemplate/search/report_cust_bill_pkg_referral.html b/httemplate/search/report_cust_bill_pkg_referral.html index ff2caa1fa..b4716d4fc 100644 --- a/httemplate/search/report_cust_bill_pkg_referral.html +++ b/httemplate/search/report_cust_bill_pkg_referral.html @@ -18,6 +18,11 @@ 'disable_empty' => 1, &> +<& /elements/tr-select-part_referral.html, + 'multiple' => 1, + 'disable_empty' => 1, +&> + <& /elements/tr-select-pkg_class.html, 'pre_options' => [ '' => 'all', '0' => '(empty class)' ], 'disable_empty' => 1, -- cgit v1.2.1 From e881063d6e46d991003cf8fae96d8d36780fedcd Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 2 Aug 2012 19:39:20 -0700 Subject: per-customer prorate day, RT#17891 --- FS/FS/Conf.pm | 7 +++++++ FS/FS/Schema.pm | 1 + FS/FS/cust_main.pm | 1 + FS/FS/part_pkg/prorate.pm | 10 +++++++--- FS/FS/part_pkg/recur_Common.pm | 15 ++++++++------- httemplate/edit/cust_main/billing.html | 11 +++++++++++ httemplate/view/cust_main/billing.html | 8 ++++++++ 7 files changed, 43 insertions(+), 10 deletions(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 37bbf6e17..8069fb41f 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -838,6 +838,13 @@ sub reason_type_options { 'type' => 'checkbox', }, + { + 'key' => 'cust_main-select-prorate_day', + 'section' => 'billing', + 'description' => 'When used with prorate or anniversary packages, allows the selection of the prorate day of month, on a per-customer basis', + 'type' => 'checkbox', + }, + { 'key' => 'encryption', 'section' => 'billing', diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index cff0afd6c..a55e0f9d2 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -1050,6 +1050,7 @@ sub tables_hashref { 'email_csv_cdr', 'char', 'NULL', 1, '', '', 'accountcode_cdr', 'char', 'NULL', 1, '', '', 'billday', 'int', 'NULL', '', '', '', + 'prorate_day', 'int', 'NULL', '', '', '', 'edit_subject', 'char', 'NULL', 1, '', '', 'locale', 'varchar', 'NULL', 16, '', '', 'calling_list_exempt', 'char', 'NULL', 1, '', '', diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 36c62808c..1da1f0ffd 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -1794,6 +1794,7 @@ sub check { || $self->ut_floatn('cdr_termination_percentage') || $self->ut_floatn('credit_limit') || $self->ut_numbern('billday') + || $self->ut_numbern('prorate_day') || $self->ut_enum('edit_subject', [ '', 'Y' ] ) || $self->ut_enum('calling_list_exempt', [ '', 'Y' ] ) || $self->ut_enum('invoice_noemail', [ '', 'Y' ] ) diff --git a/FS/FS/part_pkg/prorate.pm b/FS/FS/part_pkg/prorate.pm index f8d03dcb5..ac86f3918 100644 --- a/FS/FS/part_pkg/prorate.pm +++ b/FS/FS/part_pkg/prorate.pm @@ -44,12 +44,16 @@ use FS::part_pkg::flat; sub calc_recur { my $self = shift; - return $self->calc_prorate(@_, $self->cutoff_day) - $self->calc_discount(@_); + my $cust_pkg = $_[0]; + $self->calc_prorate(@_, $self->cutoff_day($cust_pkg)) + - $self->calc_discount(@_); } sub cutoff_day { - my $self = shift; - split(/\s*,\s*/, $self->option('cutoff_day', 1) || '1'); + my( $self, $cust_pkg ) = @_; + my $prorate_day = $cust_pkg->cust_main->prorate_day; + $prorate_day ? ( $prorate_day ) + : split(/\s*,\s*/, $self->option('cutoff_day', 1) || '1'); } 1; diff --git a/FS/FS/part_pkg/recur_Common.pm b/FS/FS/part_pkg/recur_Common.pm index 9d7341b76..03d5c2cb2 100644 --- a/FS/FS/part_pkg/recur_Common.pm +++ b/FS/FS/part_pkg/recur_Common.pm @@ -39,14 +39,15 @@ sub calc_setup { sub cutoff_day { # prorate/subscription only; we don't support sync_bill_date here - my $self = shift; - my $cust_pkg = shift; + my( $self, $cust_pkg ) = @_; my $recur_method = $self->option('recur_method',1) || 'anniversary'; - if ( $recur_method eq 'prorate' or $recur_method eq 'subscription' ) { - return $self->option('cutoff_day',1) || 1; - } else { - return (); - } + return () unless $recur_method eq 'prorate' + || $recur_method eq 'subscription'; + + #false laziness w/prorate.pm::cutoff_day + my $prorate_day = $cust_pkg->cust_main->prorate_day; + $prorate_day ? ( $prorate_day ) + : split(/\s*,\s*/, $self->option('cutoff_day', 1) || '1'); } sub calc_recur_Common { diff --git a/httemplate/edit/cust_main/billing.html b/httemplate/edit/cust_main/billing.html index d7082f23a..2925ca87c 100644 --- a/httemplate/edit/cust_main/billing.html +++ b/httemplate/edit/cust_main/billing.html @@ -522,6 +522,17 @@ % } +% if ( $conf->exists('cust_main-select-prorate_day') ) { + + <% mt('Prorate day (1-28)') |h %> + + + + +% } else { + +% } + <% mt('Invoice terms') |h %> diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html index b2a0efdef..5c46803d2 100644 --- a/httemplate/view/cust_main/billing.html +++ b/httemplate/view/cust_main/billing.html @@ -23,6 +23,14 @@ <% $balance %> +% if ( $conf->exists('cust_main-select-prorate_day') ) { + + <% mt('Prorate day of month') |h %> + <% $cust_main->prorate_day %> + + +% } + % if ( $conf->exists('cust_main-select-billday') % && ($cust_main->payby eq 'CARD' || $cust_main->payby eq 'CHEK') ) { -- cgit v1.2.1 From 07d63243b070962b8fcf8e6b59128ccf1305f8f8 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 2 Aug 2012 20:53:04 -0700 Subject: FS/FS/Trace.pm --- FS/FS/Mason/Request.pm | 18 ++++++++++++++++++ Makefile | 5 +++++ htetc/handler.pl | 21 +++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/FS/FS/Mason/Request.pm b/FS/FS/Mason/Request.pm index 0d21df4ca..36c46dc41 100644 --- a/FS/FS/Mason/Request.pm +++ b/FS/FS/Mason/Request.pm @@ -4,6 +4,7 @@ use strict; use warnings; use vars qw( $FSURL $QUERY_STRING ); use base 'HTML::Mason::Request'; +use FS::Trace; $FSURL = 'http://Set/FS_Mason_Request_FSURL/in_standalone_mode/'; $QUERY_STRING = ''; @@ -11,21 +12,27 @@ $QUERY_STRING = ''; sub new { my $class = shift; + FS::Trace->log('creating new FS::Mason::Request object'); + my $superclass = $HTML::Mason::ApacheHandler::VERSION ? 'HTML::Mason::Request::ApacheHandler' : $HTML::Mason::CGIHandler::VERSION ? 'HTML::Mason::Request::CGI' : 'HTML::Mason::Request'; + FS::Trace->log(' altering superclass'); $class->alter_superclass( $superclass ); + FS::Trace->log(' setting valid params'); #huh... shouldn't alter_superclass take care of this for us? __PACKAGE__->valid_params( %{ $superclass->valid_params() } ); + FS::Trace->log(' freeside_setup'); my %opt = @_; my $mode = $superclass =~ /Apache/i ? 'apache' : 'standalone'; $class->freeside_setup($opt{'comp'}, $mode); + FS::Trace->log(' SUPER::new'); $class->SUPER::new(@_); } @@ -38,6 +45,8 @@ my $protect_fds; sub freeside_setup { my( $class, $filename, $mode ) = @_; + FS::Trace->log(' protecting fds'); + #from rt/bin/webmux.pl(.in) if ( !$protect_fds && $ENV{'MOD_PERL'} && exists $ENV{'MOD_PERL_API_VERSION'} && $ENV{'MOD_PERL_API_VERSION'} >= 2 @@ -57,6 +66,8 @@ sub freeside_setup { if ( $filename =~ qr(/REST/\d+\.\d+/NoAuth/) ) { + FS::Trace->log(' handling RT REST/NoAuth file'); + package HTML::Mason::Commands; #? use FS::UID qw( adminsuidsetup ); @@ -65,10 +76,13 @@ sub freeside_setup { ##old installs w/fs_selfs or selfserv?? #&adminsuidsetup('fs_selfservice'); + FS::Trace->log(' adminsuidsetup fs_queue'); &adminsuidsetup('fs_queue'); } else { + FS::Trace->log(' handling regular file'); + package HTML::Mason::Commands; use vars qw( $cgi $p $fsurl ); # $lh ); #not using /mt use Encode; @@ -77,6 +91,7 @@ sub freeside_setup { if ( $mode eq 'apache' ) { $cgi = new CGI; + FS::Trace->log(' cgisuidsetup'); &cgisuidsetup($cgi); #&cgisuidsetup($r); $fsurl = rooturl(); @@ -91,6 +106,7 @@ sub freeside_setup { die "unknown mode $mode"; } + FS::Trace->log(' UTF-8-decoding form data'); # foreach my $param ( $cgi->param ) { my @values = $cgi->param($param); @@ -102,6 +118,8 @@ sub freeside_setup { } + FS::Trace->log(' done'); + } sub callback { diff --git a/Makefile b/Makefile index 95ffbf27b..864d9c279 100644 --- a/Makefile +++ b/Makefile @@ -168,6 +168,11 @@ install-docs: check-conflicts docs cp -r masondocs ${FREESIDE_DOCUMENT_ROOT} chown -R freeside:freeside ${FREESIDE_DOCUMENT_ROOT} cp htetc/handler.pl ${MASON_HANDLER} + perl -p -i -e "\ + s|%%%FREESIDE_EXPORT%%%|${FREESIDE_EXPORT}|g;\ + " ${MASON_HANDLER} || true + mkdir -p ${FREESIDE_EXPORT}/profile + chown freeside ${FREESIDE_EXPORT}/profile cp htetc/htpasswd.logout ${FREESIDE_CONF} [ ! -e ${MASONDATA} ] && mkdir ${MASONDATA} || true chown -R freeside ${MASONDATA} diff --git a/htetc/handler.pl b/htetc/handler.pl index cea366134..77b93463f 100644 --- a/htetc/handler.pl +++ b/htetc/handler.pl @@ -5,6 +5,7 @@ package HTML::Mason; use strict; use warnings; use FS::Mason qw( mason_interps ); +use FS::Trace; #use vars qw($r); @@ -38,6 +39,8 @@ sub handler #($r) = @_; my $r = shift; + FS::Trace->log('protecting fds'); + #from rt/bin/webmux.pl(.in) if ( !$protect_fds && $ENV{'MOD_PERL'} && exists $ENV{'MOD_PERL_API_VERSION'} && $ENV{'MOD_PERL_API_VERSION'} >= 2 @@ -63,6 +66,8 @@ sub handler ###Module::Refresh->refresh;### + FS::Trace->log('setting content_type / headers'); + $r->content_type('text/html; charset=utf-8'); #$r->content_type('text/html; charset=iso-8859-1'); #eorar @@ -76,6 +81,8 @@ sub handler if ( $r->filename =~ /\/rt\// ) { #RT + FS::Trace->log('handling RT file'); + # We don't need to handle non-text, non-xml items return -1 if defined( $r->content_type ) && $r->content_type !~ m!(^text/|\bxml\b)!io; @@ -84,15 +91,20 @@ sub handler local $SIG{__WARN__}; local $SIG{__DIE__}; + FS::Trace->log('initializing RT'); my_rt_init(); + FS::Trace->log('setting RT interpreter'); $ah->interp($rt_interp); } else { + FS::Trace->log('handling Freeside file'); + local $SIG{__WARN__}; local $SIG{__DIE__}; + FS::Trace->log('initializing RT'); my_rt_init(); #we don't want the RT error handlers under FS @@ -102,10 +114,12 @@ sub handler undef($SIG{__DIE__}) if defined($SIG{__DIE__} ); } + FS::Trace->log('setting Freeside interpreter'); $ah->interp($fs_interp); } + FS::Trace->log('handling request'); my %session; my $status; eval { $status = $ah->handle_request($r); }; @@ -125,6 +139,13 @@ sub handler # ); # } + FS::Trace->log('done'); + + FS::Trace->dumpfile("%%%FREESIDE_EXPORT%%%/profile/$$.".time, $r->filename) + if FS::Trace->total > 5; #10? + + FS::Trace->reset; + $status; } -- cgit v1.2.1 From aa59ea407bdb0368f0dcb36d48b69cdabf44da41 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 2 Aug 2012 20:53:12 -0700 Subject: request tracing, RT#18719 --- FS/MANIFEST | 1 + 1 file changed, 1 insertion(+) diff --git a/FS/MANIFEST b/FS/MANIFEST index 2163a2324..e8b676028 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -665,3 +665,4 @@ FS/cust_tax_exempt_pkg_void.pm t/cust_tax_exempt_pkg_void.t FS/cust_bill_pkg_discount_void.pm t/cust_bill_pkg_discount_void.t +FS/Trace.pm -- cgit v1.2.1 From c573de444fba4ff9a42127b4cfe97b85d1f89b70 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Fri, 3 Aug 2012 10:51:55 -0700 Subject: request tracing, RT#18719 --- FS/FS/Trace.pm | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 FS/FS/Trace.pm diff --git a/FS/FS/Trace.pm b/FS/FS/Trace.pm new file mode 100644 index 000000000..9ff39dd26 --- /dev/null +++ b/FS/FS/Trace.pm @@ -0,0 +1,35 @@ +package FS::Trace; + +use strict; +use Date::Format; +use File::Slurp; + +my @trace = (); + +sub log { + my( $class, $msg ) = @_; + push @trace, [ time, "[$$][". time2str('%r', time). "] $msg" ]; +} + +sub total { + $trace[-1]->[0] - $trace[0]->[0]; +} + +sub reset { + @trace = (); +} + +sub dump_ary { + map $_->[1], @trace; +} + +sub dump { + join("\n", map $_->[1], @trace). "\n"; +} + +sub dumpfile { + my( $class, $filename, $header ) = @_; + write_file( $filename, "$header\n". $class->dump ); +} + +1; -- cgit v1.2.1 From 8b4b2a97a84d17087c3fac19b8f768ca146cf897 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Fri, 3 Aug 2012 13:02:19 -0700 Subject: fix pending payment popups --- httemplate/search/cust_pay_pending.html | 1 - 1 file changed, 1 deletion(-) diff --git a/httemplate/search/cust_pay_pending.html b/httemplate/search/cust_pay_pending.html index 8b7350853..2afce0ce9 100755 --- a/httemplate/search/cust_pay_pending.html +++ b/httemplate/search/cust_pay_pending.html @@ -5,7 +5,6 @@ 'name_verb' => 'pending', 'disable_link' => 1, 'disable_by' => 1, #add otaker to cust_pay_pending? - 'html_init' => include('/elements/init_overlib.html'), 'addl_header' => [ 'Time', 'Payment Status', ], 'addl_fields' => [ sub { time2str('%r', shift->_date ) }, $status_sub, -- cgit v1.2.1 From 45346fd655ba53b82c80b920da945cc0b87ece01 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Fri, 3 Aug 2012 13:09:05 -0700 Subject: selfservice payment fees, RT#18345 --- FS/FS/ClientAPI/MasonComponent.pm | 23 ++++++++++++ FS/FS/ClientAPI/MyAccount.pm | 29 +++++++++++++++ FS/FS/Conf.pm | 42 +++++++++++----------- .../FS-SelfService/cgi/make_payment.html | 24 +++---------- fs_selfservice/FS-SelfService/cgi/selfservice.cgi | 16 ++++++++- httemplate/elements/tr-amount_fee.html | 4 ++- httemplate/misc/payment.cgi | 15 +------- 7 files changed, 97 insertions(+), 56 deletions(-) mode change 100644 => 100755 fs_selfservice/FS-SelfService/cgi/selfservice.cgi diff --git a/FS/FS/ClientAPI/MasonComponent.pm b/FS/FS/ClientAPI/MasonComponent.pm index 534b48a76..c72d26879 100644 --- a/FS/FS/ClientAPI/MasonComponent.pm +++ b/FS/FS/ClientAPI/MasonComponent.pm @@ -26,6 +26,7 @@ my %allowed_comps = map { $_=>1 } qw( my %session_comps = map { $_=>1 } qw( /elements/location.html + /elements/tr-amount_fee.html /edit/cust_main/first_pkg/select-part_pkg.html ); @@ -41,6 +42,28 @@ my %session_callbacks = ( return ''; #no error }, + '/elements/tr-amount_fee.html' => sub { + my( $custnum, $argsref ) = @_; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return "unknown custnum $custnum"; + + my $conf = new FS::Conf; + + my %args = @$argsref; + %args = ( + %args, + 'process-pkgpart' => scalar($conf->config('selfservice_process-pkgpart')), + 'process-display' => scalar($conf->config('selfservice_process-display')), + 'process-skip-first' => $conf->exists('selfservice_process-skip_first'), + 'num_payments' => scalar($cust_main->cust_pay), + 'surcharge_percentage' => scalar($conf->config('credit-card-surcharge-percentage')), + ); + @$argsref = ( %args ); + + return ''; #no error + }, + '/edit/cust_main/first_pkg/select-part_pkg.html' => sub { my( $custnum, $argsref ) = @_; my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 54799b817..151a2ed67 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -38,6 +38,7 @@ use FS::cust_main; use FS::cust_bill; use FS::legacy_cust_bill; use FS::cust_main_county; +use FS::part_pkg; use FS::cust_pkg; use FS::payby; use FS::acct_rt_transaction; @@ -926,6 +927,16 @@ sub validate_payment { my $amount = $1; return { error => 'Amount must be greater than 0' } unless $amount > 0; + #false laziness w/tr-amount_fee.html, but we don't want selfservice users + #changing the hidden form values + my $conf = new FS::Conf; + my $fee_display = $conf->config('selfservice_process-display') || 'add'; + my $fee_pkgpart = $conf->config('selfservice_process-pkgpart'); + if ( $fee_display eq 'add' && $fee_pkgpart ) { + my $fee_pkg = qsearchs('part_pkg', { pkgpart=>$fee_pkgpart } ); + $amount = sprintf('%.2f', $amount + $fee_pkg->option('setup_fee') ); + } + $p->{'discount_term'} =~ /^\s*(\d*)\s*$/ or return { 'error' => gettext('illegal_discount_term'). ': '. $p->{'discount_term'} }; my $discount_term = $1; @@ -1085,6 +1096,24 @@ sub do_process_payment { ); return { 'error' => $error } if $error; + #no error, so order the fee package if applicable... + my $conf = new FS::Conf; + my $fee_pkgpart = $conf->config('selfservice_process-pkgpart'); + if ( $fee_pkgpart ) { + + my $cust_pkg = new FS::cust_pkg { 'pkgpart' => $fee_pkgpart }; + + $error = $cust_main->order_pkg( 'cust_pkg' => $cust_pkg ); + return { 'error' => "payment processed successfully, but error ordering fee: $error" } + if $error; + + #and generate an invoice for it now too + $error = $cust_main->bill( 'pkg_list' => [ $cust_pkg ] ); + return { 'error' => "payment processed and fee ordered sucessfully, but error billing fee: $error" } + if $error; + + } + $cust_main->apply_payments; if ( $validate->{'save'} ) { diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 8069fb41f..39b59cf37 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2592,30 +2592,30 @@ and customer address. Include units.', 'type' => 'checkbox', }, - { - 'key' => 'suto_process-pkgpart', - 'section' => 'billing', - 'description' => 'Package to add to each automatic credit card and ACH payment processed by billing events. Enabling this option may be in violation of your merchant agreement(s), so please check them carefully before enabling this option.', - 'type' => 'select-part_pkg', - }, - # { -# 'key' => 'auto_process-display', +# 'key' => 'auto_process-pkgpart', # 'section' => 'billing', -# 'description' => 'When using auto_process-pkgpart, add the fee to the amount entered (default), or subtract the fee from the amount entered.', -# 'type' => 'select', -# 'select_hash' => [ -# 'add' => 'Add fee to amount entered', -# 'subtract' => 'Subtract fee from amount entered', -# ], +# 'description' => 'Package to add to each automatic credit card and ACH payment processed by billing events. Enabling this option may be in violation of your merchant agreement(s), so please check them carefully before enabling this option.', +# 'type' => 'select-part_pkg', +# }, +# +## { +## 'key' => 'auto_process-display', +## 'section' => 'billing', +## 'description' => 'When using auto_process-pkgpart, add the fee to the amount entered (default), or subtract the fee from the amount entered.', +## 'type' => 'select', +## 'select_hash' => [ +## 'add' => 'Add fee to amount entered', +## 'subtract' => 'Subtract fee from amount entered', +## ], +## }, +# +# { +# 'key' => 'auto_process-skip_first', +# 'section' => 'billing', +# 'description' => "When using auto_process-pkgpart, omit the fee if it is the customer's first payment.", +# 'type' => 'checkbox', # }, - - { - 'key' => 'auto_process-skip_first', - 'section' => 'billing', - 'description' => "When using auto_process-pkgpart, omit the fee if it is the customer's first payment.", - 'type' => 'checkbox', - }, { 'key' => 'allow_negative_charges', diff --git a/fs_selfservice/FS-SelfService/cgi/make_payment.html b/fs_selfservice/FS-SelfService/cgi/make_payment.html index bec37cac3..defd4a551 100644 --- a/fs_selfservice/FS-SelfService/cgi/make_payment.html +++ b/fs_selfservice/FS-SelfService/cgi/make_payment.html @@ -9,29 +9,15 @@ Amount Due
- $<%=sprintf("%.2f",$balance)%> -
- - - - Payment amount - -
-<%= - $amt = ''; - if ( $balance > 0 ) { - $amt = $balance; - $amt += $amt * $credit_card_surcharge_percentage/100 - if $credit_card_surcharge_percentage > 0; - $amt = sprintf("%.2f",$amt); - } - ''; -%> - $ + $<%=sprintf("%.2f",$balance)%>
+ +<%= $tr_amount_fee %> + <%= include('discount_term') %> + Card type diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi old mode 100644 new mode 100755 index fe8d08209..61361b8ee --- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi +++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi @@ -476,7 +476,21 @@ sub process_order_recharge { } sub make_payment { - payment_info( 'session_id' => $session_id ); + + my $payment_info = payment_info( 'session_id' => $session_id ); + + my $tr_amount_fee = mason_comp( + 'session_id' => $session_id, + 'comp' => '/elements/tr-amount_fee.html', + 'args' => [ 'amount' => $payment_info->{'balance'}, + ], + ); + + $tr_amount_fee = $tr_amount_fee->{'error'} || $tr_amount_fee->{'output'}; + + $payment_info->{'tr_amount_fee'} = $tr_amount_fee; + + $payment_info; } sub payment_results { diff --git a/httemplate/elements/tr-amount_fee.html b/httemplate/elements/tr-amount_fee.html index a1a9e3433..12488521a 100644 --- a/httemplate/elements/tr-amount_fee.html +++ b/httemplate/elements/tr-amount_fee.html @@ -90,7 +90,9 @@ if ( $amount > 0 ) { $amount += $fee if $fee && $fee_display eq 'subtract'; - &{ $opt{post_fee_callback} }( \$amount ) if $opt{post_fee_callback}; + #&{ $opt{post_fee_callback} }( \$amount ) if $opt{post_fee_callback}; + $amount += $amount * $opt{'surcharge_percentage'}/100 + if $opt{'surcharge_percentage'} > 0; $amount = sprintf("%.2f", $amount); } diff --git a/httemplate/misc/payment.cgi b/httemplate/misc/payment.cgi index 1ae15b930..e221c600a 100644 --- a/httemplate/misc/payment.cgi +++ b/httemplate/misc/payment.cgi @@ -16,7 +16,7 @@ 'process-display' => scalar($conf->config('manual_process-display')), 'process-skip-first' => $conf->exists('manual_process-skip_first'), 'num_payments' => scalar($cust_main->cust_pay), - 'post_fee_callback' => $post_fee_callback, + 'surcharge_percentage' => scalar($conf->config('credit-card-surcharge-percentage')), &> <& /elements/tr-select-discount_term.html, @@ -269,19 +269,6 @@ if ( $balance > 0 ) { $amount = $balance; } -my $post_fee_callback = sub { - my( $amountref ) = @_; - - return unless $$amountref > 0; - - my $conf = new FS::Conf; - - my $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage'); - $$amountref += $$amountref * $cc_surcharge_pct/100 if $cc_surcharge_pct > 0; - - $$amountref = sprintf("%.2f", $$amountref); -}; - my $payunique = "webui-payment-". time. "-$$-". rand() * 2**32; -- cgit v1.2.1 From 72f0bcbaece30fed81dd101cca1685b2c69013ec Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Fri, 3 Aug 2012 13:11:36 -0700 Subject: remove inadvertant debugging --- FS/FS/ClientAPI/MyAccount.pm | 2 -- 1 file changed, 2 deletions(-) diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 151a2ed67..3fd621493 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -196,8 +196,6 @@ sub login { } else { -warn Dumper($p); - my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } ) or return { error => 'Domain '. $p->{'domain'}. ' not found' }; -- cgit v1.2.1 From 2d7ff76b7fc593f420421c5d3e9c561eb67fd368 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Fri, 3 Aug 2012 15:26:01 -0700 Subject: nvoice voiding, RT#18677 --- FS/FS/cust_bill_pkg_void.pm | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/FS/FS/cust_bill_pkg_void.pm b/FS/FS/cust_bill_pkg_void.pm index b7c6feed5..8949ba7a3 100644 --- a/FS/FS/cust_bill_pkg_void.pm +++ b/FS/FS/cust_bill_pkg_void.pm @@ -4,10 +4,13 @@ use base qw( FS::TemplateItem_Mixin FS::Record ); use strict; use FS::Record qw( qsearch qsearchs dbh fields ); use FS::cust_bill_void; -use FS::cust_bill_pkg_detail_void; -use FS::cust_bill_pkg_display_void; -use FS::cust_bill_pkg_discount_void; +use FS::cust_bill_pkg_detail; +use FS::cust_bill_pkg_display; +use FS::cust_bill_pkg_discount; use FS::cust_bill_pkg; +use FS::cust_bill_pkg_tax_location; +use FS::cust_bill_pkg_tax_rate_location; +use FS::cust_tax_exempt_pkg; =head1 NAME -- cgit v1.2.1 From bb1b909a073b9cc318291aefa0ed0f3743616642 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Fri, 3 Aug 2012 15:26:25 -0700 Subject: nvoice voiding, RT#18677 --- FS/FS/cust_bill_pkg.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 304d51d6a..96fa408a8 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -21,6 +21,7 @@ use FS::cust_tax_adjustment; use FS::cust_bill_pkg_void; use FS::cust_bill_pkg_detail_void; use FS::cust_bill_pkg_display_void; +use FS::cust_bill_pkg_discount_void; use FS::cust_bill_pkg_tax_location_void; use FS::cust_bill_pkg_tax_rate_location_void; use FS::cust_tax_exempt_pkg_void; -- cgit v1.2.1 From 56d9a1fd046c50466c4d2a1ac6fca2d3776bf86b Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Fri, 3 Aug 2012 17:05:51 -0700 Subject: log rending time in trace files, RT#18719 --- htetc/handler.pl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/htetc/handler.pl b/htetc/handler.pl index 77b93463f..2e6fbe586 100644 --- a/htetc/handler.pl +++ b/htetc/handler.pl @@ -141,7 +141,9 @@ sub handler FS::Trace->log('done'); - FS::Trace->dumpfile("%%%FREESIDE_EXPORT%%%/profile/$$.".time, $r->filename) + FS::Trace->dumpfile( "%%%FREESIDE_EXPORT%%%/profile/$$.".time, + FS::Trace->total. ' '. $r->filename + ) if FS::Trace->total > 5; #10? FS::Trace->reset; -- cgit v1.2.1 From 0d8eb2d4d4a372680d0fa564fbfcba0d6674b259 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sat, 4 Aug 2012 09:26:23 -0700 Subject: fix skipping processing fee for first payment, RT#18345 --- FS/FS/ClientAPI/MasonComponent.pm | 2 +- FS/FS/ClientAPI/MyAccount.pm | 11 +++++++++-- httemplate/misc/payment.cgi | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/FS/FS/ClientAPI/MasonComponent.pm b/FS/FS/ClientAPI/MasonComponent.pm index c72d26879..61208c129 100644 --- a/FS/FS/ClientAPI/MasonComponent.pm +++ b/FS/FS/ClientAPI/MasonComponent.pm @@ -55,7 +55,7 @@ my %session_callbacks = ( %args, 'process-pkgpart' => scalar($conf->config('selfservice_process-pkgpart')), 'process-display' => scalar($conf->config('selfservice_process-display')), - 'process-skip-first' => $conf->exists('selfservice_process-skip_first'), + 'process-skip_first' => $conf->exists('selfservice_process-skip_first'), 'num_payments' => scalar($cust_main->cust_pay), 'surcharge_percentage' => scalar($conf->config('credit-card-surcharge-percentage')), ); diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 3fd621493..eddb16456 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -930,7 +930,12 @@ sub validate_payment { my $conf = new FS::Conf; my $fee_display = $conf->config('selfservice_process-display') || 'add'; my $fee_pkgpart = $conf->config('selfservice_process-pkgpart'); - if ( $fee_display eq 'add' && $fee_pkgpart ) { + my $fee_skip_first = $conf->exists('selfservice_process-skip_first'); + if ( $fee_display eq 'add' + and $fee_pkgpart + and ! $fee_skip_first || scalar($cust_main->cust_pay) + ) + { my $fee_pkg = qsearchs('part_pkg', { pkgpart=>$fee_pkgpart } ); $amount = sprintf('%.2f', $amount + $fee_pkg->option('setup_fee') ); } @@ -1097,7 +1102,9 @@ sub do_process_payment { #no error, so order the fee package if applicable... my $conf = new FS::Conf; my $fee_pkgpart = $conf->config('selfservice_process-pkgpart'); - if ( $fee_pkgpart ) { + my $fee_skip_first = $conf->exists('selfservice_process-skip_first'); + + if ( $fee_pkgpart and ! $fee_skip_first || scalar($cust_main->cust_pay) ) { my $cust_pkg = new FS::cust_pkg { 'pkgpart' => $fee_pkgpart }; diff --git a/httemplate/misc/payment.cgi b/httemplate/misc/payment.cgi index e221c600a..2d7307233 100644 --- a/httemplate/misc/payment.cgi +++ b/httemplate/misc/payment.cgi @@ -14,7 +14,7 @@ 'amount' => $amount, 'process-pkgpart' => scalar($conf->config('manual_process-pkgpart')), 'process-display' => scalar($conf->config('manual_process-display')), - 'process-skip-first' => $conf->exists('manual_process-skip_first'), + 'process-skip_first' => $conf->exists('manual_process-skip_first'), 'num_payments' => scalar($cust_main->cust_pay), 'surcharge_percentage' => scalar($conf->config('credit-card-surcharge-percentage')), &> -- cgit v1.2.1 From f5a3626fdc2ff793648cebc86f96bf3eac08cc65 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 6 Aug 2012 17:00:55 -0700 Subject: eliminate warnings --- FS/FS/cust_main.pm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 1da1f0ffd..3742bfdad 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -454,8 +454,10 @@ sub insert { warn " setting $l.custnum\n" if $DEBUG > 1; my $loc = $self->$l; - $loc->set(custnum => $self->custnum); - $error ||= $loc->replace; + unless ( $loc->custnum ) { + $loc->set(custnum => $self->custnum); + $error ||= $loc->replace; + } if ( $error ) { $dbh->rollback if $oldAutoCommit; -- cgit v1.2.1 From cd6bb5eafb6c7df7ea3eb7b78226a0d9c4b572ed Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 7 Aug 2012 15:43:07 -0700 Subject: cust_svc::ignore_quantity hack applies to services not in package too --- FS/FS/cust_svc.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index 2ec8f12c2..acd2fcdb7 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -335,10 +335,10 @@ sub check { ($part_svc) = grep { $_->svcpart == $self->svcpart } $cust_pkg->part_svc; return "No svcpart ". $self->svcpart. " services in pkgpart ". $cust_pkg->pkgpart - unless $part_svc; + unless $part_svc || $ignore_quantity; return "Already ". $part_svc->get('num_cust_svc'). " ". $part_svc->svc. " services for pkgnum ". $self->pkgnum - if $part_svc->get('num_avail') == 0 and !$ignore_quantity; + if $part_svc->get('num_avail') <= 0 and !$ignore_quantity; } $self->SUPER::check; -- cgit v1.2.1 From 2ec6f6df3efd366cb3c3532714dfc93bfa6dcef9 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 7 Aug 2012 16:07:51 -0700 Subject: cust_svc::ignore_quantity hack applies to services not in package too --- FS/FS/cust_svc.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index acd2fcdb7..52069316d 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -338,7 +338,7 @@ sub check { unless $part_svc || $ignore_quantity; return "Already ". $part_svc->get('num_cust_svc'). " ". $part_svc->svc. " services for pkgnum ". $self->pkgnum - if $part_svc->get('num_avail') <= 0 and !$ignore_quantity; + if !$ignore_quantity && $part_svc->get('num_avail') <= 0 ; } $self->SUPER::check; -- cgit v1.2.1 From 7792fb0d3dfa4e77f41b8055e96a8938153a7677 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 7 Aug 2012 17:12:18 -0700 Subject: better debugging for CCH import failure, RT#18817 --- FS/FS/cust_tax_location.pm | 2 +- FS/FS/part_pkg_taxrate.pm | 2 +- FS/FS/tax_class.pm | 2 +- FS/FS/tax_rate.pm | 2 +- FS/FS/tax_rate_location.pm | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/FS/FS/cust_tax_location.pm b/FS/FS/cust_tax_location.pm index 161a6547b..1a9bf5a41 100644 --- a/FS/FS/cust_tax_location.pm +++ b/FS/FS/cust_tax_location.pm @@ -298,7 +298,7 @@ sub batch_import { } if ( scalar( @columns ) ) { $dbh->rollback if $oldAutoCommit; - return "Unexpected trailing columns in line (wrong format?): $line"; + return "Unexpected trailing columns in line (wrong format?) importing cust_tax_location: $line"; } my $error = &{$hook}(\%cust_tax_location); diff --git a/FS/FS/part_pkg_taxrate.pm b/FS/FS/part_pkg_taxrate.pm index e29c3d0b4..c83f700d9 100644 --- a/FS/FS/part_pkg_taxrate.pm +++ b/FS/FS/part_pkg_taxrate.pm @@ -384,7 +384,7 @@ sub batch_import { } if ( scalar( @columns ) ) { $dbh->rollback if $oldAutoCommit; - return "Unexpected trailing columns in line (wrong format?): $line"; + return "Unexpected trailing columns in line (wrong format?) importing part_pkg_taxrate: $line"; } my $error = &{$hook}(\%part_pkg_taxrate); diff --git a/FS/FS/tax_class.pm b/FS/FS/tax_class.pm index 4f0396982..bfec2c06c 100644 --- a/FS/FS/tax_class.pm +++ b/FS/FS/tax_class.pm @@ -339,7 +339,7 @@ sub batch_import { } if ( scalar( @columns ) ) { $dbh->rollback if $oldAutoCommit; - return "Unexpected trailing columns in line (wrong format?): $line"; + return "Unexpected trailing columns in line (wrong format?) importing tax_class: $line"; } my $error = &{$hook}(\%tax_class); diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm index e9496e4f5..77cd5cb52 100644 --- a/FS/FS/tax_rate.pm +++ b/FS/FS/tax_rate.pm @@ -760,7 +760,7 @@ sub batch_import { } if ( scalar( @columns ) ) { $dbh->rollback if $oldAutoCommit; - return "Unexpected trailing columns in line (wrong format?): $line"; + return "Unexpected trailing columns in line (wrong format?) importing tax_rate: $line"; } my $error = &{$hook}(\%tax_rate); diff --git a/FS/FS/tax_rate_location.pm b/FS/FS/tax_rate_location.pm index 1a6c47dcf..b4be8b90e 100644 --- a/FS/FS/tax_rate_location.pm +++ b/FS/FS/tax_rate_location.pm @@ -301,7 +301,7 @@ sub batch_import { } if ( scalar( @columns ) ) { $dbh->rollback if $oldAutoCommit; - return "Unexpected trailing columns in line (wrong format?): $line"; + return "Unexpected trailing columns in line (wrong format?) importing tax-rate_location: $line"; } my $error = &{$hook}(\%tax_rate_location); -- cgit v1.2.1 From 21891ab9181cb54c36d78d6aacccadc1aaf910d7 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 7 Aug 2012 17:45:24 -0700 Subject: ignore extra columns in CCH diff (bad data from last update?) --- FS/FS/tax_rate.pm | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm index 77cd5cb52..3fdaa1efe 100644 --- a/FS/FS/tax_rate.pm +++ b/FS/FS/tax_rate.pm @@ -758,10 +758,13 @@ sub batch_import { foreach my $field ( @fields ) { $tax_rate{$field} = shift @columns; } - if ( scalar( @columns ) ) { - $dbh->rollback if $oldAutoCommit; - return "Unexpected trailing columns in line (wrong format?) importing tax_rate: $line"; - } + + #ignoring extra columns (bad data from last update?) and seeing if that + # allows the upgrade to proceed + #if ( scalar( @columns ) ) { + # $dbh->rollback if $oldAutoCommit; + # return "Unexpected trailing columns in line (wrong format?) importing tax_rate: $line"; + #} my $error = &{$hook}(\%tax_rate); if ( $error ) { @@ -1635,16 +1638,16 @@ sub process_download_and_update { if (-d $dir) { - if (-d "$dir.4") { - opendir(my $dirh, "$dir.4") or die "failed to open $dir.4: $!\n"; + if (-d "$dir.9") { + opendir(my $dirh, "$dir.9") or die "failed to open $dir.9: $!\n"; foreach my $file (readdir($dirh)) { - unlink "$dir.4/$file" if (-f "$dir.4/$file"); + unlink "$dir.9/$file" if (-f "$dir.9/$file"); } closedir($dirh); - rmdir "$dir.4"; + rmdir "$dir.9"; } - for (3, 2, 1) { + for (8, 7, 6, 5, 4, 3, 2, 1) { if ( -e "$dir.$_" ) { rename "$dir.$_", "$dir.". ($_+1) or die "can't rename $dir.$_: $!\n"; } -- cgit v1.2.1 From 21254f0e0062b92c19530c49c6eacc9ce3e93827 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 7 Aug 2012 18:39:05 -0700 Subject: Business::BatchPayment fixes for TD EFT format, #17878 --- FS/FS/Cron/pay_batch.pm | 2 +- FS/FS/pay_batch.pm | 11 +++++++---- FS/FS/pay_batch/td_eft1464.pm | 9 +++++++++ httemplate/edit/payment_gateway.html | 3 ++- httemplate/search/elements/cust_pay_batch_top.html | 3 ++- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/FS/FS/Cron/pay_batch.pm b/FS/FS/Cron/pay_batch.pm index c7cedafb9..0ab37dd13 100644 --- a/FS/FS/Cron/pay_batch.pm +++ b/FS/FS/Cron/pay_batch.pm @@ -103,7 +103,7 @@ sub batch_receive { if ( $gateway->batch_processor->can('default_transport') ) { warn "Importing results from '".$gateway->label."'\n" if $DEBUG; $error = eval { - FS::pay_batch->import_from_gateway( $gateway, debug => $DEBUG ) + FS::pay_batch->import_from_gateway( gateway =>$gateway, debug => $DEBUG ) } || $@; if ( $error ) { # this we can roll back diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm index 813d096b4..e98cf5aeb 100644 --- a/FS/FS/pay_batch.pm +++ b/FS/FS/pay_batch.pm @@ -807,8 +807,8 @@ sub try_to_resolve { } ); - if ( @unresolved ) { - my $days = $conf->config('batch-auto_resolve_days') || ''; + if ( @unresolved and $conf->exists('batch-auto_resolve_days') ) { + my $days = $conf->config('batch-auto_resolve_days'); # can be zero # either 'approve' or 'decline' my $action = $conf->config('batch-auto_resolve_status') || ''; return unless @@ -861,6 +861,9 @@ sub prepare_for_export { return "error updating pay_batch status: $error\n" if $error; } elsif ($status eq 'I' && $curuser->access_right('Reprocess batches')) { $first_download = 0; + } elsif ($status eq 'R' && + $curuser->access_right('Redownload resolved batches')) { + $first_download = 0; } else { die "No pending batch.\n"; } @@ -1080,7 +1083,7 @@ sub _upgrade_data { for my $format (keys %export_info) { my $mod = "FS::pay_batch::$format"; if ( $mod->can('_upgrade_gateway') - and length( $conf->config("batchconfig-$format") ) ) { + and exists( $conf->config("batchconfig-$format") ) ) { local $@; my ($module, %gw_options) = $mod->_upgrade_gateway; @@ -1109,7 +1112,7 @@ sub _upgrade_data { # and if appropriate, make it the system default for my $payby (qw(CARD CHEK)) { - if ( $conf->config("batch-fixed_format-$payby") eq $format ) { + if ( ($conf->config("batch-fixed_format-$payby") || '') eq $format ) { warn "Setting as default for $payby.\n"; $conf->set("batch-gateway-$payby", $gateway->gatewaynum); $conf->delete("batch-fixed_format-$payby"); diff --git a/FS/FS/pay_batch/td_eft1464.pm b/FS/FS/pay_batch/td_eft1464.pm index 3a6befef5..93612f1ea 100644 --- a/FS/FS/pay_batch/td_eft1464.pm +++ b/FS/FS/pay_batch/td_eft1464.pm @@ -154,5 +154,14 @@ $name = 'td_eft1464'; }, ); +sub _upgrade_gateway { + my $conf = FS::Conf->new; + my @batchconfig = $conf->config('batchconfig-td_eft1464'); + my %options; + @options{ qw(originator datacentre short_name long_name return_branch + return_account cpa_code) } = @batchconfig; + ( 'TD_EFT', %options ); +} + 1; diff --git a/httemplate/edit/payment_gateway.html b/httemplate/edit/payment_gateway.html index e5897b035..dfe52f109 100644 --- a/httemplate/edit/payment_gateway.html +++ b/httemplate/edit/payment_gateway.html @@ -91,6 +91,7 @@ my %modules = ( 'KeyBank' => 'Business::BatchPayment', 'Paymentech' => 'Business::BatchPayment', + 'TD_EFT' => 'Business::BatchPayment', ); my %modules_for_namespace; @@ -141,7 +142,7 @@ my $fields = [ { field => 'gateway_options', type => 'textarea', - rows => '8', + rows => '12', cols => '40', curr_value_callback => sub { my($cgi, $object, $fref) = @_; join("\r", $object->options ); diff --git a/httemplate/search/elements/cust_pay_batch_top.html b/httemplate/search/elements/cust_pay_batch_top.html index 739e65b50..1dcc37ac1 100644 --- a/httemplate/search/elements/cust_pay_batch_top.html +++ b/httemplate/search/elements/cust_pay_batch_top.html @@ -33,6 +33,7 @@ Download batch in format +% } + + <% include( '/elements/tr-input-text.html', + 'field' => 'national_id1', + 'value' => $nric, + 'label' => 'NRIC', + ) + %> + <% include( '/elements/tr-input-text.html', + 'field' => 'national_id2', + 'value' => $old, + 'label' => 'Old IC/Passport', + ) + %> + +% } else { +% warn "unknown national_id-country $id_country"; +% } +% } + % if ( $conf->exists('cust_main-enable_birthdate') ) { <% include( '/elements/tr-input-date-field.html', { 'name' => 'birthdate', diff --git a/httemplate/edit/process/cust_main.cgi b/httemplate/edit/process/cust_main.cgi index 866452de1..622dd87c5 100755 --- a/httemplate/edit/process/cust_main.cgi +++ b/httemplate/edit/process/cust_main.cgi @@ -110,6 +110,36 @@ if ( $cgi->param('no_credit_limit') ) { $new->tagnum( [ $cgi->param('tagnum') ] ); +if ( my $id_country = $conf->config('national_id-country') ) { + if ( $id_country eq 'MY' ) { + + if ( $cgi->param('national_id1') =~ /\S/ ) { + my $nric = $cgi->param('national_id1'); + $nric =~ s/\s//g; + if ( $nric =~ /^(\d{6})\-?(\d{2})\-?(\d{4})$/ ) { + $new->national_id( "$1-$2-$3" ); + } else { + $error ||= "Illegal NRIC: ". $cgi->param('national_id1'); + } + } elsif ( $cgi->param('national_id2') =~ /\S/ ) { + my $oldic = $cgi->param('national_id2'); + $oldic =~ s/\s//g; + if ( $oldic =~ /^\w\d{9}$/ ) { + $new->national_id($oldic); + } else { + $error ||= "Illegal Old IC/Passport: ". $cgi->param('national_id2'); + } + } else { + $error ||= 'Either NRIC or Old IC/Passport is required'; + } + + } else { + warn "unknown national_id-country $id_country"; + } +} elsif ( $cgi->param('national_id0') ) { + $new->national_id( $cgi->param('national_id0') ); +} + my %usedatetime = ( 'birthdate' => 1, 'spouse_birthdate' => 1, 'anniversary_date' => 1, diff --git a/httemplate/view/cust_main/misc.html b/httemplate/view/cust_main/misc.html index dc6da53f1..9713b2469 100644 --- a/httemplate/view/cust_main/misc.html +++ b/httemplate/view/cust_main/misc.html @@ -102,6 +102,26 @@ <% $cust_main->signupdate ? time2str($date_format, $cust_main->signupdate) : '' %> +% my $id_country = $conf->config('national_id-country'); +% if ( $id_country ) { +% if ( $id_country eq 'MY' ) { + +% my($old, $nric) = ( '', ''); +% if ( $cust_main->national_id =~ /^\w\d{9}$/ ) { + <% mt('Old IC/Passport') |h %> +% } elsif ( $cust_main->national_id =~ /^\d{6}\-\d{2}\-\d{4}$/ ) { + <% mt('NRIC') |h %> +% } else { +% warn "unknown national_id format"; + +% } + <% $cust_main->national_id |h %> + +% } else { +% warn "unknown national_id-country $id_country"; +% } +% } + % if ( $conf->exists('cust_main-enable_birthdate') ) { % my $dt = $cust_main->birthdate ne '' % ? DateTime->from_epoch( 'epoch' => $cust_main->birthdate, -- cgit v1.2.1 From 7b19b666fc36f8dc28747a11bd2b389d8d2ef75f Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 10 Aug 2012 16:07:50 -0700 Subject: sqlradius data usage report, #18823 --- httemplate/elements/menu.html | 2 ++ httemplate/search/elements/search-html.html | 4 ++-- httemplate/search/elements/search.html | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 019afe94e..c656cb9eb 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -256,6 +256,8 @@ tie my %report_inventory, 'Tie::IxHash', tie my %report_rating, 'Tie::IxHash'; $report_rating{'RADIUS sessions'} = [ $fsurl.'search/sqlradius.html', '' ] if $curuser->access_right("Usage: RADIUS sessions"); +$report_rating{'RADIUS data usage'} = [ $fsurl.'search/report_sqlradius_usage.html', '' ] + if $curuser->access_right("Usage: RADIUS sessions"); $report_rating{'Call Detail Records (CDRs)'} = [ $fsurl.'search/report_cdr.html', '' ] if $curuser->access_right("Usage: Call Detail Records (CDRs)"); $report_rating{'Unrateable CDRs'} = [ $fsurl.'search/cdr.html?freesidestatus=failed;cdrbatchnum=_ALL_' ] diff --git a/httemplate/search/elements/search-html.html b/httemplate/search/elements/search-html.html index c27771494..cf60d8f82 100644 --- a/httemplate/search/elements/search-html.html +++ b/httemplate/search/elements/search-html.html @@ -134,9 +134,9 @@ % and !$opt{'disable_download'} % and $type ne 'html-print' ) { - + - Download full results
+ <% $opt{'download_label'} || 'Download full results' %>
% $cgi->param('_type', "$xlsname.xls" ); as query_string %>">Excel spreadsheet
diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html index 9bc66b6fa..eca68a2f8 100644 --- a/httemplate/search/elements/search.html +++ b/httemplate/search/elements/search.html @@ -162,7 +162,11 @@ Example: # Excel-specific listref of ( hashrefs or coderefs ) # each hashref: http://search.cpan.org/dist/Spreadsheet-WriteExcel/lib/Spreadsheet/WriteExcel.pm#Format_methods_and_Format_properties 'xls_format' => => [], - + + + # miscellany + 'download_label' => 'Download this report', + # defaults to 'Download full results' &> -- cgit v1.2.1 From db80d13447786bc554d40d4817a1b0d7a5b5e09f Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 10 Aug 2012 17:09:46 -0700 Subject: rest of sqlradius data usage report --- httemplate/search/report_sqlradius_usage.html | 40 ++++++ httemplate/search/sqlradius_usage.html | 194 ++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 httemplate/search/report_sqlradius_usage.html create mode 100644 httemplate/search/sqlradius_usage.html diff --git a/httemplate/search/report_sqlradius_usage.html b/httemplate/search/report_sqlradius_usage.html new file mode 100644 index 000000000..01215e834 --- /dev/null +++ b/httemplate/search/report_sqlradius_usage.html @@ -0,0 +1,40 @@ +<& /elements/header.html, mt($title) &> + +
+ + 'all', +&> + +% my @exporttypes = map { "'$_'" } qw(sqlradius broadband_sqlradius); +<& /elements/tr-select-table.html, + 'label' => 'Export', + 'table' => 'part_export', + 'name_col' => 'label', + 'hashref' => {}, + 'extra_sql' => ' WHERE exporttype IN('.join(',', @exporttypes).')', + 'disable_empty' => 1, + 'order_by' => 'ORDER BY exportnum', +&> + +<& /elements/tr-input-beginning_ending.html &> + +
+ +
+ + +
+ +<& /elements/footer.html &> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Usage: RADIUS sessions'); + # yes? + +my $title = 'Data Usage Report'; + + diff --git a/httemplate/search/sqlradius_usage.html b/httemplate/search/sqlradius_usage.html new file mode 100644 index 000000000..dd06c10fa --- /dev/null +++ b/httemplate/search/sqlradius_usage.html @@ -0,0 +1,194 @@ +% if ( @include_agents ) { +% # jumbo report +<& /elements/header.html, $title &> +% foreach my $agent ( @include_agents ) { +% $cgi->param('agentnum', $agent->agentnum); #for download links +
+<% $agent->agent %>

+ <& sqlradius_usage.html, + export => $export, + agentnum => $agent->agentnum, + nohtmlheader => 1, + usage_by_username => \%usage_by_username, + download_label => 'Download this section', + &> +
+

+% } +<& /elements/footer.html &> +% } else { +<& elements/search.html, + 'title' => $title, + 'name' => 'services', + 'query' => $sql_query, + 'count_query' => $sql_query->{'count_query'}, + 'header' => [ #FS::UI::Web::cust_header(), + '#', + 'Customer', + 'Package', + @svc_header, + 'Upload', + 'Download', + 'Total', + ], + 'footer' => \@footer, + 'fields' => [ #\&FS::UI::Web::cust_fields, + 'display_custnum', + 'name', + 'pkg', + @svc_fields, + @svc_usage, + ], + 'links' => [ #( map { $_ ne 'Cust. Status' ? $link_cust : '' } + # FS::UI::Web::cust_header() ), + $link_cust, + $link_cust, + '', #package + ( map { $link_svc } @svc_header ), + '', + '', + '', + ], + 'align' => #FS::UI::Web::cust_aligns() . + 'rlc' . ('l' x scalar(@svc_header)) . 'rrr' , + 'nohtmlheader' => ($opt{'nohtmlheader'} || 0), + 'download_label' => $opt{'download_label'}, +&> +% } +<%init> + +my %opt = @_; + +die "access denied" unless + $FS::CurrentUser::CurrentUser->access_right('List services'); + +my $title = 'Data Usage Report - '; +my $agentnum; +my @include_agents; + +if ( $opt{'agentnum'} ) { + $agentnum = $opt{'agentnum'}; +} elsif ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + $agentnum = $1; +} + +if ( $agentnum ) { + my $agent = FS::agent->by_key($agentnum); + $title = $agent->agent." $title"; +} else { + @include_agents = qsearch('agent', {}); +} + +# usage query params +my( $beginning, $ending ) = FS::UI::Web::parse_beginning_ending($cgi); + +if ( $beginning ) { + $title .= time2str('%h %o %Y ', $beginning); +} +$title .= 'through '; +if ( $ending == 4294967295 ) { + $title .= 'now'; +} else { + $title .= time2str('%h %o %Y', $ending); +} + +my $export; +my %usage_by_username; +if ( exists($opt{usage_by_username}) ) { + # There's no agent separation in the radacct data. So in the jumbo report + # do this procedure once, and pass the hash into all the per-agent sections. + %usage_by_username = %{ $opt{usage_by_username} }; + $export = $opt{export}; +} else { + + $cgi->param('exportnum') =~ /^(\d+)$/ + or die "illegal export: '".$cgi->param('exportnum')."'"; + $export = FS::part_export->by_key($1) + or die "exportnum $1 not found"; + $export->exporttype =~ /sqlradius/ + or die "exportnum ".$export->exportnum." is type ".$export->exporttype. + ", not sqlradius"; + + my $usage = $export->usage_sessions( { + stoptime_start => $beginning, + stoptime_end => $ending, + summarize => 1 + } ); + # arrayref of hashrefs of + # (username, acctsessiontime, acctinputoctets, acctoutputoctets) + # (XXX needs to include 'realm' for sqlradius_withdomain) + # rearrange to be indexed by username. + + foreach (@$usage) { + my $username = $_->{'username'}; + my @row = ( + $_->{'acctinputoctets'}, + $_->{'acctoutputoctets'}, + $_->{'acctinputoctets'} + $_->{'acctoutputoctets'} + ); + $usage_by_username{$username} = \@row; + } +} + +#warn Dumper(\%usage_by_username); +my @total_usage = (0, 0, 0, 0); # session time, input, output, input + output +my @svc_usage = map { + my $i = $_; + sub { + my $username = $export->export_username(shift); + return '' if !exists($usage_by_username{$username}); + my $value = $usage_by_username{ $username }->[$i]; + $total_usage[$i] += $value; + FS::UI::bytecount::display_bytecount($value); + } +} (0,1,2); + +# set up svcdb-specific stuff +my $export_username = sub { + $export->export_username(shift); # countrycode + phone, formatted MAC, etc. +}; + +my %svc_header = ( + svc_acct => [ 'Username' ], + svc_broadband => [ 'MAC address', 'IP address' ], +# svc_phone => [ 'Phone' ], #not yet supported, no search method + # (not sure input/output octets is relevant) +); +my %svc_fields = ( + svc_acct => [ $export_username ], + svc_broadband => [ $export_username, 'ip_addr' ], +# svc_phone => [ $export_username ], +); + +# what kind of service we're operating on +my $svcdb = FS::part_export::export_info()->{$export->exporttype}->{'svc'}; +my $class = "FS::$svcdb"; +my @svc_header = @{ $svc_header{$svcdb} }; +my @svc_fields = @{ $svc_fields{$svcdb} }; + +# svc_x search params +my %search_hash = ( 'agentnum' => $agentnum ); + +my $sql_query = $class->search(\%search_hash); +$sql_query->{'select'} .= ', part_pkg.pkg'; +$sql_query->{'addl_from'} .= ' LEFT JOIN part_pkg USING (pkgpart)'; + +my $link_svc = [ $p.'view/cust_svc.cgi?', 'svcnum' ]; + +my $link_cust = [ $p.'view/cust_main.cgi?', 'custnum' ]; + +# columns between the customer name and the usage fields +my $skip_cols = 1 + scalar(@svc_header); + +my @footer = ( + '', + FS::Record->scalar_sql($sql_query->{count_query}) . ' services', + (' ') x $skip_cols, + map { + sub { # defer this until the rows have been processed + FS::UI::bytecount::display_bytecount($total_usage[$_]) + } + } (0,1,2) +); + + -- cgit v1.2.1 From 43a3bfc79a80263967e5cba41ddbc72cec9c1e97 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 13 Aug 2012 13:00:41 -0700 Subject: should fix intermittant slowness, RT#18719 --- Makefile | 1 + htetc/handler.pl | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 864d9c279..903c0eb00 100644 --- a/Makefile +++ b/Makefile @@ -170,6 +170,7 @@ install-docs: check-conflicts docs cp htetc/handler.pl ${MASON_HANDLER} perl -p -i -e "\ s|%%%FREESIDE_EXPORT%%%|${FREESIDE_EXPORT}|g;\ + s'%%%RT_ENABLED%%%'${RT_ENABLED}'g; \ " ${MASON_HANDLER} || true mkdir -p ${FREESIDE_EXPORT}/profile chown freeside ${FREESIDE_EXPORT}/profile diff --git a/htetc/handler.pl b/htetc/handler.pl index 2e6fbe586..d7c56572c 100644 --- a/htetc/handler.pl +++ b/htetc/handler.pl @@ -7,6 +7,14 @@ use warnings; use FS::Mason qw( mason_interps ); use FS::Trace; +if ( %%%RT_ENABLED%%% ) { + require RT; + $> = scalar(getpwnam('freeside')); + RT::LoadConfig(); + RT::Init(); + $> = $<; +} + #use vars qw($r); # Bring in ApacheHandler, necessary for mod_perl integration. @@ -151,19 +159,10 @@ sub handler $status; } -my $rt_initialized = 0; - sub my_rt_init { return unless $RT::VERSION; - - if ( $rt_initialized ) { - RT::ConnectToDatabase(); - RT::InitSignalHandlers(); - } else { - RT::LoadConfig(); - RT::Init(); - $rt_initialized++; - } + RT::ConnectToDatabase(); + RT::InitSignalHandlers(); } 1; -- cgit v1.2.1 From 0bd09a9eff467033be42f922f2297a7b488e7dc1 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 13 Aug 2012 16:21:36 -0700 Subject: per-agent subtotals on sales report, #18566 --- httemplate/graph/cust_bill_pkg.cgi | 54 ++++++++++++++++++++++-- httemplate/graph/elements/monthly.html | 36 ++++++++++------ httemplate/graph/elements/report.html | 15 ++++++- httemplate/graph/report_cust_bill_pkg.html | 67 ++++++++++++++++++++---------- 4 files changed, 133 insertions(+), 39 deletions(-) diff --git a/httemplate/graph/cust_bill_pkg.cgi b/httemplate/graph/cust_bill_pkg.cgi index e7a3bd27e..c334ae9e7 100644 --- a/httemplate/graph/cust_bill_pkg.cgi +++ b/httemplate/graph/cust_bill_pkg.cgi @@ -8,6 +8,7 @@ 'graph_labels' => \@labels, 'colors' => \@colors, 'links' => \@links, + 'no_graph' => \@no_graph, 'remove_empty' => 1, 'bottom_total' => 1, 'bottom_link' => $bottom_link, @@ -118,6 +119,7 @@ my @params = (); my @labels = (); my @colors = (); my @links = (); +my @no_graph; my @components = ( 'SRU' ); # split/omit components as appropriate @@ -134,6 +136,11 @@ elsif ( $use_usage == 2 ) { $components[-1] =~ s/U//; } +# Categorization of line items goes +# Agent -> Referral -> Package class -> Component (setup/recur/usage) +# If per-agent totals are enabled, they go under the Agent level. +# There aren't any other kinds of subtotals. + foreach my $agent ( $all_agent || $sel_agent || qsearch('agent', { 'disabled' => '' } ) ) { my $col_scheme = Color::Scheme->new @@ -146,7 +153,11 @@ foreach my $agent ( $all_agent || $sel_agent || qsearch('agent', { 'disabled' => ### fixup the color handling for package classes... ### and usage - foreach my $part_referral ( $all_part_referral || $sel_part_referral || qsearch('part_referral', { 'disabled' => '' } ) ) { + foreach my $part_referral ( + $all_part_referral || + $sel_part_referral || + qsearch('part_referral', { 'disabled' => '' } ) + ) { foreach my $pkg_class ( @pkg_class ) { foreach my $component ( @components ) { @@ -186,9 +197,46 @@ foreach my $agent ( $all_agent || $sel_agent || qsearch('agent', { 'disabled' => @onetime_colors = ($col_scheme->colors)[2,6,10,3,7,11] unless @onetime_colors; push @colors, shift @recur_colors; - - } + push @no_graph, 0; + + } #foreach $component + } #foreach $pkg_class + } #foreach $part_referral + + if ( $cgi->param('agent_totals') and !$all_agent ) { + my $row_agentnum = $agent->agentnum; + # Include all components that are anywhere on this report + my $component = join('', @components); + + my @row_params = ( 'agentnum' => $row_agentnum, + 'use_override' => $use_override, + 'average_per_cust_pkg' => $average_per_cust_pkg, + 'distribute' => $distribute, + 'charges' => $component, + ); + my $row_link = "$link;". + "agentnum=$row_agentnum;". + "distribute=$distribute;". + "charges=$component"; + + # Also apply any refnum/classnum filters + if ( !$all_class and scalar(@pkg_class) == 1 ) { + # then a specific class has been chosen, but it may be the empty class + my $row_classnum = ref($pkg_class[0]) ? $pkg_class[0]->classnum : 0; + push @row_params, 'classnum' => $row_classnum; + $row_link .= ";classnum=$row_classnum"; } + if ( $sel_part_referral ) { + push @row_params, 'refnum' => $sel_part_referral->refnum; + $row_link .= ";refnum=".$sel_part_referral->refnum; + } + + push @items, 'cust_bill_pkg'; + push @labels, mt('[_1] - Subtotal', $agent->agent); + push @params, \@row_params; + push @links, $row_link; + push @colors, '000000'; # better idea? + push @no_graph, 1; } $hue += $hue_increment; diff --git a/httemplate/graph/elements/monthly.html b/httemplate/graph/elements/monthly.html index 839a3873e..c736de696 100644 --- a/httemplate/graph/elements/monthly.html +++ b/httemplate/graph/elements/monthly.html @@ -20,6 +20,7 @@ Example: 'link_fromparam' => 'param_from', #defaults to 'begin' 'link_toparam' => 'param_to', #defaults to 'end' 'daily' => 1, # omit for monthly granularity + 'no_graph' => \@no_graph, # items to leave off the graph (subtotals) #optional, pulled from CGI params if not specified 'start_month' => $smonth, @@ -49,18 +50,19 @@ Example: 'items' => $data->{'items'}, 'data' => $data->{'data'}, 'row_labels' => $data->{'item_labels'}, - 'graph_labels' => $opt{'graph_labels'} || $data->{'item_labels'}, + 'graph_labels' => \@graph_labels, 'col_labels' => $col_labels, 'axis_labels' => $data->{label}, - 'colors' => $data->{colors}, + 'colors' => \@colors, 'links' => \@links, + 'no_graph' => \@no_graph, 'bottom_link' => \@bottom_link, 'transpose' => $opt{'daily'}, - map { $_, $opt{$_} } (qw(title - nototal - graph_type - bottom_total - sprintf + map { $_, $opt{$_} } (qw(title + nototal + graph_type + bottom_total + sprintf disable_money chart_options)), ) %> @@ -103,7 +105,7 @@ if ( $opt{'daily'} ) { # daily granularity my %reportopts = ( 'items' => \@items, 'params' => $opt{'params'}, - 'item_labels' => ( $cgi->param('_type') =~ /^(png)$/ + 'item_labels' => ( $cgi->param('_type') =~ /^(png)$/ ? $opt{'graph_labels'} : $opt{'labels'} ), @@ -140,12 +142,20 @@ my $col_labels = [ map { my $m = $_; $m =~ s/^(\d+)\//$mon[$1-1] / ; $m } @{$data->{label}} ]; $col_labels = $data->{label} if $opt{'daily'}; +my @colors; +my @graph_labels; +my @no_graph; if ( $opt{'remove_empty'} ) { - # need to filter out series labels for collapsed rows - $opt{'graph_labels'} = [ - map { $opt{'graph_labels'}[$_] } - @{ $data->{indices} } - ]; + # then filter out per-item things for collapsed rows + foreach my $i (@{ $data->{'indices'} }) { + push @colors, $opt{'colors'}[$i]; + push @graph_labels, $opt{'graph_labels'}[$i]; + push @no_graph, $opt{'no_graph'}[$i]; + } +} else { + @colors = @{ $opt{'colors'} }; + @graph_labels = @{ $opt{'graph_labels'} }; + @no_graph = @{ $opt{'no_graph'} || [] }; } my @links; diff --git a/httemplate/graph/elements/report.html b/httemplate/graph/elements/report.html index f7746165a..98b477826 100644 --- a/httemplate/graph/elements/report.html +++ b/httemplate/graph/elements/report.html @@ -14,6 +14,7 @@ Example: 'graph_labels' => \@graph_labels, #defaults to row_labels 'links' => \@links, #optional + 'no_graph' => \@no_graph, #optional #these run parallel to the elements of each @item 'col_labels' => \@col_labels, #required @@ -128,7 +129,19 @@ any delimiter and linked from the elements in @data. % <% $output %> % } elsif ( $cgi->param('_type') eq 'png' ) { -% +% # delete any items that shouldn't be on the graph +% if ( my $no_graph = $opt{'no_graph'} ) { +% my $i = 0; +% while (@$no_graph) { +% if ( shift @$no_graph ) { +% splice @data, $i, 1; +% splice @{$opt{'graph_labels'}}, $i, 1; +% splice @{$opt{'colors'}}, $i, 1; +% $i--; # because everything is shifted down +% } +% $i++; +% } +% } % my $graph_type = 'LinesPoints'; % if ( $opt{'graph_type'} =~ /^(LinesPoints|Mountain|Bars)$/ ) { % $graph_type = $1; diff --git a/httemplate/graph/report_cust_bill_pkg.html b/httemplate/graph/report_cust_bill_pkg.html index 4cedcef17..31792e8dd 100644 --- a/httemplate/graph/report_cust_bill_pkg.html +++ b/httemplate/graph/report_cust_bill_pkg.html @@ -11,28 +11,45 @@ Show projected data for future months -<% include('/elements/tr-select-agent.html', - 'label' => 'Agent ', - 'disable_empty' => 0, - 'pre_options' => [ 'all' => 'all (aggregate)' ], - 'empty_label' => 'all (breakdown)', - ) -%> - -<% include('/elements/tr-select-part_referral.html', - 'label' => 'Advertising source ', - 'disable_empty' => 0, - 'pre_options' => [ 'all' => 'all (aggregate)' ], - 'empty_label' => 'all (breakdown)', - ) -%> - -<% include('/elements/tr-select-pkg_class.html', - 'pre_options' => [ 'all' => 'all (aggregate)', - '0' => 'all (breakdown)' ], - 'empty_label' => '(empty class)', - ) -%> + + +<& /elements/tr-select-agent.html, + 'field' => 'agentnum', + 'label' => 'Agent ', + 'disable_empty' => 0, + 'pre_options' => [ 'all' => 'all (aggregate)' ], + 'empty_label' => 'all (breakdown)', + 'onchange' => 'enable_agent_totals', +&> + +<& /elements/tr-select-part_referral.html, + 'field' => 'refnum', + 'label' => 'Advertising source ', + 'disable_empty' => 0, + 'pre_options' => [ 'all' => 'all (aggregate)' ], + 'empty_label' => 'all (breakdown)', + 'onchange' => 'enable_agent_totals' +&> + +<& /elements/tr-select-pkg_class.html, + 'field' => 'classnum', + 'pre_options' => [ 'all' => 'all (aggregate)', + '0' => 'all (breakdown)' ], + 'empty_label' => '(empty class)', + 'onchange' => 'enable_agent_totals', +&> <%ARGS> diff --git a/rt/share/html/Elements/ListActions b/rt/share/html/Elements/ListActions index 999d3fe5b..8929ff731 100755 --- a/rt/share/html/Elements/ListActions +++ b/rt/share/html/Elements/ListActions @@ -65,7 +65,7 @@ if ( ref( $session{'Actions'}{''} ) eq 'ARRAY' ) { unshift @actions, @{ delete $session{'Actions'}{''} }; } -my $actions_pointer = $m->request_args->{'results'}; +my $actions_pointer = $DECODED_ARGS->{'results'}; if ($actions_pointer && ref( $session{'Actions'}->{$actions_pointer} ) eq 'ARRAY' ) { unshift @actions, @{ delete $session{'Actions'}->{$actions_pointer} }; diff --git a/rt/share/html/Elements/MessageBox b/rt/share/html/Elements/MessageBox index 61995e057..69227bfa9 100755 --- a/rt/share/html/Elements/MessageBox +++ b/rt/share/html/Elements/MessageBox @@ -46,7 +46,7 @@ %# %# END BPS TAGGED BLOCK }}} % $m->callback( %ARGS, CallbackName => 'AfterTextArea' ); @@ -89,4 +89,5 @@ $Width => RT->Config->Get('MessageBoxWidth', $session{'CurrentUser'} $Height => RT->Config->Get('MessageBoxHeight', $session{'CurrentUser'} ) || 15 $Wrap => RT->Config->Get('MessageBoxWrap', $session{'CurrentUser'} ) || 'SOFT' $IncludeSignature => RT->Config->Get('MessageBoxIncludeSignature'); +$IncludeArticle => 1; diff --git a/rt/share/html/Elements/QueueSummaryByStatus b/rt/share/html/Elements/QueueSummaryByStatus index 09f274f74..f649d2850 100644 --- a/rt/share/html/Elements/QueueSummaryByStatus +++ b/rt/share/html/Elements/QueueSummaryByStatus @@ -122,9 +122,13 @@ my $statuses = {}; use RT::Report::Tickets; my $report = RT::Report::Tickets->new( RT->SystemUser ); -my $query = @queues - ? join(' OR ', map "Queue = ".$_->{id}, @queues) - : 'id < 0'; +my $query = + "(". + join(" OR ", map {s{(['\\])}{\\$1}g; "Status = '$_'"} @statuses) #' + .") AND (". + join(' OR ', map "Queue = ".$_->{id}, @queues) + .")"; +$query = 'id < 0' unless @queues; $report->SetupGroupings( Query => $query, GroupBy => [qw(Status Queue)] ); while ( my $entry = $report->Next ) { diff --git a/rt/share/html/Elements/RT__CustomField/ColumnMap b/rt/share/html/Elements/RT__CustomField/ColumnMap index ecb219d9e..b04398434 100644 --- a/rt/share/html/Elements/RT__CustomField/ColumnMap +++ b/rt/share/html/Elements/RT__CustomField/ColumnMap @@ -118,7 +118,7 @@ my $COLUMN_MAP = { RemoveCheckBox => { title => sub { my $name = 'RemoveCustomField'; - my $checked = $m->request_args->{ $name .'All' }? 'checked="checked"': ''; + my $checked = $DECODED_ARGS->{ $name .'All' }? 'checked="checked"': ''; return \qq{ a > .sf-sub-indicator { -moz-border-radius-topright: 17px; -webkit-border-top-right-radius: 17px; -webkit-border-bottom-left-radius: 17px; + border-top-right-radius: 17px; + border-bottom-left-radius: 17px; } .sf-shadow ul.sf-shadow-off { background: transparent; diff --git a/rt/share/html/NoAuth/css/base/ticket-form.css b/rt/share/html/NoAuth/css/base/ticket-form.css index daab263b1..869eba774 100644 --- a/rt/share/html/NoAuth/css/base/ticket-form.css +++ b/rt/share/html/NoAuth/css/base/ticket-form.css @@ -82,21 +82,17 @@ iframe.richtext-editor { .messagebox-container.action-response iframe { background-color: #fcc !important; -} - -/* -% if ( RT->Config->Get("UseSideBySideLayout", $session{'CurrentUser'}) ) { -*/ +} -#ticket-create-metadata, -#ticket-update-metadata { +.sidebyside #ticket-create-metadata, +.sidebyside #ticket-update-metadata { float: right; width: 40%; clear: right; } -#ticket-create-message, -#ticket-update-message { +.sidebyside #ticket-create-message, +.sidebyside #ticket-update-message { float: left; width: 58%; clear: left; @@ -104,10 +100,10 @@ iframe.richtext-editor { @media (max-width: 950px) { /* Revert to a single column when we're less than 1000px wide */ - #ticket-create-metadata, - #ticket-update-metadata, - #ticket-create-message, - #ticket-update-message + .sidebyside #ticket-create-metadata, + .sidebyside #ticket-update-metadata, + .sidebyside #ticket-create-message, + .sidebyside #ticket-update-message { float: none; width: auto; @@ -115,15 +111,12 @@ iframe.richtext-editor { } } -#comp-Ticket-Update #body { +.sidebyside #comp-Ticket-Update #body { padding-top: 3em; } -#ticket-create-message .button[name="AddMoreAttach"], -#ticket-update-message .button[name="AddMoreAttach"] { +.sidebyside #ticket-create-message .button[name="AddMoreAttach"], +.sidebyside #ticket-update-message .button[name="AddMoreAttach"] { float: right; } -/* -% } -*/ diff --git a/rt/share/html/NoAuth/css/base/ui.timepickr.css b/rt/share/html/NoAuth/css/base/ui.timepickr.css deleted file mode 100644 index e2dacf7a9..000000000 --- a/rt/share/html/NoAuth/css/base/ui.timepickr.css +++ /dev/null @@ -1,56 +0,0 @@ -/* - jQuery ui.timepickr - http://code.google.com/p/jquery-utils/ - - copyright Maxime Haineault - http://haineault.com - - MIT License (http://www.opensource.org/licenses/mit-license.php -*/ -.ui-timepickr { - position:absolute; - width:480px; -} - -.ui-timepickr-row { - margin:0; - padding:0; - margin-top:2px; - display:none; - position:relative; -} - -.ui-timepickr-button { - float:left; - margin:0; - padding:0; - list-style:none; - list-style-type:none; -} - -.ui-timepickr-button span { - font-size:.7em; - padding:4px 6px 4px 6px; - margin-left:2px; - text-align:center; - cursor:pointer; - display:block; - text-align:center; - - - /* system theme (default) */ - border-width:1px; - border-style:solid; - /*border-color:ThreeDLightShadow ThreeDShadow ThreeDShadow ThreeDLightShadow; - color:ButtonText; - background:ButtonFace;*/ -} - -.ui-timepickr-button span.ui-state-hover { - /*color:HighlightText; - background:Highlight;*/ -} - -.ui-state-hover span { - /*background:#c30;*/ -} diff --git a/rt/share/html/NoAuth/css/base/ui.timepickr.custom.css b/rt/share/html/NoAuth/css/base/ui.timepickr.custom.css deleted file mode 100644 index ad2aa66ce..000000000 --- a/rt/share/html/NoAuth/css/base/ui.timepickr.custom.css +++ /dev/null @@ -1,54 +0,0 @@ -%# BEGIN BPS TAGGED BLOCK {{{ -%# -%# COPYRIGHT: -%# -%# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC -%# -%# -%# (Except where explicitly superseded by other copyright notices) -%# -%# -%# LICENSE: -%# -%# This work is made available to you under the terms of Version 2 of -%# the GNU General Public License. A copy of that license should have -%# been provided with this software, but in any event can be snarfed -%# from www.gnu.org. -%# -%# This work is distributed in the hope that it will be useful, but -%# WITHOUT ANY WARRANTY; without even the implied warranty of -%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -%# General Public License for more details. -%# -%# You should have received a copy of the GNU General Public License -%# along with this program; if not, write to the Free Software -%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -%# 02110-1301 or visit their web page on the internet at -%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. -%# -%# -%# CONTRIBUTION SUBMISSION POLICY: -%# -%# (The following paragraph is not intended to limit the rights granted -%# to you to modify and distribute this software under the terms of -%# the GNU General Public License and is only of importance to you if -%# you choose to contribute your changes and enhancements to the -%# community by submitting them to Best Practical Solutions, LLC.) -%# -%# By intentionally submitting any modifications, corrections or -%# derivatives to this work, or any other work intended for use with -%# Request Tracker, to Best Practical Solutions, LLC, you confirm that -%# you are the copyright holder for those contributions and you grant -%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, -%# royalty-free, perpetual, license to use, copy, create derivative -%# works based on those contributions, and sublicense and distribute -%# those contributions and any derivatives thereof. -%# -%# END BPS TAGGED BLOCK }}} -.ui-timepickr { - font-size: 1.1em; -} - -.ui-timepickr-button span { - background: white; -} diff --git a/rt/share/html/NoAuth/css/web2/nav.css b/rt/share/html/NoAuth/css/web2/nav.css index be63c5984..e404b61c8 100644 --- a/rt/share/html/NoAuth/css/web2/nav.css +++ b/rt/share/html/NoAuth/css/web2/nav.css @@ -239,6 +239,7 @@ border: 1px solid #ccc; -moz-border-radius-bottomleft: 0.5em; -webkit-border-bottom-left-radius: 0.5em; + border-bottom-left-radius: 0.5em; border-right: none; border-top: none; list-style-type: none; diff --git a/rt/share/html/NoAuth/js/jquery-ui-1.8.4.custom.min.js b/rt/share/html/NoAuth/js/jquery-ui-1.8.4.custom.min.js index e90b4fe4b..0466005dc 100644 --- a/rt/share/html/NoAuth/js/jquery-ui-1.8.4.custom.min.js +++ b/rt/share/html/NoAuth/js/jquery-ui-1.8.4.custom.min.js @@ -222,3 +222,53 @@ c=this._daylightSavingAdjust(new Date(c,e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(t function(a){if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b)); return this.each(function(){typeof a=="string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new L;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.4";window["DP_jQuery_"+y]=d})(jQuery); ; +/*! + * jQuery UI Mouse 1.8.4 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Mouse + * + * Depends: + * jquery.ui.widget.js + */ +(function(c){c.widget("ui.mouse",{options:{cancel:":input,option",distance:1,delay:0},_mouseInit:function(){var a=this;this.element.bind("mousedown."+this.widgetName,function(b){return a._mouseDown(b)}).bind("click."+this.widgetName,function(b){if(a._preventClickEvent){a._preventClickEvent=false;b.stopImmediatePropagation();return false}});this.started=false},_mouseDestroy:function(){this.element.unbind("."+this.widgetName)},_mouseDown:function(a){a.originalEvent=a.originalEvent||{};if(!a.originalEvent.mouseHandled){this._mouseStarted&& +this._mouseUp(a);this._mouseDownEvent=a;var b=this,e=a.which==1,f=typeof this.options.cancel=="string"?c(a.target).parents().add(a.target).filter(this.options.cancel).length:false;if(!e||f||!this._mouseCapture(a))return true;this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet)this._mouseDelayTimer=setTimeout(function(){b.mouseDelayMet=true},this.options.delay);if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a)){this._mouseStarted=this._mouseStart(a)!==false;if(!this._mouseStarted){a.preventDefault(); +return true}}this._mouseMoveDelegate=function(d){return b._mouseMove(d)};this._mouseUpDelegate=function(d){return b._mouseUp(d)};c(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);c.browser.safari||a.preventDefault();return a.originalEvent.mouseHandled=true}},_mouseMove:function(a){if(c.browser.msie&&!(document.documentMode>=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&& +this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){c(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=false;this._preventClickEvent=a.target==this._mouseDownEvent.target;this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX- +a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery); +/* + * jQuery UI Slider 1.8.4 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Slider + * + * Depends: + * jquery.ui.core.js + * jquery.ui.mouse.js + * jquery.ui.widget.js + */ +(function(d){d.widget("ui.slider",d.ui.mouse,{widgetEventPrefix:"slide",options:{animate:false,distance:0,max:100,min:0,orientation:"horizontal",range:false,step:1,value:0,values:null},_create:function(){var a=this,b=this.options;this._mouseSliding=this._keySliding=false;this._animateOff=true;this._handleIndex=null;this._detectOrientation();this._mouseInit();this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget ui-widget-content ui-corner-all");b.disabled&&this.element.addClass("ui-slider-disabled ui-disabled"); +this.range=d([]);if(b.range){if(b.range===true){this.range=d("
");if(!b.values)b.values=[this._valueMin(),this._valueMin()];if(b.values.length&&b.values.length!==2)b.values=[b.values[0],b.values[0]]}else this.range=d("
");this.range.appendTo(this.element).addClass("ui-slider-range");if(b.range==="min"||b.range==="max")this.range.addClass("ui-slider-range-"+b.range);this.range.addClass("ui-widget-header")}d(".ui-slider-handle",this.element).length===0&&d("").appendTo(this.element).addClass("ui-slider-handle"); +if(b.values&&b.values.length)for(;d(".ui-slider-handle",this.element).length").appendTo(this.element).addClass("ui-slider-handle");this.handles=d(".ui-slider-handle",this.element).addClass("ui-state-default ui-corner-all");this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(c){c.preventDefault()}).hover(function(){b.disabled||d(this).addClass("ui-state-hover")},function(){d(this).removeClass("ui-state-hover")}).focus(function(){if(b.disabled)d(this).blur(); +else{d(".ui-slider .ui-state-focus").removeClass("ui-state-focus");d(this).addClass("ui-state-focus")}}).blur(function(){d(this).removeClass("ui-state-focus")});this.handles.each(function(c){d(this).data("index.ui-slider-handle",c)});this.handles.keydown(function(c){var e=true,f=d(this).data("index.ui-slider-handle"),h,g,i;if(!a.options.disabled){switch(c.keyCode){case d.ui.keyCode.HOME:case d.ui.keyCode.END:case d.ui.keyCode.PAGE_UP:case d.ui.keyCode.PAGE_DOWN:case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:e= +false;if(!a._keySliding){a._keySliding=true;d(this).addClass("ui-state-active");h=a._start(c,f);if(h===false)return}break}i=a.options.step;h=a.options.values&&a.options.values.length?(g=a.values(f)):(g=a.value());switch(c.keyCode){case d.ui.keyCode.HOME:g=a._valueMin();break;case d.ui.keyCode.END:g=a._valueMax();break;case d.ui.keyCode.PAGE_UP:g=a._trimAlignValue(h+(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.PAGE_DOWN:g=a._trimAlignValue(h-(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:if(h=== +a._valueMax())return;g=a._trimAlignValue(h+i);break;case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:if(h===a._valueMin())return;g=a._trimAlignValue(h-i);break}a._slide(c,f,g);return e}}).keyup(function(c){var e=d(this).data("index.ui-slider-handle");if(a._keySliding){a._keySliding=false;a._stop(c,e);a._change(c,e);d(this).removeClass("ui-state-active")}});this._refreshValue();this._animateOff=false},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider"); +this._mouseDestroy();return this},_mouseCapture:function(a){var b=this.options,c,e,f,h,g;if(b.disabled)return false;this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();c=this._normValueFromMouse({x:a.pageX,y:a.pageY});e=this._valueMax()-this._valueMin()+1;h=this;this.handles.each(function(i){var j=Math.abs(c-h.values(i));if(e>j){e=j;f=d(this);g=i}});if(b.range===true&&this.values(1)===b.min){g+=1;f=d(this.handles[g])}if(this._start(a, +g)===false)return false;this._mouseSliding=true;h._handleIndex=g;f.addClass("ui-state-active").focus();b=f.offset();this._clickOffset=!d(a.target).parents().andSelf().is(".ui-slider-handle")?{left:0,top:0}:{left:a.pageX-b.left-f.width()/2,top:a.pageY-b.top-f.height()/2-(parseInt(f.css("borderTopWidth"),10)||0)-(parseInt(f.css("borderBottomWidth"),10)||0)+(parseInt(f.css("marginTop"),10)||0)};this._slide(a,g,c);return this._animateOff=true},_mouseStart:function(){return true},_mouseDrag:function(a){var b= +this._normValueFromMouse({x:a.pageX,y:a.pageY});this._slide(a,this._handleIndex,b);return false},_mouseStop:function(a){this.handles.removeClass("ui-state-active");this._mouseSliding=false;this._stop(a,this._handleIndex);this._change(a,this._handleIndex);this._clickOffset=this._handleIndex=null;return this._animateOff=false},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b;if(this.orientation==="horizontal"){b= +this.elementSize.width;a=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{b=this.elementSize.height;a=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}b=a/b;if(b>1)b=1;if(b<0)b=0;if(this.orientation==="vertical")b=1-b;a=this._valueMax()-this._valueMin();return this._trimAlignValue(this._valueMin()+b*a)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b); +c.values=this.values()}return this._trigger("start",a,c)},_slide:function(a,b,c){var e;if(this.options.values&&this.options.values.length){e=this.values(b?0:1);if(this.options.values.length===2&&this.options.range===true&&(b===0&&c>e||b===1&&c1){this.options.values[a]=this._trimAlignValue(b);this._refreshValue();this._change(null,a)}if(arguments.length)if(d.isArray(arguments[0])){c=this.options.values;e=arguments[0];for(f=0;fthis._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=a%b;a=a-c;if(Math.abs(c)*2>=b)a+=c>0?b:-b;return parseFloat(a.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var a= +this.options.range,b=this.options,c=this,e=!this._animateOff?b.animate:false,f,h={},g,i,j,l;if(this.options.values&&this.options.values.length)this.handles.each(function(k){f=(c.values(k)-c._valueMin())/(c._valueMax()-c._valueMin())*100;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";d(this).stop(1,1)[e?"animate":"css"](h,b.animate);if(c.options.range===true)if(c.orientation==="horizontal"){if(k===0)c.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({width:f- +g+"%"},{queue:false,duration:b.animate})}else{if(k===0)c.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({height:f-g+"%"},{queue:false,duration:b.animate})}g=f});else{i=this.value();j=this._valueMin();l=this._valueMax();f=l!==j?(i-j)/(l-j)*100:0;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";this.handle.stop(1,1)[e?"animate":"css"](h,b.animate);if(a==="min"&&this.orientation==="horizontal")this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"}, +b.animate);if(a==="max"&&this.orientation==="horizontal")this.range[e?"animate":"css"]({width:100-f+"%"},{queue:false,duration:b.animate});if(a==="min"&&this.orientation==="vertical")this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},b.animate);if(a==="max"&&this.orientation==="vertical")this.range[e?"animate":"css"]({height:100-f+"%"},{queue:false,duration:b.animate})}}});d.extend(d.ui.slider,{version:"1.8.4"})})(jQuery); diff --git a/rt/share/html/NoAuth/js/jquery-ui-patch-datepicker.js b/rt/share/html/NoAuth/js/jquery-ui-patch-datepicker.js index 40cc0db99..2ac101f93 100644 --- a/rt/share/html/NoAuth/js/jquery-ui-patch-datepicker.js +++ b/rt/share/html/NoAuth/js/jquery-ui-patch-datepicker.js @@ -58,4 +58,35 @@ return data; }; + + $.datepicker._checkOffset_orig = $.datepicker._checkOffset; + $.datepicker._checkOffset = function(inst, offset, isFixed) { + // copied from the original + var dpHeight = inst.dpDiv.outerHeight(); + var inputHeight = inst.input ? inst.input.outerHeight() : 0; + var viewHeight = document.documentElement.clientHeight + $(document).scrollTop(); + + // save the original offset rather than the new offset because the + // original function modifies the passed arg as a side-effect + var old_offset = { top: offset.top, left: offset.left }; + offset = $.datepicker._checkOffset_orig(inst, offset, isFixed); + + // Negate any up or down positioning by adding instead of subtracting + offset.top += Math.min(old_offset.top, (old_offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ? + Math.abs(dpHeight + inputHeight) : 0); + + return offset; + }; + + + $.timepicker._newInst_orig = $.timepicker._newInst; + $.timepicker._newInst = function($input, o) { + var tp_inst = $.timepicker._newInst_orig($input, o); + tp_inst._defaults.onClose = function(dateText, dp_inst) { + if ($.isFunction(o.onClose)) + o.onClose.call($input[0], dateText, dp_inst, tp_inst); + }; + return tp_inst; + }; + })(jQuery); diff --git a/rt/share/html/NoAuth/js/jquery-ui-timepicker-addon.js b/rt/share/html/NoAuth/js/jquery-ui-timepicker-addon.js new file mode 100644 index 000000000..0a4ff026e --- /dev/null +++ b/rt/share/html/NoAuth/js/jquery-ui-timepicker-addon.js @@ -0,0 +1,1326 @@ +/* +* jQuery timepicker addon +* By: Trent Richardson [http://trentrichardson.com] +* Version 1.0.0 +* Last Modified: 02/05/2012 +* +* Copyright 2012 Trent Richardson +* Dual licensed under the MIT and GPL licenses. +* http://trentrichardson.com/Impromptu/GPL-LICENSE.txt +* http://trentrichardson.com/Impromptu/MIT-LICENSE.txt +* +* HERES THE CSS: +* .ui-timepicker-div .ui-widget-header { margin-bottom: 8px; } +* .ui-timepicker-div dl { text-align: left; } +* .ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; } +* .ui-timepicker-div dl dd { margin: 0 10px 10px 65px; } +* .ui-timepicker-div td { font-size: 90%; } +* .ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; } +*/ + +(function($) { + +// Prevent "Uncaught RangeError: Maximum call stack size exceeded" +$.ui.timepicker = $.ui.timepicker || {}; +if ($.ui.timepicker.version) { + return; +} + +$.extend($.ui, { timepicker: { version: "1.0.0" } }); + +/* Time picker manager. + Use the singleton instance of this class, $.timepicker, to interact with the time picker. + Settings for (groups of) time pickers are maintained in an instance object, + allowing multiple different settings on the same page. */ + +function Timepicker() { + this.regional = []; // Available regional settings, indexed by language code + this.regional[''] = { // Default regional settings + currentText: 'Now', + closeText: 'Done', + ampm: false, + amNames: ['AM', 'A'], + pmNames: ['PM', 'P'], + timeFormat: 'hh:mm tt', + timeSuffix: '', + timeOnlyTitle: 'Choose Time', + timeText: 'Time', + hourText: 'Hour', + minuteText: 'Minute', + secondText: 'Second', + millisecText: 'Millisecond', + timezoneText: 'Time Zone' + }; + this._defaults = { // Global defaults for all the datetime picker instances + showButtonPanel: true, + timeOnly: false, + showHour: true, + showMinute: true, + showSecond: false, + showMillisec: false, + showTimezone: false, + showTime: true, + stepHour: 1, + stepMinute: 1, + stepSecond: 1, + stepMillisec: 1, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + timezone: '+0000', + hourMin: 0, + minuteMin: 0, + secondMin: 0, + millisecMin: 0, + hourMax: 23, + minuteMax: 59, + secondMax: 59, + millisecMax: 999, + minDateTime: null, + maxDateTime: null, + onSelect: null, + hourGrid: 0, + minuteGrid: 0, + secondGrid: 0, + millisecGrid: 0, + alwaysSetTime: true, + separator: ' ', + altFieldTimeOnly: true, + showTimepicker: true, + timezoneIso8609: false, + timezoneList: null, + addSliderAccess: false, + sliderAccessArgs: null + }; + $.extend(this._defaults, this.regional['']); +}; + +$.extend(Timepicker.prototype, { + $input: null, + $altInput: null, + $timeObj: null, + inst: null, + hour_slider: null, + minute_slider: null, + second_slider: null, + millisec_slider: null, + timezone_select: null, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + timezone: '+0000', + hourMinOriginal: null, + minuteMinOriginal: null, + secondMinOriginal: null, + millisecMinOriginal: null, + hourMaxOriginal: null, + minuteMaxOriginal: null, + secondMaxOriginal: null, + millisecMaxOriginal: null, + ampm: '', + formattedDate: '', + formattedTime: '', + formattedDateTime: '', + timezoneList: null, + + /* Override the default settings for all instances of the time picker. + @param settings object - the new settings to use as defaults (anonymous object) + @return the manager object */ + setDefaults: function(settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + //######################################################################## + // Create a new Timepicker instance + //######################################################################## + _newInst: function($input, o) { + var tp_inst = new Timepicker(), + inlineSettings = {}; + + for (var attrName in this._defaults) { + var attrValue = $input.attr('time:' + attrName); + if (attrValue) { + try { + inlineSettings[attrName] = eval(attrValue); + } catch (err) { + inlineSettings[attrName] = attrValue; + } + } + } + tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, o, { + beforeShow: function(input, dp_inst) { + if ($.isFunction(o.beforeShow)) + return o.beforeShow(input, dp_inst, tp_inst); + }, + onChangeMonthYear: function(year, month, dp_inst) { + // Update the time as well : this prevents the time from disappearing from the $input field. + tp_inst._updateDateTime(dp_inst); + if ($.isFunction(o.onChangeMonthYear)) + o.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst); + }, + onClose: function(dateText, dp_inst) { + if (tp_inst.timeDefined === true && $input.val() != '') + tp_inst._updateDateTime(dp_inst); + if ($.isFunction(o.onClose)) + o.onClose.call($input[0], dateText, dp_inst, tp_inst); + }, + timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker'); + }); + tp_inst.amNames = $.map(tp_inst._defaults.amNames, function(val) { return val.toUpperCase(); }); + tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function(val) { return val.toUpperCase(); }); + + if (tp_inst._defaults.timezoneList === null) { + var timezoneList = []; + for (var i = -11; i <= 12; i++) + timezoneList.push((i >= 0 ? '+' : '-') + ('0' + Math.abs(i).toString()).slice(-2) + '00'); + if (tp_inst._defaults.timezoneIso8609) + timezoneList = $.map(timezoneList, function(val) { + return val == '+0000' ? 'Z' : (val.substring(0, 3) + ':' + val.substring(3)); + }); + tp_inst._defaults.timezoneList = timezoneList; + } + + tp_inst.hour = tp_inst._defaults.hour; + tp_inst.minute = tp_inst._defaults.minute; + tp_inst.second = tp_inst._defaults.second; + tp_inst.millisec = tp_inst._defaults.millisec; + tp_inst.ampm = ''; + tp_inst.$input = $input; + + if (o.altField) + tp_inst.$altInput = $(o.altField) + .css({ cursor: 'pointer' }) + .focus(function(){ $input.trigger("focus"); }); + + if(tp_inst._defaults.minDate==0 || tp_inst._defaults.minDateTime==0) + { + tp_inst._defaults.minDate=new Date(); + } + if(tp_inst._defaults.maxDate==0 || tp_inst._defaults.maxDateTime==0) + { + tp_inst._defaults.maxDate=new Date(); + } + + // datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime.. + if(tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date) + tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime()); + if(tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date) + tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime()); + if(tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date) + tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime()); + if(tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date) + tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime()); + return tp_inst; + }, + + //######################################################################## + // add our sliders to the calendar + //######################################################################## + _addTimePicker: function(dp_inst) { + var currDT = (this.$altInput && this._defaults.altFieldTimeOnly) ? + this.$input.val() + ' ' + this.$altInput.val() : + this.$input.val(); + + this.timeDefined = this._parseTime(currDT); + this._limitMinMaxDateTime(dp_inst, false); + this._injectTimePicker(); + }, + + //######################################################################## + // parse the time string from input value or _setTime + //######################################################################## + _parseTime: function(timeString, withDate) { + var regstr = this._defaults.timeFormat.toString() + .replace(/h{1,2}/ig, '(\\d?\\d)') + .replace(/m{1,2}/ig, '(\\d?\\d)') + .replace(/s{1,2}/ig, '(\\d?\\d)') + .replace(/l{1}/ig, '(\\d?\\d?\\d)') + .replace(/t{1,2}/ig, this._getPatternAmpm()) + .replace(/z{1}/ig, '(z|[-+]\\d\\d:?\\d\\d)?') + .replace(/\s/g, '\\s?') + this._defaults.timeSuffix + '$', + order = this._getFormatPositions(), + ampm = '', + treg; + + if (!this.inst) this.inst = $.datepicker._getInst(this.$input[0]); + + if (withDate || !this._defaults.timeOnly) { + // the time should come after x number of characters and a space. + // x = at least the length of text specified by the date format + var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat'); + // escape special regex characters in the seperator + var specials = new RegExp("[.*+?|()\\[\\]{}\\\\]", "g"); + regstr = '^.{' + dp_dateFormat.length + ',}?' + this._defaults.separator.replace(specials, "\\$&") + regstr; + } + + treg = timeString.match(new RegExp(regstr, 'i')); + + if (treg) { + if (order.t !== -1) { + if (treg[order.t] === undefined || treg[order.t].length === 0) { + ampm = ''; + this.ampm = ''; + } else { + ampm = $.inArray(treg[order.t].toUpperCase(), this.amNames) !== -1 ? 'AM' : 'PM'; + this.ampm = this._defaults[ampm == 'AM' ? 'amNames' : 'pmNames'][0]; + } + } + + if (order.h !== -1) { + if (ampm == 'AM' && treg[order.h] == '12') + this.hour = 0; // 12am = 0 hour + else if (ampm == 'PM' && treg[order.h] != '12') + this.hour = (parseFloat(treg[order.h]) + 12).toFixed(0); // 12pm = 12 hour, any other pm = hour + 12 + else this.hour = Number(treg[order.h]); + } + + if (order.m !== -1) this.minute = Number(treg[order.m]); + if (order.s !== -1) this.second = Number(treg[order.s]); + if (order.l !== -1) this.millisec = Number(treg[order.l]); + if (order.z !== -1 && treg[order.z] !== undefined) { + var tz = treg[order.z].toUpperCase(); + switch (tz.length) { + case 1: // Z + tz = this._defaults.timezoneIso8609 ? 'Z' : '+0000'; + break; + case 5: // +hhmm + if (this._defaults.timezoneIso8609) + tz = tz.substring(1) == '0000' + ? 'Z' + : tz.substring(0, 3) + ':' + tz.substring(3); + break; + case 6: // +hh:mm + if (!this._defaults.timezoneIso8609) + tz = tz == 'Z' || tz.substring(1) == '00:00' + ? '+0000' + : tz.replace(/:/, ''); + else if (tz.substring(1) == '00:00') + tz = 'Z'; + break; + } + this.timezone = tz; + } + + return true; + + } + return false; + }, + + //######################################################################## + // pattern for standard and localized AM/PM markers + //######################################################################## + _getPatternAmpm: function() { + var markers = [], + o = this._defaults; + if (o.amNames) + $.merge(markers, o.amNames); + if (o.pmNames) + $.merge(markers, o.pmNames); + markers = $.map(markers, function(val) { return val.replace(/[.*+?|()\[\]{}\\]/g, '\\$&'); }); + return '(' + markers.join('|') + ')?'; + }, + + //######################################################################## + // figure out position of time elements.. cause js cant do named captures + //######################################################################## + _getFormatPositions: function() { + var finds = this._defaults.timeFormat.toLowerCase().match(/(h{1,2}|m{1,2}|s{1,2}|l{1}|t{1,2}|z)/g), + orders = { h: -1, m: -1, s: -1, l: -1, t: -1, z: -1 }; + + if (finds) + for (var i = 0; i < finds.length; i++) + if (orders[finds[i].toString().charAt(0)] == -1) + orders[finds[i].toString().charAt(0)] = i + 1; + + return orders; + }, + + //######################################################################## + // generate and inject html for timepicker into ui datepicker + //######################################################################## + _injectTimePicker: function() { + var $dp = this.inst.dpDiv, + o = this._defaults, + tp_inst = this, + // Added by Peter Medeiros: + // - Figure out what the hour/minute/second max should be based on the step values. + // - Example: if stepMinute is 15, then minMax is 45. + hourMax = parseInt((o.hourMax - ((o.hourMax - o.hourMin) % o.stepHour)) ,10), + minMax = parseInt((o.minuteMax - ((o.minuteMax - o.minuteMin) % o.stepMinute)) ,10), + secMax = parseInt((o.secondMax - ((o.secondMax - o.secondMin) % o.stepSecond)) ,10), + millisecMax = parseInt((o.millisecMax - ((o.millisecMax - o.millisecMin) % o.stepMillisec)) ,10), + dp_id = this.inst.id.toString().replace(/([^A-Za-z0-9_])/g, ''); + + // Prevent displaying twice + //if ($dp.find("div#ui-timepicker-div-"+ dp_id).length === 0) { + if ($dp.find("div#ui-timepicker-div-"+ dp_id).length === 0 && o.showTimepicker) { + var noDisplay = ' style="display:none;"', + html = '
' + + '
' + o.timeText + '
' + + '
' + + '
' + o.hourText + '
', + hourGridSize = 0, + minuteGridSize = 0, + secondGridSize = 0, + millisecGridSize = 0, + size = null; + + // Hours + html += '
'; + if (o.showHour && o.hourGrid > 0) { + html += '
'; + + for (var h = o.hourMin; h <= hourMax; h += parseInt(o.hourGrid,10)) { + hourGridSize++; + var tmph = (o.ampm && h > 12) ? h-12 : h; + if (tmph < 10) tmph = '0' + tmph; + if (o.ampm) { + if (h == 0) tmph = 12 +'a'; + else if (h < 12) tmph += 'a'; + else tmph += 'p'; + } + html += ''; + } + + html += '
' + tmph + '
'; + } + html += '
'; + + // Minutes + html += '
' + o.minuteText + '
'+ + '
'; + + if (o.showMinute && o.minuteGrid > 0) { + html += '
'; + + for (var m = o.minuteMin; m <= minMax; m += parseInt(o.minuteGrid,10)) { + minuteGridSize++; + html += ''; + } + + html += '
' + ((m < 10) ? '0' : '') + m + '
'; + } + html += '
'; + + // Seconds + html += '
' + o.secondText + '
'+ + '
'; + + if (o.showSecond && o.secondGrid > 0) { + html += '
'; + + for (var s = o.secondMin; s <= secMax; s += parseInt(o.secondGrid,10)) { + secondGridSize++; + html += ''; + } + + html += '
' + ((s < 10) ? '0' : '') + s + '
'; + } + html += '
'; + + // Milliseconds + html += '
' + o.millisecText + '
'+ + '
'; + + if (o.showMillisec && o.millisecGrid > 0) { + html += '
'; + + for (var l = o.millisecMin; l <= millisecMax; l += parseInt(o.millisecGrid,10)) { + millisecGridSize++; + html += ''; + } + + html += '
' + ((l < 10) ? '0' : '') + l + '
'; + } + html += '
'; + + // Timezone + html += '
' + o.timezoneText + '
'; + html += '
'; + + html += '
'; + $tp = $(html); + + // if we only want time picker... + if (o.timeOnly === true) { + $tp.prepend( + '
' + + '
' + o.timeOnlyTitle + '
' + + '
'); + $dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide(); + } + + this.hour_slider = $tp.find('#ui_tpicker_hour_'+ dp_id).slider({ + orientation: "horizontal", + value: this.hour, + min: o.hourMin, + max: hourMax, + step: o.stepHour, + slide: function(event, ui) { + tp_inst.hour_slider.slider( "option", "value", ui.value); + tp_inst._onTimeChange(); + } + }); + + + // Updated by Peter Medeiros: + // - Pass in Event and UI instance into slide function + this.minute_slider = $tp.find('#ui_tpicker_minute_'+ dp_id).slider({ + orientation: "horizontal", + value: this.minute, + min: o.minuteMin, + max: minMax, + step: o.stepMinute, + slide: function(event, ui) { + tp_inst.minute_slider.slider( "option", "value", ui.value); + tp_inst._onTimeChange(); + } + }); + + this.second_slider = $tp.find('#ui_tpicker_second_'+ dp_id).slider({ + orientation: "horizontal", + value: this.second, + min: o.secondMin, + max: secMax, + step: o.stepSecond, + slide: function(event, ui) { + tp_inst.second_slider.slider( "option", "value", ui.value); + tp_inst._onTimeChange(); + } + }); + + this.millisec_slider = $tp.find('#ui_tpicker_millisec_'+ dp_id).slider({ + orientation: "horizontal", + value: this.millisec, + min: o.millisecMin, + max: millisecMax, + step: o.stepMillisec, + slide: function(event, ui) { + tp_inst.millisec_slider.slider( "option", "value", ui.value); + tp_inst._onTimeChange(); + } + }); + + this.timezone_select = $tp.find('#ui_tpicker_timezone_'+ dp_id).append('').find("select"); + $.fn.append.apply(this.timezone_select, + $.map(o.timezoneList, function(val, idx) { + return $("