From a6b56c331ccd2fa42c74c5f01555ff407c14e3cf Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 11 Aug 2015 17:05:16 -0700 Subject: throw an error during RBC batch import if the batch has the wrong account number, #37476 --- FS/FS/pay_batch/RBC.pm | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/FS/FS/pay_batch/RBC.pm b/FS/FS/pay_batch/RBC.pm index 53f810852..644c73c8b 100644 --- a/FS/FS/pay_batch/RBC.pm +++ b/FS/FS/pay_batch/RBC.pm @@ -5,6 +5,7 @@ use vars qw(@ISA %import_info %export_info $name); use Date::Format 'time2str'; use FS::Conf; use Encode 'encode'; +use feature 'state'; my $conf; my ($client_num, $shortname, $longname, $trans_code, $testmode, $i, $declined, $totaloffset); @@ -30,9 +31,10 @@ $name = 'RBC'; 'filetype' => 'fixed', #this only really applies to Debit Detail, but we otherwise only need first char 'formatre' => - '^(.).{18}(.{4}).{3}(.).{11}(.{19}).{6}(.{30}).{17}(.{9})(.{18}).{6}(.{14}).{23}(.).{9}\r?$', + '^(.).{3}(.{10}).{5}(.{4}).{3}(.).{11}(.{19}).{6}(.{30}).{17}(.{9})(.{18}).{6}(.{14}).{23}(.).{9}\r?$', 'fields' => [ qw( recordtype + clientnum batchnum subtype paybatchnum @@ -43,11 +45,24 @@ $name = 'RBC'; status ) ], 'hook' => sub { - my $hash = shift; - $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 ); - $hash->{'_date'} = time; - $hash->{'payinfo'} =~ s/^(\S+).*/$1/; # these often have trailing spaces - $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'bank'}; + # pull client_num from config and check it against what's in the batch + state $clientnum ||= do { + my $conf = FS::Conf->new; + my @config = $conf->config("batchconfig-RBC"); + $config[0]; + }; + + my $hash = shift; + $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 ); + $hash->{'_date'} = time; + $hash->{'payinfo'} =~ s/^(\S+).*/$1/; # these often have trailing spaces + $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'bank'}; + + if ( $clientnum and $hash->{clientnum} ne $clientnum ) { + die "RBC client number in batch (".$hash->{clientnum}.") does not ". + "match configuration.\n"; + } + ''; }, 'approved' => sub { my $hash = shift; -- cgit v1.2.1 From a73684bba1b297715a95eabb8845c5212523f4e1 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Wed, 12 Aug 2015 14:26:14 -0400 Subject: #31495 Date changes for Earthlink --- FS/FS/cdr/earthlink.pm | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/FS/FS/cdr/earthlink.pm b/FS/FS/cdr/earthlink.pm index 5042f6fa5..c6c4e1535 100644 --- a/FS/FS/cdr/earthlink.pm +++ b/FS/FS/cdr/earthlink.pm @@ -3,11 +3,13 @@ package FS::cdr::earthlink; use strict; use vars qw( @ISA %info $date); use Time::Local; -use FS::cdr qw(_cdr_date_parser_maker _cdr_min_parser_maker); +use FS::cdr qw(_cdr_min_parser_maker); use Date::Parse; @ISA = qw(FS::cdr); +my ($tmp_mday, $tmp_mon, $tmp_year); + %info = ( 'name' => 'Earthlink', 'weight' => 120, @@ -15,14 +17,30 @@ use Date::Parse; 'import_fields' => [ skip(3), #Account number/ SERVICE LOC / BILL NUMBER - sub { my($cdr, $date) = @_; - $date; - }, #date + sub { my($cdr, $date) = @_; + $date =~ /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/ + or die "unparseable date: $date"; + ($tmp_mon, $tmp_mday, $tmp_year) = ($1, $2, $3); + }, #date sub { my($cdr, $time) = @_; + $time =~ /^(\d{1,2}):(\d{1,2}):(\d{1,2}) (AM|PM)$/ + or die "unparsable time: $time"; #maybe we shouldn't die... + my $hour = $1; + $hour += 12 if $4 eq 'PM' && $hour != 12; + $hour = 0 if $4 eq 'AM' && $hour == 12; + + my $dt = DateTime->new( + year => $tmp_year, + month => $tmp_mon, + day => $tmp_mday, + hour => $hour, + minute => $2, + second => $3, + time_zone => 'local', + ); + $cdr->set('startdate', $dt->epoch); - my $datetime = $date. " ". $time; - $cdr->set('startdate', $datetime ); - }, #time + }, skip(1), #TollFreeNumber sub { my($cdr, $src) = @_; $src =~ s/\D//g; -- cgit v1.2.1 From 4a6b0868fabbc617f05b1f9981c52b28d3cb2bcb Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 12 Aug 2015 21:48:43 -0500 Subject: RT#25026: Option to include taxes in sales report --- FS/FS/Report/Table.pm | 32 +++++++++++++++++++++++++++--- httemplate/graph/cust_bill_pkg.cgi | 18 +++++++++++++---- httemplate/graph/report_cust_bill_pkg.html | 7 ++++++- httemplate/search/cust_bill_pkg.cgi | 12 ----------- 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/FS/FS/Report/Table.pm b/FS/FS/Report/Table.pm index 63e5318c3..4b1ad05d6 100644 --- a/FS/FS/Report/Table.pm +++ b/FS/FS/Report/Table.pm @@ -485,9 +485,9 @@ sub cust_pkg_recur_cost { =item cust_bill_pkg: the total package charges on invoice line items. -'charges': limit the type of charges included (setup, recur, usage, discount). -Should be a string containing one or more of 'S', 'R', 'U', or 'D'; if -unspecified, defaults to all three. +'charges': limit the type of charges included (setup, recur, usage, discount, taxes). +Should be a string containing one or more of 'S', 'R', or 'U'; or 'D' or 'T' (discount +and taxes should not be combined with the others.) If unspecified, defaults to 'SRU'. 'classnum': limit to this package class. @@ -517,6 +517,7 @@ sub cust_bill_pkg { $sum += $self->cust_bill_pkg_recur(@_) if $charges{R}; $sum += $self->cust_bill_pkg_detail(@_) if $charges{U}; $sum += $self->cust_bill_pkg_discount(@_) if $charges{D}; + $sum += $self->cust_bill_pkg_taxes(@_) if $charges{T}; if ($opt{'average_per_cust_pkg'}) { my $count = $self->cust_bill_pkg_count_pkgnum(@_); @@ -727,6 +728,31 @@ sub cust_bill_pkg_discount { $self->scalar_sql($total_sql); } +sub cust_bill_pkg_taxes { + my $self = shift; + my ($speriod, $eperiod, $agentnum, %opt) = @_; + + $agentnum ||= $opt{'agentnum'}; + + my @where = ( + '(cust_bill_pkg.pkgnum != 0 OR feepart IS NOT NULL)', + $self->with_classnum($opt{'classnum'}, $opt{'use_override'}), + $self->with_report_option(%opt), + $self->in_time_period_and_agent($speriod, $eperiod, $agentnum), + $self->with_refnum(%opt), + $self->with_cust_classnum(%opt) + ); + + my $total_sql = "SELECT COALESCE(SUM(cust_bill_pkg_tax_location.amount),0) + FROM cust_bill_pkg + $cust_bill_pkg_join + LEFT JOIN cust_bill_pkg_tax_location + ON (cust_bill_pkg.billpkgnum = cust_bill_pkg_tax_location.taxable_billpkgnum) + WHERE " . join(' AND ', grep $_, @where); + + $self->scalar_sql($total_sql); +} + ##### package churn report ##### =item active_pkg: The number of packages that were active at the start of diff --git a/httemplate/graph/cust_bill_pkg.cgi b/httemplate/graph/cust_bill_pkg.cgi index b5486f4af..83eb0e837 100644 --- a/httemplate/graph/cust_bill_pkg.cgi +++ b/httemplate/graph/cust_bill_pkg.cgi @@ -1,5 +1,4 @@ <% include('elements/monthly.html', - #Dumper( 'title' => $title, 'graph_type' => $graph_type, 'items' => \@items, @@ -28,6 +27,7 @@ my $bottom_link = "$link;"; my $use_usage = $cgi->param('use_usage') || 0; my $use_setup = $cgi->param('use_setup') || 0; my $use_discount = $cgi->param('use_discount') || 2; +my $use_taxes = $cgi->param('use_taxes') || 0; my $use_override = $cgi->param('use_override') ? 1 : 0; my $average_per_cust_pkg = $cgi->param('average_per_cust_pkg') ? 1 : 0; @@ -50,6 +50,7 @@ my %charge_labels = ( 'R' => 'recurring', 'U' => 'usage', 'D' => 'discount', + 'T' => 'taxes', ); #XXX or virtual @@ -194,8 +195,14 @@ if ( $use_discount == 1 ) { push @components, 'D'; } # else leave discounts off entirely; never combine them with setup/recur +# could in theory combine with setup/recur/usage, +# but would require reverse engineering the tax calculation +if ( $use_taxes == 1 ) { + push @components, 'T'; +} + # Categorization of line items goes -# Agent -> Referral -> Package class -> Component (setup/recur/usage) +# Agent -> Referral -> Package class -> Component (setup/recur/usage/discount/taxes) # If per-agent totals are enabled, they go under the Agent level. # There aren't any other kinds of subtotals. @@ -255,6 +262,8 @@ foreach my $agent ( $all_agent || $sel_agent || $FS::CurrentUser::CurrentUser->a if ( $component eq 'D' ) { # discounts ignore 'charges' and 'distribute' $row_link = "${p}search/cust_bill_pkg_discount.html?"; + } elsif ( $component eq 'T' ) { + $row_link = "${p}search/cust_bill_pkg.cgi?istax=1;"; } $row_link .= ($all_agent ? '' : "agentnum=$row_agentnum;"). @@ -314,6 +323,8 @@ foreach my $agent ( $all_agent || $sel_agent || $FS::CurrentUser::CurrentUser->a if ( $component eq 'D' ) { # discounts ignore 'charges' and 'distribute' $row_link ="${p}search/cust_bill_pkg_discount.html?"; + } elsif ( $component eq 'T' ) { + $row_link = "${p}search/cust_bill_pkg.cgi?istax=1;"; } $row_link .= ($all_agent ? '' : "agentnum=$row_agentnum;"). @@ -386,9 +397,8 @@ foreach my $agent ( $all_agent || $sel_agent || $FS::CurrentUser::CurrentUser->a $anum++; -} +} # foreach $agent -#use Data::Dumper; if ( $cgi->param('debug') == 1 ) { $FS::Report::Table::DEBUG = 1; } diff --git a/httemplate/graph/report_cust_bill_pkg.html b/httemplate/graph/report_cust_bill_pkg.html index 1db86e393..96bfdc09a 100644 --- a/httemplate/graph/report_cust_bill_pkg.html +++ b/httemplate/graph/report_cust_bill_pkg.html @@ -165,7 +165,6 @@ window.onload = class_mode_changed; -   @@ -196,6 +195,12 @@ window.onload = class_mode_changed; 'options' => [ 1, 2 ], 'labels' => { 1 => 'Separate', 2 => 'Do not show' }, &> +<& /elements/tr-select.html, + 'label' => 'Taxes', + 'field' => 'use_taxes', + 'options' => [ 1, 2 ], + 'labels' => { 1 => 'Separate', 2 => 'Do not show' }, +&> Colors diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index 82e87fba9..c59a6d0e7 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -659,18 +659,6 @@ if ( $cgi->param('nottax') ) { } #end of "normal case" - # classnum (of underlying package) - # not specified: all classes - # 0: empty class - # N: classnum - if ( grep { $_ eq 'classnum' } $cgi->param ) { - my @classnums = grep /^\d+$/, $cgi->param('classnum'); - push @where, "COALESCE(part_fee.classnum, $part_pkg.classnum, 0) IN ( ". - join(',', @classnums ). - ' )' - if @classnums; - } - } # nottax / istax -- cgit v1.2.1 From d19d491320789ae2e621d35cc7d67ac1c7696367 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 14 Aug 2015 14:38:43 -0700 Subject: add "tax collected" to tax liability report, #26770 --- FS/FS/Report/Tax.pm | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/FS/FS/Report/Tax.pm b/FS/FS/Report/Tax.pm index 23c16452e..0923d55cf 100644 --- a/FS/FS/Report/Tax.pm +++ b/FS/FS/Report/Tax.pm @@ -52,9 +52,10 @@ sub report_internal { } # %breakdown: short name => field identifier + # null classnum should remain null, not be converted to zero %breakdown = ( 'taxclass' => 'cust_main_county.taxclass', - 'pkgclass' => 'part_pkg.classnum', + 'pkgclass' => 'COALESCE(part_fee.classnum,part_pkg.classnum)', 'city' => 'cust_main_county.city', 'district' => 'cust_main_county.district', 'state' => 'cust_main_county.state', @@ -69,7 +70,8 @@ sub report_internal { my $join_cust_pkg = $join_cust. ' LEFT JOIN cust_pkg USING ( pkgnum ) - LEFT JOIN part_pkg USING ( pkgpart ) '; + LEFT JOIN part_pkg USING ( pkgpart ) + LEFT JOIN part_fee USING ( feepart ) '; my $from_join_cust_pkg = " FROM cust_bill_pkg $join_cust_pkg "; @@ -239,7 +241,7 @@ sub report_internal { # there isn't one for 'sales', because we calculate sales by adding up # the taxable and exempt columns. - # TAX QUERIES (billed tax, credited tax) + # TAX QUERIES (billed tax, credited tax, collected tax) # ----------- # sum of billed tax: @@ -252,14 +254,16 @@ sub report_internal { if ( $breakdown{pkgclass} ) { # If we're not grouping by package class, this is unnecessary, and # probably really expensive. + # Remember that fees also have package classes. $taxfrom .= " LEFT JOIN cust_bill_pkg AS taxable ON (cust_bill_pkg_tax_location.taxable_billpkgnum = taxable.billpkgnum) LEFT JOIN cust_pkg ON (taxable.pkgnum = cust_pkg.pkgnum) - LEFT JOIN part_pkg USING (pkgpart)"; + LEFT JOIN part_pkg USING (pkgpart) + LEFT JOIN part_fee ON (taxable.feepart = part_fee.feepart) "; } - my $istax = "cust_bill_pkg.pkgnum = 0"; + my $istax = "cust_bill_pkg.pkgnum = 0 and cust_bill_pkg.feepart is null"; $sql{tax} = "$select SUM(cust_bill_pkg_tax_location.amount) $taxfrom @@ -272,8 +276,8 @@ sub report_internal { $group_all"; # sum of credits applied against billed tax - # ($creditfrom includes join of taxable item to part_pkg if with_pkgclass - # is on) + # ($creditfrom includes join of taxable item to part_pkg/part_fee if + # with_pkgclass is on) my $creditfrom = $taxfrom . ' JOIN cust_credit_bill_pkg USING (billpkgtaxlocationnum)' . ' JOIN cust_credit_bill USING (creditbillnum)'; @@ -296,6 +300,27 @@ sub report_internal { $creditwhere AND $istax $group_all"; + # sum of tax paid + # this suffers from the same ambiguity as anything else that applies + # received payments to specific packages, but in reality the discrepancy + # should be minimal since people either pay their bill or don't. + # the join is on billpkgtaxlocationnum to avoid cross-producting. + + my $paidfrom = $taxfrom . + ' JOIN cust_bill_pay_pkg'. + ' ON (cust_bill_pay_pkg.billpkgtaxlocationnum ='. + ' cust_bill_pkg_tax_location.billpkgtaxlocationnum)'; + + $sql{tax_paid} = "$select SUM(cust_bill_pay_pkg.amount) + $paidfrom + $where AND $istax + $group"; + + $all_sql{tax_paid} = "$select_all SUM(cust_bill_pay_pkg.amount) + $paidfrom + $where AND $istax + $group_all"; + my %data; my %total; # note that we use keys(%sql) here and keys(%all_sql) later. nothing @@ -303,7 +328,7 @@ sub report_internal { # as for the individual category queries foreach my $k (keys(%sql)) { my $stmt = $sql{$k}; - warn "\n".uc($k).":\n".$stmt."\n" if $DEBUG; + warn "\n".uc($k).":\n".$stmt."\n" if $DEBUG > 1; my $sth = dbh->prepare($stmt); # eight columns: pkgclass, taxclass, state, county, city, district # taxnums (comma separated), value @@ -322,7 +347,7 @@ sub report_internal { push @$bin, [ $k, $row->[6], $row->[7] ]; } } - warn "DATA:\n".Dumper(\%data) if $DEBUG > 1; + warn "DATA:\n".Dumper(\%data) if $DEBUG; foreach my $k (keys %all_sql) { warn "\nTOTAL ".uc($k).":\n".$all_sql{$k}."\n" if $DEBUG; -- cgit v1.2.1 From 039381569561964c572407409fe61f6d4da97afe Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 14 Aug 2015 14:43:02 -0700 Subject: fix anomalous behavior in line item report with consolidated taxes, needed for #26770, from...#18676, I think? --- httemplate/search/cust_bill_pkg.cgi | 181 +++++++++++++---------------------- httemplate/search/report_tax-xls.cgi | 9 +- httemplate/search/report_tax.cgi | 21 ++-- 3 files changed, 87 insertions(+), 124 deletions(-) diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index c59a6d0e7..ac686eab8 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -461,6 +461,9 @@ if ( $cgi->param('nottax') ) { } + # This is the only place we should attempt to show credits on here: + # the total of credit applications to the line item. + my $credit_sub = 'SELECT SUM(amount) AS credit_amount, billpkgnum FROM cust_credit_bill_pkg GROUP BY billpkgnum'; @@ -579,51 +582,51 @@ if ( $cgi->param('nottax') ) { } elsif ( $cgi->param('istax') ) { - @peritem = ( 'setup' ); # taxes only have setup - @peritem_desc = ( 'Tax charge' ); + # ensure that it is a tax: + push @where, 'cust_bill_pkg.pkgnum = 0', + 'cust_bill_pkg.feepart IS NULL'; + + # We MUST NOT join cust_bill_pkg to any table that it's 1:many to. + # Otherwise we get duplication of the cust_bill_pkg records, + # inaccurate totals, nonsensical paging behavior, etc. + # We CAN safely join it to a subquery that has unique billpkgnums, and + # that's what we'll do. - push @where, 'cust_bill_pkg.pkgnum = 0'; + my $tax_subquery; + my @tax_where; # tax location when using tax_rate_location if ( $cgi->param('vendortax') ) { - $join_pkg .= ' LEFT JOIN cust_bill_pkg_tax_rate_location USING ( billpkgnum ) '. - ' LEFT JOIN tax_rate_location USING ( taxratelocationnum )'; + $tax_subquery = ' + SELECT billpkgnum, SUM(amount) as tax_total + FROM cust_bill_pkg_tax_rate_location AS tax + JOIN tax_rate_location USING (taxratelocationnum) + '; foreach (qw( state county city locationtaxid)) { if ( scalar($cgi->param($_)) ) { my $place = dbh->quote( $cgi->param($_) ); - push @where, "tax_rate_location.$_ = $place"; + push @tax_where, "tax_rate_location.$_ = $place"; } } - push @total, 'SUM( - COALESCE(cust_bill_pkg_tax_rate_location.amount, - cust_bill_pkg.setup + cust_bill_pkg.recur) - )'; - push @total_desc, "$money_char%.2f total"; - } else { # the internal-tax case - $join_pkg .= ' - LEFT JOIN cust_bill_pkg_tax_location USING (billpkgnum) - JOIN cust_main_county USING (taxnum) - '; - - # don't double-count the components of consolidated taxes - @total = ( 'COUNT(DISTINCT cust_bill_pkg.billpkgnum)', - 'SUM(cust_bill_pkg_tax_location.amount)' ); - @total_desc = "$money_char%.2f total"; + my $tax_select = 'SELECT tax.billpkgnum, SUM(tax.amount) as tax_total'; + my $tax_from = ' FROM cust_bill_pkg_tax_location AS tax JOIN cust_main_county USING (taxnum)'; # package classnum if ( grep { $_ eq 'classnum' } $cgi->param ) { my @classnums = grep /^\d*$/, $cgi->param('classnum'); - $join_pkg .= ' - JOIN cust_pkg AS taxed_pkg - ON (cust_bill_pkg_tax_location.pkgnum = taxed_pkg.pkgnum) - JOIN part_pkg AS taxed_part_pkg - ON (taxed_pkg.pkgpart = taxed_part_pkg.pkgpart) + $tax_from .= ' + JOIN cust_bill_pkg AS taxed_item + ON (tax.taxable_billpkgnum = taxed_item.billpkgnum) + LEFT JOIN cust_pkg AS taxed_pkg ON (taxed_item.pkgnum = taxed_pkg.pkgnum) + LEFT JOIN part_pkg AS taxed_part_pkg ON (taxed_pkg.pkgpart = taxed_part_pkg.pkgpart) + LEFT JOIN part_fee AS taxed_part_fee ON (taxed_item.feepart = taxed_part_fee.feepart) '; - push @where, "COALESCE(taxed_part_pkg.classnum, 0) IN ( ". + push @tax_where, + "COALESCE(taxed_part_pkg.classnum, taxed_part_fee.classnum,0) IN ( ". join(',', @classnums ). ' )' if @classnums; @@ -631,19 +634,20 @@ if ( $cgi->param('nottax') ) { # taxclass if ( $cgi->param('taxclassNULL') ) { - push @where, 'cust_main_county.taxclass IS NULL'; + push @tax_where, 'cust_main_county.taxclass IS NULL'; } # taxname if ( $cgi->param('taxnameNULL') ) { - push @where, 'cust_main_county.taxname IS NULL OR '. + push @tax_where, 'cust_main_county.taxname IS NULL OR '. 'cust_main_county.taxname = \'Tax\''; } elsif ( $cgi->param('taxname') ) { - push @where, 'cust_main_county.taxname = '. + push @tax_where, 'cust_main_county.taxname = '. dbh->quote($cgi->param('taxname')); } # itemdesc, for breakdown from the vendor tax report + # (is this even used? vendor tax report shouldn't use cust_bill_pkg.cgi) if ( $cgi->param('itemdesc') ) { if ( $cgi->param('itemdesc') eq 'Tax' ) { push @where, "($itemdesc = 'Tax' OR $itemdesc is null)"; @@ -652,103 +656,54 @@ if ( $cgi->param('nottax') ) { } } - # specific taxnums + # specific taxnums (the usual way) if ( $cgi->param('taxnum') =~ /^([\d,]+)$/) { - push @where, "cust_main_county.taxnum IN ($1)"; + push @tax_where, "cust_main_county.taxnum IN ($1)"; } - } #end of "normal case" - -} # nottax / istax - - -#total payments -my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount) - FROM cust_bill_pay_pkg - WHERE cust_bill_pkg.billpkgnum = cust_bill_pay_pkg.billpkgnum - "; -push @select, "($pay_sub) AS pay_amount"; - - -# credit -if ( $cgi->param('credit') ) { + $tax_subquery = "$tax_select $tax_from"; - my $credit_where; + } # end of internal-tax case - my($cr_begin, $cr_end) = FS::UI::Web::parse_beginning_ending($cgi, 'credit'); - $credit_where = "WHERE cust_credit_bill._date >= $cr_begin " . - "AND cust_credit_bill._date <= $cr_end"; - - my $credit_sub; - - if ( $cgi->param('istax') ) { - # then we need to group/join by billpkgtaxlocationnum, to get only the - # relevant part of partial taxes - my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount, - reason.reason as reason_text, access_user.username AS username_text, - billpkgtaxlocationnum, billpkgnum - FROM cust_credit_bill_pkg - JOIN cust_credit_bill USING (creditbillnum) - JOIN cust_credit USING (crednum) - LEFT JOIN reason USING (reasonnum) - LEFT JOIN access_user USING (usernum) - $credit_where - GROUP BY billpkgnum, billpkgtaxlocationnum, reason.reason, - access_user.username"; - - if ( $cgi->param('out') ) { + if (@tax_where) { + $tax_subquery .= ' + WHERE ' . join(' AND ', map "($_)", @tax_where); + } + $tax_subquery .= ' GROUP BY tax.billpkgnum '; - # find credits that are applied to the line items, but not to - # a cust_bill_pkg_tax_location link - $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit - USING (billpkgnum)"; - push @where, 'item_credit.billpkgtaxlocationnum IS NULL'; + # now join THAT into the main report + # (inner join, so that tax line items that don't match the tax_where + # conditions don't appear in the output.) - } else { + $join_pkg .= " JOIN ($tax_subquery) AS _istax USING (billpkgnum) "; - # find credits that are applied to the CBPTL links that are - # considered "interesting" by the report criteria - $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit - USING (billpkgtaxlocationnum)"; + push @select, 'tax_total'; - } + @peritem = ( 'setup' ); # total tax on the invoice, not just the filtered tax + @peritem_desc = ( 'Tax charge' ); - } else { - # then only group by billpkgnum - my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount, - reason.reason as reason_text, access_user.username AS username_text, - billpkgnum - FROM cust_credit_bill_pkg - JOIN cust_credit_bill USING (creditbillnum) - JOIN cust_credit USING (crednum) - LEFT JOIN reason USING (reasonnum) - LEFT JOIN access_user USING (usernum) - $credit_where - GROUP BY billpkgnum, reason.reason, access_user.username"; - $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit USING (billpkgnum)"; + @total = ( 'COUNT(cust_bill_pkg.billpkgnum)', + 'SUM(cust_bill_pkg.setup)' ); + @total_desc = ( "$money_char%.2f total tax" ); + + if ( @tax_where ) { + # then also show the filtered tax + push @peritem, 'tax_total'; + push @peritem_desc, 'Tax in category'; + push @total, 'SUM(tax_total)'; + push @total_desc, "$money_char%.2f tax in this category"; + # would also be nice to include a line explaining what the category is } - push @where, 'item_credit.billpkgnum IS NOT NULL'; - push @select, 'item_credit.credit_amount', - 'item_credit.username_text', - 'item_credit.reason_text'; - push @peritem, 'credit_amount', 'username_text', 'reason_text'; - push @peritem_desc, 'Credited', 'By', 'Reason'; - push @total, 'SUM(credit_amount)'; - push @total_desc, "$money_char%.2f credited"; - -} else { - - #still want a credit total column +} # nottax / istax - my $credit_sub = " - SELECT SUM(cust_credit_bill_pkg.amount) - FROM cust_credit_bill_pkg - WHERE cust_bill_pkg.billpkgnum = cust_credit_bill_pkg.billpkgnum - "; - push @select, "($credit_sub) AS credit_amount"; +#total payments +my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount) + FROM cust_bill_pay_pkg + WHERE cust_bill_pkg.billpkgnum = cust_bill_pay_pkg.billpkgnum + "; +push @select, "($pay_sub) AS pay_amount"; -} push @select, 'cust_main.custnum', FS::UI::Web::cust_sql_fields(); diff --git a/httemplate/search/report_tax-xls.cgi b/httemplate/search/report_tax-xls.cgi index d0ef434f4..743f14788 100755 --- a/httemplate/search/report_tax-xls.cgi +++ b/httemplate/search/report_tax-xls.cgi @@ -146,7 +146,7 @@ my $colhead = $format[0]->{colhead}; # print header $ws->merge_range($y, 1, $y, 5, 'Sales', $colhead); $ws->merge_range($y, 6, $y+1, 8, 'Rate', $colhead); -$ws->merge_range($y, 9, $y, 14, 'Tax', $colhead); +$ws->merge_range($y, 9, $y, 15, 'Tax', $colhead); $y++; $colhead = $format[0]->{colhead_small}; @@ -156,16 +156,17 @@ $ws->write($y, 9, 'Estimated', $colhead); $ws->write($y, 10, 'Invoiced', $colhead); $ws->write($y, 12, 'Credited', $colhead); $ws->write($y, 14, 'Net due', $colhead); +$ws->write($y, 15, 'Collected',$colhead); $y++; # print data -my $rownum = 0; +my $rownum = 1; my $prev_row = { pkgclass => 'DUMMY PKGCLASS' }; foreach my $row (@rows) { $x = 0; if ( $row->{pkgclass} ne $prev_row->{pkgclass} ) { - $rownum = 0; + $rownum = 1; if ( $params{breakdown}->{pkgclass} ) { $ws->merge_range($y, 0, $y, 14, $pkgclass_name{$row->{pkgclass}}, @@ -206,6 +207,8 @@ foreach my $row (@rows) { $ws->write_string($y, $x, " = ", $f->{bigmath}); $x++; $ws->write($y, $x, $row->{tax} - $row->{credit}, $f->{currency}); + $x++; + $ws->write($y, $x, $row->{tax_paid} || 0, $f->{currency}); $rownum++; $y++; diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi index 491cd42c5..1d906473e 100644 --- a/httemplate/search/report_tax.cgi +++ b/httemplate/search/report_tax.cgi @@ -32,6 +32,8 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } Tax credited Net tax due + + Tax collected @@ -131,13 +133,16 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } % # credited tax − - +%# currently broken <% $money_sprintf->( $row->{credit} ) %> - +%# % # net tax due = <% $money_sprintf->( $row->{tax} - $row->{credit} ) %> +% # tax collected +   + <% $money_sprintf->( $row->{tax_paid} ) %> % $rownum++; % $prev_row = $row; @@ -218,12 +223,12 @@ if ( $params{agentnum} ) { my $saleslink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;nottax=1"; my $taxlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;istax=1"; my $exemptlink = $p. "search/cust_tax_exempt_pkg.cgi?$dateagentlink"; -my $creditlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;credit=1;istax=1"; - -if ( $params{'credit_date'} eq 'cust_credit_bill' ) { - $creditlink =~ s/begin/credit_begin/; - $creditlink =~ s/end/credit_end/; -} +#my $creditlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;credit=1;istax=1"; +#if ( $params{'credit_date'} eq 'cust_credit_bill' ) { +# $creditlink =~ s/begin/credit_begin/; +# $creditlink =~ s/end/credit_end/; +#} +my $creditlink = ''; # disabled until we find a sane way to do this my %pkgclass_name = map { $_->classnum, $_->classname } qsearch('pkg_class'); $pkgclass_name{''} = 'Unclassified'; -- cgit v1.2.1 From 324209d9ea24ca2f6a6f387088263933c06e39df Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 14 Aug 2015 16:32:21 -0700 Subject: fix links from package report to services that use view/svc_Common, #37621 --- httemplate/search/cust_pkg.cgi | 9 ++++++--- httemplate/view/cust_svc.cgi | 8 +++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/httemplate/search/cust_pkg.cgi b/httemplate/search/cust_pkg.cgi index c88b3a1d5..f1e686a83 100755 --- a/httemplate/search/cust_pkg.cgi +++ b/httemplate/search/cust_pkg.cgi @@ -310,11 +310,14 @@ my $process_svc_labels = sub { foreach ( map { [ $_->label ] } @{ $part_svc->cust_pkg_svc } ) { push @out, [ { 'data' => $_->[0]. ':', - 'align'=> 'right', }, + 'align'=> 'right', + }, + { 'data' => $_->[1], 'align'=> 'left', - 'link' => $p. 'view/' . - $_->[2]. '.cgi?'. $_->[3], }, + 'link' => $p. 'view/cust_svc.cgi?' . $_->[3], + }, + ]; } } diff --git a/httemplate/view/cust_svc.cgi b/httemplate/view/cust_svc.cgi index 8ccfce3ff..aaf367882 100644 --- a/httemplate/view/cust_svc.cgi +++ b/httemplate/view/cust_svc.cgi @@ -1,4 +1,4 @@ -<% $cgi->redirect(popurl(1)."$svcdb.cgi?". $svcnum ) %> +<% $cgi->redirect($url) %> <%init> #needed here? we're just redirecting. i guess it could reveal the svcdb of a @@ -18,6 +18,12 @@ my $part_svc = qsearchs('part_svc',{'svcpart'=> $cust_svc->svcpart } ); die "Unknown svcpart" unless $part_svc; my $svcdb = $part_svc->svcdb; +my $url = svc_url( + 'm' => $m, + 'action' => 'view', + 'svcdb' => $svcdb, + 'query' => $svcnum, + ); -- cgit v1.2.1 From 58f03ab35257dc71b826d1c6b82fc8f5274dd53e Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 14 Aug 2015 17:43:25 -0700 Subject: update test, #37591 --- ...=Test%20one-time%20charge:quantity=1:bill_now=1 | 347 ++------------------- 1 file changed, 19 insertions(+), 328 deletions(-) diff --git a/FS-Test/share/output/edit/process/quick-charge.cgi/amount=100.00:custnum=2:pkg=Test%20one-time%20charge:quantity=1:bill_now=1 b/FS-Test/share/output/edit/process/quick-charge.cgi/amount=100.00:custnum=2:pkg=Test%20one-time%20charge:quantity=1:bill_now=1 index 038dde819..22d8b4224 100644 --- a/FS-Test/share/output/edit/process/quick-charge.cgi/amount=100.00:custnum=2:pkg=Test%20one-time%20charge:quantity=1:bill_now=1 +++ b/FS-Test/share/output/edit/process/quick-charge.cgi/amount=100.00:custnum=2:pkg=Test%20one-time%20charge:quantity=1:bill_now=1 @@ -1,328 +1,19 @@ - - - - - -

System error

- - - - - - - - - - - - - -
error: Error during compilation of /var/www/html/freeside/edit/process/quick-charge.cgi:
syntax error at /var/www/html/freeside/edit/process/quick-charge.cgi line 126, near ")
( "
Global symbol "$amount" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 132.
Global symbol "$setup_cost" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 133.
Global symbol "$quantity" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 134.
Global symbol "$override" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 147.
syntax error at /var/www/html/freeside/edit/process/quick-charge.cgi line 1, near "if"
Global symbol "$error" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 1.
Global symbol "$error" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 2.
syntax error at /var/www/html/freeside/edit/process/quick-charge.cgi line 4, near "}"
(Might be a runaway multi-line '' string starting on line 3)
Global symbol "$message" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 5.
syntax error at /var/www/html/freeside/edit/process/quick-charge.cgi line 10, near "}"
(Might be a runaway multi-line '' string starting on line 5)
/var/www/html/freeside/edit/process/quick-charge.cgi has too many errors.
context:  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: % if ( $error ) {
2: % $cgi->param('error', $error );
3: <% $cgi->redirect($p.'quick-charge.html?'. $cgi->query_string) %>
4: % } else {
5: <% header(emt($message)) %>
6:  <SCRIPT TYPE="text/javascript">
7:  window.top.location.reload();
8:  </SCRIPT>
9:  </BODY></HTML>
10: % }
11: <%init>
12: 
13: my $curuser = $FS::CurrentUser::CurrentUser;
14: die "access denied"
... 
122: 
123:  if ( $param->{'taxclass'} eq '(select)' ) {
124:  $error .= "Must select a tax class. "
125:  unless ($conf->config('tax_data_vendor'))
126:  ( $override || $param->{taxproductnum} )
127:  );
128:  $cgi->param('taxclass', '');
129:  }
130: 
131:  my %charge = (
132:  'amount' => $amount,
133:  'setup_cost' => $setup_cost,
134:  'quantity' => $quantity,
135:  'bill_now' => scalar($cgi->param('bill_now')),
136:  'invoice_terms' => scalar($cgi->param('invoice_terms')),
137:  'start_date' => ( scalar($cgi->param('start_date'))
138:  ? parse_datetime($cgi->param('start_date'))
... 
143:  'pkg' => scalar($cgi->param('pkg')),
144:  'setuptax' => scalar($cgi->param('setuptax')),
145:  'taxclass' => scalar($cgi->param('taxclass')),
146:  'taxproductnum' => scalar($cgi->param('taxproductnum')),
147:  'tax_override' => $override,
148:  'classnum' => scalar($cgi->param('classnum')),
149:  'additional' => \@description,
150:  );
151: 
... 
-
code stack:  - /usr/share/perl5/HTML/Mason/Interp.pm:453
- /usr/share/perl5/HTML/Mason/Request.pm:252
- /usr/share/perl5/HTML/Mason/Request.pm:215
- /usr/share/perl5/HTML/Mason/ApacheHandler.pm:94
- /usr/local/share/perl/5.20.2/FS/Mason/Request.pm:37
- /usr/share/perl5/Class/Container.pm:275
- /usr/share/perl5/Class/Container.pm:353
- /usr/share/perl5/HTML/Mason/Interp.pm:351
- /usr/share/perl5/HTML/Mason/ApacheHandler.pm:874
- /usr/share/perl5/HTML/Mason/ApacheHandler.pm:828
- /usr/local/etc/freeside/handler.pl:144
- -e:0
-
- -raw error
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - -
Error during compilation of /var/www/html/freeside/edit/process/quick-charge.cgi:
-syntax error at /var/www/html/freeside/edit/process/quick-charge.cgi line 126, near ")
-               ( "
-Global symbol "$amount" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 132.
-Global symbol "$setup_cost" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 133.
-Global symbol "$quantity" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 134.
-Global symbol "$override" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 147.
-syntax error at /var/www/html/freeside/edit/process/quick-charge.cgi line 1, near "if"
-Global symbol "$error" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 1.
-Global symbol "$error" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 2.
-syntax error at /var/www/html/freeside/edit/process/quick-charge.cgi line 4, near "}"
-  (Might be a runaway multi-line '' string starting on line 3)
-Global symbol "$message" requires explicit package name at /var/www/html/freeside/edit/process/quick-charge.cgi line 5.
-syntax error at /var/www/html/freeside/edit/process/quick-charge.cgi line 10, near "}"
-  (Might be a runaway multi-line '' string starting on line 5)
-/var/www/html/freeside/edit/process/quick-charge.cgi has too many errors.
-
-
-Trace begun at /usr/share/perl5/HTML/Mason/Interp.pm line 854
-HTML::Mason::Interp::_compilation_error('HTML::Mason::Interp=HASH(0x7fbd843931b8)', '/var/www/html/freeside/edit/process/quick-charge.cgi', 'HTML::Mason::Exception::Compilation=HASH(0x7fbd8b2466a0)') called at /usr/share/perl5/HTML/Mason/Interp.pm line 453
-HTML::Mason::Interp::load('HTML::Mason::Interp=HASH(0x7fbd843931b8)', '/edit/process/quick-charge.cgi') called at /usr/share/perl5/HTML/Mason/Request.pm line 252
-eval {...} at /usr/share/perl5/HTML/Mason/Request.pm line 235
-HTML::Mason::Request::_initialize('FS::Mason::Request=HASH(0x7fbd8af54d08)') called at /usr/share/perl5/HTML/Mason/Request.pm line 215
-HTML::Mason::Request::new('FS::Mason::Request', 'error_mode', 'output', 'error_format', 'html', 'interp', 'HTML::Mason::Interp=HASH(0x7fbd843931b8)', 'comp', '/edit/process/quick-charge.cgi', 'args', 'ARRAY(0x7fbd8ba9b970)', 'ah', 'HTML::Mason::ApacheHandler=HASH(0x7fbd843be610)', 'apache_req', 'Apache2::RequestRec=SCALAR(0x7fbd8b414c40)', 'container', 'HASH(0x7fbd8ba9c9b8)') called at /usr/share/perl5/HTML/Mason/ApacheHandler.pm line 94
-HTML::Mason::Request::ApacheHandler::new('FS::Mason::Request', 'error_mode', 'output', 'error_format', 'html', 'interp', 'HTML::Mason::Interp=HASH(0x7fbd843931b8)', 'comp', '/edit/process/quick-charge.cgi', 'args', 'ARRAY(0x7fbd8ba9b970)', 'ah', 'HTML::Mason::ApacheHandler=HASH(0x7fbd843be610)', 'apache_req', 'Apache2::RequestRec=SCALAR(0x7fbd8b414c40)', 'container', 'HASH(0x7fbd8ba9c9b8)') called at /usr/local/share/perl/5.20.2/FS/Mason/Request.pm line 37
-FS::Mason::Request::new('FS::Mason::Request', 'error_mode', 'output', 'error_format', 'html', 'interp', 'HTML::Mason::Interp=HASH(0x7fbd843931b8)', 'comp', '/edit/process/quick-charge.cgi', 'args', 'ARRAY(0x7fbd8ba9b970)', 'ah', 'HTML::Mason::ApacheHandler=HASH(0x7fbd843be610)', 'apache_req', 'Apache2::RequestRec=SCALAR(0x7fbd8b414c40)', 'container', 'HASH(0x7fbd8ba9c9b8)') called at /usr/share/perl5/Class/Container.pm line 275
-Class::Container::call_method('HTML::Mason::Interp=HASH(0x7fbd843931b8)', 'request', 'new', 'interp', 'HTML::Mason::Interp=HASH(0x7fbd843931b8)', 'comp', '/edit/process/quick-charge.cgi', 'args', 'ARRAY(0x7fbd8ba9b970)', 'ah', 'HTML::Mason::ApacheHandler=HASH(0x7fbd843be610)', 'apache_req', 'Apache2::RequestRec=SCALAR(0x7fbd8b414c40)', 'container', 'HASH(0x7fbd8ba9c9b8)') called at /usr/share/perl5/Class/Container.pm line 353
-Class::Container::create_delayed_object('interp', 'HTML::Mason::Interp=HASH(0x7fbd843931b8)', 'comp', '/edit/process/quick-charge.cgi', 'args', 'ARRAY(0x7fbd8ba9b970)', 'ah', 'HTML::Mason::ApacheHandler=HASH(0x7fbd843be610)', 'apache_req', 'Apache2::RequestRec=SCALAR(0x7fbd8b414c40)', 'container', 'HASH(0x7fbd8ba9c9b8)') called at /usr/share/perl5/HTML/Mason/Interp.pm line 351
-HTML::Mason::Interp::make_request('HTML::Mason::Interp=HASH(0x7fbd843931b8)', 'comp', '/edit/process/quick-charge.cgi', 'args', 'ARRAY(0x7fbd8ba9b970)', 'ah', 'HTML::Mason::ApacheHandler=HASH(0x7fbd843be610)', 'apache_req', 'Apache2::RequestRec=SCALAR(0x7fbd8b414c40)') called at /usr/share/perl5/HTML/Mason/ApacheHandler.pm line 874
-eval {...} at /usr/share/perl5/HTML/Mason/ApacheHandler.pm line 873
-HTML::Mason::ApacheHandler::prepare_request('HTML::Mason::ApacheHandler=HASH(0x7fbd843be610)', 'Apache2::RequestRec=SCALAR(0x7fbd8b414c40)') called at /usr/share/perl5/HTML/Mason/ApacheHandler.pm line 828
-HTML::Mason::ApacheHandler::handle_request('HTML::Mason::ApacheHandler=HASH(0x7fbd843be610)', 'Apache2::RequestRec=SCALAR(0x7fbd8b414c40)') called at /usr/local/etc/freeside/handler.pl line 144
-eval {...} at /usr/local/etc/freeside/handler.pl line 144
-HTML::Mason::handler('Apache2::RequestRec=SCALAR(0x7fbd8b414c40)') called at -e line 0
-eval {...} at -e line 0
-
- - + + + + One-time charge added + + + + + + + +
One-time charge added
+
+
+ + + -- cgit v1.2.1 From e5c7f9c125f212b867541b7768f15a9cf7418734 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 14 Aug 2015 18:04:07 -0700 Subject: update test, #37420 --- FS-Test/share/output/search/cust_pkg.cgi/pkgnum | 11329 ---------------------- FS-Test/share/output/view/svc_phone.cgi/403 | 2 +- 2 files changed, 1 insertion(+), 11330 deletions(-) delete mode 100644 FS-Test/share/output/search/cust_pkg.cgi/pkgnum diff --git a/FS-Test/share/output/search/cust_pkg.cgi/pkgnum b/FS-Test/share/output/search/cust_pkg.cgi/pkgnum deleted file mode 100644 index 495e8089a..000000000 --- a/FS-Test/share/output/search/cust_pkg.cgi/pkgnum +++ /dev/null @@ -1,11329 +0,0 @@ - - - - - - Package Search Results - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
freeside - Freeside Test 5.0.1 - Logged in as test  logout
Preferences -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- Adv - -
- - - - - -
- -
-
- Advanced - -
- - - - - -
- - - - - -
- - Adv
- -
- - - - - -
- -
-
- Advanced - -
- - - - - -
- -
-
- Advanced - -
- - - - - -
- - - - - - - -
- - -
- -

- Package Search Results -

- -
- - - - - - - Change these packages
Email a notice to these customers - -

- - - - - - - - - - - - - - - -
- -
- - 601 total packages - - - ( show per page ) - - -
- -
- -
- - Download full results
- - as Excel spreadsheet
- - as CSV file
- - - as printable copy - -
- - - - 1 - - - 2 - - - 3 - - - 4 - - - 5 - - - 6 - - - 7 - - Next - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- # - - Quan. - - Package - - Class - - Status - - Sales Person - - Ordered by - - Setup - - Base Recur - - Freq. - - Setup - - Last bill - - Next bill - - Adjourn - - Susp. - - Susp. delay - - Expire - - Contract end - - Changed - - Cancel - - Reason - - Cust. Status - - Customer - - Services -
5011Monthly anniversary phone x4Activetest$0.00$60.00monthlyJan 02 2016Feb 02 2016Mar 02 2016ActiveCole, Albertha
Test svc_phone:24465928708548
4791Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 02 2015Feb 02 2016Mar 02 2016ActiveHaley, Schaden and Ebert (Prosacco, Clementina)
Test svc_phone:7422901680427
2951Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 02 2015Feb 02 2016Mar 02 2016ActiveLuettgen-Jacobs (Hintz, Junior)
Test svc_phone:14991580189167
1791Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 02 2015Feb 02 2016Mar 02 2016ActiveLeuschke-Stamm (Dibbert, Betsy)
Test svc_phone:2189396399
2611Monthly anniversary phone x4Activetest$0.00$60.00monthlyJan 02 2016Feb 02 2016Mar 02 2016ActiveMoore, Lina
Test svc_phone:5717780937
5111Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 02 2015Feb 02 2016Mar 02 2016ActiveMante LLC (Kessler, Enid)
Test svc_phone:9686105497
2031Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 02 2015Feb 02 2016Mar 02 2016ActiveMoore-Cummerata (DuBuque, Russ)
Test svc_phone:8632406717
5511Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 02 2015Feb 02 2016Mar 02 2016ActiveChristiansen LLC (Howe, Luis)
Test svc_phone:5191928764
3911Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 03 2015Feb 03 2016Mar 03 2016ActiveWolff and Sons (Heller, Dagmar)
Test svc_phone:16892538421
3111Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 03 2015Feb 03 2016Mar 03 2016ActiveBarton-Goodwin (Schroeder, Brian)
Test svc_phone:8239761006014
791Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 03 2015Feb 03 2016Mar 03 2016ActiveHaag-Schumm (Ullrich, Shemar)
Test svc_phone:8935173249
2231Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 03 2015Feb 03 2016Mar 03 2016ActiveHoeger-Brown (Shields, Serenity)
Test svc_phone:68981950057600
2131Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 03 2015Feb 03 2016Mar 03 2016ActiveLehner, Ryann
Test svc_phone:2636239939
3311Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 03 2015Feb 03 2016Mar 03 2016ActiveTreutel, Kuhn and Sipes (Wintheiser, Elyse)
Test svc_phone:17840460291257
431Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 03 2015Feb 03 2016Mar 03 2016ActiveStracke Inc (Kuhlman, Kaya)
Test svc_phone:7315522562
2491Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 04 2015Feb 04 2016Mar 04 2016ActivePacocha, Matilde
Test svc_phone:18235477397
3571Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 04 2015Feb 04 2016Mar 04 2016ActiveMante, Demond
Test svc_phone:751008639787292
1151Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 04 2015Feb 04 2016Mar 04 2016ActiveKunze, Ryan and Dare (Schultz, Jasper)
Test svc_phone:495453230818559
3231Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 04 2015Feb 04 2016Mar 04 2016ActiveLind-Bahringer (Ratke, Roma)
Test svc_phone:4989851645
5491Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 05 2015Feb 05 2016Mar 05 2016ActiveHudson, Stephanie
Test svc_phone:87252993767044
1311Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 05 2015Feb 05 2016Mar 05 2016ActiveShanahan LLC (Brown, Ceasar)
Test svc_phone:4508409161
3711Monthly anniversary phone x4Activetest$0.00$60.00monthlyJan 06 2016Feb 06 2016Mar 06 2016ActiveKuhlman, Quitzon and Greenholt (Quitzon, Ophelia)
Test svc_phone:15363386908
2751Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 06 2015Feb 06 2016Mar 06 2016ActiveDach, Lueilwitz and Koepp (Kovacek, Frank)
Test svc_phone:1732869050
2351Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 06 2015Feb 06 2016Mar 06 2016ActiveCorkery-D'Amore (Wyman, Bethel)
Test svc_phone:9464935873
4771Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 06 2015Feb 06 2016Mar 06 2016ActiveLakin, Lindsay
Test svc_phone:8078845348
4391Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 06 2015Feb 06 2016Mar 06 2016ActivePfeffer, Shanahan and Cruickshank (Kutch, Rosario)
Test svc_phone:1214016847277551
5991Monthly anniversary phone x4Activetest$0.00$60.00monthlyJan 06 2016Feb 06 2016Mar 06 2016ActiveMills, Ziemann and Satterfield (Lang, Cathy)
Test svc_phone:0806183894522
3471Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 07 2015Feb 07 2016Mar 07 2016ActiveBernier-Nader (Hane, Floy)
Test svc_phone:1894866195856273
2731Monthly anniversary phone x4Activetest$0.00$60.00monthlyAug 07 2015Feb 07 2016Mar 07 2016ActiveRuecker, Lucious
Test svc_phone:16095013569
3191Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 07 2015Feb 07 2016Mar 07 2016ActiveHessel and Sons (Zemlak, Kaya)
Test svc_phone:13810128409238
3071Monthly anniversary phone x4Activetest$0.00$60.00monthlyAug 08 2015Feb 08 2016Mar 08 2016ActiveRoberts-Schinner (Flatley, Amelia)
Test svc_phone:4593519604
3211Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 08 2015Feb 08 2016Mar 08 2016ActiveEmmerich, Neil
Test svc_phone:25963062543138
3811Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 08 2015Feb 08 2016Mar 08 2016ActivePowlowski, Veda
Test svc_phone:3911632965
4291Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 08 2015Feb 08 2016Mar 08 2016ActiveFrami, Miller
Test svc_phone:33762771108367
5151Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 08 2015Feb 08 2016Mar 08 2016ActiveMraz LLC (Labadie, Trisha)
Test svc_phone:14527731997
1911Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 08 2015Feb 08 2016Mar 08 2016ActiveO'Reilly-Mraz (Pagac, Kennedi)
Test svc_phone:078151255309299
4651Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 08 2015Feb 08 2016Mar 08 2016ActiveUpton, Otho
Test svc_phone:2646555583
1431Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 08 2015Feb 08 2016Mar 08 2016ActiveBernhard LLC (Hintz, Winston)
Test svc_phone:95994707748468
2851Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 08 2015Feb 08 2016Mar 08 2016ActiveMuller, Kenyatta
Test svc_phone:3521080416
911Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 09 2015Feb 09 2016Mar 09 2016ActiveKuhn-Ruecker (Nienow, Kacie)
Test svc_phone:1535633738761521
3551Monthly anniversary phone x4Activetest$0.00$60.00monthlyAug 09 2015Feb 09 2016Mar 09 2016ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
Test svc_phone:15790441533145
691Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 10 2015Feb 10 2016Mar 10 2016ActiveConn, Marisol
Test svc_phone:19732087174151
5871Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 10 2015Feb 10 2016Mar 10 2016ActiveRyan and Sons (Bashirian, Bert)
Test svc_phone:6566177741620
71Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 10 2015Feb 10 2016Mar 10 2016ActiveFlatley-Hagenes (Donnelly, Odessa)
Test svc_phone:19671718037
1991Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 10 2015Feb 10 2016Mar 10 2016ActiveSchultz, Hyatt and Ruecker (Yundt, Berta)
Test svc_phone:14559187832
5711Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 10 2015Feb 10 2016Mar 10 2016ActiveWalker Inc (Block, Felix)
Test svc_phone:9749671732
951Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 11 2015Feb 11 2016Mar 11 2016ActiveBuckridge, Spinka and Gerlach (Larkin, Lue)
Test svc_phone:1076194311
4171Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 11 2015Feb 11 2016Mar 11 2016ActiveCremin, Aliya
Test svc_phone:67497022590689
2971Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 12 2015Feb 12 2016Mar 12 2016ActiveDibbert, Roman
Test svc_phone:7801192536
3351Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 12 2015Feb 12 2016Mar 12 2016ActivePaucek, Swaniawski and Carter (DuBuque, Freda)
Test svc_phone:17561074962584
351Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 12 2015Feb 12 2016Mar 12 2016ActiveFeest, Bechtelar and Harber (Douglas, Geovany)
Test svc_phone:4609716945803
1751Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 12 2015Feb 12 2016Mar 12 2016ActiveSimonis Inc (Runolfsson, Kareem)
Test svc_phone:6741985321
1631Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 12 2015Feb 12 2016Mar 12 2016ActiveWiegand-Kohler (Murray, Amparo)
Test svc_phone:85524284918759
3931Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 12 2015Feb 12 2016Mar 12 2016ActiveKris, Josie
Test svc_phone:6110246771
4311Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 12 2015Feb 12 2016Mar 12 2016ActiveZboncak, Schmidt and Howell (Pouros, Robb)
Test svc_phone:3839925171
111Monthly anniversary phone x4Activetest$0.00$60.00monthlyJan 13 2016Feb 13 2016Mar 13 2016ActiveWatsica-Crooks (Will, Marguerite)
Test svc_phone:7941182146
2371Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 13 2015Feb 13 2016Mar 13 2016ActiveJakubowski, Jarrell
Test svc_phone:10163759294554
5591Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 13 2015Feb 13 2016Mar 13 2016ActiveHegmann, Kessler and Gibson (Roob, Henderson)
Test svc_phone:49053079571982
3951Monthly anniversary phone x4Activetest$0.00$60.00monthlyJan 13 2016Feb 13 2016Mar 13 2016ActiveKirlin-Feest (Aufderhar, Trisha)
Test svc_phone:34512365885294
1171Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 13 2015Feb 13 2016Mar 13 2016ActiveBrekke, Tillman
Test svc_phone:158607899401245
811Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 13 2015Feb 13 2016Mar 13 2016ActiveHackett, Garnet
Test svc_phone:152553597965486
4551Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 13 2015Feb 13 2016Mar 13 2016ActiveLind Group (Padberg, Irving)
Test svc_phone:7002098074566
5351Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 13 2015Feb 13 2016Mar 13 2016ActiveConn-McLaughlin (O'Connell, Gayle)
Test svc_phone:6049664310378
451Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 14 2015Feb 14 2016Mar 14 2016ActiveMcDermott, Alejandra
Test svc_phone:5473351513
3791Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 14 2015Feb 14 2016Mar 14 2016ActiveLangosh, Shanahan and Huels (Morissette, Florence)
Test svc_phone:5869395581
4911Monthly anniversary phone x4Activetest$0.00$60.00monthlyJan 14 2016Feb 14 2016Mar 14 2016ActiveDuBuque, Romaguera and Hagenes (Rempel, Levi)
Test svc_phone:0970039499
4431Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 14 2015Feb 14 2016Mar 14 2016ActiveO'Keefe Inc (Schamberger, Felix)
Test svc_phone:8366310646
4671Monthly anniversary phone x4Activetest$0.00$60.00monthlyAug 14 2015Feb 14 2016Mar 14 2016ActiveWeimann Inc (Cartwright, Judah)
Test svc_phone:5876977314592
2591Monthly anniversary phone x4Activetest$0.00$60.00monthlyAug 15 2015Feb 15 2016Mar 15 2016ActiveKozey and Sons (Vandervort, Harmon)
Test svc_phone:106766405260980
3091Monthly anniversary phone x4Activetest$0.00$60.00monthlyJan 15 2016Feb 15 2016Mar 15 2016ActiveStanton, Christop
Test svc_phone:18561234265014
2011Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 15 2015Feb 15 2016Mar 15 2016ActiveNikolaus, Katelyn
Test svc_phone:13116773370
331Monthly anniversary phone x4Activetest$0.00$60.00monthlyJan 16 2016Feb 16 2016Mar 16 2016ActiveKuhlman, Niko
Test svc_phone:14745441565
591Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 16 2015Feb 16 2016Mar 16 2016ActiveZemlak and Sons (Swift, Maximilian)
Test svc_phone:5055483796977
1031Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 16 2015Feb 16 2016Mar 16 2016ActiveBahringer LLC (Frami, Roslyn)
Test svc_phone:19242934458
3671Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 17 2015Feb 17 2016Mar 17 2016ActiveZulauf-Schiller (Jacobs, Angelina)
Test svc_phone:4942001551
1871Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 17 2015Feb 17 2016Mar 17 2016ActiveLuettgen LLC (Grant, Grover)
Test svc_phone:4415242593
4511Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 17 2015Feb 17 2016Mar 17 2016ActiveLeannon-Crona (Schuster, Cierra)
Test svc_phone:3037362335
4531Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 17 2015Feb 17 2016Mar 17 2016ActiveKlocko, Fleta
Test svc_phone:5389770549
2271Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 18 2015Feb 18 2016Mar 18 2016ActiveKunze Inc (Ernser, Coralie)
Test svc_phone:2280817547
2471Monthly anniversary phone x4Activetest$0.00$60.00monthlyAug 18 2015Feb 18 2016Mar 18 2016ActiveJacobson-Gorczany (Vandervort, Kiley)
Test svc_phone:2749371736
5471Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 18 2015Feb 18 2016Mar 18 2016ActiveDietrich, Keebler and Dach (Russel, Ivy)
Test svc_phone:1116321423
2871Monthly anniversary phone x4Activetest$0.00$60.00monthlyAug 18 2015Feb 18 2016Mar 18 2016ActiveBraun, Rath and Gutkowski (Wilderman, Reyes)
Test svc_phone:183790150181541
3451Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 18 2015Feb 18 2016Mar 18 2016ActiveKonopelski, Barry
Test svc_phone:0783009535773
4871Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 18 2015Feb 18 2016Mar 18 2016ActiveFritsch LLC (Jones, Mandy)
Test svc_phone:8192793749
1071Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 18 2015Feb 18 2016Mar 18 2016ActiveHodkiewicz-Raynor (Macejkovic, Leann)
Test svc_phone:6941312477183
2711Monthly anniversary phone x4Activetest$0.00$60.00monthlyAug 19 2015Feb 19 2016Mar 19 2016ActiveFay and Sons (Gerhold, Thora)
Test svc_phone:9519625792
2631Monthly anniversary phone x4Activetest$0.00$60.00monthlyJan 19 2016Feb 19 2016Mar 19 2016ActiveLegros Group (Reilly, Mabel)
Test svc_phone:210371813834331
91Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 19 2015Feb 19 2016Mar 19 2016ActiveBartoletti, Theodora
Test svc_phone:337448915280026
711Monthly anniversary phone x4Activetest$0.00$60.00monthlyAug 19 2015Feb 19 2016Mar 19 2016ActiveKuhlman-Huels (Parisian, Cristopher)
Test svc_phone:99158298078002
3431Monthly anniversary phone x4Activetest$0.00$60.00monthlyDec 19 2015Feb 19 2016Mar 19 2016ActiveKemmer-O'Connell (Schuster, Alexander)
Test svc_phone:4441731527
571Monthly anniversary phone x4Activetest$0.00$60.00monthlyOct 19 2015Feb 19 2016Mar 19 2016ActiveWeber, Aliza
Test svc_phone:215984572910627
5851Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 19 2015Feb 19 2016Mar 19 2016ActiveTurcotte, Janessa
Test svc_phone:10016834740
4631Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 19 2015Feb 19 2016Mar 19 2016ActiveBoyle-Schmeler (Maggio, Fay)
Test svc_phone:1173141702400720
5751Monthly anniversary phone x4Activetest$0.00$60.00monthlyJan 19 2016Feb 19 2016Mar 19 2016ActiveJohnston Group (Adams, Audreanne)
Test svc_phone:2786062985022
1271Monthly anniversary phone x4Activetest$0.00$60.00monthlyAug 20 2015Feb 20 2016Mar 20 2016ActiveLeannon-Pfannerstill (O'Keefe, Bernie)
Test svc_phone:5260896063
2991Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 20 2015Feb 20 2016Mar 20 2016ActiveFlatley, Yundt and Pacocha (Volkman, Tabitha)
Test svc_phone:2964457155392
1551Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 20 2015Feb 20 2016Mar 20 2016ActiveBalistreri-Schoen (Schultz, Jaylan)
Test svc_phone:261457560511658
1411Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 21 2015Feb 21 2016Mar 21 2016ActiveMohr, Florine
Test svc_phone:5805430204
931Monthly anniversary phone x4Activetest$0.00$60.00monthlySep 21 2015Feb 21 2016Mar 21 2016ActiveSwaniawski, Adrienne
Test svc_phone:7091741436337
471Monthly anniversary phone x4Activetest$0.00$60.00monthlyNov 21 2015Feb 21 2016Mar 21 2016ActiveLeffler, Abshire and Orn (Hyatt, Reggie)
Test svc_phone:580124349433539
- - - - 1 - - - 2 - - - 3 - - - 4 - - - 5 - - - 6 - - - 7 - - Next - - - -
- - - - -
- - - - - - - - diff --git a/FS-Test/share/output/view/svc_phone.cgi/403 b/FS-Test/share/output/view/svc_phone.cgi/403 index 8d53bc103..113aaf715 100644 --- a/FS-Test/share/output/view/svc_phone.cgi/403 +++ b/FS-Test/share/output/view/svc_phone.cgi/403 @@ -954,7 +954,7 @@ function areyousure_delete() { - + -- cgit v1.2.1 From 45b87d63b047f23626a3ad9301a0dc0fd8797cae Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sun, 16 Aug 2015 15:34:47 -0700 Subject: debug --- httemplate/elements/tr-select-months.html | 1 - 1 file changed, 1 deletion(-) diff --git a/httemplate/elements/tr-select-months.html b/httemplate/elements/tr-select-months.html index 3ff28f99b..b90ce1ed7 100644 --- a/httemplate/elements/tr-select-months.html +++ b/httemplate/elements/tr-select-months.html @@ -7,6 +7,5 @@ $opt{labels} = { '' => '', map { $_ => emt('[quant,_1,month]', $_) } 1 .. $max }; -warn Dumper(\%opt); <& tr-select.html, %opt &> -- cgit v1.2.1 From ef2bc5dcb69e67077ce45a624c107894765e3907 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 17 Aug 2015 12:13:14 -0700 Subject: but still show credited amount on line item report, #18676 fixes --- httemplate/search/cust_bill_pkg.cgi | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index ac686eab8..4fb9b662b 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -44,8 +44,8 @@ @currency, 'invnum', '_date', - '', #'pay_amount', - '', #'credit_amount', + 'pay_amount', + 'credit_amount', FS::UI::Web::cust_sort_fields(), ], 'links' => [ @@ -461,15 +461,6 @@ if ( $cgi->param('nottax') ) { } - # This is the only place we should attempt to show credits on here: - # the total of credit applications to the line item. - - my $credit_sub = 'SELECT SUM(amount) AS credit_amount, billpkgnum - FROM cust_credit_bill_pkg GROUP BY billpkgnum'; - - $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit - ON (cust_bill_pkg.billpkgnum = item_credit.billpkgnum)"; - if ( @tax_where or $cgi->param('taxable') ) { # process tax restrictions unshift @tax_where, @@ -704,7 +695,15 @@ my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount) "; push @select, "($pay_sub) AS pay_amount"; +#total credits +my $credit_sub = 'SELECT SUM(amount) AS credit_amount, billpkgnum + FROM cust_credit_bill_pkg GROUP BY billpkgnum'; + +$join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit + ON (cust_bill_pkg.billpkgnum = item_credit.billpkgnum)"; +push @select, 'credit_amount'; +# standard customer fields push @select, 'cust_main.custnum', FS::UI::Web::cust_sql_fields(); #salesnum -- cgit v1.2.1 From 89525f062092c185344ec7318406b1c9086d1eda Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Mon, 17 Aug 2015 23:01:31 -0500 Subject: RT#18830: Upload file to message template --- FS/FS/Schema.pm | 14 ++ FS/FS/template_image.pm | 222 ++++++++++++++++ httemplate/browse/msg_template.html | 3 +- httemplate/browse/template_image.html | 68 +++++ httemplate/edit/msg_template.html | 11 +- httemplate/elements/form-file_upload.html | 1 + .../elements/images/ui-icons_ef8c08_256x240.png | Bin 0 -> 4369 bytes httemplate/elements/template_image-dialog.html | 279 +++++++++++++++++++++ httemplate/misc/email-customers.html | 2 +- httemplate/misc/process/template_image-delete.cgi | 28 +++ httemplate/misc/process/template_image-upload.cgi | 26 ++ httemplate/misc/xmlhttp-template_image.cgi | 48 ++++ 12 files changed, 698 insertions(+), 4 deletions(-) create mode 100644 FS/FS/template_image.pm create mode 100644 httemplate/browse/template_image.html create mode 100644 httemplate/elements/images/ui-icons_ef8c08_256x240.png create mode 100644 httemplate/elements/template_image-dialog.html create mode 100644 httemplate/misc/process/template_image-delete.cgi create mode 100644 httemplate/misc/process/template_image-upload.cgi create mode 100644 httemplate/misc/xmlhttp-template_image.cgi diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 184c6c951..55dc99eca 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -204,6 +204,7 @@ sub dbdef_dist { && ( ! /^queue(_arg|_depend|_stat)?$/ || ! $opt->{'queue-no_history'} ) && ! $tables_hashref_torrus->{$_} && ! /^cacti_page$/ + && ! /^template_image$/ } $dbdef->tables ) { @@ -6346,6 +6347,19 @@ sub tables_hashref { ], }, + 'template_image' => { + 'columns' => [ + 'imgnum', 'serial', '', '', '', '', + 'name', 'varchar', '', $char_d, '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'mime_type', 'varchar', '', $char_d, '', '', + 'base64', 'text', '', '', '', '', + ], + 'primary_key' => 'imgnum', + 'unique' => [ ], + 'index' => [ ['name'], ['agentnum'] ], + }, + 'cust_msg' => { 'columns' => [ 'custmsgnum', 'serial', '', '', '', '', diff --git a/FS/FS/template_image.pm b/FS/FS/template_image.pm new file mode 100644 index 000000000..e7f4baba5 --- /dev/null +++ b/FS/FS/template_image.pm @@ -0,0 +1,222 @@ +package FS::template_image; +use base qw( FS::Agent_Mixin FS::Record ); + +use strict; +use FS::Record qw( qsearchs ); +use File::Slurp qw( slurp ); +use MIME::Base64 qw( encode_base64 ); + +my %ext_to_type = ( + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', +); + +=head1 NAME + +FS::template_image - Object methods for template_image records + +=head1 SYNOPSIS + + use FS::template_image; + + $record = new FS::template_image { + 'name' => 'logo', + 'agentnum' => $agentnum, + 'base64' => encode_base64($rawdata), + 'mime_type' => 'image/jpg', + }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::template_image object represents an uploaded image for insertion into templates. +FS::template_image inherits from FS::Record. The following fields are currently supported: + +=over 4 + +=item imgnum - primary key + +=item name - unique name, for selecting/editing images + +=item agentnum - image agent + +=item mime-type - image mime-type + +=item base64 - base64-encoded raw contents of image file + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new object. To add the object 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 + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'template_image'; } + +=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 example. 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_numbern('imgnum','agentnum') + || $self->ut_text('name','mime-type') + || $self->ut_anything('base64') + ; + return $error if $error; + + $self->SUPER::check; +} + +=item src + +Returns a data url for this image, incorporating mime_type & base64 + +=cut + +sub src { + my $self = shift; + 'data:' + . $self->mime_type + . ';base64,' + . $self->base64; +} + +=item html + +Returns html for a basic img tag for this image (no attributes) + +=cut + +sub html { + my $self = shift; + ''; +} + +=item process_image_delete + +Process for deleting an image. Run as a job using L. + +=cut + +sub process_image_delete { + my $job = shift; + my $param = shift; + my $template_image = qsearchs('template_image',{ 'imgnum' => $param->{'imgnum'} }) + or die "Could not load template_image"; + my $error = $template_image->delete; + die $error if $error; + ''; +} + +=item process_image_upload + +Process for uploading an image. Run as a job using L. + +=cut + +sub process_image_upload { + my $job = shift; + my $param = shift; + + my $files = $param->{'uploaded_files'} + or die "No files provided.\n"; + + my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files; + + my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/'; + my $file = $dir. $files{'file'}; + + my $type; + if ( $file =~ /\.(\w+)$/i ) { + my $ext = lc($1); + die "Unrecognized file extension $ext" + unless $ext_to_type{$ext}; + $type = $ext_to_type{$ext}; + } else { + die "Cannot upload image file without extension" + } + + my $template_image = new FS::template_image { + 'name' => $param->{'name'}, + 'mime_type' => $type, + 'agentnum' => $param->{'agentnum'}, + 'base64' => encode_base64( slurp($file, binmode => ':raw'), '' ), + }; + my $error = $template_image->insert(); + die $error if $error; + unlink $file; + ''; + +} + +=back + +=head1 BUGS + +Will be described here once found. + +=head1 SEE ALSO + +L + +=cut + +1; + diff --git a/httemplate/browse/msg_template.html b/httemplate/browse/msg_template.html index ef0b2dafd..1646bc169 100644 --- a/httemplate/browse/msg_template.html +++ b/httemplate/browse/msg_template.html @@ -28,6 +28,7 @@ my @menubar = (); if ( $curuser->access_right(['Edit templates', 'Edit global templates']) ) { push @menubar, 'Add a new template' => $p.'edit/msg_template.html'; } +push @menubar, 'View template images' => $p.'browse/template_image.html'; my $link = [ "${p}edit/msg_template.html?msgnum=", 'msgnum' ]; @@ -52,7 +53,7 @@ my $disable_link = sub { action => $p.'misc/disable-msg_template.cgi?msgnum=' . $template->msgnum . ($template->disabled ? ';enable=1' : ''), - actionlabel => 'Disable lemplate', + actionlabel => 'Disable template', ); }; diff --git a/httemplate/browse/template_image.html b/httemplate/browse/template_image.html new file mode 100644 index 000000000..eb4325f15 --- /dev/null +++ b/httemplate/browse/template_image.html @@ -0,0 +1,68 @@ +<% include('/elements/init_overlib.html') %> + +<% include( 'elements/browse.html', + 'title' => 'Template images', + 'name_singular' => 'image', + 'menubar' => \@menubar, + 'query' => { 'table' => 'template_image', }, + 'count_query' => 'SELECT COUNT(*) FROM template_image', + 'agent_virt' => 1, + 'agent_null_right' => ['View global templates','Edit global templates'], + 'agent_pos' => 1, + 'header' => [ 'Name', '', '' ], + 'fields' => [ 'name', $tag, $delete_text ], + 'links' => [ '', '', '' ], + 'cell_style' => [ '', '', '' ], + ) +%> + +<% include('/elements/template_image-dialog.html', + 'url' => $p.'browse/template_image.html' + ) %> + +<%init> +use FS::template_image; + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right([ 'View templates', 'View global templates', + 'Edit templates', 'Edit global templates', ]); + +my $canedit = $curuser->access_right(['Edit templates', 'Edit global templates']); + +my @menubar = (); +if ($canedit) { + push @menubar, 'Upload a new image' => 'javascript:insertImageDialog(\'upload\')'; +} +push @menubar, ( 'View message templates' => $p.'browse/msg_template.html' ); + +my $tag = sub { qq!view! }; + +my $delete_text = $canedit ? sub { + my $image = shift; + my $imgnum = $image->imgnum; + unless ($image->agentnum) { + unless ($FS::CurrentUser::CurrentUser->access_right('Edit global templates')) { + return ''; + } + } + my $out = < + + +EOF + $out .= include('/elements/progress-init.html', + "delete_template_image_$imgnum", + [ 'imgnum' ], + $p.'misc/process/template_image-delete.cgi', + $p.'browse/template_image.html', + "imgnum$imgnum", + ); + my $onclick = 'if ( confirm(\''; + $onclick .= emt('Are you sure you want to delete template image ') . $imgnum; + $onclick .= '\') ) { imgnum' . $imgnum . 'process() }'; + return $out . 'delete'; +} : ''; + + diff --git a/httemplate/edit/msg_template.html b/httemplate/edit/msg_template.html index 7f3824127..df72c5b66 100644 --- a/httemplate/edit/msg_template.html +++ b/httemplate/edit/msg_template.html @@ -27,6 +27,7 @@ 'no_submit' => $no_submit, &> <%init> +use FS::template_image; my $curuser = $FS::CurrentUser::CurrentUser; @@ -345,10 +346,16 @@ function areyousure(url, message) {
Substitutions: ' . $widget->html . -'
Click links to insert. -
Enclose substitutions and other Perl expressions in braces: +'

Click above links to insert substitution code.

+

+Enclose substitutions and other Perl expressions in braces:
{ $name } = ExampleCo (Smith, John)
{ time2str("%D", time) } = '.time2str("%D", time).' +

'; +$sidebar .= include('/elements/template_image-dialog.html', + 'callback' => 'insertHtml' + ); +$sidebar .= '

Insert Uploaded Image

'; diff --git a/httemplate/elements/form-file_upload.html b/httemplate/elements/form-file_upload.html index 45b6c97f2..3542a5a8e 100644 --- a/httemplate/elements/form-file_upload.html +++ b/httemplate/elements/form-file_upload.html @@ -69,6 +69,7 @@ Example:
+ +Creates a jquery dialog box that opens when javascript function insertImageDialog +is called, allows user to select an image and specify attributes for it, then passes +img tag with base64 encoded data url to a callback javascript function. + +Accepts the following options: + +callback - pass the html for the selected img to this javascript function; +if omitted, will only include fields for viewing/uploading image + +url - to redirect to after upload, otherwise just refreshes dialog window + + + +<% include('/elements/xmlhttp.html', + 'url' => $p.'misc/xmlhttp-template_image.cgi', + 'subs' => [ 'get_template_image' ], + ) %> + +
+ + + + + +% if ($opt{'callback'}) { + + + + + + + + + + + + + + + + + + + +% } # if $opt{'callback'} + +
+ + + +<% &ntable("#cccccc", 2) %> + +
Image + +
Width
Height
Align + +
Alt Text
+ +
+ + + +% if ($canedit) { + +

<% emt('Upload New Image') %>

+ +<% include('/elements/form-file_upload.html', + 'name' => 'TemplateImageUploadForm', + 'id' => 'TemplateImageUploadForm', + 'action' => $p.'misc/process/template_image-upload.cgi', + 'num_files' => 1, + 'fields' => [ 'name', 'agentnum' ], + 'url' => $opt{'url'} || 'javascript:refreshImageList(1)', + ) + %> + + <% &ntable("#cccccc", 2) %> + + <% include( '/elements/tr-input-text.html', + 'field' => 'name', + 'label' => 'Name', + 'required' => 1, + 'id' => 'upload_form_name', + ) + %> + + <% include( '/elements/tr-select-agent.html', + 'label' => "Agent", + 'empty_label' => 'Select agent', + 'agent_virt' => 1, + 'agent_null_right' => 'Edit global templates', + ) + %> + + <% include( '/elements/tr-file-upload.html', + 'field' => 'file', + 'label' => 'File', + ) + %> + + + + + + + + + + + +% } #if canedit + + + +
+

<% emt('Image Preview') %>

+ (<% emt('Loading image...') %>) + +
+ + +
+ + + +<%init> +my %opt = @_; + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right([ 'View templates', 'View global templates', + 'Edit templates', 'Edit global templates', ]); + +my $canedit = $curuser->access_right([ 'Edit templates', 'Edit global templates' ]); + + diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html index 47e6a5b48..8ac44afc1 100644 --- a/httemplate/misc/email-customers.html +++ b/httemplate/misc/email-customers.html @@ -36,7 +36,7 @@ should be used to set msgnum or from/subject/html_body cgi params % } -
+ %# Mixing search params with from address, subject, etc. required special-case %# handling of those, risked name conflicts, and caused massive problems with diff --git a/httemplate/misc/process/template_image-delete.cgi b/httemplate/misc/process/template_image-delete.cgi new file mode 100644 index 000000000..58c3f2c68 --- /dev/null +++ b/httemplate/misc/process/template_image-delete.cgi @@ -0,0 +1,28 @@ +<% $server->process %> + +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +# make sure user can generally edit +die "access denied" + unless $curuser->access_right([ 'Edit templates', 'Edit global templates' ]); + +# make sure user can edit this particular image +my %arg = $cgi->param('arg'); +my $imgnum = $arg{'imgnum'}; +die "bad imgnum" unless $imgnum =~ /^\d+$/; +die "access denied" unless qsearchs({ + 'table' => 'template_image', + 'select' => 'imgnum', + 'hashref' => { 'imgnum' => $imgnum }, + 'extra_sql' => ' AND ' . + $curuser->agentnums_sql( + 'null_right' => ['Edit global templates'] + ), + }); + +my $server = + new FS::UI::Web::JSRPC 'FS::template_image::process_image_delete', $cgi; + + diff --git a/httemplate/misc/process/template_image-upload.cgi b/httemplate/misc/process/template_image-upload.cgi new file mode 100644 index 000000000..c3c905981 --- /dev/null +++ b/httemplate/misc/process/template_image-upload.cgi @@ -0,0 +1,26 @@ +<% $server->process %> + +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right([ 'Edit templates', 'Edit global templates' ]); + +my %arg = $cgi->param('arg'); +my $agentnum = $arg{'agentnum'}; + +if (!$agentnum) { + die "access denied" + unless $curuser->access_right([ 'Edit global templates' ]); +} else { + die "bad agentnum" + unless $agentnum =~ /^\d+$/; + die "access denied" + unless $curuser->agentnum($agentnum); +} + +my $server = + new FS::UI::Web::JSRPC 'FS::template_image::process_image_upload', $cgi; + + diff --git a/httemplate/misc/xmlhttp-template_image.cgi b/httemplate/misc/xmlhttp-template_image.cgi new file mode 100644 index 000000000..a8c50edf0 --- /dev/null +++ b/httemplate/misc/xmlhttp-template_image.cgi @@ -0,0 +1,48 @@ +<%doc> +Returns JSON encoded array of objects with details about FS::template_image +objects. Attributes in each returned object are imgnum, name, and src. + +Accepts the following options: + +imgnum - only return object for this imgnum + +no_src - do not include the src field + + +<% encode_json(\@result) %>\ +<%init> +use FS::template_image; + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right([ 'View templates', 'View global templates', + 'Edit templates', 'Edit global templates', ]); + +my %arg = $cgi->param('arg'); + +my $search = { + 'table' => 'template_image', + 'hashref' => {}, +}; + +my $imgnum = $arg{'imgnum'} || ''; +die "Bad imgnum" unless $imgnum =~ /^\d*$/; +$search->{'hashref'}->{'imgnum'} = $imgnum if $imgnum; + +$search->{'select'} = 'imgnum, name' if $arg{'no_src'}; + +$search->{'extra_sql'} = ($imgnum ? ' AND ' : ' WHERE ') + . $curuser->agentnums_sql( + 'null_right' => ['View global templates','Edit global templates'] + ); + +my @images = qsearch($search); #needs agent virtualization + +my @result = map { +{ + 'imgnum' => $_->imgnum, + 'name' => $_->name, + 'src' => $arg{'no_src'} ? '' : $_->src, +} } @images; + + -- cgit v1.2.1 From feba5016425b52740c29653383343d0d1887a592 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 17 Aug 2015 22:22:02 -0700 Subject: display dates as real dates in Excel export, #23121, update for current RT --- rt/lib/RT/Interface/Web_Vendor.pm | 5 +++-- rt/share/html/Elements/ShowCustomFieldDate | 6 +++++- rt/share/html/Elements/ShowCustomFieldDateTime | 6 +++++- rt/share/html/Search/Elements/ResultsStructuredView | 2 -- rt/share/html/Search/Results.xls | 5 ++--- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/rt/lib/RT/Interface/Web_Vendor.pm b/rt/lib/RT/Interface/Web_Vendor.pm index c9bff6f36..ea3a498d6 100644 --- a/rt/lib/RT/Interface/Web_Vendor.pm +++ b/rt/lib/RT/Interface/Web_Vendor.pm @@ -423,12 +423,13 @@ sub ProcessColumnMapValue { my $value = shift; my %args = ( Arguments => [], Escape => 1, - FormatDate => \&default_FormatDate, @_ ); + my $FormatDate = $m->notes('FormatDate') || \&default_FormatDate; + if ( ref $value ) { if ( ref $value eq 'RT::Date' ) { - return $args{FormatDate}->($value); + return $FormatDate->($value); } elsif ( UNIVERSAL::isa( $value, 'CODE' ) ) { my @tmp = $value->( @{ $args{'Arguments'} } ); return ProcessColumnMapValue( ( @tmp > 1 ? \@tmp : $tmp[0] ), %args ); diff --git a/rt/share/html/Elements/ShowCustomFieldDate b/rt/share/html/Elements/ShowCustomFieldDate index 92ab7679a..1536935c5 100644 --- a/rt/share/html/Elements/ShowCustomFieldDate +++ b/rt/share/html/Elements/ShowCustomFieldDate @@ -49,7 +49,11 @@ my $content = $Object->Content; my $DateObj = RT::Date->new ( $session{'CurrentUser'} ); $DateObj->Set( Format => 'unknown', Value => $content, Timezone => 'utc' ); - $content = $DateObj->AsString(Time => 0, Timezone => 'utc'); + if ($m->notes('FormatDate')) { + $content = $m->notes('FormatDate')->($DateObj); + } else { + $content = $DateObj->AsString(Time => 0, Timezone => 'utc'); + } <%$content|n%> <%ARGS> diff --git a/rt/share/html/Elements/ShowCustomFieldDateTime b/rt/share/html/Elements/ShowCustomFieldDateTime index 2ba873aad..e179d6a4b 100644 --- a/rt/share/html/Elements/ShowCustomFieldDateTime +++ b/rt/share/html/Elements/ShowCustomFieldDateTime @@ -49,7 +49,11 @@ my $content = $Object->Content; my $DateObj = RT::Date->new ( $session{'CurrentUser'} ); $DateObj->Set( Format => 'ISO', Value => $content ); - $content = $DateObj->AsString; + if ($m->notes('FormatDate')) { + $content = $m->notes('FormatDate')->($DateObj); + } else { + $content = $DateObj->AsString; + } <%$content|n%> <%ARGS> diff --git a/rt/share/html/Search/Elements/ResultsStructuredView b/rt/share/html/Search/Elements/ResultsStructuredView index 0e9457c45..5b9db4ee1 100644 --- a/rt/share/html/Search/Elements/ResultsStructuredView +++ b/rt/share/html/Search/Elements/ResultsStructuredView @@ -54,7 +54,6 @@ $Format => undef #Callbacks $WriteHeader => sub { $RT::Logger->error('WriteHeader callback required'); '' } $WriteRow => sub { $RT::Logger->error('WriteRow callback required'); '' } -$FormatDate => sub { $_[0]->AsString } <%INIT> @@ -146,7 +145,6 @@ while ( my $Ticket = $Tickets->Next()) { push @out, ProcessColumnMapValue( $ColumnMap->{$col}{'value'}, Arguments => [ $Ticket, $row ], - FormatDate => $FormatDate, ); } #foreach $subcol $value = join('', '', @out, ''); diff --git a/rt/share/html/Search/Results.xls b/rt/share/html/Search/Results.xls index 8b94e22ba..d9d83568c 100644 --- a/rt/share/html/Search/Results.xls +++ b/rt/share/html/Search/Results.xls @@ -118,11 +118,11 @@ my $WriteRow = sub { $row++; }; -my $FormatDate = sub { +$m->notes('FormatDate', sub { my $DateObj = shift; return '' if $DateObj->Unix == 0; return time2str('%Y-%m-%dT%H:%M', $DateObj->Unix); -}; +}); # Write everything to the worksheet $m->comp('Elements/ResultsStructuredView', @@ -132,7 +132,6 @@ $m->comp('Elements/ResultsStructuredView', Format => $Format, WriteHeader => $WriteHeader, WriteRow => $WriteRow, - FormatDate => $FormatDate, ); # Set column widths -- cgit v1.2.1 From 9bbc67e3460dc0045df5262e89c662104e4edd9a Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 19 Aug 2015 17:07:55 -0700 Subject: make discount-show-always work correctly when an invoice has more than one package charge, #32545, fallout from #10481 --- FS/FS/cust_main/Billing.pm | 70 +++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 0bc0fbd39..5c10c639a 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -883,53 +883,53 @@ sub bill { sub _omit_zero_value_bundles { my @in = @_; - my @cust_bill_pkg = (); - my @cust_bill_pkg_bundle = (); - my $sum = 0; - my $discount_show_always = 0; + my @out = (); + my @bundle = (); + my $discount_show_always = $conf->exists('discount-show-always'); + my $show_this = 0; + + # this is a pack-and-deliver pattern. every time there's a cust_bill_pkg + # _without_ pkgpart_override, that's the start of the new bundle. if there's + # an existing bundle, and it contains a nonzero amount (or a zero amount + # that's displayable anyway), push all line items in the bundle. foreach my $cust_bill_pkg ( @in ) { - $discount_show_always = ($cust_bill_pkg->get('discounts') - && scalar(@{$cust_bill_pkg->get('discounts')}) - && $conf->exists('discount-show-always')); - - warn " pkgnum ". $cust_bill_pkg->pkgnum. " sum $sum, ". - "setup_show_zero ". $cust_bill_pkg->setup_show_zero. - "recur_show_zero ". $cust_bill_pkg->recur_show_zero. "\n" - if $DEBUG > 0; - - if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) { - push @cust_bill_pkg, @cust_bill_pkg_bundle - if $sum > 0 - || ($sum == 0 && ( $discount_show_always - || grep {$_->recur_show_zero || $_->setup_show_zero} - @cust_bill_pkg_bundle - ) - ); - @cust_bill_pkg_bundle = (); - $sum = 0; + if (scalar(@bundle) and !$cust_bill_pkg->pkgpart_override) { + # ship out this bundle and reset it + if ( $show_this ) { + push @out, @bundle; + } + @bundle = (); + $show_this = 0; } - $sum += $cust_bill_pkg->setup + $cust_bill_pkg->recur; - push @cust_bill_pkg_bundle, $cust_bill_pkg; + # add this item to the current bundle + push @bundle, $cust_bill_pkg; + # determine if it makes the bundle displayable + if ( $cust_bill_pkg->setup > 0 + or $cust_bill_pkg->recur > 0 + or $cust_bill_pkg->setup_show_zero + or $cust_bill_pkg->recur_show_zero + or ($discount_show_always + and scalar(@{ $cust_bill_pkg->get('discounts')}) + ) + ) { + $show_this++; + } } - push @cust_bill_pkg, @cust_bill_pkg_bundle - if $sum > 0 - || ($sum == 0 && ( $discount_show_always - || grep {$_->recur_show_zero || $_->setup_show_zero} - @cust_bill_pkg_bundle - ) - ); + # last bundle + if ( $show_this) { + push @out, @bundle; + } warn " _omit_zero_value_bundles: ". scalar(@in). - '->'. scalar(@cust_bill_pkg). "\n" #. Dumper(@cust_bill_pkg). "\n" + '->'. scalar(@out). "\n" #. Dumper(@out). "\n" if $DEBUG > 2; - (@cust_bill_pkg); - + @out; } sub _make_lines { -- cgit v1.2.1 From 2dddd8e1742bf2e8ebe9f2d3e560bc78bba95cff Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Thu, 20 Aug 2015 01:42:15 -0500 Subject: RT#14829: automatic payments triggered by bill now show up as Payment by fs_queue --- FS/FS/Schema.pm | 4 ++++ FS/FS/queue.pm | 21 +++++++++++++++++++++ FS/bin/freeside-queued | 5 +++++ httemplate/search/queue.html | 6 ++++++ 4 files changed, 36 insertions(+) diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 55dc99eca..a799ceebe 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -4333,6 +4333,7 @@ sub tables_hashref { 'custnum', 'int', 'NULL', '', '', '', 'secure', 'char', 'NULL', 1, '', '', 'priority', 'int', 'NULL', '', '', '', + 'usernum', 'int', 'NULL', '', '', '', ], 'primary_key' => 'jobnum', 'unique' => [], @@ -4346,6 +4347,9 @@ sub tables_hashref { { columns => [ 'custnum' ], table => 'cust_main', }, + { columns => [ 'usernum' ], + table => 'access_user', + }, ], }, diff --git a/FS/FS/queue.pm b/FS/FS/queue.pm index 1b52ac4fc..f7f09485d 100644 --- a/FS/FS/queue.pm +++ b/FS/FS/queue.pm @@ -97,6 +97,10 @@ Optional link to customer (see L). Secure flag, 'Y' indicates that when using encryption, the job needs to be run on a machine with the private key. +=item usernum + +For access_user that created the job + =cut =back @@ -151,6 +155,8 @@ sub insert { $self->custnum( $args{'custnum'} ) if $args{'custnum'}; + $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum; + my $error = $self->SUPER::insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -235,6 +241,7 @@ sub check { || $self->ut_enum('status',['', qw( new locked failed done )]) || $self->ut_anything('statustext') || $self->ut_numbern('svcnum') + || $self->ut_foreign_keyn('usernum', 'access_user', 'usernum') ; return $error if $error; @@ -357,6 +364,20 @@ sub update_statustext { #''; } +=item access_user + +Returns FS::access_user object (if any) associated with this user. + +Returns nothing if not found. + +=cut + +sub access_user { + my $self = shift; + my $usernum = $self->usernum || return (); + return qsearchs('access_user',{ 'usernum' => $usernum }) || (); +} + =back =head1 SUBROUTINES diff --git a/FS/bin/freeside-queued b/FS/bin/freeside-queued index 7c4cf1b64..398b03d12 100644 --- a/FS/bin/freeside-queued +++ b/FS/bin/freeside-queued @@ -218,8 +218,13 @@ while (1) { # don't put @args in the log, may expose passwords $log->info('starting job ('.$ljob->job.')'); warn 'running "&'. $ljob->job. '('. join(', ', @args). ")\n" if $DEBUG; + # switch user only if a job user is available + my $oldCurrentUser = $FS::CurrentUser::CurrentUser; + my $jobuser = $ljob->access_user; + local $FS::CurrentUser::CurrentUser = $jobuser if $jobuser; local $FS::UID::AutoCommit = 0; # so that we can clean up failures eval $eval; #throw away return value? suppose so + $FS::CurrentUser::CurrentUser = $oldCurrentUser if $jobuser; if ( $@ ) { dbh->rollback; my %hash = $ljob->hash; diff --git a/httemplate/search/queue.html b/httemplate/search/queue.html index 141c535da..22032b13c 100644 --- a/httemplate/search/queue.html +++ b/httemplate/search/queue.html @@ -13,6 +13,7 @@ 'Date', 'Status', 'Account', # unless $hashref->{'svcnum'} + 'Employee', '', # checkbox column ], 'fields' => [ @@ -76,6 +77,11 @@ ''; } }, + sub { + my $queue = shift; + my $access_user = $queue->access_user; + return $access_user ? $access_user->username : ''; + }, sub { my $queue = shift; my $jobnum = $queue->jobnum; -- cgit v1.2.1 From ed3c8f7e9284bcfafd37c8c693084ab12f8f9f40 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Mon, 24 Aug 2015 17:23:33 -0500 Subject: RT#14829: automatic payments triggered by bill now show up as Payment by fs_queue [fixed local CurrentUser] --- FS/FS/queue.pm | 27 ++++++++++++++------------- FS/bin/freeside-queued | 11 +++++------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/FS/FS/queue.pm b/FS/FS/queue.pm index f7f09485d..67d124d02 100644 --- a/FS/FS/queue.pm +++ b/FS/FS/queue.pm @@ -364,19 +364,20 @@ sub update_statustext { #''; } -=item access_user - -Returns FS::access_user object (if any) associated with this user. - -Returns nothing if not found. - -=cut - -sub access_user { - my $self = shift; - my $usernum = $self->usernum || return (); - return qsearchs('access_user',{ 'usernum' => $usernum }) || (); -} +# not needed in 4 +#=item access_user +# +#Returns FS::access_user object (if any) associated with this user. +# +#Returns nothing if not found. +# +#=cut +# +#sub access_user { +# my $self = shift; +# my $usernum = $self->usernum || return (); +# return qsearchs('access_user',{ 'usernum' => $usernum }) || (); +#} =back diff --git a/FS/bin/freeside-queued b/FS/bin/freeside-queued index 398b03d12..36871b295 100644 --- a/FS/bin/freeside-queued +++ b/FS/bin/freeside-queued @@ -218,13 +218,12 @@ while (1) { # don't put @args in the log, may expose passwords $log->info('starting job ('.$ljob->job.')'); warn 'running "&'. $ljob->job. '('. join(', ', @args). ")\n" if $DEBUG; - # switch user only if a job user is available - my $oldCurrentUser = $FS::CurrentUser::CurrentUser; - my $jobuser = $ljob->access_user; - local $FS::CurrentUser::CurrentUser = $jobuser if $jobuser; local $FS::UID::AutoCommit = 0; # so that we can clean up failures - eval $eval; #throw away return value? suppose so - $FS::CurrentUser::CurrentUser = $oldCurrentUser if $jobuser; + do { + # switch user only if a job user is available + local $FS::CurrentUser::CurrentUser = $ljob->access_user || $FS::CurrentUser::CurrentUser; + eval $eval; #throw away return value? suppose so + }; if ( $@ ) { dbh->rollback; my %hash = $ljob->hash; -- cgit v1.2.1 From 00e05a457f164bb5ae1734fbbff09aa00ee25d6a Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 24 Aug 2015 16:10:49 -0700 Subject: fix display of setup fee discounts when recurring fee is zero, #32545 --- FS/FS/Template_Mixin.pm | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index 757701aa8..e9b60a86c 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -3010,7 +3010,9 @@ sub _items_cust_bill_pkg { } my $summary_page = $opt{summary_page} || ''; #unused my $multisection = defined($category) || defined($locationnum); - my $discount_show_always = 0; + # this variable is the value of the config setting, not whether it applies + # to this particular line item. + my $discount_show_always = $conf->exists('discount-show-always'); my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40; @@ -3050,11 +3052,13 @@ sub _items_cust_bill_pkg { if (exists($_->{unit_amount})) { $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ); } - push @b, { %$_ } - if $_->{amount} != 0 - || $discount_show_always - || ( ! $_->{_is_setup} && $_->{recur_show_zero} ) - || ( $_->{_is_setup} && $_->{setup_show_zero} ) + push @b, { %$_ }; + # we already decided to create this display line; don't reconsider it + # now. + # if $_->{amount} != 0 + # || $discount_show_always + # || ( ! $_->{_is_setup} && $_->{recur_show_zero} ) + # || ( $_->{_is_setup} && $_->{setup_show_zero} ) ; $_ = undef; } @@ -3181,6 +3185,7 @@ sub _items_cust_bill_pkg { if ( (!$type || $type eq 'S') && ( $cust_bill_pkg->setup != 0 || $cust_bill_pkg->setup_show_zero + || ($discount_show_always and $cust_bill_pkg->unitsetup > 0) ) ) { @@ -3188,10 +3193,12 @@ sub _items_cust_bill_pkg { warn "$me _items_cust_bill_pkg adding setup\n" if $DEBUG > 1; + # append the word 'Setup' to the setup line if there's going to be + # a recur line for the same package (i.e. not a one-time charge) my $description = $desc; $description .= ' Setup' if $cust_bill_pkg->recur != 0 - || $discount_show_always + || ($discount_show_always and $cust_bill_pkg->unitrecur > 0) || $cust_bill_pkg->recur_show_zero; $description .= $cust_bill_pkg->time_period_pretty( $part_pkg, @@ -3255,11 +3262,18 @@ sub _items_cust_bill_pkg { } + # should we show a recur line? + # if type eq 'S', then NO, because we've been told not to. + # otherwise, show the recur line if: + # - there's a recurring charge + # - or recur_show_zero is on + # - or there's a positive unitrecur (so it's been discounted to zero) + # and discount-show-always is on if ( ( !$type || $type eq 'R' || $type eq 'U' ) && ( $cust_bill_pkg->recur != 0 - || $cust_bill_pkg->setup == 0 - || $discount_show_always + || !defined($s) + || ($discount_show_always and $cust_bill_pkg->unitrecur > 0) || $cust_bill_pkg->recur_show_zero ) ) @@ -3501,9 +3515,6 @@ sub _items_cust_bill_pkg { } # foreach $display - $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount - && $conf->exists('discount-show-always')); - } foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) { @@ -3515,11 +3526,11 @@ sub _items_cust_bill_pkg { $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ); } - push @b, { %$_ } - if $_->{amount} != 0 - || $discount_show_always - || ( ! $_->{_is_setup} && $_->{recur_show_zero} ) - || ( $_->{_is_setup} && $_->{setup_show_zero} ) + push @b, { %$_ }; + #if $_->{amount} != 0 + # || $discount_show_always + # || ( ! $_->{_is_setup} && $_->{recur_show_zero} ) + # || ( $_->{_is_setup} && $_->{setup_show_zero} ) } } -- cgit v1.2.1 From ed673c4be67ad4d3b549df3b5f20fe5d76d6e944 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Mon, 24 Aug 2015 19:28:54 -0500 Subject: RT#18361: Delay package from billing until services are provisioned [text change] --- httemplate/elements/template_image-dialog.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httemplate/elements/template_image-dialog.html b/httemplate/elements/template_image-dialog.html index 5691d52b5..f7fb0c291 100644 --- a/httemplate/elements/template_image-dialog.html +++ b/httemplate/elements/template_image-dialog.html @@ -94,7 +94,7 @@ url - to redirect to after upload, otherwise just refreshes dialog window <% include( '/elements/tr-select-agent.html', 'label' => "Agent", - 'empty_label' => 'Select agent', + 'empty_label' => '(global)', 'agent_virt' => 1, 'agent_null_right' => 'Edit global templates', ) -- cgit v1.2.1 From 33b89d345f9f3f687c958056aeb85471f7f4c8f5 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 25 Aug 2015 01:17:24 -0500 Subject: RT#18361: Delay package from billing until services are provisioned [start_on_hold toggles checkboxes display] --- httemplate/elements/tr-pkg_svc.html | 39 +++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/httemplate/elements/tr-pkg_svc.html b/httemplate/elements/tr-pkg_svc.html index cfef51ce2..1e9c0a38b 100644 --- a/httemplate/elements/tr-pkg_svc.html +++ b/httemplate/elements/tr-pkg_svc.html @@ -4,6 +4,12 @@ <% itable('', 4, 1) %> <% $thead %> + + %foreach my $part_svc ( @part_svc ) { % my $svcpart = $part_svc->svcpart; % my $pkg_svc = $pkg_svc{$svcpart} @@ -78,9 +84,13 @@ > - - > + + > + % foreach ( 1 .. $columns-1 ) { @@ -92,10 +102,31 @@ % } % $count++; % -% } +% } # foreach $part_svc + + % if ( scalar(@possible_exports) > 0 || scalar(@mapped_exports) > 0 ) { @@ -137,7 +168,7 @@ my $thead = "\n\n". ntable('#cccccc', 2). ''. ''. ''. - ''. + ''. ''; my $part_pkg = $opt{'object'}; -- cgit v1.2.1 From e6796fcb87b17c937eacfacacd933da7bc5f0996 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 25 Aug 2015 01:40:21 -0700 Subject: RBC download script: option to avoid closing the batch, #35228 --- FS/FS/pay_batch.pm | 40 ++++++++++++++++++++++------------------ FS/bin/freeside-rbc-download | 9 ++++++--- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm index df969a00f..2a522b46e 100644 --- a/FS/FS/pay_batch.pm +++ b/FS/FS/pay_batch.pm @@ -222,6 +222,8 @@ I - an L module I - an L object for a batch gateway. This takes precedence over I. +I - do not try to close batches + Supported format keys (defined in the specified FS::pay_batch module) are: I - required, can be CSV, fixed, variable, XML @@ -456,26 +458,28 @@ sub import_results { } # foreach (@all_values) # decide whether to close batches that had payments posted - foreach my $batchnum (keys %target_batches) { - my $pay_batch = FS::pay_batch->by_key($batchnum); - my $close = 1; - if ( defined($close_condition) ) { - # Allow the module to decide whether to close the batch. - # $close_condition can also die() to abort the whole import. - $close = eval { $close_condition->($pay_batch) }; - if ( $@ ) { - $dbh->rollback; - die $@; + if ( !$param->{no_close} ) { + foreach my $batchnum (keys %target_batches) { + my $pay_batch = FS::pay_batch->by_key($batchnum); + my $close = 1; + if ( defined($close_condition) ) { + # Allow the module to decide whether to close the batch. + # $close_condition can also die() to abort the whole import. + $close = eval { $close_condition->($pay_batch) }; + if ( $@ ) { + $dbh->rollback; + die $@; + } } - } - if ( $close ) { - my $error = $pay_batch->set_status('R'); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; + if ( $close ) { + my $error = $pay_batch->set_status('R'); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } } - } - } + } # foreach $batchnum + } # if (!$param->{no_close}) $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; diff --git a/FS/bin/freeside-rbc-download b/FS/bin/freeside-rbc-download index 376b839e1..3f692fa0f 100755 --- a/FS/bin/freeside-rbc-download +++ b/FS/bin/freeside-rbc-download @@ -10,13 +10,13 @@ use FS::Record qw(qsearch qsearchs); use FS::pay_batch; use FS::Conf; -use vars qw( $opt_v $opt_a $opt_f ); -getopts('va:f:'); +use vars qw( $opt_v $opt_a $opt_f $opt_n ); +getopts('va:f:n'); #$Net::SFTP::Foreign::debug = -1; sub usage { " Usage: - freeside-rbc-download [ -v ] [ -a archivedir ] [ -f filename ] user\n + freeside-rbc-download [ -v ] [ -n ] [ -a archivedir ] [ -f filename ] user\n " } sub debug { @@ -102,6 +102,7 @@ for my $dir ( $ftp->nlst ) { my $error = FS::pay_batch->import_results( filehandle => $fh, format => 'RBC', + no_close => ($opt_n ? 1 : 0), ); if ( $error ) { @@ -146,6 +147,8 @@ matching the pattern. This can be used to reprocess a specific file. -a directory: Archive the files in the specified directory. +-n: Do not try to close batches after applying results. + user: freeside username =head1 BUGS -- cgit v1.2.1 From 9e4ef67fa35a301ba23a8f6107e12b7db33f83c8 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 25 Aug 2015 09:25:37 -0700 Subject: warning about param in list context (in a substitution?) --- httemplate/misc/void-cust_bill.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httemplate/misc/void-cust_bill.html b/httemplate/misc/void-cust_bill.html index 1608fd051..39b071229 100644 --- a/httemplate/misc/void-cust_bill.html +++ b/httemplate/misc/void-cust_bill.html @@ -14,7 +14,7 @@ <% ntable("#cccccc", 2) %> - +
ExportServiceHide
from
Invoices
Bulk
Charge
Hold
Until
Provision
Remove Hold After Provisioning
Reason
-- cgit v1.2.1 From f4fc0bd2f813272ed1a878dd9f130fe155a6e3ff Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 25 Aug 2015 09:32:05 -0700 Subject: param in list context --- httemplate/misc/process/void-cust_bill.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httemplate/misc/process/void-cust_bill.html b/httemplate/misc/process/void-cust_bill.html index accee27fd..7773b0ba9 100755 --- a/httemplate/misc/process/void-cust_bill.html +++ b/httemplate/misc/process/void-cust_bill.html @@ -21,6 +21,6 @@ my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum}); my $custnum = $cust_bill->custnum; -my $error = $cust_bill->void( $cgi->param('reason') ); +my $error = $cust_bill->void( scalar($cgi->param('reason')) ); -- cgit v1.2.1 From c13a7adf63cc830d092bbf4a8e9bda2aa3beee56 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 25 Aug 2015 21:25:15 -0500 Subject: RT#18361: Delay package from billing until services are provisioned [bug fix to javascript] --- httemplate/elements/tr-pkg_svc.html | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/httemplate/elements/tr-pkg_svc.html b/httemplate/elements/tr-pkg_svc.html index 1e9c0a38b..7ac67b7ec 100644 --- a/httemplate/elements/tr-pkg_svc.html +++ b/httemplate/elements/tr-pkg_svc.html @@ -1,15 +1,14 @@ -<% itable('', 4, 1) %> -<% $thead %> - +<% itable('', 4, 1) %> +<% pkg_svc_thead() %> + %foreach my $part_svc ( @part_svc ) { % my $svcpart = $part_svc->svcpart; % my $pkg_svc = $pkg_svc{$svcpart} @@ -97,7 +96,7 @@ provision_hold_input.push(document.getElementById('input_provision_hold<% $svcpa % if ( $count == int( $_ * scalar(@part_svc) / $columns ) ) { % - <% $thead %> + <% pkg_svc_thead() %> % } % } % $count++; @@ -161,15 +160,21 @@ provision_hold_init(); my %opt = @_; my $cgi = $opt{'cgi'}; -my $thead = "\n\n". ntable('#cccccc', 2). - ''. - 'Quan.'. - 'Primary'. - 'Service'. - 'Hide
from
Invoices
'. - 'Bulk
Charge
'. - 'Remove Hold After Provisioning'. - ''; +my $thead_count = 0; +sub pkg_svc_thead { + $thead_count += 1; + return "\n\n". ntable('#cccccc', 2). + ''. + 'Quan.'. + 'Primary'. + 'Service'. + 'Hide
from
Invoices
'. + 'Bulk
Charge
'. + 'Remove Hold After Provisioning'. + ''. + qq!!; +; +} my $part_pkg = $opt{'object'}; my $pkgpart = $part_pkg->pkgpart; -- cgit v1.2.1 From dd0de30cf562e4e31359a9d9108fec974ecb4299 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 25 Aug 2015 21:46:01 -0500 Subject: RT#18361: Delay package from billing until services are provisioned [bug fix to javascript] --- httemplate/elements/tr-pkg_svc.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/httemplate/elements/tr-pkg_svc.html b/httemplate/elements/tr-pkg_svc.html index 7ac67b7ec..de3f95a3a 100644 --- a/httemplate/elements/tr-pkg_svc.html +++ b/httemplate/elements/tr-pkg_svc.html @@ -114,6 +114,9 @@ function provision_hold_check () { for (i = 0; i < provision_hold_td.length; i++) { provision_hold_td[i].style.display = start_on_hold.checked ? '' : 'none'; } + for (i = 0; i < provision_hold_input.length; i++) { + provision_hold_input[i].disabled = start_on_hold.checked ? false : true; + } } } function provision_hold_init () { -- cgit v1.2.1 From ca501bda179434c87d9150780a80d3d64b68e358 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Wed, 26 Aug 2015 15:17:02 -0400 Subject: Ticket #37472 Import calls with Internal ID 50 --- FS/FS/cdr/aapt.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/cdr/aapt.pm b/FS/FS/cdr/aapt.pm index 600a1920f..3c4964317 100644 --- a/FS/FS/cdr/aapt.pm +++ b/FS/FS/cdr/aapt.pm @@ -77,7 +77,7 @@ my %UNIT_SCALE = ( #Table 2.1.4 'calltypenum', # usage ID (CUSG) sub { # ID type my ($cdr, $data, $conf, $param) = @_; - if ($data != 1) { + if ($data !~ /(1|50)/) { warn "AAPT: service ID type is not telephone number.\n"; $param->{skiprow} = 1; } -- cgit v1.2.1 From 33f1c704766af0621159d5a8453379b6706d8c8a Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 27 Aug 2015 14:46:31 -0700 Subject: external message services: core refactoring of msg_template --- FS/FS/Schema.pm | 23 ++ FS/FS/cust_msg.pm | 19 +- FS/FS/msg_template.pm | 331 +++------------- FS/FS/msg_template/email.pm | 911 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1014 insertions(+), 270 deletions(-) create mode 100644 FS/FS/msg_template/email.pm diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index a799ceebe..311313a4e 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -6320,8 +6320,11 @@ sub tables_hashref { 'mime_type', 'varchar', '', $char_d, '', '', 'body', 'blob', 'NULL', '', '', '', 'disabled', 'char', 'NULL', 1, '', '', + # migrate these to msg_template_email 'from_addr', 'varchar', 'NULL', 255, '', '', 'bcc_addr', 'varchar', 'NULL', 255, '', '', + # change to not null on v5 + 'msgclass', 'varchar', 'NULL', 16, '', '', ], 'primary_key' => 'msgnum', 'unique' => [ ], @@ -6333,6 +6336,26 @@ sub tables_hashref { ], }, + 'msg_template_http' => { + 'columns' => [ + 'num', 'serial', '', '', '', '', + 'msgnum', 'int', '', '', '', '', + 'prepare_url', 'varchar', 'NULL', 255, '', '', + 'send_url', 'varchar', 'NULL', 255, '', '', + 'username', 'varchar', 'NULL', $char_d, '', '', + 'password', 'varchar', 'NULL', $char_d, '', '', + 'content', 'text', 'NULL', '', '', '', + ], + 'primary_key' => 'num', + 'unique' => [ [ 'msgnum' ], ], + 'index' => [ ], + 'foreign_keys' => [ + { columns => [ 'msgnum' ], + table => 'msg_template', + }, + ], + }, + 'template_content' => { 'columns' => [ 'contentnum', 'serial', '', '', '', '', diff --git a/FS/FS/cust_msg.pm b/FS/FS/cust_msg.pm index 72f64b9c5..934632725 100644 --- a/FS/FS/cust_msg.pm +++ b/FS/FS/cust_msg.pm @@ -45,7 +45,7 @@ from FS::Record. The following fields are currently supported: =item header - message header -=item body - message body +=item body - message body (as a complete MIME document) =item error - Email::Sender error message (or null for success) @@ -150,10 +150,27 @@ sub check { $self->SUPER::check; } +=item send + +Sends the message through its parent L. Returns an error +message on error, or an empty string. + +=cut + +sub send { + my $self = shift; + my $msg_template = $self->msg_template + or return 'message was created without a template object'; + $msg_template->send_prepared($self); +} + =item entity Returns the complete message as a L. +XXX this only works if the message in fact contains a MIME entity. Messages +created by external APIs may not look like that. + =item parts Returns a list of the MIME parts contained in the message, as L diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index c52b6336e..180e9de4d 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -4,22 +4,9 @@ use base qw( FS::Record ); use strict; use vars qw( $DEBUG $conf ); -use Date::Format qw( time2str ); -use File::Temp; -use IPC::Run qw(run); -use Text::Template; - -use HTML::Entities qw( decode_entities encode_entities ) ; -use HTML::FormatText; -use HTML::TreeBuilder; -use Encode; - -use FS::Misc qw( generate_email send_email do_print ); use FS::Conf; use FS::Record qw( qsearch qsearchs ); -use FS::UID qw( dbh ); -use FS::cust_main; use FS::cust_msg; use FS::template_content; @@ -59,6 +46,9 @@ supported: =item msgname - Name of the template. This will appear in the user interface; if it needs to be localized for some users, add it to the message catalog. +=item msgclass - The L subclass that this should belong to. +Defaults to 'email'. + =item agentnum - Agent associated with this template. Can be NULL for a global template. @@ -66,6 +56,8 @@ global template. =item from_addr - Source email address. +=item bcc_addr - Bcc all mail to this address. + =item disabled - disabled ('Y' or NULL). =back @@ -87,41 +79,20 @@ points to. You can ask the object for a copy with the I method. sub table { 'msg_template'; } +sub _rebless { + my $self = shift; + my $class = 'FS::msg_template::' . $self->msgclass; + eval "use $class;"; + bless($self, $class) unless $@; + $self; +} + =item insert [ CONTENT ] Adds this record to the database. If there is an error, returns the error, otherwise returns false. -A default (no locale) L object will be created. CONTENT -is an optional hash containing 'subject' and 'body' for this object. - -=cut - -sub insert { - my $self = shift; - my %content = @_; - - my $oldAutoCommit = $FS::UID::AutoCommit; - local $FS::UID::AutoCommit = 0; - my $dbh = dbh; - - my $error = $self->SUPER::insert; - if ( !$error ) { - $content{'msgnum'} = $self->msgnum; - $content{'subject'} ||= ''; - $content{'body'} ||= ''; - my $template_content = new FS::template_content (\%content); - $error = $template_content->insert; - } - - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - - $dbh->commit if $oldAutoCommit; - return; -} +# inherited =item delete @@ -129,61 +100,31 @@ Delete this record from the database. =cut -# the delete method can be inherited from FS::Record +# inherited =item replace [ OLD_RECORD ] [ CONTENT ] Replaces the OLD_RECORD with this one in the database. If there is an error, returns the error, otherwise returns false. -CONTENT is an optional hash containing 'subject', 'body', and 'locale'. If -supplied, an L object will be created (or modified, if -one already exists for this locale). - =cut -sub replace { +# inherited + +sub replace_check { my $self = shift; - my $old = ( ref($_[0]) and $_[0]->isa('FS::Record') ) - ? shift - : $self->replace_old; - my %content = @_; - - my $oldAutoCommit = $FS::UID::AutoCommit; - local $FS::UID::AutoCommit = 0; - my $dbh = dbh; - - my $error = $self->SUPER::replace($old); - - if ( !$error and %content ) { - $content{'locale'} ||= ''; - my $new_content = qsearchs('template_content', { - 'msgnum' => $self->msgnum, - 'locale' => $content{'locale'}, - } ); - if ( $new_content ) { - $new_content->subject($content{'subject'}); - $new_content->body($content{'body'}); - $error = $new_content->replace; - } - else { - $content{'msgnum'} = $self->msgnum; - $new_content = new FS::template_content \%content; - $error = $new_content->insert; + my $old = $self->replace_old; + # don't allow changing msgclass, except null to not-null (for upgrade) + if ( $old->msgclass ) { + if ( !$self->msgclass ) { + $self->set('msgclass', $old->msgclass); + } else { + return "Can't change message template class from ".$old->msgclass. + " to ".$self->msgclass."."; } } - - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - - warn "committing FS::msg_template->replace\n" if $DEBUG and $oldAutoCommit; - $dbh->commit if $oldAutoCommit; - return; + ''; } - - =item check @@ -206,6 +147,10 @@ sub check { || $self->ut_textn('mime_type') || $self->ut_enum('disabled', [ '', 'Y' ] ) || $self->ut_textn('from_addr') + || $self->ut_textn('bcc_addr') + # fine for now, but change this to some kind of dynamic check if we + # ever have more than two msgclasses + || $self->ut_enum('msgclass', [ qw(email http) ]), ; return $error if $error; @@ -214,25 +159,10 @@ sub check { $self->SUPER::check; } -=item content_locales - -Returns a hashref of the L objects attached to -this template, with the locale as key. - -=cut - -sub content_locales { - my $self = shift; - return $self->{'_content_locales'} ||= +{ - map { $_->locale , $_ } - qsearch('template_content', { 'msgnum' => $self->msgnum }) - }; -} - =item prepare OPTION => VALUE -Fills in the template and returns a hash of the 'from' address, 'to' -addresses, subject line, and body. +Fills in the template and returns an L object, containing the +message to be sent. This method must be provided by the subclass. Options are passed as a list of name/value pairs: @@ -276,18 +206,23 @@ A hash reference of additional substitutions =cut sub prepare { + die "unimplemented"; +} + +=item prepare_substitutions OPTION => VALUE ... + +Takes the same arguments as L, and returns a hashref of the +substitution variables. + +=cut + +sub prepare_substitutions { my( $self, %opt ) = @_; my $cust_main = $opt{'cust_main'}; # or die 'cust_main required'; my $object = $opt{'object'} or die 'object required'; - # localization - my $locale = $cust_main && $cust_main->locale || ''; - warn "no locale for cust#".$cust_main->custnum."; using default content\n" - if $DEBUG and $cust_main && !$locale; - my $content = $self->content($locale); - - warn "preparing template '".$self->msgname."\n" + warn "preparing substitutions for '".$self->msgname."'\n" if $DEBUG; my $subs = $self->substitutions; @@ -340,110 +275,19 @@ sub prepare { $hash{$_} = $opt{substitutions}->{$_} foreach keys %{$opt{substitutions}}; } - $_ = encode_entities($_ || '') foreach values(%hash); - - ### - # clean up template - ### - my $subject_tmpl = new Text::Template ( - TYPE => 'STRING', - SOURCE => $content->subject, - ); - my $subject = $subject_tmpl->fill_in( HASH => \%hash ); - - my $body = $content->body; - my ($skin, $guts) = eviscerate($body); - @$guts = map { - $_ = decode_entities($_); # turn all punctuation back into itself - s/\r//gs; # remove \r's - s/]*>/\n/gsi; # and
tags - s/

/\n/gsi; # and

- s/<\/p>//gsi; # and

- s/\240/ /gs; # and   - $_ - } @$guts; - - $body = '{ use Date::Format qw(time2str); "" }'; - while(@$skin || @$guts) { - $body .= shift(@$skin) || ''; - $body .= shift(@$guts) || ''; - } - - ### - # fill-in - ### - - my $body_tmpl = new Text::Template ( - TYPE => 'STRING', - SOURCE => $body, - ); - - $body = $body_tmpl->fill_in( HASH => \%hash ); - - ### - # and email - ### - - my @to; - if ( exists($opt{'to'}) ) { - @to = split(/\s*,\s*/, $opt{'to'}); - } elsif ( $cust_main ) { - @to = $cust_main->invoicing_list_emailonly; - } else { - die 'no To: address or cust_main object specified'; - } - - my $from_addr = $self->from_addr; - - if ( !$from_addr ) { - - my $agentnum = $cust_main ? $cust_main->agentnum : ''; - - if ( $opt{'from_config'} ) { - $from_addr = $conf->config($opt{'from_config'}, $agentnum); - } - $from_addr ||= $conf->invoice_from_full($agentnum); - } -# my @cust_msg = (); -# if ( $conf->exists('log_sent_mail') and !$opt{'preview'} ) { -# my $cust_msg = FS::cust_msg->new({ -# 'custnum' => $cust_main->custnum, -# 'msgnum' => $self->msgnum, -# 'status' => 'prepared', -# }); -# $cust_msg->insert; -# @cust_msg = ('cust_msg' => $cust_msg); -# } - - my $text_body = encode('UTF-8', - HTML::FormatText->new(leftmargin => 0, rightmargin => 70) - ->format( HTML::TreeBuilder->new_from_content($body) ) - ); - ( - 'custnum' => ( $cust_main ? $cust_main->custnum : ''), - 'msgnum' => $self->msgnum, - 'from' => $from_addr, - 'to' => \@to, - 'bcc' => $self->bcc_addr || undef, - 'subject' => $subject, - 'html_body' => $body, - 'text_body' => $text_body - ); - + return \%hash; } -=item send OPTION => VALUE +=item send OPTION => VALUE ... -Fills in the template and sends it to the customer. Options are as for -'prepare'. +Creates a message with L (taking all the same options) and sends it. =cut -# broken out from prepare() in case we want to queue the sending, -# preview it, etc. sub send { my $self = shift; - send_email(generate_email($self->prepare(@_))); + my $cust_msg = $self->prepare(@_); + $self->send_prepared($cust_msg); } =item render OPTION => VALUE ... @@ -455,6 +299,9 @@ Options are as for 'prepare', but 'from' and 'to' are meaningless. =cut +# XXX not sure where this ends up post-refactoring--a separate template +# class? it doesn't use the same rendering OR output machinery as ::email + # will also have options to set paper size, margins, etc. sub render { @@ -507,8 +354,6 @@ my $usage_warning = sub { return ['', '', '']; }; -#my $conf = new FS::Conf; - #return contexts and fill-in values # If you add anything, be sure to add a description in # httemplate/edit/msg_template.html. @@ -686,19 +531,11 @@ sub substitutions { =item content LOCALE -Returns the L object appropriate to LOCALE, if there -is one. If not, returns the one with a NULL locale. +Stub, returns nothing. =cut -sub content { - my $self = shift; - my $locale = shift; - qsearchs('template_content', - { 'msgnum' => $self->msgnum, 'locale' => $locale }) || - qsearchs('template_content', - { 'msgnum' => $self->msgnum, 'locale' => '' }); -} +sub content {} =item agent @@ -827,10 +664,16 @@ sub _upgrade_data { } $content{body} = $body; $msg_template->set('body', ''); - my $error = $msg_template->replace(%content); die $error if $error; } + + if ( !$msg_template->msgclass ) { + # set default message class + $msg_template->set('msgclass', 'email'); + my $error = $msg_template->replace; + die $error if $error; + } } ### @@ -863,56 +706,6 @@ sub _populate_initial_data { #class method } -sub eviscerate { - # Every bit as pleasant as it sounds. - # - # We do this because Text::Template::Preprocess doesn't - # actually work. It runs the entire template through - # the preprocessor, instead of the code segments. Which - # is a shame, because Text::Template already contains - # the code to do this operation. - my $body = shift; - my (@outside, @inside); - my $depth = 0; - my $chunk = ''; - while($body || $chunk) { - my ($first, $delim, $rest); - # put all leading non-delimiters into $first - ($first, $rest) = - ($body =~ /^((?:\\[{}]|[^{}])*)(.*)$/s); - $chunk .= $first; - # put a leading delimiter into $delim if there is one - ($delim, $rest) = - ($rest =~ /^([{}]?)(.*)$/s); - - if( $delim eq '{' ) { - $chunk .= '{'; - if( $depth == 0 ) { - push @outside, $chunk; - $chunk = ''; - } - $depth++; - } - elsif( $delim eq '}' ) { - $depth--; - if( $depth == 0 ) { - push @inside, $chunk; - $chunk = ''; - } - $chunk .= '}'; - } - else { - # no more delimiters - if( $depth == 0 ) { - push @outside, $chunk . $rest; - } # else ? something wrong - last; - } - $body = $rest; - } - (\@outside, \@inside); -} - =back =head1 BUGS diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm new file mode 100644 index 000000000..1133faafe --- /dev/null +++ b/FS/FS/msg_template/email.pm @@ -0,0 +1,911 @@ +package FS::msg_template::email; +use base qw( FS::msg_template ); + +use strict; +use vars qw( $DEBUG $conf ); + +# stuff needed for template generation +use Date::Format qw( time2str ); +use File::Temp; +use IPC::Run qw(run); +use Text::Template; + +use HTML::Entities qw( decode_entities encode_entities ) ; +use HTML::FormatText; +use HTML::TreeBuilder; +use Encode; + +# needed to send email +use FS::Misc qw( generate_email ); +use FS::Conf; +use Email::Sender::Simple qw( sendmail ); + +use FS::Record qw( qsearch qsearchs ); + +# needed to manage template_content objects +use FS::template_content; +use FS::UID qw( dbh ); + +use FS::cust_msg; + +FS::UID->install_callback( sub { $conf = new FS::Conf; } ); + +our $DEBUG = 1; +our $me = '[FS::msg_template::email]'; + +=head1 NAME + +FS::msg_template::email - Construct email notices with Text::Template. + +=head1 DESCRIPTION + +FS::msg_template::email is a message processor in which the template contains +L strings for the message subject line and body, and the +message is delivered by email. + +Currently the C and C fields used by this processor are +in the main msg_template table. + +=head1 METHODS + +=over 4 + +=item insert [ CONTENT ] + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +A default (no locale) L object will be created. CONTENT +is an optional hash containing 'subject' and 'body' for this object. + +=cut + +sub insert { + my $self = shift; + my %content = @_; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $error = $self->SUPER::insert; + if ( !$error ) { + $content{'msgnum'} = $self->msgnum; + $content{'subject'} ||= ''; + $content{'body'} ||= ''; + my $template_content = new FS::template_content (\%content); + $error = $template_content->insert; + } + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit if $oldAutoCommit; + return; +} + +=item replace [ OLD_RECORD ] [ CONTENT ] + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +CONTENT is an optional hash containing 'subject', 'body', and 'locale'. If +supplied, an L object will be created (or modified, if +one already exists for this locale). + +=cut + +sub replace { + my $self = shift; + my $old = ( ref($_[0]) and $_[0]->isa('FS::Record') ) + ? shift + : $self->replace_old; + my %content = @_; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $error = $self->SUPER::replace($old); + + if ( !$error and %content ) { + $content{'locale'} ||= ''; + my $new_content = qsearchs('template_content', { + 'msgnum' => $self->msgnum, + 'locale' => $content{'locale'}, + } ); + if ( $new_content ) { + $new_content->subject($content{'subject'}); + $new_content->body($content{'body'}); + $error = $new_content->replace; + } + else { + $content{'msgnum'} = $self->msgnum; + $new_content = new FS::template_content \%content; + $error = $new_content->insert; + } + } + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + warn "committing FS::msg_template->replace\n" if $DEBUG and $oldAutoCommit; + $dbh->commit if $oldAutoCommit; + return; +} + +=item content_locales + +Returns a hashref of the L objects attached to +this template, with the locale as key. + +=cut + +sub content_locales { + my $self = shift; + return $self->{'_content_locales'} ||= +{ + map { $_->locale , $_ } + qsearch('template_content', { 'msgnum' => $self->msgnum }) + }; +} + +=item prepare OPTION => VALUE + +Fills in the template and returns an L object. + +Options are passed as a list of name/value pairs: + +=over 4 + +=item cust_main + +Customer object (required). + +=item object + +Additional context object (currently, can be a cust_main, cust_pkg, +cust_bill, cust_pay, cust_pay_pending, or svc_(acct, phone, broadband, +domain) ). If the object is a svc_*, its cust_pkg will be fetched and +used for substitution. + +As a special case, this may be an arrayref of two objects. Both +objects will be available for substitution, with their field names +prefixed with 'new_' and 'old_' respectively. This is used in the +rt_ticket export when exporting "replace" events. + +=item from_config + +Configuration option to use as the source address, based on the customer's +agentnum. If unspecified (or the named option is empty), 'invoice_from' +will be used. + +The I field in the template takes precedence over this. + +=item to + +Destination address. The default is to use the customer's +invoicing_list addresses. Multiple addresses may be comma-separated. + +=item substitutions + +A hash reference of additional substitutions + +=item msgtype + +A string identifying the kind of message this is. Currently can be "invoice", +"receipt", "admin", or null. Expand this list as necessary. + +=back + +=cut + +sub prepare { + + my( $self, %opt ) = @_; + + my $cust_main = $opt{'cust_main'}; # or die 'cust_main required'; + my $object = $opt{'object'} or die 'object required'; + + my $hashref = $self->prepare_substitutions(%opt); + + # localization + my $locale = $cust_main && $cust_main->locale || ''; + warn "no locale for cust#".$cust_main->custnum."; using default content\n" + if $DEBUG and $cust_main && !$locale; + my $content = $self->content($locale); + + warn "preparing template '".$self->msgname."\n" + if $DEBUG; + + $_ = encode_entities($_ || '') foreach values(%$hashref); + + ### + # clean up template + ### + my $subject_tmpl = new Text::Template ( + TYPE => 'STRING', + SOURCE => $content->subject, + ); + + warn "$me filling in subject template\n" if $DEBUG; + my $subject = $subject_tmpl->fill_in( HASH => $hashref ); + + my $body = $content->body; + my ($skin, $guts) = eviscerate($body); + @$guts = map { + $_ = decode_entities($_); # turn all punctuation back into itself + s/\r//gs; # remove \r's + s/]*>/\n/gsi; # and
tags + s/

/\n/gsi; # and

+ s/<\/p>//gsi; # and

+ s/\240/ /gs; # and   + $_ + } @$guts; + + $body = '{ use Date::Format qw(time2str); "" }'; + while(@$skin || @$guts) { + $body .= shift(@$skin) || ''; + $body .= shift(@$guts) || ''; + } + + ### + # fill-in + ### + + my $body_tmpl = new Text::Template ( + TYPE => 'STRING', + SOURCE => $body, + ); + + warn "$me filling in body template\n" if $DEBUG; + $body = $body_tmpl->fill_in( HASH => $hashref ); + + ### + # and email + ### + + my @to; + if ( exists($opt{'to'}) ) { + @to = split(/\s*,\s*/, $opt{'to'}); + } elsif ( $cust_main ) { + @to = $cust_main->invoicing_list_emailonly; + } else { + die 'no To: address or cust_main object specified'; + } + + my $from_addr = $self->from_addr; + + if ( !$from_addr ) { + + my $agentnum = $cust_main ? $cust_main->agentnum : ''; + + if ( $opt{'from_config'} ) { + $from_addr = $conf->config($opt{'from_config'}, $agentnum); + } + $from_addr ||= $conf->invoice_from_full($agentnum); + } + + my $text_body = encode('UTF-8', + HTML::FormatText->new(leftmargin => 0, rightmargin => 70) + ->format( HTML::TreeBuilder->new_from_content($body) ) + ); + + warn "$me constructing MIME entities\n" if $DEBUG; + my %email = generate_email( + 'from' => $from_addr, + 'to' => \@to, + 'bcc' => $self->bcc_addr || undef, + 'subject' => $subject, + 'html_body' => $body, + 'text_body' => $text_body, + ); + + warn "$me creating message headers\n" if $DEBUG; + my $env_from = $from_addr; + $env_from =~ s/^\s*//; $env_from =~ s/\s*$//; + if ( $env_from =~ /^(.*)\s*<(.*@.*)>$/ ) { + # a common idiom + $env_from = $2; + } + + my $domain; + if ( $env_from =~ /\@([\w\.\-]+)/ ) { + $domain = $1; + } else { + warn 'no domain found in invoice from address '. $env_from . + '; constructing Message-ID (and saying HELO) @example.com'; + $domain = 'example.com'; + } + my $message_id = join('.', rand()*(2**32), $$, time). "\@$domain"; + + my $time = time; + my $message = MIME::Entity->build( + 'From' => $from_addr, + 'To' => join(', ', @to), + 'Sender' => $from_addr, + 'Reply-To' => $from_addr, + 'Date' => time2str("%a, %d %b %Y %X %z", $time), + 'Subject' => Encode::encode('MIME-Header', $subject), + 'Message-ID' => "<$message_id>", + 'Encoding' => '7bit', + 'Type' => 'multipart/related', + ); + + #$message->head->replace('Content-type', + # 'multipart/related; '. + # 'boundary="' . $message->head->multipart_boundary . '"; ' . + # 'type=multipart/alternative' + #); + + # XXX a facility to attach additional parts is necessary at some point + foreach my $part (@{ $email{mimeparts} }) { + warn "$me appending part ".$part->mime_type."\n" if $DEBUG; + $message->add_part( $part ); + } + + # effective To: address (not in headers) + push @to, $self->bcc_addr if $self->bcc_addr; + my $env_to = join(', ', @to); + + my $cust_msg = FS::cust_msg->new({ + 'custnum' => $cust_main->custnum, + 'msgnum' => $self->msgnum, + '_date' => $time, + 'env_from' => $env_from, + 'env_to' => $env_to, + 'header' => $message->header_as_string, + 'body' => $message->body_as_string, + 'error' => '', + 'status' => 'prepared', + 'msgtype' => ($opt{'msgtype'} || ''), + }); + + return $cust_msg; +} + +=item send_prepared CUST_MSG + +Takes the CUST_MSG object and sends it to its recipient. + +=cut + +sub send_prepared { + my $self = shift; + my $cust_msg = shift or die "cust_msg required"; + + my $domain = 'example.com'; + if ( $cust_msg->env_from =~ /\@([\w\.\-]+)/ ) { + $domain = $1; + } + + my @to = split(/\s*,\s*/, $cust_msg->env_to); + + my %smtp_opt = ( 'host' => $conf->config('smtpmachine'), + 'helo' => $domain ); + + my($port, $enc) = split('-', ($conf->config('smtp-encryption') || '25') ); + $smtp_opt{'port'} = $port; + + my $transport; + if ( defined($enc) && $enc eq 'starttls' ) { + $smtp_opt{$_} = $conf->config("smtp-$_") for qw(username password); + $transport = Email::Sender::Transport::SMTP::TLS->new( %smtp_opt ); + } else { + if ( $conf->exists('smtp-username') && $conf->exists('smtp-password') ) { + $smtp_opt{"sasl_$_"} = $conf->config("smtp-$_") for qw(username password); + } + $smtp_opt{'ssl'} = 1 if defined($enc) && $enc eq 'tls'; + $transport = Email::Sender::Transport::SMTP->new( %smtp_opt ); + } + + warn "$me sending message\n" if $DEBUG; + my $message = join("\n\n", $cust_msg->header, $cust_msg->body); + local $@; + eval { + sendmail( $message, { transport => $transport, + from => $cust_msg->env_from, + to => \@to }) + }; + my $error = ''; + if(ref($@) and $@->isa('Email::Sender::Failure')) { + $error = $@->code.' ' if $@->code; + $error .= $@->message; + } + else { + $error = $@; + } + + $cust_msg->set('error', $error); + $cust_msg->set('status', $error ? 'failed' : 'sent'); + if ( $cust_msg->custmsgnum ) { + $cust_msg->replace; + } else { + $cust_msg->insert; + } + + $error; +} + +=item render OPTION => VALUE ... + +Fills in the template and renders it to a PDF document. Returns the +name of the PDF file. + +Options are as for 'prepare', but 'from' and 'to' are meaningless. + +=cut + +# will also have options to set paper size, margins, etc. + +sub render { + my $self = shift; + eval "use PDF::WebKit"; + die $@ if $@; + my %opt = @_; + my %hash = $self->prepare(%opt); + my $html = $hash{'html_body'}; + + # Graphics/stylesheets should probably go in /var/www on the Freeside + # machine. + my $script_path = `/usr/bin/which freeside-wkhtmltopdf`; + chomp $script_path; + my $kit = PDF::WebKit->new(\$html); #%options + # hack to use our wrapper script + $kit->configure(sub { shift->wkhtmltopdf($script_path) }); + + $kit->to_pdf; +} + +=item print OPTIONS + +Render a PDF and send it to the printer. OPTIONS are as for 'render'. + +=cut + +sub print { + my( $self, %opt ) = @_; + do_print( [ $self->render(%opt) ], agentnum=>$opt{cust_main}->agentnum ); +} + +# helper sub for package dates +my $ymd = sub { $_[0] ? time2str('%Y-%m-%d', $_[0]) : '' }; + +# helper sub for money amounts +my $money = sub { ($conf->money_char || '$') . sprintf('%.2f', $_[0] || 0) }; + +# helper sub for usage-related messages +my $usage_warning = sub { + my $svc = shift; + foreach my $col (qw(seconds upbytes downbytes totalbytes)) { + my $amount = $svc->$col; next if $amount eq ''; + my $method = $col.'_threshold'; + my $threshold = $svc->$method; next if $threshold eq ''; + return [$col, $amount, $threshold] if $amount <= $threshold; + # this only returns the first one that's below threshold, if there are + # several. + } + return ['', '', '']; +}; + +#my $conf = new FS::Conf; + +#return contexts and fill-in values +# If you add anything, be sure to add a description in +# httemplate/edit/msg_template.html. +sub substitutions { + { 'cust_main' => [qw( + display_custnum agentnum agent_name + + last first company + name name_short contact contact_firstlast + address1 address2 city county state zip + country + daytime night mobile fax + + has_ship_address + ship_name ship_name_short ship_contact ship_contact_firstlast + ship_address1 ship_address2 ship_city ship_county ship_state ship_zip + ship_country + + paymask payname paytype payip + num_cancelled_pkgs num_ncancelled_pkgs num_pkgs + classname categoryname + balance + credit_limit + invoicing_list_emailonly + cust_status ucfirst_cust_status cust_statuscolor cust_status_label + + signupdate dundate + packages recurdates + ), + [ invoicing_email => sub { shift->invoicing_list_emailonly_scalar } ], + #compatibility: obsolete ship_ fields - use the non-ship versions + map ( + { my $field = $_; + [ "ship_$field" => sub { shift->$field } ] + } + qw( last first company daytime night fax ) + ), + # ship_name, ship_name_short, ship_contact, ship_contact_firstlast + # still work, though + [ expdate => sub { shift->paydate_epoch } ], #compatibility + [ signupdate_ymd => sub { $ymd->(shift->signupdate) } ], + [ dundate_ymd => sub { $ymd->(shift->dundate) } ], + [ paydate_my => sub { sprintf('%02d/%04d', shift->paydate_monthyear) } ], + [ otaker_first => sub { shift->access_user->first } ], + [ otaker_last => sub { shift->access_user->last } ], + [ payby => sub { FS::payby->shortname(shift->payby) } ], + [ company_name => sub { + $conf->config('company_name', shift->agentnum) + } ], + [ company_address => sub { + $conf->config('company_address', shift->agentnum) + } ], + [ company_phonenum => sub { + $conf->config('company_phonenum', shift->agentnum) + } ], + [ selfservice_server_base_url => sub { + $conf->config('selfservice_server-base_url') #, shift->agentnum) + } ], + ], + # next_bill_date + 'cust_pkg' => [qw( + pkgnum pkg_label pkg_label_long + location_label + status statuscolor + + start_date setup bill last_bill + adjourn susp expire + labels_short + ), + [ pkg => sub { shift->part_pkg->pkg } ], + [ pkg_category => sub { shift->part_pkg->categoryname } ], + [ pkg_class => sub { shift->part_pkg->classname } ], + [ cancel => sub { shift->getfield('cancel') } ], # grrr... + [ start_ymd => sub { $ymd->(shift->getfield('start_date')) } ], + [ setup_ymd => sub { $ymd->(shift->getfield('setup')) } ], + [ next_bill_ymd => sub { $ymd->(shift->getfield('bill')) } ], + [ last_bill_ymd => sub { $ymd->(shift->getfield('last_bill')) } ], + [ adjourn_ymd => sub { $ymd->(shift->getfield('adjourn')) } ], + [ susp_ymd => sub { $ymd->(shift->getfield('susp')) } ], + [ expire_ymd => sub { $ymd->(shift->getfield('expire')) } ], + [ cancel_ymd => sub { $ymd->(shift->getfield('cancel')) } ], + + # not necessarily correct for non-flat packages + [ setup_fee => sub { shift->part_pkg->option('setup_fee') } ], + [ recur_fee => sub { shift->part_pkg->option('recur_fee') } ], + + [ freq_pretty => sub { shift->part_pkg->freq_pretty } ], + + ], + 'cust_bill' => [qw( + invnum + _date + _date_pretty + due_date + ), + [ due_date2str => sub { shift->due_date2str('short') } ], + ], + #XXX not really thinking about cust_bill substitutions quite yet + + # for welcome and limit warning messages + 'svc_acct' => [qw( + svcnum + username + domain + ), + [ password => sub { shift->getfield('_password') } ], + [ column => sub { &$usage_warning(shift)->[0] } ], + [ amount => sub { &$usage_warning(shift)->[1] } ], + [ threshold => sub { &$usage_warning(shift)->[2] } ], + ], + 'svc_domain' => [qw( + svcnum + domain + ), + [ registrar => sub { + my $registrar = qsearchs('registrar', + { registrarnum => shift->registrarnum} ); + $registrar ? $registrar->registrarname : '' + } + ], + [ catchall => sub { + my $svc_acct = qsearchs('svc_acct', { svcnum => shift->catchall }); + $svc_acct ? $svc_acct->email : '' + } + ], + ], + 'svc_phone' => [qw( + svcnum + phonenum + countrycode + domain + ) + ], + 'svc_broadband' => [qw( + svcnum + speed_up + speed_down + ip_addr + mac_addr + ) + ], + # for payment receipts + 'cust_pay' => [qw( + paynum + _date + ), + [ paid => sub { sprintf("%.2f", shift->paid) } ], + # overrides the one in cust_main in cases where a cust_pay is passed + [ payby => sub { FS::payby->shortname(shift->payby) } ], + [ date => sub { time2str("%a %B %o, %Y", shift->_date) } ], + [ payinfo => sub { + my $cust_pay = shift; + ($cust_pay->payby eq 'CARD' || $cust_pay->payby eq 'CHEK') ? + $cust_pay->paymask : $cust_pay->decrypt($cust_pay->payinfo) + } ], + ], + # for payment decline messages + # try to support all cust_pay fields + # 'error' is a special case, it contains the raw error from the gateway + 'cust_pay_pending' => [qw( + _date + error + ), + [ paid => sub { sprintf("%.2f", shift->paid) } ], + [ payby => sub { FS::payby->shortname(shift->payby) } ], + [ date => sub { time2str("%a %B %o, %Y", shift->_date) } ], + [ payinfo => sub { + my $pending = shift; + ($pending->payby eq 'CARD' || $pending->payby eq 'CHEK') ? + $pending->paymask : $pending->decrypt($pending->payinfo) + } ], + ], + }; +} + +=item content LOCALE + +Returns the L object appropriate to LOCALE, if there +is one. If not, returns the one with a NULL locale. + +=cut + +sub content { + my $self = shift; + my $locale = shift; + qsearchs('template_content', + { 'msgnum' => $self->msgnum, 'locale' => $locale }) || + qsearchs('template_content', + { 'msgnum' => $self->msgnum, 'locale' => '' }); +} + +=item agent + +Returns the L object for this template. + +=cut + +sub _upgrade_data { + my ($self, %opts) = @_; + + ### + # First move any historical templates in config to real message templates + ### + + my @fixes = ( + [ 'alerter_msgnum', 'alerter_template', '', '', '' ], + [ 'cancel_msgnum', 'cancelmessage', 'cancelsubject', '', '' ], + [ 'decline_msgnum', 'declinetemplate', '', '', '' ], + [ 'impending_recur_msgnum', 'impending_recur_template', '', '', 'impending_recur_bcc' ], + [ 'payment_receipt_msgnum', 'payment_receipt_email', '', '', '' ], + [ 'welcome_msgnum', 'welcome_email', 'welcome_email-subject', 'welcome_email-from', '' ], + [ 'warning_msgnum', 'warning_email', 'warning_email-subject', 'warning_email-from', '' ], + ); + + my @agentnums = ('', map {$_->agentnum} qsearch('agent', {})); + foreach my $agentnum (@agentnums) { + foreach (@fixes) { + my ($newname, $oldname, $subject, $from, $bcc) = @$_; + if ($conf->exists($oldname, $agentnum)) { + my $new = new FS::msg_template({ + 'msgname' => $oldname, + 'agentnum' => $agentnum, + 'from_addr' => ($from && $conf->config($from, $agentnum)) || '', + 'bcc_addr' => ($bcc && $conf->config($from, $agentnum)) || '', + 'subject' => ($subject && $conf->config($subject, $agentnum)) || '', + 'mime_type' => 'text/html', + 'body' => join('
',$conf->config($oldname, $agentnum)), + }); + my $error = $new->insert; + die $error if $error; + $conf->set($newname, $new->msgnum, $agentnum); + $conf->delete($oldname, $agentnum); + $conf->delete($from, $agentnum) if $from; + $conf->delete($subject, $agentnum) if $subject; + } + } + + if ( $conf->exists('alert_expiration', $agentnum) ) { + my $msgnum = $conf->exists('alerter_msgnum', $agentnum); + my $template = FS::msg_template->by_key($msgnum) if $msgnum; + if (!$template) { + warn "template for alerter_msgnum $msgnum not found\n"; + next; + } + # this is now a set of billing events + foreach my $days (30, 15, 5) { + my $event = FS::part_event->new({ + 'agentnum' => $agentnum, + 'event' => "Card expiration warning - $days days", + 'eventtable' => 'cust_main', + 'check_freq' => '1d', + 'action' => 'notice', + 'disabled' => 'Y', #initialize first + }); + my $error = $event->insert( 'msgnum' => $msgnum ); + if ($error) { + warn "error creating expiration alert event:\n$error\n\n"; + next; + } + # make it work like before: + # only send each warning once before the card expires, + # only warn active customers, + # only warn customers with CARD/DCRD, + # only warn customers who get email invoices + my %conds = ( + 'once_every' => { 'run_delay' => '30d' }, + 'cust_paydate_within' => { 'within' => $days.'d' }, + 'cust_status' => { 'status' => { 'active' => 1 } }, + 'payby' => { 'payby' => { 'CARD' => 1, + 'DCRD' => 1, } + }, + 'message_email' => {}, + ); + foreach (keys %conds) { + my $condition = FS::part_event_condition->new({ + 'conditionname' => $_, + 'eventpart' => $event->eventpart, + }); + $error = $condition->insert( %{ $conds{$_} }); + if ( $error ) { + warn "error creating expiration alert event:\n$error\n\n"; + next; + } + } + $error = $event->initialize; + if ( $error ) { + warn "expiration alert event was created, but not initialized:\n$error\n\n"; + } + } # foreach $days + $conf->delete('alerter_msgnum', $agentnum); + $conf->delete('alert_expiration', $agentnum); + + } # if alerter_msgnum + + } + + ### + # Move subject and body from msg_template to template_content + ### + + foreach my $msg_template ( qsearch('msg_template', {}) ) { + if ( $msg_template->subject || $msg_template->body ) { + # create new default content + my %content; + $content{subject} = $msg_template->subject; + $msg_template->set('subject', ''); + + # work around obscure Pg/DBD bug + # https://rt.cpan.org/Public/Bug/Display.html?id=60200 + # (though the right fix is to upgrade DBD) + my $body = $msg_template->body; + if ( $body =~ /^x([0-9a-f]+)$/ ) { + # there should be no real message templates that look like that + warn "converting template body to TEXT\n"; + $body = pack('H*', $1); + } + $content{body} = $body; + $msg_template->set('body', ''); + + my $error = $msg_template->replace(%content); + die $error if $error; + } + } + + ### + # Add new-style default templates if missing + ### + $self->_populate_initial_data; + +} + +sub _populate_initial_data { #class method + #my($class, %opts) = @_; + #my $class = shift; + + eval "use FS::msg_template::InitialData;"; + die $@ if $@; + + my $initial_data = FS::msg_template::InitialData->_initial_data; + + foreach my $hash ( @$initial_data ) { + + next if $hash->{_conf} && $conf->config( $hash->{_conf} ); + + my $msg_template = new FS::msg_template($hash); + my $error = $msg_template->insert( @{ $hash->{_insert_args} || [] } ); + die $error if $error; + + $conf->set( $hash->{_conf}, $msg_template->msgnum ) if $hash->{_conf}; + + } + +} + +sub eviscerate { + # Every bit as pleasant as it sounds. + # + # We do this because Text::Template::Preprocess doesn't + # actually work. It runs the entire template through + # the preprocessor, instead of the code segments. Which + # is a shame, because Text::Template already contains + # the code to do this operation. + my $body = shift; + my (@outside, @inside); + my $depth = 0; + my $chunk = ''; + while($body || $chunk) { + my ($first, $delim, $rest); + # put all leading non-delimiters into $first + ($first, $rest) = + ($body =~ /^((?:\\[{}]|[^{}])*)(.*)$/s); + $chunk .= $first; + # put a leading delimiter into $delim if there is one + ($delim, $rest) = + ($rest =~ /^([{}]?)(.*)$/s); + + if( $delim eq '{' ) { + $chunk .= '{'; + if( $depth == 0 ) { + push @outside, $chunk; + $chunk = ''; + } + $depth++; + } + elsif( $delim eq '}' ) { + $depth--; + if( $depth == 0 ) { + push @inside, $chunk; + $chunk = ''; + } + $chunk .= '}'; + } + else { + # no more delimiters + if( $depth == 0 ) { + push @outside, $chunk . $rest; + } # else ? something wrong + last; + } + $body = $rest; + } + (\@outside, \@inside); +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + -- cgit v1.2.1 From 76e8fffdfe3b6f6f8ab422038b62e40cc10f95e8 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 27 Aug 2015 19:18:42 -0700 Subject: #21564, external message services: preview and send messages through the UI --- FS/FS/Schema.pm | 1 + FS/FS/cust_main_Mixin.pm | 41 ++-- FS/FS/cust_msg.pm | 10 +- FS/FS/msg_template.pm | 2 + FS/FS/msg_template/email.pm | 448 +++++------------------------------ httemplate/misc/email-customers.html | 31 ++- 6 files changed, 107 insertions(+), 426 deletions(-) diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 311313a4e..12211d1e1 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -6400,6 +6400,7 @@ sub tables_hashref { 'error', 'varchar', 'NULL', 255, '', '', 'status', 'varchar', '',$char_d, '', '', 'msgtype', 'varchar', 'NULL', 16, '', '', + 'preview', 'text', 'NULL', '', '', '', ], 'primary_key' => 'custmsgnum', 'unique' => [ ], diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index bdad511fa..3d05f8473 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -445,6 +445,10 @@ sub email_search_result { my $success = 0; my %sent_to = (); + if ( !$msg_template ) { + # XXX create on the fly + } + #eventually order+limit magic to reduce memory use? foreach my $obj ( qsearch($sql_query) ) { @@ -459,36 +463,19 @@ sub email_search_result { } my $cust_main = $obj->cust_main; - tie my %message, 'Tie::IxHash'; if ( !$cust_main ) { next; # unlinked object; nothing else we can do } - if ( $msg_template ) { - # Now supports other context objects. - %message = $msg_template->prepare( - 'cust_main' => $cust_main, - 'object' => $obj, - ); - } - else { - my @to = $cust_main->invoicing_list_emailonly; - next if !@to; - - %message = ( - 'from' => $from, - 'to' => \@to, - 'subject' => $subject, - 'html_body' => $html_body, - 'text_body' => $text_body, - 'custnum' => $cust_main->custnum, - ); - } #if $msg_template + my $cust_msg = $msg_template->prepare( + 'cust_main' => $cust_main, + 'object' => $obj, + ); # For non-cust_main searches, we avoid duplicates based on message - # body text. + # body text. my $unique = $cust_main->custnum; - $unique .= sha1($message{'text_body'}) if $class ne 'FS::cust_main'; + $unique .= sha1($cust_msg->text_body) if $class ne 'FS::cust_main'; if( $sent_to{$unique} ) { # avoid duplicates $dups++; @@ -497,18 +484,20 @@ sub email_search_result { $sent_to{$unique} = 1; - $error = send_email( generate_email( %message ) ); + $error = $cust_msg->send; if($error) { # queue the sending of this message so that the user can see what we # tried to do, and retry if desired + # (note the cust_msg itself also now has a status of 'failed'; that's + # fine, as it will get its status reset if we retry the job) my $queue = new FS::queue { - 'job' => 'FS::Misc::process_send_email', + 'job' => 'FS::cust_msg::process_send', 'custnum' => $cust_main->custnum, 'status' => 'failed', 'statustext' => $error, }; - $queue->insert(%message); + $queue->insert($cust_msg->custmsgnum); push @retry_jobs, $queue; } else { diff --git a/FS/FS/cust_msg.pm b/FS/FS/cust_msg.pm index 934632725..ec2c961a3 100644 --- a/FS/FS/cust_msg.pm +++ b/FS/FS/cust_msg.pm @@ -47,8 +47,12 @@ from FS::Record. The following fields are currently supported: =item body - message body (as a complete MIME document) +=item preview - HTML fragment to show as a preview of the message + =item error - Email::Sender error message (or null for success) +=item status - "prepared", "sent", or "failed" + =back =head1 METHODS @@ -137,6 +141,7 @@ sub check { || $self->ut_textn('env_to') || $self->ut_anything('header') || $self->ut_anything('body') + || $self->ut_anything('preview') || $self->ut_enum('status', \@statuses) || $self->ut_textn('error') || $self->ut_enum('msgtype', [ '', @@ -159,8 +164,9 @@ message on error, or an empty string. sub send { my $self = shift; - my $msg_template = $self->msg_template - or return 'message was created without a template object'; + # it's still allowed to have cust_msgs without message templates, but only + # for email. + my $msg_template = $self->msg_template || 'FS::msg_template::email'; $msg_template->send_prepared($self); } diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index 180e9de4d..d7d9f50a8 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -10,6 +10,8 @@ use FS::Record qw( qsearch qsearchs ); use FS::cust_msg; use FS::template_content; +use Date::Format qw(time2str); + FS::UID->install_callback( sub { $conf = new FS::Conf; } ); $DEBUG=0; diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index 1133faafe..275dc82bb 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -26,11 +26,12 @@ use FS::Record qw( qsearch qsearchs ); use FS::template_content; use FS::UID qw( dbh ); +# needed to manage prepared messages use FS::cust_msg; FS::UID->install_callback( sub { $conf = new FS::Conf; } ); -our $DEBUG = 1; +our $DEBUG = 0; our $me = '[FS::msg_template::email]'; =head1 NAME @@ -362,74 +363,12 @@ sub prepare { 'error' => '', 'status' => 'prepared', 'msgtype' => ($opt{'msgtype'} || ''), + 'preview' => $body, # html content only }); return $cust_msg; } -=item send_prepared CUST_MSG - -Takes the CUST_MSG object and sends it to its recipient. - -=cut - -sub send_prepared { - my $self = shift; - my $cust_msg = shift or die "cust_msg required"; - - my $domain = 'example.com'; - if ( $cust_msg->env_from =~ /\@([\w\.\-]+)/ ) { - $domain = $1; - } - - my @to = split(/\s*,\s*/, $cust_msg->env_to); - - my %smtp_opt = ( 'host' => $conf->config('smtpmachine'), - 'helo' => $domain ); - - my($port, $enc) = split('-', ($conf->config('smtp-encryption') || '25') ); - $smtp_opt{'port'} = $port; - - my $transport; - if ( defined($enc) && $enc eq 'starttls' ) { - $smtp_opt{$_} = $conf->config("smtp-$_") for qw(username password); - $transport = Email::Sender::Transport::SMTP::TLS->new( %smtp_opt ); - } else { - if ( $conf->exists('smtp-username') && $conf->exists('smtp-password') ) { - $smtp_opt{"sasl_$_"} = $conf->config("smtp-$_") for qw(username password); - } - $smtp_opt{'ssl'} = 1 if defined($enc) && $enc eq 'tls'; - $transport = Email::Sender::Transport::SMTP->new( %smtp_opt ); - } - - warn "$me sending message\n" if $DEBUG; - my $message = join("\n\n", $cust_msg->header, $cust_msg->body); - local $@; - eval { - sendmail( $message, { transport => $transport, - from => $cust_msg->env_from, - to => \@to }) - }; - my $error = ''; - if(ref($@) and $@->isa('Email::Sender::Failure')) { - $error = $@->code.' ' if $@->code; - $error .= $@->message; - } - else { - $error = $@; - } - - $cust_msg->set('error', $error); - $cust_msg->set('status', $error ? 'failed' : 'sent'); - if ( $cust_msg->custmsgnum ) { - $cust_msg->replace; - } else { - $cust_msg->insert; - } - - $error; -} - =item render OPTION => VALUE ... Fills in the template and renders it to a PDF document. Returns the @@ -491,183 +430,6 @@ my $usage_warning = sub { return ['', '', '']; }; -#my $conf = new FS::Conf; - -#return contexts and fill-in values -# If you add anything, be sure to add a description in -# httemplate/edit/msg_template.html. -sub substitutions { - { 'cust_main' => [qw( - display_custnum agentnum agent_name - - last first company - name name_short contact contact_firstlast - address1 address2 city county state zip - country - daytime night mobile fax - - has_ship_address - ship_name ship_name_short ship_contact ship_contact_firstlast - ship_address1 ship_address2 ship_city ship_county ship_state ship_zip - ship_country - - paymask payname paytype payip - num_cancelled_pkgs num_ncancelled_pkgs num_pkgs - classname categoryname - balance - credit_limit - invoicing_list_emailonly - cust_status ucfirst_cust_status cust_statuscolor cust_status_label - - signupdate dundate - packages recurdates - ), - [ invoicing_email => sub { shift->invoicing_list_emailonly_scalar } ], - #compatibility: obsolete ship_ fields - use the non-ship versions - map ( - { my $field = $_; - [ "ship_$field" => sub { shift->$field } ] - } - qw( last first company daytime night fax ) - ), - # ship_name, ship_name_short, ship_contact, ship_contact_firstlast - # still work, though - [ expdate => sub { shift->paydate_epoch } ], #compatibility - [ signupdate_ymd => sub { $ymd->(shift->signupdate) } ], - [ dundate_ymd => sub { $ymd->(shift->dundate) } ], - [ paydate_my => sub { sprintf('%02d/%04d', shift->paydate_monthyear) } ], - [ otaker_first => sub { shift->access_user->first } ], - [ otaker_last => sub { shift->access_user->last } ], - [ payby => sub { FS::payby->shortname(shift->payby) } ], - [ company_name => sub { - $conf->config('company_name', shift->agentnum) - } ], - [ company_address => sub { - $conf->config('company_address', shift->agentnum) - } ], - [ company_phonenum => sub { - $conf->config('company_phonenum', shift->agentnum) - } ], - [ selfservice_server_base_url => sub { - $conf->config('selfservice_server-base_url') #, shift->agentnum) - } ], - ], - # next_bill_date - 'cust_pkg' => [qw( - pkgnum pkg_label pkg_label_long - location_label - status statuscolor - - start_date setup bill last_bill - adjourn susp expire - labels_short - ), - [ pkg => sub { shift->part_pkg->pkg } ], - [ pkg_category => sub { shift->part_pkg->categoryname } ], - [ pkg_class => sub { shift->part_pkg->classname } ], - [ cancel => sub { shift->getfield('cancel') } ], # grrr... - [ start_ymd => sub { $ymd->(shift->getfield('start_date')) } ], - [ setup_ymd => sub { $ymd->(shift->getfield('setup')) } ], - [ next_bill_ymd => sub { $ymd->(shift->getfield('bill')) } ], - [ last_bill_ymd => sub { $ymd->(shift->getfield('last_bill')) } ], - [ adjourn_ymd => sub { $ymd->(shift->getfield('adjourn')) } ], - [ susp_ymd => sub { $ymd->(shift->getfield('susp')) } ], - [ expire_ymd => sub { $ymd->(shift->getfield('expire')) } ], - [ cancel_ymd => sub { $ymd->(shift->getfield('cancel')) } ], - - # not necessarily correct for non-flat packages - [ setup_fee => sub { shift->part_pkg->option('setup_fee') } ], - [ recur_fee => sub { shift->part_pkg->option('recur_fee') } ], - - [ freq_pretty => sub { shift->part_pkg->freq_pretty } ], - - ], - 'cust_bill' => [qw( - invnum - _date - _date_pretty - due_date - ), - [ due_date2str => sub { shift->due_date2str('short') } ], - ], - #XXX not really thinking about cust_bill substitutions quite yet - - # for welcome and limit warning messages - 'svc_acct' => [qw( - svcnum - username - domain - ), - [ password => sub { shift->getfield('_password') } ], - [ column => sub { &$usage_warning(shift)->[0] } ], - [ amount => sub { &$usage_warning(shift)->[1] } ], - [ threshold => sub { &$usage_warning(shift)->[2] } ], - ], - 'svc_domain' => [qw( - svcnum - domain - ), - [ registrar => sub { - my $registrar = qsearchs('registrar', - { registrarnum => shift->registrarnum} ); - $registrar ? $registrar->registrarname : '' - } - ], - [ catchall => sub { - my $svc_acct = qsearchs('svc_acct', { svcnum => shift->catchall }); - $svc_acct ? $svc_acct->email : '' - } - ], - ], - 'svc_phone' => [qw( - svcnum - phonenum - countrycode - domain - ) - ], - 'svc_broadband' => [qw( - svcnum - speed_up - speed_down - ip_addr - mac_addr - ) - ], - # for payment receipts - 'cust_pay' => [qw( - paynum - _date - ), - [ paid => sub { sprintf("%.2f", shift->paid) } ], - # overrides the one in cust_main in cases where a cust_pay is passed - [ payby => sub { FS::payby->shortname(shift->payby) } ], - [ date => sub { time2str("%a %B %o, %Y", shift->_date) } ], - [ payinfo => sub { - my $cust_pay = shift; - ($cust_pay->payby eq 'CARD' || $cust_pay->payby eq 'CHEK') ? - $cust_pay->paymask : $cust_pay->decrypt($cust_pay->payinfo) - } ], - ], - # for payment decline messages - # try to support all cust_pay fields - # 'error' is a special case, it contains the raw error from the gateway - 'cust_pay_pending' => [qw( - _date - error - ), - [ paid => sub { sprintf("%.2f", shift->paid) } ], - [ payby => sub { FS::payby->shortname(shift->payby) } ], - [ date => sub { time2str("%a %B %o, %Y", shift->_date) } ], - [ payinfo => sub { - my $pending = shift; - ($pending->payby eq 'CARD' || $pending->payby eq 'CHEK') ? - $pending->paymask : $pending->decrypt($pending->payinfo) - } ], - ], - }; -} - =item content LOCALE Returns the L object appropriate to LOCALE, if there @@ -684,168 +446,84 @@ sub content { { 'msgnum' => $self->msgnum, 'locale' => '' }); } -=item agent - -Returns the L object for this template. - =cut -sub _upgrade_data { - my ($self, %opts) = @_; +=back - ### - # First move any historical templates in config to real message templates - ### +=head2 CLASS METHODS - my @fixes = ( - [ 'alerter_msgnum', 'alerter_template', '', '', '' ], - [ 'cancel_msgnum', 'cancelmessage', 'cancelsubject', '', '' ], - [ 'decline_msgnum', 'declinetemplate', '', '', '' ], - [ 'impending_recur_msgnum', 'impending_recur_template', '', '', 'impending_recur_bcc' ], - [ 'payment_receipt_msgnum', 'payment_receipt_email', '', '', '' ], - [ 'welcome_msgnum', 'welcome_email', 'welcome_email-subject', 'welcome_email-from', '' ], - [ 'warning_msgnum', 'warning_email', 'warning_email-subject', 'warning_email-from', '' ], - ); - - my @agentnums = ('', map {$_->agentnum} qsearch('agent', {})); - foreach my $agentnum (@agentnums) { - foreach (@fixes) { - my ($newname, $oldname, $subject, $from, $bcc) = @$_; - if ($conf->exists($oldname, $agentnum)) { - my $new = new FS::msg_template({ - 'msgname' => $oldname, - 'agentnum' => $agentnum, - 'from_addr' => ($from && $conf->config($from, $agentnum)) || '', - 'bcc_addr' => ($bcc && $conf->config($from, $agentnum)) || '', - 'subject' => ($subject && $conf->config($subject, $agentnum)) || '', - 'mime_type' => 'text/html', - 'body' => join('
',$conf->config($oldname, $agentnum)), - }); - my $error = $new->insert; - die $error if $error; - $conf->set($newname, $new->msgnum, $agentnum); - $conf->delete($oldname, $agentnum); - $conf->delete($from, $agentnum) if $from; - $conf->delete($subject, $agentnum) if $subject; - } - } +=over 4 - if ( $conf->exists('alert_expiration', $agentnum) ) { - my $msgnum = $conf->exists('alerter_msgnum', $agentnum); - my $template = FS::msg_template->by_key($msgnum) if $msgnum; - if (!$template) { - warn "template for alerter_msgnum $msgnum not found\n"; - next; - } - # this is now a set of billing events - foreach my $days (30, 15, 5) { - my $event = FS::part_event->new({ - 'agentnum' => $agentnum, - 'event' => "Card expiration warning - $days days", - 'eventtable' => 'cust_main', - 'check_freq' => '1d', - 'action' => 'notice', - 'disabled' => 'Y', #initialize first - }); - my $error = $event->insert( 'msgnum' => $msgnum ); - if ($error) { - warn "error creating expiration alert event:\n$error\n\n"; - next; - } - # make it work like before: - # only send each warning once before the card expires, - # only warn active customers, - # only warn customers with CARD/DCRD, - # only warn customers who get email invoices - my %conds = ( - 'once_every' => { 'run_delay' => '30d' }, - 'cust_paydate_within' => { 'within' => $days.'d' }, - 'cust_status' => { 'status' => { 'active' => 1 } }, - 'payby' => { 'payby' => { 'CARD' => 1, - 'DCRD' => 1, } - }, - 'message_email' => {}, - ); - foreach (keys %conds) { - my $condition = FS::part_event_condition->new({ - 'conditionname' => $_, - 'eventpart' => $event->eventpart, - }); - $error = $condition->insert( %{ $conds{$_} }); - if ( $error ) { - warn "error creating expiration alert event:\n$error\n\n"; - next; - } - } - $error = $event->initialize; - if ( $error ) { - warn "expiration alert event was created, but not initialized:\n$error\n\n"; - } - } # foreach $days - $conf->delete('alerter_msgnum', $agentnum); - $conf->delete('alert_expiration', $agentnum); - - } # if alerter_msgnum +=item send_prepared CUST_MSG - } +Takes the CUST_MSG object and sends it to its recipient. This is a class +method because everything needed to send the message is stored in the +CUST_MSG already. - ### - # Move subject and body from msg_template to template_content - ### +=cut - foreach my $msg_template ( qsearch('msg_template', {}) ) { - if ( $msg_template->subject || $msg_template->body ) { - # create new default content - my %content; - $content{subject} = $msg_template->subject; - $msg_template->set('subject', ''); - - # work around obscure Pg/DBD bug - # https://rt.cpan.org/Public/Bug/Display.html?id=60200 - # (though the right fix is to upgrade DBD) - my $body = $msg_template->body; - if ( $body =~ /^x([0-9a-f]+)$/ ) { - # there should be no real message templates that look like that - warn "converting template body to TEXT\n"; - $body = pack('H*', $1); - } - $content{body} = $body; - $msg_template->set('body', ''); +sub send_prepared { + my $self = shift; + my $cust_msg = shift or die "cust_msg required"; - my $error = $msg_template->replace(%content); - die $error if $error; - } + my $domain = 'example.com'; + if ( $cust_msg->env_from =~ /\@([\w\.\-]+)/ ) { + $domain = $1; } - ### - # Add new-style default templates if missing - ### - $self->_populate_initial_data; - -} + my @to = split(/\s*,\s*/, $cust_msg->env_to); -sub _populate_initial_data { #class method - #my($class, %opts) = @_; - #my $class = shift; + my %smtp_opt = ( 'host' => $conf->config('smtpmachine'), + 'helo' => $domain ); - eval "use FS::msg_template::InitialData;"; - die $@ if $@; + my($port, $enc) = split('-', ($conf->config('smtp-encryption') || '25') ); + $smtp_opt{'port'} = $port; + + my $transport; + if ( defined($enc) && $enc eq 'starttls' ) { + $smtp_opt{$_} = $conf->config("smtp-$_") for qw(username password); + $transport = Email::Sender::Transport::SMTP::TLS->new( %smtp_opt ); + } else { + if ( $conf->exists('smtp-username') && $conf->exists('smtp-password') ) { + $smtp_opt{"sasl_$_"} = $conf->config("smtp-$_") for qw(username password); + } + $smtp_opt{'ssl'} = 1 if defined($enc) && $enc eq 'tls'; + $transport = Email::Sender::Transport::SMTP->new( %smtp_opt ); + } - my $initial_data = FS::msg_template::InitialData->_initial_data; + warn "$me sending message\n" if $DEBUG; + my $message = join("\n\n", $cust_msg->header, $cust_msg->body); + local $@; + eval { + sendmail( $message, { transport => $transport, + from => $cust_msg->env_from, + to => \@to }) + }; + my $error = ''; + if(ref($@) and $@->isa('Email::Sender::Failure')) { + $error = $@->code.' ' if $@->code; + $error .= $@->message; + } + else { + $error = $@; + } - foreach my $hash ( @$initial_data ) { + $cust_msg->set('error', $error); + $cust_msg->set('status', $error ? 'failed' : 'sent'); + if ( $cust_msg->custmsgnum ) { + $cust_msg->replace; + } else { + $cust_msg->insert; + } - next if $hash->{_conf} && $conf->config( $hash->{_conf} ); + $error; +} - my $msg_template = new FS::msg_template($hash); - my $error = $msg_template->insert( @{ $hash->{_insert_args} || [] } ); - die $error if $error; +=back - $conf->set( $hash->{_conf}, $msg_template->msgnum ) if $hash->{_conf}; - - } +=cut -} +# internal use only sub eviscerate { # Every bit as pleasant as it sounds. @@ -897,8 +575,6 @@ sub eviscerate { (\@outside, \@inside); } -=back - =head1 BUGS =head1 SEE ALSO diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html index 8ac44afc1..bffd0cf81 100644 --- a/httemplate/misc/email-customers.html +++ b/httemplate/misc/email-customers.html @@ -1,10 +1,11 @@ <%doc> -Allows emailing one or more customers, based on a search for customers. Search can -be specified either through cust_main fields as cgi params, or through a base64 encoded -frozen hash in the 'search' cgi param. Form allows selecting an existing msg_template, -or creating a custom message, and shows a preview of the message before sending. -If linked to as a popup, include the cgi parameter 'popup' for proper header handling. +Allows emailing one or more customers, based on a search for customers. +Search can be specified either through cust_main fields as cgi params, or +through a base64 encoded frozen hash in the 'search' cgi param. Form allows +selecting an existing msg_template, or creating a custom message, and shows a +preview of the message before sending. If linked to as a popup, include the +cgi parameter 'popup' for proper header handling. This may also be used as an element in other pages, enabling you to provide an alternate initial form while using this for search freezing/thawing and @@ -21,12 +22,13 @@ title - the title of the page no_search_fields - arrayref of additional fields that are not search parameters alternate_form - subroutine that returns alternate html for the initial form, -replaces msgnum/from/subject/html_body/action inputs and submit button, -not used if an action is specified +replaces msgnum/from/subject/html_body/action inputs and submit button, not +used if an action is specified -post_search_hook - sub hook for additional processing after search has been processed from cgi, -gets passed options 'conf' and 'search' (a reference to the unfrozen %search hash), -should be used to set msgnum or from/subject/html_body cgi params +post_search_hook - sub hook for additional processing after search has been +processed from cgi, gets passed options 'conf' and 'search' (a reference to +the unfrozen %search hash), should be used to set msgnum or +from/subject/html_body cgi params % if ($popup) { @@ -288,8 +290,13 @@ if ( $cgi->param('action') eq 'preview' ) { 'cust_main' => $cust, 'object' => $object, ); - my %message = $msg_template->prepare(%msgopts); - ($from, $subject, $html_body) = @message{'from', 'subject', 'html_body'}; + + my $cust_msg = $msg_template->prepare(%msgopts); + $from = $cust_msg->env_from; + $html_body = $cust_msg->preview; + if ( $cust_msg->header =~ /^subject: (.*)/mi ) { + $subject = $1; + } } } -- cgit v1.2.1 From 46bbbb1a78fd822805226abea832b6206273c091 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 28 Aug 2015 00:56:49 -0500 Subject: RT#37064: Add action link to manually refund a payment --- FS/FS/access_user.pm | 37 +++++++++ FS/FS/cust_main/Billing_Realtime.pm | 1 + FS/FS/cust_pay.pm | 96 ++++++++++++++++++++++ httemplate/edit/cust_refund.cgi | 10 +-- httemplate/edit/process/cust_refund.cgi | 31 ++++--- httemplate/misc/unapply-cust_pay.cgi | 6 +- httemplate/view/cust_main/payment_history.html | 1 + .../view/cust_main/payment_history/payment.html | 13 +-- 8 files changed, 160 insertions(+), 35 deletions(-) diff --git a/FS/FS/access_user.pm b/FS/FS/access_user.pm index a3f55bc76..ecab32d32 100644 --- a/FS/FS/access_user.pm +++ b/FS/FS/access_user.pm @@ -587,6 +587,43 @@ sub access_right { } +=item refund_rights PAYBY + +Accepts payment $payby (BILL,CASH,MCRD,MCHK,CARD,CHEK) and returns a +list of the refund rights associated with that $payby. + +Returns empty list if $payby wasn't recognized. + +=cut + +sub refund_rights { + my $self = shift; + my $payby = shift; + my @rights = (); + push @rights, 'Post refund' if $payby =~ /^(BILL|CASH|MCRD|MCHK)$/; + push @rights, 'Post check refund' if $payby eq 'BILL'; + push @rights, 'Post cash refund ' if $payby eq 'CASH'; + push @rights, 'Refund payment' if $payby =~ /^(CARD|CHEK)$/; + push @rights, 'Refund credit card payment' if $payby eq 'CARD'; + push @rights, 'Refund Echeck payment' if $payby eq 'CHEK'; + return @rights; +} + +=item refund_access_right PAYBY + +Returns true if user has L for any L +for the specified payby. + +=cut + +sub refund_access_right { + my $self = shift; + my $payby = shift; + my @rights = $self->refund_rights($payby); + return '' unless @rights; + return $self->access_right(\@rights); +} + =item default_customer_view Returns the default customer view for this user, from the diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index d973896c8..fda3ae040 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -1649,6 +1649,7 @@ sub realtime_refund_bop { $order_number = $refund->order_number if $refund->can('order_number'); + # change this to just use $cust_pay->delete_cust_bill_pay? while ( $cust_pay && $cust_pay->unapplied < $amount ) { my @cust_bill_pay = $cust_pay->cust_bill_pay; last unless @cust_bill_pay; diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index 5d4f67fe7..59d77742c 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -821,6 +821,102 @@ sub amount { $self->paid(); } +=item delete_cust_bill_pay OPTIONS + +Deletes all associated cust_bill_pay records. + +If option 'unapplied' is a specified, only deletes until +this object's 'unapplied' value is >= the specified amount. +(Deletes in order returned by L.) + +=cut + +sub delete_cust_bill_pay { + my $self = shift; + my %opt = @_; + + 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 $unapplied = $self->unapplied; #only need to look it up once + + my $error = ''; + + # Maybe we should reverse the order these get deleted in? + # ie delete newest first? + # keeping consistent with how bop refunds work, for now... + foreach my $cust_bill_pay ( $self->cust_bill_pay ) { + last if $opt{'unapplied'} && ($unapplied > $opt{'unapplied'}); + $unapplied += $cust_bill_pay->amount; + $error = $cust_bill_pay->delete; + last if $error; + } + + if ($error) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + return ''; +} + +=item refund HASHREF + +Accepts input for creating a new FS::cust_refund object. +Unapplies payment from invoices up to the amount of the refund, +creates the refund and applies payment to refund. Allows entire +process to be handled in one transaction. + +Causes a fatal error if called on CARD or CHEK payments. + +=cut + +sub refund { + my $self = shift; + my $hash = shift; + die "Cannot call cust_pay->refund on " . $self->payby + if grep { $_ eq $self->payby } qw(CARD CHEK); + + 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 $error = $self->delete_cust_bill_pay('amount' => $hash->{'amount'}); + + if ($error) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $hash->{'paynum'} = $self->paynum; + my $new = new FS::cust_refund ( $hash ); + $error = $new->insert; + + if ($error) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + return ''; +} + =back =head1 CLASS METHODS diff --git a/httemplate/edit/cust_refund.cgi b/httemplate/edit/cust_refund.cgi index fa049a39a..bfcbfe725 100755 --- a/httemplate/edit/cust_refund.cgi +++ b/httemplate/edit/cust_refund.cgi @@ -139,16 +139,8 @@ my $payinfo = $cgi->param('payinfo'); my $reason = $cgi->param('reason'); my $link = $cgi->param('popup') ? 'popup' : ''; -my @rights = (); -push @rights, 'Post refund' if $payby =~ /^(BILL|CASH|MCRD|MCHK)$/; -push @rights, 'Post check refund' if $payby eq 'BILL'; -push @rights, 'Post cash refund ' if $payby eq 'CASH'; -push @rights, 'Refund payment' if $payby =~ /^(CARD|CHEK)$/; -push @rights, 'Refund credit card payment' if $payby eq 'CARD'; -push @rights, 'Refund Echeck payment' if $payby eq 'CHEK'; - die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right(\@rights); + unless $FS::CurrentUser::CurrentUser->refund_access_right($payby); my( $paynum, $cust_pay ) = ( '', '' ); if ( $cgi->param('paynum') =~ /^(\d+)$/ ) { diff --git a/httemplate/edit/process/cust_refund.cgi b/httemplate/edit/process/cust_refund.cgi index 52fede8ec..ce72c9253 100755 --- a/httemplate/edit/process/cust_refund.cgi +++ b/httemplate/edit/process/cust_refund.cgi @@ -12,7 +12,7 @@ % } else { -<% $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum") %> +<% $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum;show=payment_history") %> % } %} <%init> @@ -30,16 +30,8 @@ my $link = $cgi->param('popup') ? 'popup' : ''; my $payby = $cgi->param('payby'); -my @rights = (); -push @rights, 'Post refund' if $payby =~ /^(BILL|CASH|MCRD|MCHK)$/; -push @rights, 'Post check refund' if $payby eq 'BILL'; -push @rights, 'Post cash refund ' if $payby eq 'CASH'; -push @rights, 'Refund payment' if $payby =~ /^(CARD|CHEK)$/; -push @rights, 'Refund credit card payment' if $payby eq 'CARD'; -push @rights, 'Refund Echeck payment' if $payby eq 'CHEK'; - die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right(\@rights); + unless $FS::CurrentUser::CurrentUser->refund_access_right($payby); $cgi->param('reasonnum') =~ /^(-?\d+)$/ or die "Illegal reasonnum"; my ($reasonnum, $error) = $m->comp('/misc/process/elements/reason'); @@ -63,12 +55,19 @@ if ( $error ) { 'reason' => $reason, %options ); } else { - my $new = new FS::cust_refund ( { - map { - $_, scalar($cgi->param($_)); - } fields('cust_refund') #huh? , 'paynum' ) - } ); - $error = $new->insert; + my %hash = map { + $_, scalar($cgi->param($_)) + } fields('cust_refund'); + my $paynum = $cgi->param('paynum'); + $paynum =~ /^(\d*)$/ or die "Illegal paynum!"; + if ($paynum) { + my $cust_pay = qsearchs('cust_pay',{ 'paynum' => $paynum }); + die "Could not find paynum $paynum" unless $cust_pay; + $error = $cust_pay->refund(\%hash); + } else { + my $new = new FS::cust_refund ( \%hash ); + $error = $new->insert; + } } diff --git a/httemplate/misc/unapply-cust_pay.cgi b/httemplate/misc/unapply-cust_pay.cgi index 8cdac180b..b0343d034 100755 --- a/httemplate/misc/unapply-cust_pay.cgi +++ b/httemplate/misc/unapply-cust_pay.cgi @@ -12,9 +12,7 @@ my $paynum = $1; my $cust_pay = qsearchs('cust_pay', { 'paynum' => $paynum } ); my $custnum = $cust_pay->custnum; -foreach my $cust_bill_pay ( $cust_pay->cust_bill_pay ) { - my $error = $cust_bill_pay->delete; - errorpage($error) if $error; -} +my $error = $cust_pay->delete_cust_bill_pay; +errorpage($error) if $error; diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html index e3599bc06..1525e9314 100644 --- a/httemplate/view/cust_main/payment_history.html +++ b/httemplate/view/cust_main/payment_history.html @@ -228,6 +228,7 @@ my %opt = ( ( 'View invoices', 'Void invoices', 'Unvoid invoices', 'Apply payment', 'Refund credit card payment', 'Refund Echeck payment', + 'Post refund', 'Post check refund', 'Post cash refund ', 'Refund payment', 'Credit card void', 'Echeck void', 'Void payments', 'Unvoid payments', 'Delete payment', 'Unapply payment', 'Apply credit', 'Delete credit', 'Unapply credit', 'Void credit', 'Unvoid credit', diff --git a/httemplate/view/cust_main/payment_history/payment.html b/httemplate/view/cust_main/payment_history/payment.html index bf88a6607..0ed2f8003 100644 --- a/httemplate/view/cust_main/payment_history/payment.html +++ b/httemplate/view/cust_main/payment_history/payment.html @@ -154,18 +154,19 @@ if ( $apply && $opt{'pkg-balances'} && $cust_pay->pkgnum ) { my $refund = ''; my $refund_days = $opt{'card_refund-days'} || 120; -my $refund_right = ''; -$refund_right = 'Refund credit card payment' if $cust_pay->payby eq 'CARD'; -$refund_right = 'Refund Echeck payment' if $cust_pay->payby eq 'CHEK'; +my @refund_right = grep { $opt{$_} } $FS::CurrentUser::CurrentUser->refund_rights($cust_pay->payby); if ( $cust_pay->closed !~ /^Y/i - && $cust_pay->payby =~ /^(CARD|CHEK)$/ + && $cust_pay->payby =~ /^(CARD|CHEK|BILL)$/ && time-$cust_pay->_date < $refund_days*86400 && $cust_pay->unrefunded > 0 - && $opt{$refund_right} + && scalar(@refund_right) ) { + my $refundtitle = ($cust_pay->payby =~ /^(CARD|CHEK)$/) + ? emt('Send a refund for this payment to the payment gateway') + : emt('Record a refund for this payment'); $refund = qq! (' . emt('refund') . ')'; } -- cgit v1.2.1 From 42837bd9ef4c47b1885564c2e56c4ca0f1e36e77 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 28 Aug 2015 11:42:09 -0700 Subject: add BILL to allowed payment type list, from #23741 --- FS/FS/Conf.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index a22e236a2..1714c575a 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2619,7 +2619,7 @@ and customer address. Include units.', 'section' => 'billing', 'description' => 'Available payment types.', 'type' => 'selectmultiple', - 'select_enum' => [ qw(CARD DCRD CHEK DCHK CASH WEST MCRD MCHK PPAL) ], + 'select_enum' => [ qw(CARD DCRD CHEK DCHK BILL CASH WEST MCRD MCHK PPAL) ], }, { -- cgit v1.2.1 From 46b9a9665971f30562b0a6a6231561116399d3a0 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 28 Aug 2015 15:40:58 -0700 Subject: typo --- FS/FS/msg_template/email.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index 275dc82bb..f8ebfa06c 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -492,7 +492,7 @@ sub send_prepared { } warn "$me sending message\n" if $DEBUG; - my $message = join("\n\n", $cust_msg->header, $cust_msg->body); + my $message = join("\n", $cust_msg->header, $cust_msg->body); local $@; eval { sendmail( $message, { transport => $transport, -- cgit v1.2.1 From 81e562e6067ccf33c24ab3713163a0eefb1438bd Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 29 Aug 2015 13:07:45 -0700 Subject: fix password reset emails based on svc_acct email address, fallout from #25533 --- FS/FS/ClientAPI/MyAccount.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 824ff67cb..6332dd75b 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -3032,7 +3032,7 @@ sub reset_passwd { my($username, $domain) = split('@', $p->{'email'}); my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } ); if ( $svc_domain ) { - $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'}, + $svc_acct = qsearchs('svc_acct', { 'username' => $username, 'domsvc' => $svc_domain->svcnum } ); if ( $svc_acct ) { @@ -3120,7 +3120,7 @@ sub reset_passwd { my $reset_session = { 'svcnum' => $svc_acct->svcnum, - 'agentnum' => + 'agentnum' => $svc_acct->cust_main->agentnum, }; my $timeout = '1 hour'; #? -- cgit v1.2.1 From a008b601383d5693a197f4bf57ed5ba7887f3065 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 29 Aug 2015 13:37:23 -0700 Subject: #21564, external message services: REST client --- FS/FS/msg_template.pm | 100 +++++++++++++++++++++++++-- FS/FS/msg_template/email.pm | 11 +-- FS/FS/msg_template/http.pm | 155 ++++++++++++++++++++++++++++++++++++++++++ bin/msg_template_http-demo.pl | 76 +++++++++++++++++++++ 4 files changed, 329 insertions(+), 13 deletions(-) create mode 100644 FS/FS/msg_template/http.pm create mode 100755 bin/msg_template_http-demo.pl diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index d7d9f50a8..827bb9883 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -35,6 +35,12 @@ FS::msg_template - Object methods for msg_template records $error = $record->check; +=head1 NOTE + +This uses a table-per-subclass ORM strategy, which is a somewhat cleaner +version of what we do elsewhere with _option tables. We could easily extract +that functionality into a base class, or even into FS::Record itself. + =head1 DESCRIPTION An FS::msg_template object represents a customer message template. @@ -81,20 +87,66 @@ points to. You can ask the object for a copy with the I method. sub table { 'msg_template'; } +sub extension_table { ''; } # subclasses don't HAVE to have extensions + sub _rebless { my $self = shift; my $class = 'FS::msg_template::' . $self->msgclass; eval "use $class;"; bless($self, $class) unless $@; + + # merge in the extension fields + if ( $self->msgnum and $self->extension_table ) { + my $extension = $self->_extension; + if ( $extension ) { + $self->{Hash} = { $self->hash, $extension->hash }; + } + } + $self; } +# Returns the subclass-specific extension record for this object. For internal +# use only; everyone else is supposed to think of this as a single record. + +sub _extension { + my $self = shift; + if ( $self->extension_table and $self->msgnum ) { + local $FS::Record::nowarn_classload = 1; + return qsearchs($self->extension_table, { msgnum => $self->msgnum }); + } + return; +} + =item insert [ CONTENT ] Adds this record to the database. If there is an error, returns the error, otherwise returns false. -# inherited +=cut + +sub insert { + my $self = shift; + $self->_rebless; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + + my $error = $self->SUPER::insert; + # calling _extension at this point makes it copy the msgnum, so links work + if ( $self->extension_table ) { + local $FS::Record::nowarn_classload = 1; + my $extension = FS::Record->new($self->extension_table, { $self->hash }); + $error ||= $extension->insert; + } + + if ( $error ) { + dbh->rollback if $oldAutoCommit; + } else { + dbh->commit if $oldAutoCommit; + } + $error; +} =item delete @@ -102,16 +154,56 @@ Delete this record from the database. =cut -# inherited +sub delete { + my $self = shift; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + + my $error; + my $extension = $self->_extension; + if ( $extension ) { + $error = $extension->delete; + } + + $error ||= $self->SUPER::delete; + + if ( $error ) { + dbh->rollback if $oldAutoCommit; + } else { + dbh->commit if $oldAutoCommit; + } + $error; +} -=item replace [ OLD_RECORD ] [ CONTENT ] +=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 -# inherited +sub replace { + my $new = shift; + my $old = shift || $new->replace_old; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + + my $error = $new->SUPER::replace($old, @_); + + my $extension = $new->_extension; + if ( $extension ) { + $error ||= $extension->replace; + } + + if ( $error ) { + dbh->rollback if $oldAutoCommit; + } else { + dbh->commit if $oldAutoCommit; + } + $error; +} sub replace_check { my $self = shift; diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index f8ebfa06c..e6d5a5a99 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -448,17 +448,10 @@ sub content { =cut -=back - -=head2 CLASS METHODS - -=over 4 - =item send_prepared CUST_MSG -Takes the CUST_MSG object and sends it to its recipient. This is a class -method because everything needed to send the message is stored in the -CUST_MSG already. +Takes the CUST_MSG object and sends it to its recipient. The "smtpmachine" +configuration option will be used to find the outgoing mail server. =cut diff --git a/FS/FS/msg_template/http.pm b/FS/FS/msg_template/http.pm new file mode 100644 index 000000000..51dfcffc2 --- /dev/null +++ b/FS/FS/msg_template/http.pm @@ -0,0 +1,155 @@ +package FS::msg_template::http; +use base qw( FS::msg_template ); + +use strict; +use vars qw( $DEBUG $conf ); + +# needed to talk to the external service +use LWP::UserAgent; +use HTTP::Request::Common; +use JSON; + +# needed to manage prepared messages +use FS::cust_msg; + +our $DEBUG = 1; +our $me = '[FS::msg_template::http]'; + +sub extension_table { 'msg_template_http' } + +=head1 NAME + +FS::msg_template::http - Send messages via a web service. + +=head1 DESCRIPTION + +FS::msg_template::http is a message processor in which the message is exported +to a web service, at both the prepare and send stages. + +=head1 METHODS + +=cut + +sub check { + my $self = shift; + return + $self->ut_textn('prepare_url') + || $self->ut_textn('send_url') + || $self->ut_textn('username') + || $self->ut_textn('password') + || $self->ut_anything('content') + || $self->SUPER::check; +} + +sub prepare { + + my( $self, %opt ) = @_; + + my $json = JSON->new->canonical(1); + + my $cust_main = $opt{'cust_main'}; # or die 'cust_main required'; + my $object = $opt{'object'} or die 'object required'; + + my $hashref = $self->prepare_substitutions(%opt); + + my $document = $json->decode( $self->content || '{}' ); + $document = { + 'msgname' => $self->msgname, + 'msgtype' => $opt{'msgtype'}, + %$document, + %$hashref + }; + + my $request_content = $json->encode($document); + warn "$me ".$self->prepare_url."\n" if $DEBUG; + warn "$request_content\n\n" if $DEBUG > 1; + my $ua = LWP::UserAgent->new; + my $request = POST( + $self->prepare_url, + 'Content-Type' => 'application/json', + 'Content' => $request_content, + ); + if ( $self->username ) { + $request->authorization_basic( $self->username, $self->password ); + } + my $response = $ua->request($request); + warn "$me received:\n" . $response->as_string . "\n\n" if $DEBUG; + + my $cust_msg = FS::cust_msg->new({ + 'custnum' => $cust_main->custnum, + 'msgnum' => $self->msgnum, + '_date' => time, + 'msgtype' => ($opt{'msgtype'} || ''), + }); + + if ( $response->is_success ) { + $cust_msg->set(body => $response->decoded_content); + $cust_msg->set(status => 'prepared'); + } else { + $cust_msg->set(status => 'failed'); + $cust_msg->set(error => $response->decoded_content); + } + + $cust_msg; +} + +=item send_prepared CUST_MSG + +Takes the CUST_MSG object and sends it to its recipient. + +=cut + +sub send_prepared { + my $self = shift; + my $cust_msg = shift or die "cust_msg required"; + # don't just fail if called as a class method + if (!ref $self) { + $self = $cust_msg->msg_template; + } + + # use cust_msg->header for anything? we _could_... + my $request_content = $cust_msg->body; + + warn "$me ".$self->send_url."\n" if $DEBUG; + warn "$request_content\n\n" if $DEBUG > 1; + my $ua = LWP::UserAgent->new; + my $request = POST( + $self->send_url, + 'Content-Type' => 'application/json', + 'Content' => $request_content, + ); + if ( $self->username ) { + $request->authorization_basic( $self->username, $self->password ); + } + my $response = $ua->request($request); + warn "$me received:\n" . $response->as_string . "\n\n" if $DEBUG; + + my $error; + if ( $response->is_success ) { + $cust_msg->set(status => 'sent'); + } else { + $error = $response->decoded_content; + $cust_msg->set(error => $error); + $cust_msg->set(status => 'failed'); + } + + if ( $cust_msg->custmsgnum ) { + $cust_msg->replace; + } else { + $cust_msg->insert; + } + + $error; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; diff --git a/bin/msg_template_http-demo.pl b/bin/msg_template_http-demo.pl new file mode 100755 index 000000000..8d184fc85 --- /dev/null +++ b/bin/msg_template_http-demo.pl @@ -0,0 +1,76 @@ +=head1 NAME + +FS::msg_template::http example server. + +=head1 DESCRIPTION + +This is an incredibly crude Mojo web service for demonstrating how to talk +to the HTTP customer messaging interface in Freeside. + +It implements an endpoint for the "password reset" messaging case which +creates a simple password reset message using some template variables, +and a "send" endpoint that just delivers the message by sendmail. The +configuration to use this as your password reset handler would be: + +prepare_url = 'http://localhost:3000/prepare/password_reset' +send_url = 'http://localhost:3000/send' +No username, no password, no additional content. + +=cut + +use Mojolicious::Lite; +use Mojo::JSON qw(decode_json encode_json); +use Email::Simple; +use Email::Simple::Creator; +use Email::Sender::Simple qw(sendmail); + +post '/prepare/password_reset' => sub { + my $self = shift; + + my $json_data = $self->req->body; + #print STDERR $json_data; + my $input = decode_json($json_data); + if ( $input->{username} ) { + my $output = { + 'to' => $input->{invoicing_email}, + 'subject' => "Password reset for $input->{username}", + 'body' => " +To complete your $input->{company_name} password reset, please go to +$input->{selfservice_server_base_url}/selfservice.cgi?action=process_forgot_password;session_id=$input->{session_id} + +This link will expire in 24 hours.", + }; + + return $self->render( json => $output ); + + } else { + + return $self->render( text => 'Username required', status => 500 ); + + } +}; + +post '/send' => sub { + my $self = shift; + + my $json_data = $self->req->body; + my $input = decode_json($json_data); + my $email = Email::Simple->create( + header => [ + From => $ENV{USER}.'@localhost', + To => $input->{to}, + Subject => $input->{subject}, + ], + body => $input->{body}, + ); + local $@; + eval { sendmail($email) }; + if ( $@ ) { + return $self->render( text => $@->message, status => 500 ); + } else { + return $self->render( text => '' ); + } +}; + +app->start; + -- cgit v1.2.1 From 2631ae913a1546b2f54f1355017e34b8b4a088bd Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 29 Aug 2015 13:58:58 -0700 Subject: #21564: queueable sending --- FS/FS/cust_msg.pm | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/FS/FS/cust_msg.pm b/FS/FS/cust_msg.pm index ec2c961a3..db026808c 100644 --- a/FS/FS/cust_msg.pm +++ b/FS/FS/cust_msg.pm @@ -205,6 +205,25 @@ sub parts { =back +=head1 SUBROUTINES + +=over 4 + +=item process_send CUSTMSGNUM + +Given a C value, sends the message. It must already +have been prepared (via L). + +=cut + +sub process_send { + my $custmsgnum = shift; + my $cust_msg = FS::cust_msg->by_key($custmsgnum) + or die "cust_msg #$custmsgnum not found"; + my $error = $cust_msg->send; + die $error if $error; +} + =head1 SEE ALSO L, L, L. -- cgit v1.2.1 From 1d4c8f82596a7f3695ba2203e301e497dec9194c Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Sat, 29 Aug 2015 17:17:44 -0500 Subject: RT#37064: Add action link to manually refund a payment [fixed redir link] --- httemplate/edit/process/cust_refund.cgi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httemplate/edit/process/cust_refund.cgi b/httemplate/edit/process/cust_refund.cgi index ce72c9253..6ad468b6c 100755 --- a/httemplate/edit/process/cust_refund.cgi +++ b/httemplate/edit/process/cust_refund.cgi @@ -12,7 +12,7 @@ % } else { -<% $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum;show=payment_history") %> +<% $cgi->redirect(popurl(3). "view/cust_main.cgi?custnum=$custnum;show=payment_history") %> % } %} <%init> -- cgit v1.2.1 From beeeec140a0479d5757031d9ace0e40871d41d22 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sat, 29 Aug 2015 16:00:43 -0700 Subject: eliminate "defined(@array) is deprecated" warnings --- FS/FS/Record.pm | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index d6892a96c..fafceacb5 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -1300,8 +1300,7 @@ sub insert { my $table = $self->table; # Encrypt before the database - if ( defined(eval '@FS::'. $table . '::encrypted_fields') - && scalar( eval '@FS::'. $table . '::encrypted_fields') + if ( scalar( eval '@FS::'. $table . '::encrypted_fields') && $conf_encryption ) { foreach my $field (eval '@FS::'. $table . '::encrypted_fields') { @@ -1543,9 +1542,8 @@ sub replace { # Encrypt for replace my $saved = {}; - if ( $conf_encryption - && defined(eval '@FS::'. $new->table . '::encrypted_fields') - && scalar( eval '@FS::'. $new->table . '::encrypted_fields') + if ( scalar( eval '@FS::'. $new->table . '::encrypted_fields') + && $conf_encryption ) { foreach my $field (eval '@FS::'. $new->table . '::encrypted_fields') { next if $field eq 'payinfo' -- cgit v1.2.1 From 42292b1e5813f6e9657f08137dffc68a3c810b01 Mon Sep 17 00:00:00 2001 From: Rob Van Dam Date: Thu, 6 Aug 2015 16:56:15 -0600 Subject: Renamed $br to $br_permonth to clarify value is base_recur_permonth, NOT base_recur --- FS/FS/part_pkg/discount_Mixin.pm | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/FS/FS/part_pkg/discount_Mixin.pm b/FS/FS/part_pkg/discount_Mixin.pm index 31802758c..47cb2516b 100644 --- a/FS/FS/part_pkg/discount_Mixin.pm +++ b/FS/FS/part_pkg/discount_Mixin.pm @@ -40,8 +40,8 @@ sub calc_discount { my($self, $cust_pkg, $sdate, $details, $param ) = @_; my $conf = new FS::Conf; - my $br = $self->base_recur_permonth($cust_pkg, $sdate); - $br += $param->{'override_charges'} if $param->{'override_charges'}; + my $br_permonth = $self->base_recur_permonth($cust_pkg, $sdate); + $br_permonth += $param->{'override_charges'} if $param->{'override_charges'}; my $tot_discount = 0; #UI enforces just 1 for now, will need ordering when they can be stacked @@ -83,7 +83,7 @@ sub calc_discount { my $amount = 0; $amount += $discount->amount if $cust_pkg->pkgpart == $param->{'real_pkgpart'}; - $amount += sprintf('%.2f', $discount->percent * $br / 100 ); + $amount += sprintf('%.2f', $discount->percent * $br_permonth / 100 ); my $chg_months = defined($param->{'months'}) ? $param->{'months'} : $cust_pkg->part_pkg->freq; @@ -133,7 +133,7 @@ sub calc_discount { }; } - $amount = min($amount, $br); + $amount = min($amount, $br_permonth); $amount *= $months; } @@ -147,9 +147,9 @@ sub calc_discount { && !defined $param->{'setup_charge'} ) { - $discount_left = $br - $amount; + $discount_left = $br_permonth - $amount; if ( $discount_left < 0 ) { - $amount = $br; + $amount = $br_permonth; $param->{'discount_left_setup'}{$discount->discountnum} = 0 - $discount_left; } @@ -188,7 +188,7 @@ sub calc_discount { #} #push @$details, $d; - #push @$details, sprintf( $format, $money_char, $br ); + #push @$details, sprintf( $format, $money_char, $br_permonth ); } -- cgit v1.2.1 From 0f8882fa700977c08688d3bdf6412ad06f0e618e Mon Sep 17 00:00:00 2001 From: Rob Van Dam Date: Thu, 6 Aug 2015 17:05:28 -0600 Subject: Changed min() call to reduce possibility of rounding error returning a discount > base_recur. Also added comments for other possible problem areas --- FS/FS/part_pkg/discount_Mixin.pm | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/FS/FS/part_pkg/discount_Mixin.pm b/FS/FS/part_pkg/discount_Mixin.pm index 47cb2516b..abde93f8f 100644 --- a/FS/FS/part_pkg/discount_Mixin.pm +++ b/FS/FS/part_pkg/discount_Mixin.pm @@ -42,7 +42,10 @@ sub calc_discount { my $br_permonth = $self->base_recur_permonth($cust_pkg, $sdate); $br_permonth += $param->{'override_charges'} if $param->{'override_charges'}; - + + my $br = $self->base_recur($cust_pkg, $sdate); + $br += $param->{'override_charges'} * ($cust_pkg->part_pkg->freq || 0) if $param->{'override_charges'}; + my $tot_discount = 0; #UI enforces just 1 for now, will need ordering when they can be stacked @@ -83,7 +86,7 @@ sub calc_discount { my $amount = 0; $amount += $discount->amount if $cust_pkg->pkgpart == $param->{'real_pkgpart'}; - $amount += sprintf('%.2f', $discount->percent * $br_permonth / 100 ); + $amount += sprintf('%.2f', $discount->percent * $br_permonth / 100 ); # FIXME: should this use $br / $freq to avoid rounding errors? my $chg_months = defined($param->{'months'}) ? $param->{'months'} : $cust_pkg->part_pkg->freq; @@ -133,8 +136,7 @@ sub calc_discount { }; } - $amount = min($amount, $br_permonth); - $amount *= $months; + $amount = min($amount * $months, $br); } $amount = sprintf('%.2f', $amount + 0.00000001 ); #so 1.005 rounds to 1.01 @@ -147,9 +149,9 @@ sub calc_discount { && !defined $param->{'setup_charge'} ) { - $discount_left = $br_permonth - $amount; + $discount_left = $br_permonth - $amount; # FIXME: $amount is no longer permonth at this point! if ( $discount_left < 0 ) { - $amount = $br_permonth; + $amount = $br_permonth; # FIXME: seems like this should *= $months $param->{'discount_left_setup'}{$discount->discountnum} = 0 - $discount_left; } -- cgit v1.2.1 From d622148c3ad8e98aef7ae1ca163e28483e023609 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 29 Aug 2015 21:26:34 -0700 Subject: fix typo --- httemplate/search/elements/search.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html index beb017300..a279f5327 100644 --- a/httemplate/search/elements/search.html +++ b/httemplate/search/elements/search.html @@ -348,8 +348,8 @@ if ( $opt{'disableable'} ) { sub { shift->disabled ? 'FF0000' : '00CC00'; }; splice @{ $opt{'links'} }, $pos, 0, '' if $opt{'links'}; - splice @{ $opt{'link_onlicks'} }, $pos, 0, '' - if $opt{'link_onlicks'}; + splice @{ $opt{'link_onclicks'} }, $pos, 0, '' + if $opt{'link_onclicks'}; } #add show/hide disabled links -- cgit v1.2.1 From 9fd03716b831bd00a725a63edbe19cfe6b88aea0 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 29 Aug 2015 21:27:20 -0700 Subject: #21564: user interface for REST client --- FS/FS/msg_template.pm | 15 +- httemplate/browse/msg_template.html | 65 ----- httemplate/browse/msg_template/email.html | 74 ++++++ httemplate/browse/msg_template/http.html | 68 ++++++ httemplate/edit/msg_template.html | 383 +---------------------------- httemplate/edit/msg_template/email.html | 385 ++++++++++++++++++++++++++++++ httemplate/edit/msg_template/http.html | 82 +++++++ httemplate/edit/process/msg_template.html | 18 +- httemplate/elements/menu.html | 2 +- 9 files changed, 642 insertions(+), 450 deletions(-) delete mode 100644 httemplate/browse/msg_template.html create mode 100644 httemplate/browse/msg_template/email.html create mode 100644 httemplate/browse/msg_template/http.html create mode 100644 httemplate/edit/msg_template/email.html create mode 100644 httemplate/edit/msg_template/http.html diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index 827bb9883..4c2ac4bd4 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -5,7 +5,7 @@ use strict; use vars qw( $DEBUG $conf ); use FS::Conf; -use FS::Record qw( qsearch qsearchs ); +use FS::Record qw( qsearch qsearchs dbh ); use FS::cust_msg; use FS::template_content; @@ -95,11 +95,16 @@ sub _rebless { eval "use $class;"; bless($self, $class) unless $@; - # merge in the extension fields + # merge in the extension fields (but let fields in $self override them) + # except don't ever override the extension's primary key, it's immutable if ( $self->msgnum and $self->extension_table ) { my $extension = $self->_extension; if ( $extension ) { - $self->{Hash} = { $self->hash, $extension->hash }; + my $ext_key = $extension->get($extension->primary_key); + $self->{Hash} = { $extension->hash, + $self->hash, + $extension->primary_key => $ext_key + }; } } @@ -194,6 +199,8 @@ sub replace { my $extension = $new->_extension; if ( $extension ) { + # merge changes into the extension record and replace it + $extension->{Hash} = { $extension->hash, $new->hash }; $error ||= $extension->replace; } @@ -212,7 +219,7 @@ sub replace_check { if ( $old->msgclass ) { if ( !$self->msgclass ) { $self->set('msgclass', $old->msgclass); - } else { + } elsif ( $old->msgclass ne $self->msgclass ) { return "Can't change message template class from ".$old->msgclass. " to ".$self->msgclass."."; } diff --git a/httemplate/browse/msg_template.html b/httemplate/browse/msg_template.html deleted file mode 100644 index 1646bc169..000000000 --- a/httemplate/browse/msg_template.html +++ /dev/null @@ -1,65 +0,0 @@ -<% include( 'elements/browse.html', - 'title' => 'Message templates', - 'name_singular' => 'template', - 'menubar' => \@menubar, - 'query' => { 'table' => 'msg_template', }, - 'count_query' => 'SELECT COUNT(*) FROM msg_template', - 'disableable' => 1, - 'disabled_statuspos' => (scalar(@locales) + 3), - 'agent_virt' => 1, - 'agent_null_right' => ['View global templates','Edit global templates'], - 'agent_pos' => 1, - 'header' => [ 'Name', '', map ('', @locales), '' ], - 'fields' => [ 'msgname', @locales, $disable_link_label ], - 'links' => [ $link, @locale_links, '' ], - 'link_onclicks' => [ '', map('', @locale_links), $disable_link ], - 'cell_style' => [ '', '', map ($locale_style, @locales), $locale_style ], - ) -%> -<%init> - -my $curuser = $FS::CurrentUser::CurrentUser; - -die "access denied" - unless $curuser->access_right([ 'View templates', 'View global templates', - 'Edit templates', 'Edit global templates', ]); - -my @menubar = (); -if ( $curuser->access_right(['Edit templates', 'Edit global templates']) ) { - push @menubar, 'Add a new template' => $p.'edit/msg_template.html'; -} -push @menubar, 'View template images' => $p.'browse/template_image.html'; - -my $link = [ "${p}edit/msg_template.html?msgnum=", 'msgnum' ]; - -my $locale_style = 'font-size:0.8em; padding:3px'; - -my (@locales, @locale_links); -foreach my $l ( FS::Locales->locales ) { - push @locales, sub { - exists ( $_[0]->content_locales->{$l} ) - ? +{ FS::Locales->locale_info($l) }->{'label'} - : ''; - }; - push @locale_links, sub { - my $content = $_[0]->content_locales->{$l} or return ''; - [ "${p}edit/msg_template.html?locale=$l;msgnum=", 'msgnum' ]; - }; -} - -my $disable_link = sub { - my $template = shift; - include('/elements/popup_link_onclick.html', - action => $p.'misc/disable-msg_template.cgi?msgnum=' . - $template->msgnum . - ($template->disabled ? ';enable=1' : ''), - actionlabel => 'Disable template', - ); -}; - -my $disable_link_label = sub { - my $template = shift; - $template->disabled ? '(enable)' : '(disable)' ; -}; - - diff --git a/httemplate/browse/msg_template/email.html b/httemplate/browse/msg_template/email.html new file mode 100644 index 000000000..d0ef4e3e9 --- /dev/null +++ b/httemplate/browse/msg_template/email.html @@ -0,0 +1,74 @@ +<& /browse/elements/browse.html, + 'title' => 'Message templates', + 'name_singular' => 'template', + 'menubar' => \@menubar, + 'query' => $query, + 'count_query' => $count_query, + 'disableable' => 1, + 'disabled_statuspos' => (scalar(@locales) + 3), + 'agent_virt' => 1, + 'agent_null_right' => ['View global templates','Edit global templates'], + 'agent_pos' => 1, + 'header' => [ 'Name', '', map ('', @locales), '' ], + 'fields' => [ 'msgname', @locales, $disable_link_label ], + 'links' => [ $link, @locale_links, '' ], + 'link_onclicks' => [ '', map('', @locale_links), $disable_link ], + 'cell_style' => [ '', '', map ($locale_style, @locales), $locale_style ], +&> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right([ 'View templates', 'View global templates', + 'Edit templates', 'Edit global templates', ]); + +my @menubar = (); +if ( $curuser->access_right(['Edit templates', 'Edit global templates']) ) { + push @menubar, 'Add a new template' => $fsurl.'edit/msg_template/email.html'; +} +push @menubar, 'Template images' => $fsurl.'browse/template_image.html'; + +push @menubar, 'External message interfaces' => $fsurl.'browse/msg_template/http.html'; + +my $query = { + 'table' => 'msg_template', + 'select' => '*', + 'hashref' => { 'msgclass' => 'email' }, +}; + +my $count_query = "SELECT COUNT(*) FROM msg_template WHERE msgclass = 'email'"; + +my $link = [ $fsurl.'edit/msg_template/email.html?msgnum=', 'msgnum' ]; + +my $locale_style = 'font-size:0.8em; padding:3px'; + +my (@locales, @locale_links); +foreach my $l ( FS::Locales->locales ) { + push @locales, sub { + exists ( $_[0]->content_locales->{$l} ) + ? +{ FS::Locales->locale_info($l) }->{'label'} + : ''; + }; + push @locale_links, sub { + my $content = $_[0]->content_locales->{$l} or return ''; + [ $fsurl."edit/msg_template/email.html?locale=$l;msgnum=", 'msgnum' ]; + }; +} + +my $disable_link = sub { + my $template = shift; + include('/elements/popup_link_onclick.html', + action => $fsurl.'misc/disable-msg_template.cgi?msgnum=' . + $template->msgnum . + ($template->disabled ? ';enable=1' : ''), + actionlabel => 'Disable template', + ); +}; + +my $disable_link_label = sub { + my $template = shift; + $template->disabled ? '(enable)' : '(disable)' ; +}; + + diff --git a/httemplate/browse/msg_template/http.html b/httemplate/browse/msg_template/http.html new file mode 100644 index 000000000..888fda441 --- /dev/null +++ b/httemplate/browse/msg_template/http.html @@ -0,0 +1,68 @@ +<& /browse/elements/browse.html, + 'title' => 'External message interfaces', + 'name_singular' => 'interface', # what else do we call them? + 'menubar' => \@menubar, + 'query' => $query, + 'count_query' => $count_query, + 'disableable' => 1, + 'disabled_statuspos' => 4, + 'agent_virt' => 1, + 'agent_null_right' => ['View global templates','Edit global templates'], + 'agent_pos' => 1, + 'header' => [ 'Name', + # 'Agent', + 'Prepare', + 'Send', + '' ], + 'fields' => [ 'msgname', + 'prepare_url', + 'send_url', + $disable_link_label + ], + 'links' => [ $link, ], + 'link_onclicks' => [ '', '', '', $disable_link ], + 'cell_style' => [ '', '', $url_style, $url_style ], +&> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right([ 'View templates', 'View global templates', + 'Edit templates', 'Edit global templates', ]); + +my @menubar = (); +if ( $curuser->access_right(['Edit templates', 'Edit global templates']) ) { + push @menubar, 'Add a new interface' => $fsurl.'edit/msg_template/http.html'; +} +push @menubar, 'Email templates' => $fsurl.'browse/msg_template/email.html'; +push @menubar, 'Template images' => $fsurl.'browse/template_image.html'; + +my $query = { + 'table' => 'msg_template', + 'select' => '*', + 'hashref' => { 'msgclass' => 'http' }, +}; + +my $count_query = "SELECT COUNT(*) FROM msg_template WHERE msgclass = 'http'"; + +my $link = [ $fsurl.'edit/msg_template/http.html?msgnum=', 'msgnum' ]; + +my $url_style = 'font-size:0.8em; padding:3px'; # also for (disable) label + +my $disable_link = sub { + my $template = shift; + include('/elements/popup_link_onclick.html', + action => $fsurl.'misc/disable-msg_template.cgi?msgnum=' . + $template->msgnum . + ($template->disabled ? ';enable=1' : ''), + actionlabel => 'Disable template', + ); +}; + +my $disable_link_label = sub { + my $template = shift; + $template->disabled ? '(enable)' : '(disable)' ; +}; + + diff --git a/httemplate/edit/msg_template.html b/httemplate/edit/msg_template.html index df72c5b66..889b10731 100644 --- a/httemplate/edit/msg_template.html +++ b/httemplate/edit/msg_template.html @@ -1,380 +1,9 @@ -<& elements/edit.html, - 'html_init' => '$sidebar
', - 'body_etc' => $body_etc, - 'name_singular' => 'template', - 'table' => 'msg_template', - 'viewall_dir' => 'browse', - 'agent_virt' => 1, - 'agent_null' => 1, - 'agent_null_right' => [ 'View global templates', 'Edit global templates' ], - - 'fields' => \@fields, - 'labels' => { - 'msgnum' => 'Template', - 'agentnum' => 'Agent', - 'msgname' => 'Template name', - 'from_addr' => 'From: ', - 'bcc_addr' => 'Bcc: ', - 'locale' => 'Locale', - 'subject' => 'Subject: ', - 'body' => 'Message body', - }, - 'edit_callback' => \&edit_callback, - 'error_callback' => \&edit_callback, - 'html_bottom' => '', - 'html_table_bottom'=> \&html_table_bottom, - 'html_foot' => ( $no_submit ? '' : "
" ), - 'no_submit' => $no_submit, -&> <%init> -use FS::template_image; - -my $curuser = $FS::CurrentUser::CurrentUser; - -die "access denied" - unless $curuser->access_right([ 'View templates', 'View global templates', - 'Edit templates', 'Edit global templates', - ]); - -my $body_etc = ''; -$body_etc = q!onload="document.getElementById('locale').onchange()"! - if $cgi->param('locale') eq 'new'; - -my $msgnum = $cgi->param('msgnum'); -my $msg_template = $msgnum ? qsearchs('msg_template', {msgnum=>$msgnum}) : ''; - -my $no_submit = 0; -my @fields = (); -if ( $curuser->access_right('Edit global templates') - || ( $curuser->access_right('Edit templates') - && $msg_template - && $msg_template->agentnum - && $curuser->agentnums_href->{$msg_template->agentnum} - ) - ) -{ - push @fields, - { field => 'agentnum', - type => 'select-agent', - }, - { field => 'msgname', size=>60, }, - { field => 'from_addr', size=>60, }, - { field => 'bcc_addr', size=>60, }, - { type => 'tablebreak-tabs', - include_opt_callback => \&menubar_opt_callback, - }, - # template_content fields - { field => 'locale', type => 'hidden' }, - { field => 'subject', size=>60, }, - { field => 'body', - type => 'htmlarea', - width => 763, - config=> { extraPlugins => 'blockprotect' }, - }, - ; -} else { #readonly - - $no_submit = 1; - - push @fields, - { field => 'agentnum', - type => 'select-agent', - fixed => 1, - }, - { field => 'msgname', type => 'fixed', }, - { field => 'from_addr', type => 'fixed', }, - { field => 'bcc_addr', type => 'fixed', }, - { type => 'tablebreak-tabs', - include_opt_callback => \&menubar_opt_callback, - }, - # template_content fields - { field => 'locale', type => 'hidden' }, - { field => 'subject', type => 'fixed', }, - { field => 'body', - type => 'fixed', - noescape => 1, - }, - ; - +my $msgclass = 'email'; +if ( $cgi->param('msgnum') =~ /^(\d+)$/ ) { + my $msg_template = FS::msg_template->by_key($1) + or die "unknown msgnum $1"; + $msgclass = $msg_template->msgclass; } - -sub new_callback { - my ($cgi, $object, $fields_listref, $opt_hashref) = @_; - my $template_content = new FS::template_content { 'locale' => '' }; - $object->{'Hash'} = { $object->hash, $template_content->hash }; -} - -sub edit_callback { - my ($cgi, $object, $fields_listref, $opt_hashref) = @_; - $cgi->param('locale') =~ /^(\w*)$/ or die 'bad locale '.$cgi->param('locale'); - my $locale = $1; - - # fetch the content object and merge its fields - my %args = ( - 'msgnum' => $object->msgnum, - 'locale' => $locale - ); - my $template_content = qsearchs('template_content', \%args) - || new FS::template_content( { %args }); - $object->{'Hash'} = { $object->hash, $template_content->hash }; - - # set up the locale selector if this is a new content - if ( $locale eq 'new' ) { - - # make a list of available locales - my $content_locales = $object->content_locales; - my @locales = grep { !exists($content_locales->{$_}) } - FS::Conf->new->config('available-locales'); - my %labels; - foreach (@locales) { - my %info = FS::Locales->locale_info($_); - $labels{$_} = $info{'label'}; - } - unshift @locales, 'new'; - $labels{'new'} = 'Select language'; - - # insert a field def - my $i = 0; - $i++ until ( $fields_listref->[$i]->{'field'} eq 'locale' ); - my $locale_field = $fields_listref->[$i]; - - my $onchange_locale = "document.getElementById('submit').disabled = - (this.options[this.selectedIndex].value == 'new');"; - - %$locale_field = ( - field => 'locale', - type => 'select', - options => \@locales, - labels => \%labels, - curr_value => 'new', - onchange => $onchange_locale, - ); - } -} - -sub menubar_opt_callback { - my $object = shift; - # generate no tabs for new msg_templates. - my $msgnum = $object->msgnum or return; - my (@tabs, @options, %labels); - push @tabs, mt('Default'), ''; - my $display_new = 0; - my $selected = ''; - foreach my $l (FS::Locales->locales) { - if ( exists $object->content_locales->{$l} ) { - my %info = FS::Locales->locale_info($l); - push @tabs, - $info{'label'}, - ';locale='.$l; - $selected = $info{'label'} if $object->locale eq $l; - } - else { - $display_new = 1; # there is at least one unused locale left - } - } - push @tabs, mt('New'), ';locale=new' if $display_new; - $selected = mt('New') if $object->locale eq 'new'; - $selected ||= mt('Default'); - ( - 'url_base' => $p.'edit/msg_template.html?msgnum='.$msgnum, - 'selected' => $selected, - 'tabs' => \@tabs - ); -} - -my $onchange_locale = ''; - -# Create hints pane - -my %substitutions = ( - 'cust_main' => [ - '$display_custnum'=> 'Customer#', - '$agentnum' => 'Agent#', - '$agent_name' => 'Agent name', - '$payby' => 'Payment method', - '$paymask' => 'Card/account# (masked)', - '$payname' => 'Name on card/bank name', - '$paytype' => 'Account type', - '$payip' => 'IP address used to submit payment info', - '$num_ncancelled_pkgs' => '# of active packages', - '$num_cancelled_pkgs' => '# of cancelled packages', - '$num_pkgs' => '# of packages', - '$classname' => 'Customer class', - '$categoryname' => 'Customer category', - '$balance' => 'Current balance', - '$credit_limit' => 'Credit limit', - '$invoicing_list_emailonly' => 'Billing email address', - #'$cust_status' => 'Status (raw internal label)', - '$cust_status_label' => 'Status (display label)', - '$cust_statuscolor' => 'Status color code', - '$company_name' => 'Our company name', - '$company_address'=> 'Our company address', - '$company_phonenum' => 'Our phone number', - '$selfservice_server_base_url' => 'Base URL of customer self-service', - ], - 'contact' => [ # duplicate this for shipping - '$name' => 'Company and contact name', - '$name_short' => 'Company or contact name', - '$company' => 'Company name', - '$contact' => 'Contact name (last, first)', - '$contact_firstlast'=> 'Contact name (first last)', - '$first' => 'First name', - '$last' => 'Last name', - '$address1' => 'Address line 1', - '$address2' => 'Address line 2', - '$city' => 'City', - '$county' => 'County', - '$state' => 'State', - '$zip' => 'Zip', - '$country' => 'Country', - '$daytime' => 'Day phone', - '$night' => 'Night phone', - '$mobile' => 'Mobile phone', - '$fax' => 'Fax', - ], - 'service' => [ - '$ship_address1' => 'Address line 1', - '$ship_address2' => 'Address line 2', - '$ship_city' => 'City', - '$ship_county' => 'County', - '$ship_state' => 'State', - '$ship_zip' => 'Zip', - '$ship_country' => 'Country', - ], - 'cust_bill' => [ - '$invnum' => 'Invoice#', - '$_date_pretty' => 'Invoice date', - '$due_date' => 'Invoice due date (timestamp)', - '$due_date2str' => 'Invoice due date (human readable)', - ], - 'cust_pkg' => [ - '$pkgnum' => 'Package#', - '$pkg' => 'Package description', - '$pkg_label' => 'Description + comment', - '$status' => 'Status', - '$statuscolor' => 'Status color code', - '$start_ymd' => 'Start date', - '$setup_ymd' => 'Setup date', - '$last_bill_ymd' => 'Last bill date', - '$next_bill_ymd' => 'Next bill date', - '$susp_ymd' => 'Suspended on date', - '$cancel_ymd' => 'Canceled on date', - '$adjourn_ymd' => 'Adjournment date', - '$expire_ymd' => 'Expiration date', - '$labels_short' => 'Service labels', - '$location_label' => 'Service location', - ], - 'svc_acct' => [ - '$svcnum' => 'Service#', - '$username' => 'Login name', - '$password' => 'Password', - '$domain' => 'Domain name', - ], - 'svc_domain' => [ - '$svcnum' => 'Service#', - '$domain' => 'Domain name', - '$registrar' => 'Registrar name', - '$catchall' => 'Catchall email', - ], - 'svc_phone' => [ - '$svcnum' => 'Service#', - '$phonenum' => 'Phone number', - '$countrycode' => 'Country code', - '$domain' => 'Domain name' - ], - 'svc_broadband' => [ - '$svcnum' => 'Service#', - '$ip_addr' => 'IP address', - '$mac_addr' => 'MAC address', - '$speed_up' => 'Upstream speed', - '$speed_down' => 'Downstream speed', - ], - 'cust_pay' => [ - '$paynum' => 'Payment#', - '$paid' => 'Amount', - '$payby' => 'Payment method', - '$date' => 'Payment date', - '$payinfo' => 'Card/account# (masked)', - '$error' => 'Decline reason', - ], -); - -tie my %sections, 'Tie::IxHash', ( -'contact' => 'Name and contact info (billing)', -'service' => 'Service address', -'cust_main' => 'Customer status and payment info', -'cust_pkg' => 'Package fields', -'cust_bill' => 'Invoice fields', -'cust_pay' => 'Payment fields', -'svc_acct' => 'Login service fields', -'svc_domain'=> 'Domain service fields', -'svc_phone' => 'Phone service fields', -'svc_broadband' => 'Broadband service fields', -); - -my $widget = new HTML::Widgets::SelectLayers( - 'options' => \%sections, - 'form_name' => 'dummy', - 'html_between'=>'', - 'selected_layer'=>(keys(%sections))[0], - 'layer_callback' => sub { - my $section = shift; - my $html = include('/elements/table-grid.html'); - my @hints = @{ $substitutions{$section} }; - while(@hints) { - my $key = shift @hints; - $html .= qq!\n$key!; - $html .= "\n".shift(@hints).''; - } - $html .= "\n"; - return $html; - }, -); - -my $sidebar = ' - -
-Substitutions: ' -. $widget->html . -'

Click above links to insert substitution code.

-

-Enclose substitutions and other Perl expressions in braces: -
{ $name } = ExampleCo (Smith, John) -
{ time2str("%D", time) } = '.time2str("%D", time).' -

'; -$sidebar .= include('/elements/template_image-dialog.html', - 'callback' => 'insertHtml' - ); -$sidebar .= '

Insert Uploaded Image

-
-'; - -sub html_table_bottom { - my $object = shift; - $cgi->param('locale') =~ /^(\w+)$/; - my $locale = $1; - my $html; - if ( $locale and $locale ne 'new' ) { - # set up a delete link - my $msgnum = $object->msgnum; - my $url = $p."misc/delete-template_content.html?msgnum=$msgnum;locale=$1"; - my $link = qq!! . - 'Delete this template' . - ''; - $html = qq! - $link!; - } - $html; -} - +print $cgi->redirect($fsurl."edit/msg_template/$msgclass.html?".$cgi->query_string); diff --git a/httemplate/edit/msg_template/email.html b/httemplate/edit/msg_template/email.html new file mode 100644 index 000000000..dc70ef6ec --- /dev/null +++ b/httemplate/edit/msg_template/email.html @@ -0,0 +1,385 @@ +<& /edit/elements/edit.html, + 'post_url' => $fsurl.'edit/process/msg_template.html', + 'html_init' => '$sidebar
', + 'body_etc' => $body_etc, + 'name_singular' => 'template', + 'table' => 'msg_template', + 'viewall_dir' => 'browse', + 'agent_virt' => 1, + 'agent_null' => 1, + 'agent_null_right' => [ 'View global templates', 'Edit global templates' ], + + 'fields' => \@fields, + 'labels' => { + 'msgnum' => 'Template', + 'agentnum' => 'Agent', + 'msgname' => 'Template name', + 'from_addr' => 'From: ', + 'bcc_addr' => 'Bcc: ', + 'locale' => 'Locale', + 'subject' => 'Subject: ', + 'body' => 'Message body', + }, + 'edit_callback' => \&edit_callback, + 'error_callback' => \&edit_callback, + 'html_bottom' => '', + 'html_table_bottom'=> \&html_table_bottom, + 'html_foot' => ( $no_submit ? '' : "
" ), + 'no_submit' => $no_submit, +&> +<%init> +use FS::template_image; + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right([ 'View templates', 'View global templates', + 'Edit templates', 'Edit global templates', + ]); + +my $body_etc = ''; +$body_etc = q!onload="document.getElementById('locale').onchange()"! + if $cgi->param('locale') eq 'new'; + +my $msgnum = $cgi->param('msgnum'); +my $msg_template = $msgnum ? qsearchs('msg_template', {msgnum=>$msgnum}) : ''; + +my $no_submit = 0; +my @fields = (); +if ( $curuser->access_right('Edit global templates') + || ( $curuser->access_right('Edit templates') + && $msg_template + && $msg_template->agentnum + && $curuser->agentnums_href->{$msg_template->agentnum} + ) + ) +{ + push @fields, + { field => 'msgclass', + type => 'hidden', + value => 'email', + }, + { field => 'agentnum', + type => 'select-agent', + }, + { field => 'msgname', size=>60, }, + { field => 'from_addr', size=>60, }, + { field => 'bcc_addr', size=>60, }, + { type => 'tablebreak-tabs', + include_opt_callback => \&menubar_opt_callback, + }, + # template_content fields + { field => 'locale', type => 'hidden' }, + { field => 'subject', size=>60, }, + { field => 'body', + type => 'htmlarea', + width => 763, + config=> { extraPlugins => 'blockprotect' }, + }, + ; +} else { #readonly + + $no_submit = 1; + + push @fields, + { field => 'agentnum', + type => 'select-agent', + fixed => 1, + }, + { field => 'msgname', type => 'fixed', }, + { field => 'from_addr', type => 'fixed', }, + { field => 'bcc_addr', type => 'fixed', }, + { type => 'tablebreak-tabs', + include_opt_callback => \&menubar_opt_callback, + }, + # template_content fields + { field => 'locale', type => 'hidden' }, + { field => 'subject', type => 'fixed', }, + { field => 'body', + type => 'fixed', + noescape => 1, + }, + ; + +} + +sub new_callback { + my ($cgi, $object, $fields_listref, $opt_hashref) = @_; + my $template_content = new FS::template_content { 'locale' => '' }; + $object->{'Hash'} = { $object->hash, $template_content->hash }; +} + +sub edit_callback { + my ($cgi, $object, $fields_listref, $opt_hashref) = @_; + $cgi->param('locale') =~ /^(\w*)$/ or die 'bad locale '.$cgi->param('locale'); + my $locale = $1; + + # fetch the content object and merge its fields + my %args = ( + 'msgnum' => $object->msgnum, + 'locale' => $locale + ); + my $template_content = qsearchs('template_content', \%args) + || new FS::template_content( { %args }); + $object->{'Hash'} = { $object->hash, $template_content->hash }; + + # set up the locale selector if this is a new content + if ( $locale eq 'new' ) { + + # make a list of available locales + my $content_locales = $object->content_locales; + my @locales = grep { !exists($content_locales->{$_}) } + FS::Conf->new->config('available-locales'); + my %labels; + foreach (@locales) { + my %info = FS::Locales->locale_info($_); + $labels{$_} = $info{'label'}; + } + unshift @locales, 'new'; + $labels{'new'} = 'Select language'; + + # insert a field def + my $i = 0; + $i++ until ( $fields_listref->[$i]->{'field'} eq 'locale' ); + my $locale_field = $fields_listref->[$i]; + + my $onchange_locale = "document.getElementById('submit').disabled = + (this.options[this.selectedIndex].value == 'new');"; + + %$locale_field = ( + field => 'locale', + type => 'select', + options => \@locales, + labels => \%labels, + curr_value => 'new', + onchange => $onchange_locale, + ); + } +} + +sub menubar_opt_callback { + my $object = shift; + # generate no tabs for new msg_templates. + my $msgnum = $object->msgnum or return; + my (@tabs, @options, %labels); + push @tabs, mt('Default'), ''; + my $display_new = 0; + my $selected = ''; + foreach my $l (FS::Locales->locales) { + if ( exists $object->content_locales->{$l} ) { + my %info = FS::Locales->locale_info($l); + push @tabs, + $info{'label'}, + ';locale='.$l; + $selected = $info{'label'} if $object->locale eq $l; + } + else { + $display_new = 1; # there is at least one unused locale left + } + } + push @tabs, mt('New'), ';locale=new' if $display_new; + $selected = mt('New') if $object->locale eq 'new'; + $selected ||= mt('Default'); + ( + 'url_base' => $fsurl.'edit/msg_template.html?msgnum='.$msgnum, + 'selected' => $selected, + 'tabs' => \@tabs + ); +} + +my $onchange_locale = ''; + +# Create hints pane + +my %substitutions = ( + 'cust_main' => [ + '$display_custnum'=> 'Customer#', + '$agentnum' => 'Agent#', + '$agent_name' => 'Agent name', + '$payby' => 'Payment method', + '$paymask' => 'Card/account# (masked)', + '$payname' => 'Name on card/bank name', + '$paytype' => 'Account type', + '$payip' => 'IP address used to submit payment info', + '$num_ncancelled_pkgs' => '# of active packages', + '$num_cancelled_pkgs' => '# of cancelled packages', + '$num_pkgs' => '# of packages', + '$classname' => 'Customer class', + '$categoryname' => 'Customer category', + '$balance' => 'Current balance', + '$credit_limit' => 'Credit limit', + '$invoicing_list_emailonly' => 'Billing email address', + #'$cust_status' => 'Status (raw internal label)', + '$cust_status_label' => 'Status (display label)', + '$cust_statuscolor' => 'Status color code', + '$company_name' => 'Our company name', + '$company_address'=> 'Our company address', + '$company_phonenum' => 'Our phone number', + '$selfservice_server_base_url' => 'Base URL of customer self-service', + ], + 'contact' => [ # duplicate this for shipping + '$name' => 'Company and contact name', + '$name_short' => 'Company or contact name', + '$company' => 'Company name', + '$contact' => 'Contact name (last, first)', + '$contact_firstlast'=> 'Contact name (first last)', + '$first' => 'First name', + '$last' => 'Last name', + '$address1' => 'Address line 1', + '$address2' => 'Address line 2', + '$city' => 'City', + '$county' => 'County', + '$state' => 'State', + '$zip' => 'Zip', + '$country' => 'Country', + '$daytime' => 'Day phone', + '$night' => 'Night phone', + '$mobile' => 'Mobile phone', + '$fax' => 'Fax', + ], + 'service' => [ + '$ship_address1' => 'Address line 1', + '$ship_address2' => 'Address line 2', + '$ship_city' => 'City', + '$ship_county' => 'County', + '$ship_state' => 'State', + '$ship_zip' => 'Zip', + '$ship_country' => 'Country', + ], + 'cust_bill' => [ + '$invnum' => 'Invoice#', + '$_date_pretty' => 'Invoice date', + '$due_date' => 'Invoice due date (timestamp)', + '$due_date2str' => 'Invoice due date (human readable)', + ], + 'cust_pkg' => [ + '$pkgnum' => 'Package#', + '$pkg' => 'Package description', + '$pkg_label' => 'Description + comment', + '$status' => 'Status', + '$statuscolor' => 'Status color code', + '$start_ymd' => 'Start date', + '$setup_ymd' => 'Setup date', + '$last_bill_ymd' => 'Last bill date', + '$next_bill_ymd' => 'Next bill date', + '$susp_ymd' => 'Suspended on date', + '$cancel_ymd' => 'Canceled on date', + '$adjourn_ymd' => 'Adjournment date', + '$expire_ymd' => 'Expiration date', + '$labels_short' => 'Service labels', + '$location_label' => 'Service location', + ], + 'svc_acct' => [ + '$svcnum' => 'Service#', + '$username' => 'Login name', + '$password' => 'Password', + '$domain' => 'Domain name', + ], + 'svc_domain' => [ + '$svcnum' => 'Service#', + '$domain' => 'Domain name', + '$registrar' => 'Registrar name', + '$catchall' => 'Catchall email', + ], + 'svc_phone' => [ + '$svcnum' => 'Service#', + '$phonenum' => 'Phone number', + '$countrycode' => 'Country code', + '$domain' => 'Domain name' + ], + 'svc_broadband' => [ + '$svcnum' => 'Service#', + '$ip_addr' => 'IP address', + '$mac_addr' => 'MAC address', + '$speed_up' => 'Upstream speed', + '$speed_down' => 'Downstream speed', + ], + 'cust_pay' => [ + '$paynum' => 'Payment#', + '$paid' => 'Amount', + '$payby' => 'Payment method', + '$date' => 'Payment date', + '$payinfo' => 'Card/account# (masked)', + '$error' => 'Decline reason', + ], +); + +tie my %sections, 'Tie::IxHash', ( +'contact' => 'Name and contact info (billing)', +'service' => 'Service address', +'cust_main' => 'Customer status and payment info', +'cust_pkg' => 'Package fields', +'cust_bill' => 'Invoice fields', +'cust_pay' => 'Payment fields', +'svc_acct' => 'Login service fields', +'svc_domain'=> 'Domain service fields', +'svc_phone' => 'Phone service fields', +'svc_broadband' => 'Broadband service fields', +); + +my $widget = new HTML::Widgets::SelectLayers( + 'options' => \%sections, + 'form_name' => 'dummy', + 'html_between'=>'
', + 'selected_layer'=>(keys(%sections))[0], + 'layer_callback' => sub { + my $section = shift; + my $html = include('/elements/table-grid.html'); + my @hints = @{ $substitutions{$section} }; + while(@hints) { + my $key = shift @hints; + $html .= qq!\n$key!; + $html .= "\n".shift(@hints).''; + } + $html .= "\n"; + return $html; + }, +); + +my $sidebar = ' + +
+Substitutions: ' +. $widget->html . +'

Click above links to insert substitution code.

+

+Enclose substitutions and other Perl expressions in braces: +
{ $name } = ExampleCo (Smith, John) +
{ time2str("%D", time) } = '.time2str("%D", time).' +

'; +$sidebar .= include('/elements/template_image-dialog.html', + 'callback' => 'insertHtml' + ); +$sidebar .= '

Insert Uploaded Image

+
+'; + +sub html_table_bottom { + my $object = shift; + $cgi->param('locale') =~ /^(\w+)$/; + my $locale = $1; + my $html; + if ( $locale and $locale ne 'new' ) { + # set up a delete link + my $msgnum = $object->msgnum; + my $url = $fsurl."misc/delete-template_content.html?msgnum=$msgnum;locale=$1"; + my $link = qq!! . + 'Delete this template' . + ''; + $html = qq! + $link!; + } + $html; +} + + diff --git a/httemplate/edit/msg_template/http.html b/httemplate/edit/msg_template/http.html new file mode 100644 index 000000000..e82cc0c60 --- /dev/null +++ b/httemplate/edit/msg_template/http.html @@ -0,0 +1,82 @@ +<& /edit/elements/edit.html, + 'post_url' => $fsurl.'edit/process/msg_template.html', + 'name_singular' => 'message interface', + 'table' => 'msg_template', + 'viewall_dir' => 'browse', + 'agent_virt' => 1, + 'agent_null' => 1, + 'agent_null_right' => [ 'View global templates', 'Edit global templates' ], + + 'fields' => [], # callback takes care of this + 'new_callback' => $edit_callback, + 'edit_callback' => $edit_callback, + 'error_callback' => $edit_callback, + 'labels' => \%labels, + 'no_submit' => $no_submit, +&> +<%init> +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right([ 'View templates', 'View global templates', + 'Edit templates', 'Edit global templates', + ]); + +my %labels = ( + 'msgnum' => 'Template', # it's still a template number + 'agentnum' => 'Agent', + 'msgname' => 'Interface name', + 'prepare_url' => 'Prepare URL', + 'send_url' => 'Send URL', + 'username' => 'HTTP username', + 'password' => 'HTTP password', + 'content' => 'Additional POST content', +); + +my $no_submit = 0; + +my $edit_callback = sub { + my ($cgi, $msg_template, $fields, $opt) = @_; + if ( $curuser->access_right('Edit global templates') + || ( $curuser->access_right('Edit templates') + && $msg_template + && $msg_template->agentnum + && $curuser->agentnums_href->{$msg_template->agentnum} + ) + ) { + @$fields = ( + { field => 'msgclass', + type => 'hidden', + value => 'http', + }, + { field => 'agentnum', + type => 'select-agent', + }, + { field => 'msgname', size=>60, required => 1 }, + { field => 'prepare_url', size=>60, required => 1 }, + { field => 'send_url', size=>60, required => 1 }, + { field => 'username', size=>20 }, + { field => 'password', size=>20 }, + { field => 'content', type => 'textarea' }, + ); + } else { #readonly + + $no_submit = 1; + + @$fields = ( + { field => 'agentnum', + type => 'select-agent', + fixed => 1, + }, + { field => 'msgname', type => 'fixed', }, + { field => 'prepare_url', type => 'fixed', }, + { field => 'send_url', type => 'fixed', }, + { field => 'username', type => 'fixed', }, + { field => 'password', type => 'fixed', }, + { field => 'content', type => 'fixed' }, + ); + + } +}; + + diff --git a/httemplate/edit/process/msg_template.html b/httemplate/edit/process/msg_template.html index e146adf76..d8b125ae0 100644 --- a/httemplate/edit/process/msg_template.html +++ b/httemplate/edit/process/msg_template.html @@ -1,7 +1,7 @@ <% include( 'elements/process.html', 'table' => 'msg_template', - 'viewall_dir' => 'browse', - #'popup_reload'=> 1, + 'fields' => $fields, + 'viewall_url' => "browse/msg_template/$msgclass.html", 'debug' => 0, 'precheck_callback' => \&precheck_callback, 'args_callback' => \&args_callback, @@ -11,9 +11,21 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right(['Edit templates','Edit global templates']); +my $msgclass = 'email'; +if ( $cgi->param('msgclass') =~ /^(\w+)$/ ) { + $msgclass = $1; +} + +my $fields = [ fields('msg_template') ]; +my $class = "FS::msg_template::$msgclass"; +eval "use $class;"; +if ( $class->extension_table ) { + push @$fields, fields($class->extension_table); +} + sub precheck_callback { my $cgi = shift; - # validate some fields + # validate locale field (for email-type records) $cgi->param('locale') =~ /^(\w*)$/; my $locale = $1; return mt('Language required') if $locale eq 'new'; # the user didn't choose diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index a5fb15bc2..55645cf31 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -767,7 +767,7 @@ tie my %config_nms, 'Tie::IxHash', ; tie my %config_misc, 'Tie::IxHash'; -$config_misc{'Message templates'} = [ $fsurl.'browse/msg_template.html', 'Templates for customer notices' ] +$config_misc{'Message templates'} = [ $fsurl.'browse/msg_template/email.html', 'Templates for customer notices' ] if $curuser->access_right(['View templates', 'View global templates', 'Edit templates', 'Edit global templates', ]); $config_misc{'Advertising sources'} = [ $fsurl.'browse/part_referral.html', 'Where a customer heard about your service.' ] -- cgit v1.2.1 From 1f343115f761ab39020a6aa76d3698fe4c8f2d61 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 29 Aug 2015 21:27:39 -0700 Subject: fix improper relative paths, incidental to #21564 --- httemplate/elements/htmlarea.html | 4 ++-- httemplate/elements/template_image-dialog.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/httemplate/elements/htmlarea.html b/httemplate/elements/htmlarea.html index 7c40e61c7..d8b25121a 100644 --- a/httemplate/elements/htmlarea.html +++ b/httemplate/elements/htmlarea.html @@ -12,7 +12,7 @@ Example: % #init - % #editor @@ -35,7 +35,7 @@ my $config = { 'skin' => 'kama', 'toolbarCanCollapse' => JSON::true, 'removePlugins' => 'elementspath', - 'basePath' => $p.'elements/ckeditor/', + 'basePath' => $fsurl.'elements/ckeditor/', 'enterMode' => 2, %{ $opt{config} || {} }, }; diff --git a/httemplate/elements/template_image-dialog.html b/httemplate/elements/template_image-dialog.html index f7fb0c291..b471d28da 100644 --- a/httemplate/elements/template_image-dialog.html +++ b/httemplate/elements/template_image-dialog.html @@ -14,7 +14,7 @@ url - to redirect to after upload, otherwise just refreshes dialog window <% include('/elements/xmlhttp.html', - 'url' => $p.'misc/xmlhttp-template_image.cgi', + 'url' => $fsurl.'misc/xmlhttp-template_image.cgi', 'subs' => [ 'get_template_image' ], ) %> @@ -75,7 +75,7 @@ url - to redirect to after upload, otherwise just refreshes dialog window <% include('/elements/form-file_upload.html', 'name' => 'TemplateImageUploadForm', 'id' => 'TemplateImageUploadForm', - 'action' => $p.'misc/process/template_image-upload.cgi', + 'action' => $fsurl.'misc/process/template_image-upload.cgi', 'num_files' => 1, 'fields' => [ 'name', 'agentnum' ], 'url' => $opt{'url'} || 'javascript:refreshImageList(1)', -- cgit v1.2.1 From 18b3f884eb44c9d0dea2cedc82c5788f7031e162 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sun, 30 Aug 2015 22:30:10 -0700 Subject: fix invoice deletion vs. cust_pay_batch records, #37837 --- FS/FS/cust_bill.pm | 16 ++++++++++++++-- FS/FS/cust_pay_batch.pm | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 7ea586a90..410fa7bf7 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -297,13 +297,13 @@ sub _delete { foreach my $table (qw( cust_credit_bill - cust_bill_pay - cust_pay_batch cust_bill_pay_batch + cust_bill_pay cust_bill_batch cust_bill_pkg )) { #cust_event # problematic + #cust_pay_batch # unnecessary foreach my $linked ( $self->$table() ) { my $error = $linked->delete; @@ -2913,6 +2913,18 @@ sub call_details { ( $header, grep { $_ ne $header } @details ); } +=item cust_pay_batch + +Returns all L records linked to this invoice. Deprecated, +will be removed. + +=cut + +sub cust_pay_batch { + carp "FS::cust_bill->cust_pay_batch is deprecated"; + my $self = shift; + qsearch('cust_pay_batch', { 'invnum' => $self->invnum }); +} =back diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm index a5fa89b19..8dd644681 100644 --- a/FS/FS/cust_pay_batch.pm +++ b/FS/FS/cust_pay_batch.pm @@ -3,7 +3,7 @@ use base qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record ); use strict; use vars qw( $DEBUG ); -use Carp qw( confess ); +use Carp qw( carp confess ); use Business::CreditCard 0.28; use FS::Record qw(dbh qsearch qsearchs); @@ -502,6 +502,19 @@ sub unbatch_and_delete { } +=item cust_bill + +Returns the invoice linked to this batched payment. Deprecated, will be +removed. + +=cut + +sub cust_bill { + carp "FS::cust_pay_batch->cust_bill is deprecated"; + my $self = shift; + $self->invnum ? qsearchs('cust_bill', { invnum => $self->invnum }) : ''; +} + =back =head1 BUGS -- cgit v1.2.1 From e949bed143088a77ba89a56ff2a25609a1050159 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 31 Aug 2015 13:32:13 -0700 Subject: change test utils to run from the source tree, #37340 --- FS-Test/Makefile.PL | 34 ---------------------------------- FS-Test/bin/freeside-test-fetch | 8 +++++++- FS-Test/bin/freeside-test-run | 14 ++++++++------ FS-Test/bin/freeside-test-start | 7 +++++-- FS-Test/bin/freeside-test-stop | 3 --- FS-Test/lib/FS/Test.pm | 11 ++++++++--- FS-Test/share/ui_tests | 4 ++-- 7 files changed, 30 insertions(+), 51 deletions(-) delete mode 100644 FS-Test/Makefile.PL mode change 100644 => 100755 FS-Test/bin/freeside-test-run diff --git a/FS-Test/Makefile.PL b/FS-Test/Makefile.PL deleted file mode 100644 index cea088a4c..000000000 --- a/FS-Test/Makefile.PL +++ /dev/null @@ -1,34 +0,0 @@ -use 5.006; -use strict; -use warnings FATAL => 'all'; -use ExtUtils::MakeMaker; -use File::ShareDir::Install; - -install_share dist => 'share'; - -WriteMakefile( - NAME => 'FS::Test', - AUTHOR => q{Mark Wells }, - VERSION_FROM => 'lib/FS/Test.pm', - ABSTRACT => 'Freeside test suite', - LICENSE => 'agpl_3', - PL_FILES => {}, - EXE_FILES => [ glob 'bin/*' ], - MIN_PERL_VERSION => 5.006, - CONFIGURE_REQUIRES => { - 'ExtUtils::MakeMaker' => 0, - 'File::ShareDir::Install' => 0, - }, - BUILD_REQUIRES => { - 'Test::More' => 0, - }, - PREREQ_PM => { - 'WWW::Mechanize' => 0, - }, - dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, - clean => { FILES => 'FS-Test-*' }, -); - -package MY; -use File::ShareDir::Install qw(postamble); - diff --git a/FS-Test/bin/freeside-test-fetch b/FS-Test/bin/freeside-test-fetch index ccc8528ff..953005704 100755 --- a/FS-Test/bin/freeside-test-fetch +++ b/FS-Test/bin/freeside-test-fetch @@ -1,8 +1,14 @@ #!/usr/bin/perl use strict; -use FS::Test; use Getopt::Std; +use File::Spec; + +my @dirs = File::Spec->splitdir(File::Spec->rel2abs(__FILE__)); +splice @dirs, -2; # bin/freeside-test-run +push @INC, File::Spec->catdir( @dirs, 'lib' ); +eval "use FS::Test;"; +die $@ if $@; my %opt; diff --git a/FS-Test/bin/freeside-test-run b/FS-Test/bin/freeside-test-run old mode 100644 new mode 100755 index add1e9b25..853bdc9a8 --- a/FS-Test/bin/freeside-test-run +++ b/FS-Test/bin/freeside-test-run @@ -2,18 +2,20 @@ set -ae -tempdir=$( mktemp -d freeside-test.XXXX ) -sharedir=$(perl -MFS::Test -e "print FS::Test::share_dir()") +tempdir=$( mktemp -d --tmpdir freeside-test.XXXX ) +# get the parent directory of bin/(this script) +testroot=$( dirname ${BASH_SOURCE[0]} ) +testroot=$( cd $testroot; cd ..; pwd ) echo "Starting test mode." -freeside-test-start +$testroot/bin/freeside-test-start echo "Saving results to $tempdir." -freeside-test-fetch -d $tempdir +$testroot/bin/freeside-test-fetch -d $tempdir echo "Exiting test mode." -freeside-test-stop || true +$testroot/bin/freeside-test-stop || true diffname=freeside-test.`date +%Y%m%d`.diff echo "Writing diff to $diffname." -diff -urb "$sharedir/output" "$tempdir" > $diffname +diff -urb "$testroot/share/output" "$tempdir" > $diffname diffstat -C $diffname diff --git a/FS-Test/bin/freeside-test-start b/FS-Test/bin/freeside-test-start index 4f12e325b..ed8e131a9 100755 --- a/FS-Test/bin/freeside-test-start +++ b/FS-Test/bin/freeside-test-start @@ -4,12 +4,15 @@ VERSION='5.0.1' set -ae +# get the parent directory of bin/(this script) +testroot=$( dirname ${BASH_SOURCE[0]} ) +testroot=$( cd $testroot; cd ..; pwd ) + echo "Stopping services." sudo service freeside stop sudo service apache2 stop newname=freeside_`date +%Y%m%d` -sharedir=$(perl -MFS::Test -e "print FS::Test::share_dir()") # get company_name from existing DB, strip whitespace # (if there is no existing DB, continue anyway) @@ -34,7 +37,7 @@ fi if [ $createdb = YES ]; then echo "Creating new database from stock schema." createdb --owner=freeside freeside - sudo -u freeside psql freeside -q -f $sharedir/test.sql > /dev/null + sudo -u freeside psql freeside -q -f $testroot/share/test.sql > /dev/null fi newtime=$(sudo -u freeside \ diff --git a/FS-Test/bin/freeside-test-stop b/FS-Test/bin/freeside-test-stop index 7c67f54ec..6ec505a1f 100755 --- a/FS-Test/bin/freeside-test-stop +++ b/FS-Test/bin/freeside-test-stop @@ -24,9 +24,6 @@ oldname=$( psql -tl | \ if [ -n "$oldname" ]; then echo "Renaming $oldname to freeside." psql postgres -c "ALTER DATABASE $oldname RENAME TO freeside" - echo "Restarting services." - sudo service apache2 restart - sudo service freeside restart fi echo "Done." diff --git a/FS-Test/lib/FS/Test.pm b/FS-Test/lib/FS/Test.pm index aedf502b5..bc9797779 100644 --- a/FS-Test/lib/FS/Test.pm +++ b/FS-Test/lib/FS/Test.pm @@ -4,14 +4,15 @@ use 5.006; use strict; use warnings FATAL => 'all'; -use File::ShareDir 'dist_dir'; +#use File::ShareDir 'dist_dir'; use WWW::Mechanize; use File::chdir; use URI; use File::Slurp qw(write_file); use Class::Accessor 'antlers'; +use File::Spec; -our $VERSION = '0.01'; +our $VERSION = '0.02'; =head1 NAME @@ -29,7 +30,11 @@ database image, the test plan, and probably other stuff. =cut sub share_dir { - dist_dir('FS-Test') +# dist_dir('FS-Test') +# we no longer install this anywhere + my @dirs = File::Spec->splitdir(File::Spec->rel2abs(__FILE__)); + splice @dirs, -3; # lib/FS/Test.pm + File::Spec->catdir( @dirs, 'share' ); } =item new OPTIONS diff --git a/FS-Test/share/ui_tests b/FS-Test/share/ui_tests index 8292ae521..2370a1d9b 100644 --- a/FS-Test/share/ui_tests +++ b/FS-Test/share/ui_tests @@ -1,7 +1,7 @@ # reports menu items search/cust_main.cgi?browse=last -search/cust_bill.html?OPEN90_date -search/cust_bill.html?date +search/cust_bill.html?keywords=OPEN90_date:order_by=invnum +search/cust_bill.html?keywords=date:order_by=invnum search/cust_bill.html?magic=_date&agentnum=1&beginning=&ending=10%2F01%2F2015&charged_lt=&charged_gt=200.00&owed_lt=&owed_gt=&open=1 search/cust_bill_pkg.cgi?agentnum=1&status=&cust_classnum=&beginning=01%2F01%2F2016&ending=01%2F31%2F2016 search/cust_pay.html?magic=_date&unapplied=0&beginning=01%2F01%2F2016&agentnum=1&order_by=paynum -- cgit v1.2.1 From b5cfff7585a9107889dfd55208c52d24d27c4b1c Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 31 Aug 2015 13:32:38 -0700 Subject: repeatability cleanup, #37340 --- FS/FS/TicketSystem/RT_External.pm | 8 ++++---- FS/FS/UI/Web.pm | 1 + FS/FS/part_pkg/prorate_calendar.pm | 2 +- httemplate/browse/part_pkg.cgi | 1 + httemplate/browse/part_svc.cgi | 2 +- httemplate/edit/part_pkg.cgi | 23 +++++++++++++++-------- httemplate/elements/email-link.html | 3 ++- httemplate/elements/form-create_ticket.html | 2 +- httemplate/search/cust_bill.html | 4 ++-- 9 files changed, 28 insertions(+), 18 deletions(-) diff --git a/FS/FS/TicketSystem/RT_External.pm b/FS/FS/TicketSystem/RT_External.pm index 9f07732c7..b5414b97c 100644 --- a/FS/FS/TicketSystem/RT_External.pm +++ b/FS/FS/TicketSystem/RT_External.pm @@ -315,22 +315,22 @@ sub href_params_new_ticket { my $subtype = $object->table; my $pkey = $object->get($object->primary_key); - my %param = ( + my @param = ( 'Queue' => ($cust_main->agent->ticketing_queueid || $default_queueid), 'new-MemberOf'=> "freeside://freeside/$subtype/$pkey", 'Requestors' => $requestors, ); - ( $self->baseurl.'Ticket/Create.html', %param ); + ( $self->baseurl.'Ticket/Create.html', @param ); } sub href_new_ticket { my $self = shift; - my( $base, %param ) = $self->href_params_new_ticket(@_); + my( $base, @param ) = $self->href_params_new_ticket(@_); my $uri = new URI $base; - $uri->query_form(%param); + $uri->query_form(@param); $uri; } diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm index 13b2e2dc0..0e54aa26f 100644 --- a/FS/FS/UI/Web.pm +++ b/FS/FS/UI/Web.pm @@ -623,6 +623,7 @@ sub random_id { if (!defined $NO_RANDOM_IDS) { my $conf = FS::Conf->new; $NO_RANDOM_IDS = $conf->exists('no_random_ids') ? 1 : 0; + warn "TEST MODE--RANDOM ID NUMBERS DISABLED\n" if $NO_RANDOM_IDS; } if ( $NO_RANDOM_IDS ) { if ( $digits > 0 ) { diff --git a/FS/FS/part_pkg/prorate_calendar.pm b/FS/FS/part_pkg/prorate_calendar.pm index 83a80f5d0..c50cae0d7 100644 --- a/FS/FS/part_pkg/prorate_calendar.pm +++ b/FS/FS/part_pkg/prorate_calendar.pm @@ -36,7 +36,7 @@ use base 'FS::part_pkg::flat'; }, 'fieldorder' => [ 'cutoff_day', 'prorate_defer_bill', 'prorate_round_day', 'prorate_verbose' ], 'freq' => 'm', - 'weight' => 20, + 'weight' => 23, ); my %freq_max_days = ( # the length of the shortest period of each cycle type diff --git a/httemplate/browse/part_pkg.cgi b/httemplate/browse/part_pkg.cgi index c2f1430d7..07f104e55 100755 --- a/httemplate/browse/part_pkg.cgi +++ b/httemplate/browse/part_pkg.cgi @@ -591,6 +591,7 @@ push @fields, }, ]; } + sort grep { $options{$_} =~ /\S/ } grep { $_ !~ /^(setup|recur)_fee$/ and $_ !~ /^report_option_\d+$/ } diff --git a/httemplate/browse/part_svc.cgi b/httemplate/browse/part_svc.cgi index ec5f321dd..88f8d8d19 100755 --- a/httemplate/browse/part_svc.cgi +++ b/httemplate/browse/part_svc.cgi @@ -161,7 +161,7 @@ function part_export_areyousure(href) { % } % % my($n1)=''; -% foreach my $field ( @fields ) { +% foreach my $field ( sort @fields ) { % % #a few lines of false laziness w/edit/part_svc.cgi % my $def = FS::part_svc->svc_table_fields($svcdb)->{$field}; diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi index 9f5510d65..570c5ac75 100755 --- a/httemplate/edit/part_pkg.cgi +++ b/httemplate/edit/part_pkg.cgi @@ -989,9 +989,13 @@ my $html_bottom = sub { #$html .= ''; my $href = $plans{$layer}->{'fields'}; - my @fields = exists($plans{$layer}->{'fieldorder'}) - ? @{$plans{$layer}->{'fieldorder'}} - : keys %{ $href }; + my @fields; + if ( $plans{$layer}->{'fieldorder'} ) { + @fields = @{ $plans{$layer}->{'fieldorder'} }; + } else { + warn "FS::part_pkg::$layer has no fieldorder.\n"; + @fields = keys %$href; + } # hash of dependencies for each of the Pricing Plan fields. # make sure NOT to use double-quotes inside the 'msg' value. @@ -1015,7 +1019,7 @@ my $html_bottom = sub { } } }; - + foreach my $field ( grep $_ !~ /^(setup|recur)_fee$/, @fields ) { if(!exists($href->{$field})) { @@ -1029,7 +1033,8 @@ my $html_bottom = sub { next if !$display; } - $html .= ''. $href->{$field}{'name'}. ''; + $html .= ''. $href->{$field}{'name'}. ' + '; my $format = sub { shift }; $format = $href->{$field}{'format'} if exists($href->{$field}{'format'}); @@ -1128,9 +1133,11 @@ my $html_bottom = sub { $html .= ''; } $html .= ''; - - $html .= qq('; + + $html .= include('/elements/hidden.html', + field => $layer.'__OPTIONS', + value => join(',', @fields) + ); $html; diff --git a/httemplate/elements/email-link.html b/httemplate/elements/email-link.html index 2612faabb..16935cf98 100644 --- a/httemplate/elements/email-link.html +++ b/httemplate/elements/email-link.html @@ -10,7 +10,8 @@ die "'table' required" if !$table; die "'search_hash' required" if !$search_hash; my $uri = new URI; -$uri->query_form($search_hash); +my @params = map { $_, $search_hash->{$_} } sort keys %$search_hash; +$uri->query_form(@params); my $query = $uri->query; my $label = ($opt{'label'} || 'Email a notice to these customers'); diff --git a/httemplate/elements/form-create_ticket.html b/httemplate/elements/form-create_ticket.html index 362e82397..d76c0d83e 100644 --- a/httemplate/elements/form-create_ticket.html +++ b/httemplate/elements/form-create_ticket.html @@ -6,7 +6,7 @@ function updateTicketLink() { link.href = "<% $new_base.'?'. join(';', map( { ($_ eq 'Queue') ? () : "$_=$new_param{$_}"} - keys %new_param),'Queue=') %>" + selector.options[selector.selectedIndex].value; + sort keys %new_param),'Queue=') %>" + selector.options[selector.selectedIndex].value; } Tickets diff --git a/httemplate/search/cust_bill.html b/httemplate/search/cust_bill.html index 017e8298f..6e3617b28 100755 --- a/httemplate/search/cust_bill.html +++ b/httemplate/search/cust_bill.html @@ -196,7 +196,7 @@ my $html_init = join("\n", map { ( my $action = $_ ) =~ s/_$//; include('/elements/progress-init.html', $_.'form', - [ keys %search ], + [ sort keys %search ], "../misc/${_}invoices.cgi", { 'message' => "Invoices re-${action}ed" }, #would be nice to show the number of them, but... $_, #key @@ -206,7 +206,7 @@ my $html_init = join("\n", map { my @values = ref($search{$f}) ? @{ $search{$f} } : $search{$f}; map qq!!, @values; } - keys %search + sort keys %search ), qq!! } qw( print_ email_ fax_ ftp_ spool_ ) ). -- cgit v1.2.1 From 6cdf768f2f350f81fed9a552e34fd07a2ef1846f Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 31 Aug 2015 14:19:18 -0700 Subject: refresh tests with August 2015 updates --- FS-Test/share/output/browse/part_pkg.cgi/active=1 | 6 +- .../output/browse/part_svc.cgi/orderby=active | 268 +- .../share/output/edit/cust_main-contacts.html/135 | 2 +- FS-Test/share/output/edit/cust_main.cgi/135 | 2 +- FS-Test/share/output/edit/part_pkg.cgi/2 | 852 ++- ...ggregate:use_setup=1:use_usage=0:use_discount=1 | 2 +- ...nth=4:start_year=2015:end_month=3:end_year=2016 | 2 +- ...nd_year=2016:agentnum=1:cust_classnum=:refnum=1 | 2 +- ...01%2F01%2F2016:ending=02%2F28%2F2016:agentnum=1 | 2 +- .../share/output/search/cust_bill.html/OPEN90_date | 5993 ------------------- FS-Test/share/output/search/cust_bill.html/date | 6054 -------------------- .../keywords=OPEN90_date:order_by=invnum | 6044 +++++++++++++++++++ .../cust_bill.html/keywords=date:order_by=invnum | 6044 +++++++++++++++++++ ..._lt=:charged_gt=200.00:owed_lt=:owed_gt=:open=1 | 2 +- ...:beginning=01%2F01%2F2016:ending=01%2F31%2F2016 | 6 +- ...um=0:classnum=1:begin=1438412400:end=1441090800 | 6 +- ...um=0:classnum=1:begin=1438412400:end=1441090800 | 6 +- ...um=0:classnum=1:begin=1438412400:end=1441090800 | 6 +- .../share/output/search/cust_main.cgi/browse=last | 2 +- ...nning=01%2F01%2F2016:agentnum=1:order_by=paynum | 2 +- .../cust_pkg.cgi/keywords=pkgnum:order_by=pkgnum | 202 +- ...2F31%2F2015:pkgpart=5:pkgpart=2:order_by=pkgnum | 204 +- .../search/cust_pkg.cgi/magic=bill:custnum=135 | 8 +- ...8412400:end=1441090800:order_by=cust_pkg.pkgnum | 2 +- ...02%2F01%2F2016:ending=02%2F28%2F2016:classnum=0 | 2 +- ...atus=active,suspended:date=1454313600:pkgpart=2 | 2 +- .../days=0:as_of=03%2F01%2F2016 | 2 +- .../search/svc_acct.cgi/magic=all:sortby=username | 2 +- ...ecords=100:_type=html:offset=0:order_by=ip_addr | 2 +- .../search/svc_domain.cgi/magic=all:sortby=domain | 2 +- .../search/svc_phone.cgi/magic=all:sortby=phonenum | 2 +- .../output/search/unprovisioned_services.html | 2 +- .../invnum=681:notice_name=Invoice | 6 +- FS-Test/share/output/view/cust_bill.cgi/681 | 2 +- FS-Test/share/output/view/cust_main.cgi/135 | 2 +- .../cust_main.cgi/custnum=135:show=change_history | 2 +- .../view/cust_main.cgi/custnum=135:show=packages | 2 +- .../cust_main.cgi/custnum=135:show=payment_history | 2 +- .../view/cust_main.cgi/custnum=2:show=packages | 212 +- .../cust_main.cgi/custnum=2:show=payment_history | 31 +- FS-Test/share/output/view/svc_acct.cgi/406 | 4 +- FS-Test/share/output/view/svc_broadband.cgi/401 | 2 +- FS-Test/share/output/view/svc_domain.cgi/402 | 6 +- FS-Test/share/output/view/svc_phone.cgi/403 | 4 +- 44 files changed, 13465 insertions(+), 12545 deletions(-) delete mode 100644 FS-Test/share/output/search/cust_bill.html/OPEN90_date delete mode 100644 FS-Test/share/output/search/cust_bill.html/date create mode 100644 FS-Test/share/output/search/cust_bill.html/keywords=OPEN90_date:order_by=invnum create mode 100644 FS-Test/share/output/search/cust_bill.html/keywords=date:order_by=invnum diff --git a/FS-Test/share/output/browse/part_pkg.cgi/active=1 b/FS-Test/share/output/browse/part_pkg.cgi/active=1 index 6eeffd57a..5f5f8a313 100644 --- a/FS-Test/share/output/browse/part_pkg.cgi/active=1 +++ b/FS-Test/share/output/browse/part_pkg.cgi/active=1 @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); @@ -1131,7 +1131,7 @@ function filter_change() { window.location = 'http://localhost/freeside/browse/ -
prorate_round_day: 1
prorate_verbose: 1
cutoff_day: 1
+
cutoff_day: 1
prorate_round_day: 1
prorate_verbose: 1
@@ -1185,7 +1185,7 @@ function filter_change() { window.location = 'http://localhost/freeside/browse/ -
prorate_round_day: 1
prorate_verbose: 1
cutoff_day: 1
+
cutoff_day: 1
prorate_round_day: 1
prorate_verbose: 1
diff --git a/FS-Test/share/output/browse/part_svc.cgi/orderby=active b/FS-Test/share/output/browse/part_svc.cgi/orderby=active index 3ce994cf8..ce7d11994 100644 --- a/FS-Test/share/output/browse/part_svc.cgi/orderby=active +++ b/FS-Test/share/output/browse/part_svc.cgi/orderby=active @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); @@ -962,7 +962,7 @@ function part_export_areyousure(href) { - sec_phrase + blocknum Fixed (unchangeable) @@ -974,8 +974,8 @@ function part_export_areyousure(href) { - popnum - Access number + cf_privatekey + Fixed (unchangeable) @@ -986,8 +986,8 @@ function part_export_areyousure(href) { - sectornum - Tower sector + dir + Home directory Fixed (unchangeable) @@ -998,20 +998,21 @@ function part_export_areyousure(href) { - uid - UID + domsvc + Domain Fixed (unchangeable) - + 1 + Yes - gid - GID + finger + Real name Fixed (unchangeable) @@ -1022,8 +1023,8 @@ function part_export_areyousure(href) { - finger - Real name + gid + GID Fixed (unchangeable) @@ -1034,8 +1035,8 @@ function part_export_areyousure(href) { - dir - Home directory + pbxsvc + PBX Fixed (unchangeable) @@ -1046,8 +1047,8 @@ function part_export_areyousure(href) { - shell - Shell + popnum + Access number Fixed (unchangeable) @@ -1070,8 +1071,8 @@ function part_export_areyousure(href) { - slipip - IP address + routernum + Fixed (unchangeable) @@ -1082,7 +1083,7 @@ function part_export_areyousure(href) { - routernum + sec_phrase Fixed (unchangeable) @@ -1094,8 +1095,8 @@ function part_export_areyousure(href) { - blocknum - + sectornum + Tower sector Fixed (unchangeable) @@ -1106,21 +1107,20 @@ function part_export_areyousure(href) { - domsvc - Domain + shell + Shell Fixed (unchangeable) - 1 + - Yes - pbxsvc - PBX + slipip + IP address Fixed (unchangeable) @@ -1131,8 +1131,8 @@ function part_export_areyousure(href) { - cf_privatekey - + uid + UID Fixed (unchangeable) @@ -1188,8 +1188,8 @@ function part_export_areyousure(href) { - suffix - + au_eligibility_type + AU Eligibility Type Fixed (unchangeable) @@ -1200,8 +1200,8 @@ function part_export_areyousure(href) { - catchall - + au_registrant_name + AU Registrant Name Fixed (unchangeable) @@ -1212,8 +1212,8 @@ function part_export_areyousure(href) { - quota - Quota + catchall + Fixed (unchangeable) @@ -1224,7 +1224,7 @@ function part_export_areyousure(href) { - registrarnum + expiration_date Fixed (unchangeable) @@ -1236,8 +1236,8 @@ function part_export_areyousure(href) { - registrarkey - + quota + Quota Fixed (unchangeable) @@ -1248,7 +1248,7 @@ function part_export_areyousure(href) { - setup_date + registrarkey Fixed (unchangeable) @@ -1260,7 +1260,7 @@ function part_export_areyousure(href) { - renewal_interval + registrarnum Fixed (unchangeable) @@ -1272,7 +1272,7 @@ function part_export_areyousure(href) { - expiration_date + renewal_interval Fixed (unchangeable) @@ -1284,8 +1284,8 @@ function part_export_areyousure(href) { - au_registrant_name - AU Registrant Name + setup_date + Fixed (unchangeable) @@ -1296,8 +1296,8 @@ function part_export_areyousure(href) { - au_eligibility_type - AU Eligibility Type + suffix + Fixed (unchangeable) @@ -1341,8 +1341,8 @@ function part_export_areyousure(href) { - description - Descriptive label + altitude + Altitude Fixed (unchangeable) @@ -1353,8 +1353,8 @@ function part_export_areyousure(href) { - routernum - Router/block + authkey + Authentication key Fixed (unchangeable) @@ -1377,8 +1377,8 @@ function part_export_areyousure(href) { - sectornum - Tower/sector + description + Descriptive label Fixed (unchangeable) @@ -1389,32 +1389,32 @@ function part_export_areyousure(href) { - speed_up - Upload speed (Kbps) + latitude + Latitude Fixed (unchangeable) - 1024 + - speed_down - Download speed (Kbps) + longitude + Longitude Fixed (unchangeable) - 1024 + - authkey - Authentication key + performance_profile + Peformance Profile Fixed (unchangeable) @@ -1425,8 +1425,8 @@ function part_export_areyousure(href) { - latitude - Latitude + plan_id + Service Plan Id Fixed (unchangeable) @@ -1437,8 +1437,8 @@ function part_export_areyousure(href) { - longitude - Longitude + poe_location + POE Location Fixed (unchangeable) @@ -1449,8 +1449,8 @@ function part_export_areyousure(href) { - altitude - Altitude + radio_location + Radio Location Fixed (unchangeable) @@ -1461,8 +1461,8 @@ function part_export_areyousure(href) { - vlan_profile - VLAN profile + radio_serialnum + Radio Serial Number Fixed (unchangeable) @@ -1473,8 +1473,8 @@ function part_export_areyousure(href) { - performance_profile - Peformance Profile + routernum + Router/block Fixed (unchangeable) @@ -1485,8 +1485,8 @@ function part_export_areyousure(href) { - plan_id - Service Plan Id + rssi + RSSI Fixed (unchangeable) @@ -1497,8 +1497,8 @@ function part_export_areyousure(href) { - radio_serialnum - Radio Serial Number + sectornum + Tower/sector Fixed (unchangeable) @@ -1509,8 +1509,8 @@ function part_export_areyousure(href) { - radio_location - Radio Location + serviceid + Torrus serviceid Fixed (unchangeable) @@ -1521,8 +1521,8 @@ function part_export_areyousure(href) { - poe_location - POE Location + shared_svcnum + Shared Service Fixed (unchangeable) @@ -1533,32 +1533,32 @@ function part_export_areyousure(href) { - rssi - RSSI + speed_down + Download speed (Kbps) Fixed (unchangeable) - + 1024 - suid - SUID + speed_up + Upload speed (Kbps) Fixed (unchangeable) - + 1024 - shared_svcnum - Shared Service + suid + SUID Fixed (unchangeable) @@ -1569,8 +1569,8 @@ function part_export_areyousure(href) { - serviceid - Torrus serviceid + vlan_profile + VLAN profile Fixed (unchangeable) @@ -1614,8 +1614,8 @@ function part_export_areyousure(href) { - countrycode - Country code + circuit_svcnum + Circuit Fixed (unchangeable) @@ -1626,20 +1626,20 @@ function part_export_areyousure(href) { - sim_imsi - IMSI + countrycode + Country code Fixed (unchangeable) - + 1 - phone_name - Name + domsvc + Domain Fixed (unchangeable) @@ -1650,8 +1650,8 @@ function part_export_areyousure(href) { - pbxsvc - PBX + e911_class + E911 Service Class Fixed (unchangeable) @@ -1662,8 +1662,8 @@ function part_export_areyousure(href) { - domsvc - Domain + e911_type + E911 Service Type Fixed (unchangeable) @@ -1674,8 +1674,8 @@ function part_export_areyousure(href) { - forwarddst - Forward Destination + email + Email Fixed (unchangeable) @@ -1686,8 +1686,8 @@ function part_export_areyousure(href) { - email - Email + forwarddst + Forward Destination Fixed (unchangeable) @@ -1698,8 +1698,8 @@ function part_export_areyousure(href) { - lnp_status - LNP Status + lnp_desired_due_date + LNP Desired Due Date Fixed (unchangeable) @@ -1710,8 +1710,8 @@ function part_export_areyousure(href) { - portable - Portable? + lnp_due_date + LNP Due Date Fixed (unchangeable) @@ -1722,8 +1722,8 @@ function part_export_areyousure(href) { - lrn - LRN + lnp_other_provider + LNP Other Provider Fixed (unchangeable) @@ -1734,8 +1734,8 @@ function part_export_areyousure(href) { - lnp_desired_due_date - LNP Desired Due Date + lnp_other_provider_account + LNP Other Provider Account # Fixed (unchangeable) @@ -1746,8 +1746,8 @@ function part_export_areyousure(href) { - lnp_due_date - LNP Due Date + lnp_reject_reason + LNP Reject Reason Fixed (unchangeable) @@ -1758,8 +1758,8 @@ function part_export_areyousure(href) { - lnp_other_provider - LNP Other Provider + lnp_status + LNP Status Fixed (unchangeable) @@ -1770,8 +1770,8 @@ function part_export_areyousure(href) { - lnp_other_provider_account - LNP Other Provider Account # + lrn + LRN Fixed (unchangeable) @@ -1782,8 +1782,8 @@ function part_export_areyousure(href) { - lnp_reject_reason - LNP Reject Reason + max_simultaneous + Maximum number of simultaneous users Fixed (unchangeable) @@ -1794,8 +1794,8 @@ function part_export_areyousure(href) { - sms_carrierid - SMS Carrier + pbxsvc + PBX Fixed (unchangeable) @@ -1806,8 +1806,8 @@ function part_export_areyousure(href) { - sms_account - SMS Carrier Account + phone_name + Name Fixed (unchangeable) @@ -1818,8 +1818,8 @@ function part_export_areyousure(href) { - max_simultaneous - Maximum number of simultaneous users + portable + Portable? Fixed (unchangeable) @@ -1830,8 +1830,8 @@ function part_export_areyousure(href) { - e911_class - E911 Service Class + sim_imsi + IMSI Fixed (unchangeable) @@ -1842,8 +1842,8 @@ function part_export_areyousure(href) { - e911_type - E911 Service Type + sip_server + SIP Host Fixed (unchangeable) @@ -1854,20 +1854,20 @@ function part_export_areyousure(href) { - circuit_svcnum - Circuit + sms_account + SMS Carrier Account Fixed (unchangeable) - 1 + - sip_server - SIP Host + sms_carrierid + SMS Carrier Fixed (unchangeable) diff --git a/FS-Test/share/output/edit/cust_main-contacts.html/135 b/FS-Test/share/output/edit/cust_main-contacts.html/135 index 6dd5bd895..ae160d6f1 100644 --- a/FS-Test/share/output/edit/cust_main-contacts.html/135 +++ b/FS-Test/share/output/edit/cust_main-contacts.html/135 @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/edit/cust_main.cgi/135 b/FS-Test/share/output/edit/cust_main.cgi/135 index 84196c7cc..3f60d0069 100644 --- a/FS-Test/share/output/edit/cust_main.cgi/135 +++ b/FS-Test/share/output/edit/cust_main.cgi/135 @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/edit/part_pkg.cgi/2 b/FS-Test/share/output/edit/part_pkg.cgi/2 index 8a9f1fc00..9aba1583e 100644 --- a/FS-Test/share/output/edit/part_pkg.cgi/2 +++ b/FS-Test/share/output/edit/part_pkg.cgi/2 @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); @@ -1325,11 +1325,6 @@ function confirm_submit(f) { >Anniversary, with intro price - - - @@ -1340,6 +1335,11 @@ function confirm_submit(f) { >Prorate (Nth of month billing), with intro period + + + @@ -3742,10 +3742,15 @@ spawn_bill_dst_pkgpart(this);" + +
- +
Quan.PrimaryServiceHide
from
Invoices
Bulk
Charge
Hold
Until
Provision
@@ -3769,9 +3774,13 @@ spawn_bill_dst_pkgpart(this);" - + @@ -3796,15 +3805,19 @@ spawn_bill_dst_pkgpart(this);" - +
Quan.PrimaryServiceHide
from
Invoices
Bulk
Charge
Remove Hold After Provisioning
- + +
- + +
- +
Quan.PrimaryServiceHide
from
Invoices
Bulk
Charge
Hold
Until
Provision
- + @@ -3854,15 +3871,19 @@ spawn_bill_dst_pkgpart(this);" - +
Quan.PrimaryServiceHide
from
Invoices
Bulk
Charge
Remove Hold After Provisioning
@@ -3827,9 +3840,13 @@ spawn_bill_dst_pkgpart(this);" - + +
- + +
- +
Quan.PrimaryServiceHide
from
Invoices
Bulk
Charge
Hold
Until
Provision
- +
Quan.PrimaryServiceHide
from
Invoices
Bulk
Charge
Remove Hold After Provisioning
@@ -3885,14 +3906,42 @@ spawn_bill_dst_pkgpart(this);" - + +
+ + @@ -4261,7 +4310,39 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Charge recurring fee for period
Auto-add a start date to the 1st, ignoring the current month.
Prorate first month to synchronize with the customer's other packages
When synchronizing, defer the bill until the customer's next bill date
When synchronizing, round the prorated period to the nearest full day
Continue recurring billing while suspended
Adjust next bill date forward when unsuspending
Bill the last period on cancellation
Bill immediately upon suspension
Optional External ID
Show prorate details on the invoice
Time limit for this package
Upload limit for this package
Download limit for this package
Transfer limit for this package
Cost of recharge for this package
Recharge time for this package
Recharge upload for this package
Recharge download for this package
Recharge transfer for this package
Allow usage from previous period to roll over into current period
Reset usage to these values on manual package recharge
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Charge recurring fee for period +
Auto-add a start date to the 1st, ignoring the current month. +
Prorate first month to synchronize with the customer's other packages +
When synchronizing, defer the bill until the customer's next bill date +
When synchronizing, round the prorated period to the nearest full day +
Continue recurring billing while suspended +
Adjust next bill date forward when unsuspending +
Bill the last period on cancellation +
Bill immediately upon suspension +
Optional External ID +
Show prorate details on the invoice +
Time limit for this package +
Upload limit for this package +
Download limit for this package +
Transfer limit for this package +
Cost of recharge for this package +
Recharge time for this package +
Recharge upload for this package +
Recharge download for this package +
Recharge transfer for this package +
Allow usage from previous period to roll over into current period +
Reset usage to these values on manual package recharge +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4270,7 +4351,21 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Continue recurring billing while suspended
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
Initial free days
Delay setup fee in addition to recurring fee
Number of days before recurring billing commences to notify customer. (0 means no warning)
+
Continue recurring billing while suspended +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
Initial free days +
Delay setup fee in addition to recurring fee +
Number of days before recurring billing commences to notify customer. (0 means no warning) +
+ + @@ -4279,34 +4374,143 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Duration of the introductory period, in number of months
Introductory recurring fee for this package
Charge recurring fee for period
Auto-add a start date to the 1st, ignoring the current month.
Prorate first month to synchronize with the customer's other packages
When synchronizing, defer the bill until the customer's next bill date
When synchronizing, round the prorated period to the nearest full day
Continue recurring billing while suspended
Adjust next bill date forward when unsuspending
Bill the last period on cancellation
Bill immediately upon suspension
Optional External ID
Time limit for this package
Upload limit for this package
Download limit for this package
Transfer limit for this package
Cost of recharge for this package
Recharge time for this package
Recharge upload for this package
Recharge download for this package
Recharge transfer for this package
Allow usage from previous period to roll over into current period
Reset usage to these values on manual package recharge
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Duration of the introductory period, in number of months +
Introductory recurring fee for this package +
Charge recurring fee for period +
Auto-add a start date to the 1st, ignoring the current month. +
Prorate first month to synchronize with the customer's other packages +
When synchronizing, defer the bill until the customer's next bill date +
When synchronizing, round the prorated period to the nearest full day +
Continue recurring billing while suspended +
Adjust next bill date forward when unsuspending +
Bill the last period on cancellation +
Bill immediately upon suspension +
Optional External ID +
Time limit for this package +
Upload limit for this package +
Download limit for this package +
Transfer limit for this package +
Cost of recharge for this package +
Recharge time for this package +
Recharge upload for this package +
Recharge download for this package +
Recharge transfer for this package +
Allow usage from previous period to roll over into current period +
Reset usage to these values on manual package recharge +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + - -
-
Billing Day (1 - 28)
Defer the first bill until the billing day
When prorating first month, also bill for one full period after that
Round the prorated period to the nearest full day
Show prorate details on the invoice
Auto-add a start date to the 1st, ignoring the current month.
Continue recurring billing while suspended
Adjust next bill date forward when unsuspending
Bill the last period on cancellation
Bill immediately upon suspension
Optional External ID
Time limit for this package
Upload limit for this package
Download limit for this package
Transfer limit for this package
Cost of recharge for this package
Recharge time for this package
Recharge upload for this package
Recharge download for this package
Recharge transfer for this package
Allow usage from previous period to roll over into current period
Reset usage to these values on manual package recharge
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Continue recurring billing while suspended +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
Initial free days +
Delay setup fee in addition to recurring fee +
Number of days before recurring billing commences to notify customer. (0 means no warning) +
+ +
- @@ -4315,7 +4519,23 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Action to take upon reaching end of prepaid period
Action to take upon reaching a usage limit.
Time limit for this package
Upload limit for this package
Download limit for this package
Transfer limit for this package
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Action to take upon reaching end of prepaid period +
Action to take upon reaching a usage limit. +
Time limit for this package +
Upload limit for this package +
Download limit for this package +
Transfer limit for this package +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4324,7 +4544,30 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Billing day
Time limit for this package
Upload limit for this package
Download limit for this package
Transfer limit for this package
Cost of recharge for this package
Recharge time for this package
Recharge upload for this package
Recharge download for this package
Recharge transfer for this package
Allow usage from previous period to roll over into current period
Reset usage to these values on manual package recharge
Optional External ID
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Billing day +
Time limit for this package +
Upload limit for this package +
Download limit for this package +
Transfer limit for this package +
Cost of recharge for this package +
Recharge time for this package +
Recharge upload for this package +
Recharge download for this package +
Recharge transfer for this package +
Allow usage from previous period to roll over into current period +
Reset usage to these values on manual package recharge +
Optional External ID +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4333,7 +4576,30 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Hours included
Additional charge per hour
Maximum overage charge for hours (0 means no cap)
Upload gigabytes included
Additional charge per gigabyte upload
Maximum overage charge for upload (0 means no cap)
Download gigabytes included
Additional charge per gigabyte download
Maximum overage charge for download (0 means no cap)
Total gigabytes included
Additional charge per gigabyte total
Maximum overage charge for total gigabytes (0 means no cap)
Global cap on all overage charges (0 means no cap)
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Hours included +
Additional charge per hour +
Maximum overage charge for hours (0 means no cap) +
Upload gigabytes included +
Additional charge per gigabyte upload +
Maximum overage charge for upload (0 means no cap) +
Download gigabytes included +
Additional charge per gigabyte download +
Maximum overage charge for download (0 means no cap) +
Total gigabytes included +
Additional charge per gigabyte total +
Maximum overage charge for total gigabytes (0 means no cap) +
Global cap on all overage charges (0 means no cap) +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4342,7 +4608,31 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Hours included per day
Additional charge per hour
Maximum daily charge for hours (0 means no cap)
Upload megabytes included per day
Additional charge per megabyte upload
Maximum daily charge for upload (0 means no cap)
Download megabytes included per day
Additional charge per megabyte download
Maximum daily charge for download (0 means no cap)
Total megabytes included per day
Additional charge per megabyte total
Maximum daily charge for total megabytes (0 means no cap)
Daily cap on all overage charges (0 means no cap)
Monthly (billing frequency) cap on all overage charges (0 means no cap)
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Hours included per day +
Additional charge per hour +
Maximum daily charge for hours (0 means no cap) +
Upload megabytes included per day +
Additional charge per megabyte upload +
Maximum daily charge for upload (0 means no cap) +
Download megabytes included per day +
Additional charge per megabyte download +
Maximum daily charge for download (0 means no cap) +
Total megabytes included per day +
Additional charge per megabyte total +
Maximum daily charge for total megabytes (0 means no cap) +
Daily cap on all overage charges (0 means no cap) +
Monthly (billing frequency) cap on all overage charges (0 means no cap) +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4351,7 +4641,18 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Charge recurring fee for period
Recurring fee method
Billing Day (1 - 28) for prorating or subscription
When prorating, defer the first bill until the billing day
When prorating, round to the nearest full day
When prorating first month, also bill for one full period after that
Show prorate details on the invoice
CDR service matching method
Rating method Rate calls by using destination prefix to look up a region and rate according to the internal prefix and rate tables
Simply pass through and charge the "upstream_price" amount.
A single price per minute for all calls.
Rounding for destination prefix rating
Rate plan +
Charge recurring fee for period +
Recurring fee method +
Billing Day (1 - 28) for prorating or subscription +
When prorating, defer the first bill until the billing day +
When prorating, round to the nearest full day +
When prorating first month, also bill for one full period after that +
Show prorate details on the invoice +
CDR service matching method +
Rating method + Rate calls by using destination prefix to look up a region and rate according to the internal prefix and rate tables
Simply pass through and charge the "upstream_price" amount.
A single price per minute for all calls.
Rounding for destination prefix rating +
Rate plan +
Optional alternate intrastate rate plan +
Number of calls included at no usage charge
Charge per minute when using "single price per minute" rating method
Minutes included when using the "single price per minute" or "prefix" rating method
Granularity when using "single price per minute" rating method
Handling of calls without a rate in the rate table
Default prefix optionally prepended to customer DID numbers when searching for CDR records
Disable rating of CDR records based on the "src" field in addition to "charged_party"
Destination prefix for domestic CDR records
Destination prefix for international CDR records
Disable automatic toll-free processing
Only charge for CDRs where the amaflags field is set to "2" ("BILL"/"BILLING").
Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values:
Only charge for CDRs where the CDR Type is set to this cdrtypenum:
Do not charge for CDRs where the CDR Type is set to this cdrtypenum:
Only charge for CDRs where the CDR Call Type is set to this calltypenum:
Do not charge for CDRs where the CDR Call Type is set to this calltypenum:
Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values:
Only charge for CDRs where the Disposition is set to any of these (comma-separated) values:
Do not charge for CDRs where the dcontext is set to any of these (comma-separated) values:
Do not charge for CDRs where the destination number starts with any of these values:
Do not charge for CDRs where the dstchannel starts with:
Do not charge for CDRs where the source is more than this many digits:
Do charge for CDRs where source is equal or greater than the specified digits, when accountcode is toll free
Optional alternate rate plan when accountcode is toll free:
When using an alternate rate plan for toll-free accountcodes, the CDR field to use in rating calculations
Do not charge for CDRs where the destination is less than this many digits:
Do charge for CDRs where dst is less than the specified digits, when accountcode is toll free
Do not charge for CDRs where the lastapp matches this value:
Do not charge for CDRs where max_callers is less than or equal to this value:
Do not charge for calls between numbers belonging to the same customer
Calculate usage based on the duration field instead of the billsec field
Rewrite these (comma-separated) destination numbers to 411 for rating purposes (also ignore any carrierid check):
CDR display format for invoices
CDR display format for selfservice
Inbound CDR display format for selfservice
Always put usage details in separate section. The section is defined in the next option.
Section in which to place usage charges (whether separated or not):
Include usage summary with recurring charges when usage is in separate section
Show details for included / no-charge calls.
Generate an invoice immediately for every call (as well any setup fee, upon first payment). Useful for prepaid.
Bill for all phone numbers that were active during the billing period
Only bill CDRs with a date during the package billing period
Consider for tax purposes the number of lines to be svc_phones that may be provisioned rather than those that actually are.
Continue recurring billing while suspended
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Number of calls included at no usage charge +
Charge per minute when using "single price per minute" rating method +
Minutes included when using the "single price per minute" or "prefix" rating method +
Granularity when using "single price per minute" rating method +
Handling of calls without a rate in the rate table +
Default prefix optionally prepended to customer DID numbers when searching for CDR records +
Disable rating of CDR records based on the "src" field in addition to "charged_party" +
Destination prefix for domestic CDR records +
Destination prefix for international CDR records +
Disable automatic toll-free processing +
Only charge for CDRs where the amaflags field is set to "2" ("BILL"/"BILLING"). +
Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: +
Only charge for CDRs where the CDR Type is set to this cdrtypenum: +
Do not charge for CDRs where the CDR Type is set to this cdrtypenum: +
Only charge for CDRs where the CDR Call Type is set to this calltypenum: +
Do not charge for CDRs where the CDR Call Type is set to this calltypenum: +
Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: +
Only charge for CDRs where the Disposition is set to any of these (comma-separated) values: +
Do not charge for CDRs where the dcontext is set to any of these (comma-separated) values: +
Do not charge for CDRs where the destination number starts with any of these values: +
Do not charge for CDRs where the dstchannel starts with: +
Do not charge for CDRs where the source is more than this many digits: +
Do charge for CDRs where source is equal or greater than the specified digits, when accountcode is toll free +
Optional alternate rate plan when accountcode is toll free: +
When using an alternate rate plan for toll-free accountcodes, the CDR field to use in rating calculations +
Do not charge for CDRs where the destination is less than this many digits: +
Do charge for CDRs where dst is less than the specified digits, when accountcode is toll free +
Do not charge for CDRs where the lastapp matches this value: +
Do not charge for CDRs where max_callers is less than or equal to this value: +
Do not charge for calls between numbers belonging to the same customer +
Calculate usage based on the duration field instead of the billsec field +
Rewrite these (comma-separated) destination numbers to 411 for rating purposes (also ignore any carrierid check): +
CDR display format for invoices +
CDR display format for selfservice +
Inbound CDR display format for selfservice +
Always put usage details in separate section. The section is defined in the next option. +
Section in which to place usage charges (whether separated or not): +
Include usage summary with recurring charges when usage is in separate section +
Show details for included / no-charge calls. +
Generate an invoice immediately for every call (as well any setup fee, upon first payment). Useful for prepaid. +
Bill for all phone numbers that were active during the billing period +
Only bill CDRs with a date during the package billing period +
Consider for tax purposes the number of lines to be svc_phones that may be provisioned rather than those that actually are. +
Continue recurring billing while suspended +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4394,7 +4750,46 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Charge recurring fee for period
Recurring fee method
Billing Day (1 - 28) for prorating or subscription
When prorating, defer the first bill until the billing day
When prorating, round to the nearest full day
When prorating first month, also bill for one full period after that
Show prorate details on the invoice
Charge per minute
Minutes included
Granularity
Default prefix optionally prepended to customer DID numbers when searching for CDR records
Only charge for CDRs where the amaflags field is set to "2" ("BILL"/"BILLING").
Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values:
Only charge for CDRs where the CDR Type is set to this cdrtypenum:
Do not charge for CDRs where the CDR Type is set to this cdrtypenum:
Only charge for CDRs where the CDR Call Type is set to this cdrtypenum:
Do not charge for CDRs where the CDR Call Type is set to this cdrtypenum:
Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values:
Only charge for CDRs where the Disposition is set to any of these (comma-separated) values:
Do not charge for CDRs where the dcontext is set to any of these (comma-separated) values:
Do not charge for CDRs where the dstchannel starts with:
Do not charge for CDRs where the destination is less than this many digits:
Do not charge for CDRs where the lastapp matches this value
Calculate usage based on the duration field instead of the billsec field
CDR invoice display format
Always put usage details in separate section
Include usage summary with recurring charges when usage is in separate section
Section in which to place usage charges (whether separated or not)
Generate an invoice immediately for every call. Useful for prepaid.
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Charge recurring fee for period +
Recurring fee method +
Billing Day (1 - 28) for prorating or subscription +
When prorating, defer the first bill until the billing day +
When prorating, round to the nearest full day +
When prorating first month, also bill for one full period after that +
Show prorate details on the invoice +
Charge per minute +
Minutes included +
Granularity +
Default prefix optionally prepended to customer DID numbers when searching for CDR records +
Only charge for CDRs where the amaflags field is set to "2" ("BILL"/"BILLING"). +
Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: +
Only charge for CDRs where the CDR Type is set to this cdrtypenum: +
Do not charge for CDRs where the CDR Type is set to this cdrtypenum: +
Only charge for CDRs where the CDR Call Type is set to this cdrtypenum: +
Do not charge for CDRs where the CDR Call Type is set to this cdrtypenum: +
Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: +
Only charge for CDRs where the Disposition is set to any of these (comma-separated) values: +
Do not charge for CDRs where the dcontext is set to any of these (comma-separated) values: +
Do not charge for CDRs where the dstchannel starts with: +
Do not charge for CDRs where the destination is less than this many digits: +
Do not charge for CDRs where the lastapp matches this value +
Calculate usage based on the duration field instead of the billsec field +
CDR invoice display format +
Always put usage details in separate section +
Include usage summary with recurring charges when usage is in separate section +
Section in which to place usage charges (whether separated or not) +
Generate an invoice immediately for every call. Useful for prepaid. +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4403,7 +4798,64 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Charge recurring fee for period
Recurring fee method
Billing Day (1 - 28) for prorating or subscription
When prorating, defer the first bill until the billing day
When prorating, round to the nearest full day
When prorating first month, also bill for one full period after that
Show prorate details on the invoice
CDR service matching method
Call direction when using phone number matching
Tier plan
Rounding for destination prefix rating
Number of calls included at no usage charge
Minutes included
Granularity
Default prefix optionally prepended to customer DID numbers when searching for CDR records
Disable rating of CDR records based on the "src" field in addition to "charged_party"
Only charge for CDRs where the amaflags field is set to "2" ("BILL"/"BILLING").
Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values:
Only charge for CDRs where the CDR Type is set to this cdrtypenum:
Do not charge for CDRs where the CDR Type is set to this cdrtypenum:
Only charge for CDRs where the CDR Call Type is set to this calltypenum:
Do not charge for CDRs where the CDR Call Type is set to this calltypenum:
Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values:
Only charge for CDRs where the Disposition is set to any of these (comma-separated) values:
Do not charge for CDRs where the dcontext is set to any of these (comma-separated) values:
Do not charge for CDRs where the destination number starts with any of these values:
Do not charge for CDRs where the dstchannel starts with:
Do not charge for CDRs where the source is more than this many digits:
When using an alternate rate plan for toll-free accountcodes, the CDR field to use in rating calculations
Do not charge for CDRs where the destination is less than this many digits:
Do not charge for CDRs where the lastapp matches this value:
Do not charge for CDRs where max_callers is less than or equal to this value:
Do not charge for calls between numbers belonging to the same customer
Calculate usage based on the duration field instead of the billsec field
Rewrite these (comma-separated) destination numbers to 411 for rating purposes (also ignore any carrierid check):
CDR display format for invoices
CDR display format for selfservice
Inbound CDR display format for selfservice
Always put usage details in separate section. The section is defined in the next option.
Section in which to place usage charges (whether separated or not):
Include usage summary with recurring charges when usage is in separate section
Show details for included / no-charge calls.
Generate an invoice immediately for every call (as well any setup fee, upon first payment). Useful for prepaid.
Bill for all phone numbers that were active during the billing period
Only bill CDRs with a date during the package billing period
Consider for tax purposes the number of lines to be svc_phones that may be provisioned rather than those that actually are.
Continue recurring billing while suspended
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Charge recurring fee for period +
Recurring fee method +
Billing Day (1 - 28) for prorating or subscription +
When prorating, defer the first bill until the billing day +
When prorating, round to the nearest full day +
When prorating first month, also bill for one full period after that +
Show prorate details on the invoice +
CDR service matching method +
Call direction when using phone number matching +
Tier plan +
Rounding for destination prefix rating +
Number of calls included at no usage charge +
Minutes included +
Granularity +
Default prefix optionally prepended to customer DID numbers when searching for CDR records +
Disable rating of CDR records based on the "src" field in addition to "charged_party" +
Only charge for CDRs where the amaflags field is set to "2" ("BILL"/"BILLING"). +
Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: +
Only charge for CDRs where the CDR Type is set to this cdrtypenum: +
Do not charge for CDRs where the CDR Type is set to this cdrtypenum: +
Only charge for CDRs where the CDR Call Type is set to this calltypenum: +
Do not charge for CDRs where the CDR Call Type is set to this calltypenum: +
Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: +
Only charge for CDRs where the Disposition is set to any of these (comma-separated) values: +
Do not charge for CDRs where the dcontext is set to any of these (comma-separated) values: +
Do not charge for CDRs where the destination number starts with any of these values: +
Do not charge for CDRs where the dstchannel starts with: +
Do not charge for CDRs where the source is more than this many digits: +
When using an alternate rate plan for toll-free accountcodes, the CDR field to use in rating calculations +
Do not charge for CDRs where the destination is less than this many digits: +
Do not charge for CDRs where the lastapp matches this value: +
Do not charge for CDRs where max_callers is less than or equal to this value: +
Do not charge for calls between numbers belonging to the same customer +
Calculate usage based on the duration field instead of the billsec field +
Rewrite these (comma-separated) destination numbers to 411 for rating purposes (also ignore any carrierid check): +
CDR display format for invoices +
CDR display format for selfservice +
Inbound CDR display format for selfservice +
Always put usage details in separate section. The section is defined in the next option. +
Section in which to place usage charges (whether separated or not): +
Include usage summary with recurring charges when usage is in separate section +
Show details for included / no-charge calls. +
Generate an invoice immediately for every call (as well any setup fee, upon first payment). Useful for prepaid. +
Bill for all phone numbers that were active during the billing period +
Only bill CDRs with a date during the package billing period +
Consider for tax purposes the number of lines to be svc_phones that may be provisioned rather than those that actually are. +
Continue recurring billing while suspended +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4412,7 +4864,28 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Charge recurring fee for period
Recurring fee method
Billing Day (1 - 28) for prorating or subscription
When prorating, defer the first bill until the billing day
When prorating, round to the nearest full day
When prorating first month, also bill for one full period after that
Show prorate details on the invoice
CDR invoice display format
Section in which to place separate usage charges
Include usage summary with recurring charges when usage is in separate section
Always put usage details in separate section
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Charge recurring fee for period +
Recurring fee method +
Billing Day (1 - 28) for prorating or subscription +
When prorating, defer the first bill until the billing day +
When prorating, round to the nearest full day +
When prorating first month, also bill for one full period after that +
Show prorate details on the invoice +
CDR invoice display format +
Section in which to place separate usage charges +
Include usage summary with recurring charges when usage is in separate section +
Always put usage details in separate section +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4421,7 +4894,14 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Nibble rate
+
Nibble rate +
+ + @@ -4430,7 +4910,21 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Setup fee for each new service
Recurring fee for each service
Only charge fees for these services
Show a count of services on the invoice, instead of a detailed list
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Setup fee for each new service +
Recurring fee for each service +
Only charge fees for these services +
Show a count of services on the invoice, instead of a detailed list +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4439,7 +4933,22 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Don't prorate recurring fees on services active for a partial month
Setup fee for each new service
Recurring fee for each service
Only charge fees for these services
Show a count of services on the invoice, instead of a detailed list
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Don't prorate recurring fees on services active for a partial month +
Setup fee for each new service +
Recurring fee for each service +
Only charge fees for these services +
Show a count of services on the invoice, instead of a detailed list +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4448,7 +4957,25 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Billing Day (1 - 28)
When prorating first month, also bill for one full period after that
Disable prorating bulk packages (charge full price for packages active only a portion of the month)
Separate customer from package display on invoices
Bill wholesale on cost only, disabling the price fallback
Defer the first bill until the billing day
Round the prorated period to the nearest full day
Show prorate details on the invoice
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Billing Day (1 - 28) +
When prorating first month, also bill for one full period after that +
Disable prorating bulk packages (charge full price for packages active only a portion of the month) +
Separate customer from package display on invoices +
Bill wholesale on cost only, disabling the price fallback +
Defer the first bill until the billing day +
Round the prorated period to the nearest full day +
Show prorate details on the invoice +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4457,7 +4984,28 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Charge recurring fee for period
Recurring fee method
Billing Day (1 - 28) for prorating or subscription
When prorating, defer the first bill until the billing day
When prorating, round to the nearest full day
When prorating first month, also bill for one full period after that
Show prorate details on the invoice
CDR invoice display format
Section in which to place separate usage charges
Include usage summary with recurring charges when usage is in separate section
Always put usage details in separate section
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Charge recurring fee for period +
Recurring fee method +
Billing Day (1 - 28) for prorating or subscription +
When prorating, defer the first bill until the billing day +
When prorating, round to the nearest full day +
When prorating first month, also bill for one full period after that +
Show prorate details on the invoice +
CDR invoice display format +
Section in which to place separate usage charges +
Include usage summary with recurring charges when usage is in separate section +
Always put usage details in separate section +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4466,7 +5014,27 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Included megabytes/sec (95th percentile)
Charge per megabyte/sec (95th percentile)
Auto-add a start date to the 1st, ignoring the current month.
When synchronizing, defer the bill until the customer's next bill date
When synchronizing, round the prorated period to the nearest full day
Continue recurring billing while suspended
Adjust next bill date forward when unsuspending
Bill the last period on cancellation
Bill immediately upon suspension
Optional External ID
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Included megabytes/sec (95th percentile) +
Charge per megabyte/sec (95th percentile) +
Auto-add a start date to the 1st, ignoring the current month. +
When synchronizing, defer the bill until the customer's next bill date +
When synchronizing, round the prorated period to the nearest full day +
Continue recurring billing while suspended +
Adjust next bill date forward when unsuspending +
Bill the last period on cancellation +
Bill immediately upon suspension +
Optional External ID +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4475,7 +5043,27 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Included gigabytes
Charge per gigabyte
Auto-add a start date to the 1st, ignoring the current month.
When synchronizing, defer the bill until the customer's next bill date
When synchronizing, round the prorated period to the nearest full day
Continue recurring billing while suspended
Adjust next bill date forward when unsuspending
Bill the last period on cancellation
Bill immediately upon suspension
Optional External ID
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Included gigabytes +
Charge per gigabyte +
Auto-add a start date to the 1st, ignoring the current month. +
When synchronizing, defer the bill until the customer's next bill date +
When synchronizing, round the prorated period to the nearest full day +
Continue recurring billing while suspended +
Adjust next bill date forward when unsuspending +
Bill the last period on cancellation +
Bill immediately upon suspension +
Optional External ID +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4484,7 +5072,23 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Units included
Additional charge per unit
DBI data source
Database username
Database username
SQL query
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Units included +
Additional charge per unit +
DBI data source +
Database username +
Database username +
SQL query +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4493,7 +5097,27 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Recurring fee method
Billing Day (1 - 28) for prorating or subscription
When prorating, defer the first bill until the billing day
When prorating, round to the nearest full day
When prorating first month, also bill for one full period after that
Show prorate details on the invoice
DBI data source
Database username
Database password
SQL query
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Recurring fee method +
Billing Day (1 - 28) for prorating or subscription +
When prorating, defer the first bill until the billing day +
When prorating, round to the nearest full day +
When prorating first month, also bill for one full period after that +
Show prorate details on the invoice +
DBI data source +
Database username +
Database password +
SQL query +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4502,7 +5126,23 @@ spawn_supp_dst_pkgpart(this);" STYLE="display: none; z-index: 0" > -
Recurring fee method
Billing Day (1 - 28) for prorating or subscription
When prorating, defer the first bill until the billing day
When prorating, round to the nearest full day
When prorating first month, also bill for one full period after that
Show prorate details on the invoice
Credit the customer for the unused portion of service at cancellation
Credit the customer for the unused portion of service when suspendingN/A
Credit the customer for the unused portion of service when changing packages
Automatically suspend for one day before cancelling
+
Recurring fee method +
Billing Day (1 - 28) for prorating or subscription +
When prorating, defer the first bill until the billing day +
When prorating, round to the nearest full day +
When prorating first month, also bill for one full period after that +
Show prorate details on the invoice +
Credit the customer for the unused portion of service at cancellation +
Credit the customer for the unused portion of service when suspending + N/A
Credit the customer for the unused portion of service when changing packages +
Automatically suspend for one day before cancelling +
+ + @@ -4521,12 +5161,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -4574,12 +5214,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -4627,12 +5267,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat').style.zIndex = 0; document.getElementById('plandflat_delayed').style.display = "none"; document.getElementById('plandflat_delayed').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -4674,7 +5314,7 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_introrate').style.zIndex = 1; } - if (planlayer == "prorate_calendar" ) { + if (planlayer == "prorate" ) { document.getElementById('plandflat').style.display = "none"; document.getElementById('plandflat').style.zIndex = 0; @@ -4682,10 +5322,10 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate').style.display = "none"; - document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -4723,11 +5363,11 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandcurrency_fixed').style.display = "none"; document.getElementById('plandcurrency_fixed').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = ""; - document.getElementById('plandprorate_calendar').style.zIndex = 1; + document.getElementById('plandprorate').style.display = ""; + document.getElementById('plandprorate').style.zIndex = 1; } - if (planlayer == "prorate" ) { + if (planlayer == "prorate_delayed" ) { document.getElementById('plandflat').style.display = "none"; document.getElementById('plandflat').style.zIndex = 0; @@ -4735,10 +5375,10 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; + document.getElementById('plandprorate').style.display = "none"; + document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_calendar').style.display = "none"; document.getElementById('plandprorate_calendar').style.zIndex = 0; - document.getElementById('plandprorate_delayed').style.display = "none"; - document.getElementById('plandprorate_delayed').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -4776,11 +5416,11 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandcurrency_fixed').style.display = "none"; document.getElementById('plandcurrency_fixed').style.zIndex = 0; - document.getElementById('plandprorate').style.display = ""; - document.getElementById('plandprorate').style.zIndex = 1; + document.getElementById('plandprorate_delayed').style.display = ""; + document.getElementById('plandprorate_delayed').style.zIndex = 1; } - if (planlayer == "prorate_delayed" ) { + if (planlayer == "prorate_calendar" ) { document.getElementById('plandflat').style.display = "none"; document.getElementById('plandflat').style.zIndex = 0; @@ -4788,10 +5428,10 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; + document.getElementById('plandprorate_delayed').style.display = "none"; + document.getElementById('plandprorate_delayed').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -4829,8 +5469,8 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandcurrency_fixed').style.display = "none"; document.getElementById('plandcurrency_fixed').style.zIndex = 0; - document.getElementById('plandprorate_delayed').style.display = ""; - document.getElementById('plandprorate_delayed').style.zIndex = 1; + document.getElementById('plandprorate_calendar').style.display = ""; + document.getElementById('plandprorate_calendar').style.zIndex = 1; } if (planlayer == "prepaid" ) { @@ -4841,12 +5481,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; document.getElementById('plandsubscription').style.zIndex = 0; document.getElementById('plandsqlradacct_hour').style.display = "none"; @@ -4894,12 +5534,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsqlradacct_hour').style.display = "none"; @@ -4947,12 +5587,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5000,12 +5640,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5053,12 +5693,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5106,12 +5746,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5159,12 +5799,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5212,12 +5852,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5265,12 +5905,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5318,12 +5958,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5371,12 +6011,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5424,12 +6064,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5477,12 +6117,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5530,12 +6170,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5583,12 +6223,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5636,12 +6276,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5689,12 +6329,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; @@ -5742,12 +6382,12 @@ spawn_supp_dst_pkgpart(this);" document.getElementById('plandflat_delayed').style.zIndex = 0; document.getElementById('plandflat_introrate').style.display = "none"; document.getElementById('plandflat_introrate').style.zIndex = 0; - document.getElementById('plandprorate_calendar').style.display = "none"; - document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprorate').style.display = "none"; document.getElementById('plandprorate').style.zIndex = 0; document.getElementById('plandprorate_delayed').style.display = "none"; document.getElementById('plandprorate_delayed').style.zIndex = 0; + document.getElementById('plandprorate_calendar').style.display = "none"; + document.getElementById('plandprorate_calendar').style.zIndex = 0; document.getElementById('plandprepaid').style.display = "none"; document.getElementById('plandprepaid').style.zIndex = 0; document.getElementById('plandsubscription').style.display = "none"; diff --git a/FS-Test/share/output/graph/cust_bill_pkg.cgi/start_month=4:start_year=2015:end_month=3:end_year=2016:agentnum=1:cust_classnum=:refnum=1:class_mode=pkg:classnum=0:classnum=1:report_optionnum=0:class_agg_break=aggregate:use_setup=1:use_usage=0:use_discount=1 b/FS-Test/share/output/graph/cust_bill_pkg.cgi/start_month=4:start_year=2015:end_month=3:end_year=2016:agentnum=1:cust_classnum=:refnum=1:class_mode=pkg:classnum=0:classnum=1:report_optionnum=0:class_agg_break=aggregate:use_setup=1:use_usage=0:use_discount=1 index 0cd3f850c..e61ca51a1 100644 --- a/FS-Test/share/output/graph/cust_bill_pkg.cgi/start_month=4:start_year=2015:end_month=3:end_year=2016:agentnum=1:cust_classnum=:refnum=1:class_mode=pkg:classnum=0:classnum=1:report_optionnum=0:class_agg_break=aggregate:use_setup=1:use_usage=0:use_discount=1 +++ b/FS-Test/share/output/graph/cust_bill_pkg.cgi/start_month=4:start_year=2015:end_month=3:end_year=2016:agentnum=1:cust_classnum=:refnum=1:class_mode=pkg:classnum=0:classnum=1:report_optionnum=0:class_agg_break=aggregate:use_setup=1:use_usage=0:use_discount=1 @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/graph/cust_pkg.html/start_month=4:start_year=2015:end_month=3:end_year=2016 b/FS-Test/share/output/graph/cust_pkg.html/start_month=4:start_year=2015:end_month=3:end_year=2016 index 6d2242390..24ef4580a 100644 --- a/FS-Test/share/output/graph/cust_pkg.html/start_month=4:start_year=2015:end_month=3:end_year=2016 +++ b/FS-Test/share/output/graph/cust_pkg.html/start_month=4:start_year=2015:end_month=3:end_year=2016 @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/graph/money_time.cgi/start_month=4:start_year=2015:end_month=3:end_year=2016:agentnum=1:cust_classnum=:refnum=1 b/FS-Test/share/output/graph/money_time.cgi/start_month=4:start_year=2015:end_month=3:end_year=2016:agentnum=1:cust_classnum=:refnum=1 index 9ed8214dd..106bad477 100644 --- a/FS-Test/share/output/graph/money_time.cgi/start_month=4:start_year=2015:end_month=3:end_year=2016:agentnum=1:cust_classnum=:refnum=1 +++ b/FS-Test/share/output/graph/money_time.cgi/start_month=4:start_year=2015:end_month=3:end_year=2016:agentnum=1:cust_classnum=:refnum=1 @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/graph/money_time_daily.cgi/beginning=01%2F01%2F2016:ending=02%2F28%2F2016:agentnum=1 b/FS-Test/share/output/graph/money_time_daily.cgi/beginning=01%2F01%2F2016:ending=02%2F28%2F2016:agentnum=1 index bc1f09d27..9a0954667 100644 --- a/FS-Test/share/output/graph/money_time_daily.cgi/beginning=01%2F01%2F2016:ending=02%2F28%2F2016:agentnum=1 +++ b/FS-Test/share/output/graph/money_time_daily.cgi/beginning=01%2F01%2F2016:ending=02%2F28%2F2016:agentnum=1 @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/search/cust_bill.html/OPEN90_date b/FS-Test/share/output/search/cust_bill.html/OPEN90_date deleted file mode 100644 index bf1c2dd46..000000000 --- a/FS-Test/share/output/search/cust_bill.html/OPEN90_date +++ /dev/null @@ -1,5993 +0,0 @@ - - - - - - Invoice Search Results - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
freeside - Freeside Test 5.0.1 - Logged in as test  logout
Preferences -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- Adv - -
- - - - - -
- -
-
- Advanced - -
- - - - - -
- - - - - -
- - Adv
- -
- - - - - -
- -
-
- Advanced - -
- - - - - -
- -
-
- Advanced - -
- - - - - -
- - - - - - - -
- - -
- -

- Invoice Search Results -

- -
- - - Print these invoices | Email these invoices - -

- - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - - - - - - -
- - - -
- - - - - - - - - - -
- - - -
- - - - - - - - - - -
- - - -
- - - - - - - - - - -
- - - -
- - - - - - - - - - - - - - - -
- -
- - 219 total invoices - - - ( show per page ) - - -
- - $20769.29 gross sales
- − $0.00 discounted
- − $0.00 credited
- = $20769.29 net sales
- $20769.29 outstanding balance
-
- -
- - Download full results
- - as Excel spreadsheet
- - as CSV file
- - - as printable copy - -
- - - - 1 - - - 2 - - - 3 - - Next - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Invoice # - - Gross Amount - - Discount - - Credits - - Net Amount - - Balance - - Date - - Cust. Status - - Customer -
1$129.19$0.00$0.00$129.19$129.19Aug 07 2015ActiveRuecker, Lucious
4$191.77$0.00$0.00$191.77$191.77Aug 09 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
10$143.07$0.00$0.00$143.07$143.07Aug 17 2015ActiveBoyer, Lamont
12$165.65$0.00$0.00$165.65$165.65Aug 18 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
19$158.71$0.00$0.00$158.71$158.71Aug 22 2015ActiveLehner-Klein (Smitham, Pansy)
20$108.23$0.00$0.00$108.23$108.23Aug 26 2015ActiveChristiansen, Leone
23$105.97$0.00$0.00$105.97$105.97Aug 31 2015ActiveWaters, Godfrey
35$90.00$0.00$0.00$90.00$90.00Sep 01 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
33$120.00$0.00$0.00$120.00$120.00Sep 01 2015ActiveBoyer, Lamont
39$30.00$0.00$0.00$30.00$30.00Sep 01 2015ActiveRuecker, Lucious
46$30.00$0.00$0.00$30.00$30.00Sep 01 2015ActiveWaters, Godfrey
26$120.00$0.00$0.00$120.00$120.00Sep 01 2015ActiveLehner-Klein (Smitham, Pansy)
29$120.00$0.00$0.00$120.00$120.00Sep 01 2015ActiveChristiansen, Leone
43$90.00$0.00$0.00$90.00$90.00Sep 01 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
54$60.00$0.00$0.00$60.00$60.00Sep 07 2015ActiveRuecker, Lucious
55$212.00$0.00$0.00$212.00$212.00Sep 08 2015ActiveO'Reilly-Mraz (Pagac, Kennedi)
57$60.00$0.00$0.00$60.00$60.00Sep 09 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
59$182.00$0.00$0.00$182.00$182.00Sep 12 2015ActiveSimonis Inc (Runolfsson, Kareem)
60$179.00$0.00$0.00$179.00$179.00Sep 13 2015ActiveConn-McLaughlin (O'Connell, Gayle)
63$188.00$0.00$0.00$188.00$188.00Sep 14 2015ActiveO'Keefe Inc (Schamberger, Felix)
65$60.00$0.00$0.00$60.00$60.00Sep 18 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
69$161.00$0.00$0.00$161.00$161.00Sep 19 2015ActiveBoyle-Schmeler (Maggio, Fay)
74$129.00$0.00$0.00$129.00$129.00Sep 20 2015ActiveStokes, Janelle
73$164.00$0.00$0.00$164.00$164.00Sep 20 2015ActiveFlatley, Yundt and Pacocha (Volkman, Tabitha)
72$164.00$0.00$0.00$164.00$164.00Sep 20 2015ActiveBalistreri-Schoen (Schultz, Jaylan)
75$115.00$0.00$0.00$115.00$115.00Sep 21 2015ActiveSwaniawski, Adrienne
79$60.00$0.00$0.00$60.00$60.00Sep 22 2015ActiveLehner-Klein (Smitham, Pansy)
81$113.00$0.00$0.00$113.00$113.00Sep 23 2015ActiveBernhard, Kris
83$143.00$0.00$0.00$143.00$143.00Sep 25 2015ActiveWolff Inc (Hessel, Brianne)
86$101.00$0.00$0.00$101.00$101.00Sep 27 2015ActiveGleichner, Delmer
88$108.00$0.00$0.00$108.00$108.00Sep 28 2015ActiveDouglas, Willow
92$60.00$0.00$0.00$60.00$60.00Sep 28 2015ActiveWaters, Godfrey
94$131.00$0.00$0.00$131.00$131.00Sep 29 2015ActiveBernhard-Treutel (Shanahan, Kevin)
96$128.00$0.00$0.00$128.00$128.00Sep 30 2015ActiveTorp, Sawayn and Friesen (Pollich, Maritza)
122$120.00$0.00$0.00$120.00$120.00Oct 01 2015ActiveBoyer, Lamont
104$120.00$0.00$0.00$120.00$120.00Oct 01 2015ActiveLehner-Klein (Smitham, Pansy)
110$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveBernhard-Treutel (Shanahan, Kevin)
124$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
150$30.00$0.00$0.00$30.00$30.00Oct 01 2015ActiveBernhard, Kris
100$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveTorp, Sawayn and Friesen (Pollich, Maritza)
105$30.00$0.00$0.00$30.00$30.00Oct 01 2015ActiveSwaniawski, Adrienne
137$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
117$120.00$0.00$0.00$120.00$120.00Oct 01 2015ActiveO'Reilly-Mraz (Pagac, Kennedi)
143$120.00$0.00$0.00$120.00$120.00Oct 01 2015ActiveStokes, Janelle
134$120.00$0.00$0.00$120.00$120.00Oct 01 2015ActiveFlatley, Yundt and Pacocha (Volkman, Tabitha)
115$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveSimonis Inc (Runolfsson, Kareem)
108$120.00$0.00$0.00$120.00$120.00Oct 01 2015ActiveChristiansen, Leone
127$30.00$0.00$0.00$30.00$30.00Oct 01 2015ActiveRuecker, Lucious
151$30.00$0.00$0.00$30.00$30.00Oct 01 2015ActiveWaters, Godfrey
98$30.00$0.00$0.00$30.00$30.00Oct 01 2015ActiveDouglas, Willow
153$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveConn-McLaughlin (O'Connell, Gayle)
114$120.00$0.00$0.00$120.00$120.00Oct 01 2015ActiveBalistreri-Schoen (Schultz, Jaylan)
131$120.00$0.00$0.00$120.00$120.00Oct 01 2015ActiveGleichner, Delmer
129$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveWolff Inc (Hessel, Brianne)
147$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveBoyle-Schmeler (Maggio, Fay)
146$120.00$0.00$0.00$120.00$120.00Oct 01 2015ActiveO'Keefe Inc (Schamberger, Felix)
161$236.13$0.00$0.00$236.13$236.13Oct 02 2015ActiveChristiansen LLC (Howe, Luis)
165$209.19$0.00$0.00$209.19$209.19Oct 03 2015ActiveWolff and Sons (Heller, Dagmar)
167$185.64$0.00$0.00$185.64$185.64Oct 06 2015ActiveRomaguera, Tianna
171$60.00$0.00$0.00$60.00$60.00Oct 07 2015ActiveRuecker, Lucious
175$60.00$0.00$0.00$60.00$60.00Oct 08 2015ActiveO'Reilly-Mraz (Pagac, Kennedi)
178$128.23$0.00$0.00$128.23$128.23Oct 08 2015ActivePowlowski, Veda
181$60.00$0.00$0.00$60.00$60.00Oct 09 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
180$174.03$0.00$0.00$174.03$174.03Oct 09 2015ActiveSchowalter, Oswald
184$60.00$0.00$0.00$60.00$60.00Oct 12 2015ActiveSimonis Inc (Runolfsson, Kareem)
188$123.39$0.00$0.00$123.39$123.39Oct 13 2015ActiveJakubowski, Jarrell
189$60.00$0.00$0.00$60.00$60.00Oct 13 2015ActiveConn-McLaughlin (O'Connell, Gayle)
191$60.00$0.00$0.00$60.00$60.00Oct 14 2015ActiveO'Keefe Inc (Schamberger, Felix)
190$154.68$0.00$0.00$154.68$154.68Oct 14 2015ActiveMarquardt, Abbey
193$154.68$0.00$0.00$154.68$154.68Oct 14 2015ActiveWilderman, Annalise
195$171.45$0.00$0.00$171.45$171.45Oct 16 2015ActiveBahringer LLC (Frami, Roslyn)
198$60.00$0.00$0.00$60.00$60.00Oct 18 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
200$118.55$0.00$0.00$118.55$118.55Oct 18 2015ActiveKonopelski, Barry
206$60.00$0.00$0.00$60.00$60.00Oct 19 2015ActiveBoyle-Schmeler (Maggio, Fay)
204$117.58$0.00$0.00$117.58$117.58Oct 19 2015ActiveWeber, Aliza
210$60.00$0.00$0.00$60.00$60.00Oct 20 2015ActiveFlatley, Yundt and Pacocha (Volkman, Tabitha)
208$60.00$0.00$0.00$60.00$60.00Oct 20 2015ActiveBalistreri-Schoen (Schultz, Jaylan)
212$60.00$0.00$0.00$60.00$60.00Oct 21 2015ActiveSwaniawski, Adrienne
216$60.00$0.00$0.00$60.00$60.00Oct 22 2015ActiveLehner-Klein (Smitham, Pansy)
219$60.00$0.00$0.00$60.00$60.00Oct 23 2015ActiveBernhard, Kris
220$148.23$0.00$0.00$148.23$148.23Oct 24 2015ActiveDuBuque Inc (King, Thomas)
223$60.00$0.00$0.00$60.00$60.00Oct 25 2015ActiveWolff Inc (Hessel, Brianne)
228$60.00$0.00$0.00$60.00$60.00Oct 28 2015ActiveDouglas, Willow
227$60.00$0.00$0.00$60.00$60.00Oct 28 2015ActiveTorp, Sawayn and Friesen (Pollich, Maritza)
233$135.48$0.00$0.00$135.48$135.48Oct 28 2015ActiveKohler Group (Bechtelar, Leopoldo)
234$60.00$0.00$0.00$60.00$60.00Oct 28 2015ActiveWaters, Godfrey
230$60.00$0.00$0.00$60.00$60.00Oct 28 2015ActiveBernhard-Treutel (Shanahan, Kevin)
235$106.94$0.00$0.00$106.94$106.94Oct 30 2015ActiveProhaska, Ellis
236$105.97$0.00$0.00$105.97$105.97Oct 31 2015ActiveMorar, Braulio
320$30.00$0.00$0.00$30.00$30.00Nov 01 2015ActiveBernhard, Kris
313$215.00$0.00$0.00$215.00$215.00Nov 01 2015ActiveMann, Wilderman and Stiedemann (Maggio, Remington)
304$30.00$0.00$0.00$30.00$30.00Nov 01 2015ActivePowlowski, Veda
238$120.00$0.00$0.00$120.00$120.00Nov 01 2015ActiveSchowalter, Oswald
268$120.00$0.00$0.00$120.00$120.00Nov 01 2015ActiveBalistreri-Schoen (Schultz, Jaylan)
239$30.00$0.00$0.00$30.00$30.00Nov 01 2015ActiveDouglas, Willow
297$30.00$0.00$0.00$30.00$30.00Nov 01 2015ActiveKonopelski, Barry
314$120.00$0.00$0.00$120.00$120.00Nov 01 2015ActiveO'Keefe Inc (Schamberger, Felix)
266$120.00$0.00$0.00$120.00$120.00Nov 01 2015ActiveMarquardt, Abbey
330$30.00$0.00$0.00$30.00$30.00Nov 01 2015ActiveProhaska, Ellis
285$90.00$0.00$0.00$90.00$90.00Nov 01 2015ActiveWolff Inc (Hessel, Brianne)
- - - - 1 - - - 2 - - - 3 - - Next - - - -
- - - - -
- - - - - - - - diff --git a/FS-Test/share/output/search/cust_bill.html/date b/FS-Test/share/output/search/cust_bill.html/date deleted file mode 100644 index 5681b4224..000000000 --- a/FS-Test/share/output/search/cust_bill.html/date +++ /dev/null @@ -1,6054 +0,0 @@ - - - - - - Invoice Search Results - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
freeside - Freeside Test 5.0.1 - Logged in as test  logout
Preferences -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- Adv - -
- - - - - -
- -
-
- Advanced - -
- - - - - -
- - - - - -
- - Adv
- -
- - - - - -
- -
-
- Advanced - -
- - - - - -
- -
-
- Advanced - -
- - - - - -
- - - - - - - -
- - -
- -

- Invoice Search Results -

- -
- - - Print these invoices | Email these invoices - -

- - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - - - - - - -
- - -
- - - - - - - - - - -
- - -
- - - - - - - - - - -
- - -
- - - - - - - - - - -
- - -
- - - - - - - - - - - - - - - -
- -
- - 1584 total invoices - - - ( show per page ) - - -
- - $141620.74 gross sales
- − $0.00 discounted
- − $0.00 credited
- = $141620.74 net sales
- $48352.49 outstanding balance
-
- -
- - Download full results
- - as Excel spreadsheet
- - as CSV file
- - - as printable copy - -
- - - - 1 - - - 2 - - - 3 - - - 4 - - - 5 - - - 6 - - ... - - 8 - - - 9 - - ... - - 11 - - ... - - 13 - - - 14 - - - 15 - - - 16 - - Next - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Invoice # - - Gross Amount - - Discount - - Credits - - Net Amount - - Balance - - Date - - Cust. Status - - Customer -
1$129.19$0.00$0.00$129.19$129.19Aug 07 2015ActiveRuecker, Lucious
3$194.68$0.00$0.00$194.68$0.00Aug 08 2015ActiveRoberts-Schinner (Flatley, Amelia)
2$177.91$0.00$0.00$177.91$0.00Aug 08 2015ActiveZemlak, Asia
4$191.77$0.00$0.00$191.77$191.77Aug 09 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
5$162.41$0.00$0.00$162.41$0.00Aug 12 2015ActiveSteuber, Ryley
6$158.55$0.00$0.00$158.55$0.00Aug 13 2015ActiveLeuschke, Edd
7$154.68$0.00$0.00$154.68$0.00Aug 14 2015ActiveGrady, Aniya
8$189.68$0.00$0.00$189.68$0.00Aug 14 2015ActiveWeimann Inc (Cartwright, Judah)
9$174.35$0.00$0.00$174.35$0.00Aug 15 2015ActiveKozey and Sons (Vandervort, Harmon)
10$143.07$0.00$0.00$143.07$143.07Aug 17 2015ActiveBoyer, Lamont
11$143.07$0.00$0.00$143.07$0.00Aug 17 2015ActiveDonnelly, Raleigh
13$174.20$0.00$0.00$174.20$0.00Aug 18 2015ActiveBraun, Rath and Gutkowski (Wilderman, Reyes)
12$165.65$0.00$0.00$165.65$165.65Aug 18 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
14$170.32$0.00$0.00$170.32$0.00Aug 19 2015ActiveKuhlman-Huels (Parisian, Cristopher)
15$135.32$0.00$0.00$135.32$0.00Aug 19 2015ActiveBrown, Danial
16$162.74$0.00$0.00$162.74$0.00Aug 19 2015ActiveFay and Sons (Gerhold, Thora)
17$159.84$0.00$0.00$159.84$0.00Aug 20 2015ActiveLeannon-Pfannerstill (O'Keefe, Bernie)
18$156.94$0.00$0.00$156.94$0.00Aug 21 2015ActiveMedhurst Group (Medhurst, Rafaela)
19$158.71$0.00$0.00$158.71$158.71Aug 22 2015ActiveLehner-Klein (Smitham, Pansy)
20$108.23$0.00$0.00$108.23$108.23Aug 26 2015ActiveChristiansen, Leone
21$92.75$0.00$0.00$92.75$0.00Aug 30 2015ActiveKessler, Dana
23$105.97$0.00$0.00$105.97$105.97Aug 31 2015ActiveWaters, Godfrey
22$88.87$0.00$0.00$88.87$0.00Aug 31 2015ActiveSchultz, Colten
25$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveZemlak, Asia
45$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveWeimann Inc (Cartwright, Judah)
28$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveLeannon-Pfannerstill (O'Keefe, Bernie)
27$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveKuhlman-Huels (Parisian, Cristopher)
43$90.00$0.00$0.00$90.00$90.00Sep 01 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
32$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveSchultz, Colten
26$120.00$0.00$0.00$120.00$120.00Sep 01 2015ActiveLehner-Klein (Smitham, Pansy)
46$30.00$0.00$0.00$30.00$30.00Sep 01 2015ActiveWaters, Godfrey
33$120.00$0.00$0.00$120.00$120.00Sep 01 2015ActiveBoyer, Lamont
35$90.00$0.00$0.00$90.00$90.00Sep 01 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
30$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveSteuber, Ryley
37$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveKozey and Sons (Vandervort, Harmon)
44$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveLeuschke, Edd
34$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveBrown, Danial
38$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveFay and Sons (Gerhold, Thora)
40$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveBraun, Rath and Gutkowski (Wilderman, Reyes)
41$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveRoberts-Schinner (Flatley, Amelia)
29$120.00$0.00$0.00$120.00$120.00Sep 01 2015ActiveChristiansen, Leone
31$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveMedhurst Group (Medhurst, Rafaela)
39$30.00$0.00$0.00$30.00$30.00Sep 01 2015ActiveRuecker, Lucious
36$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveKessler, Dana
24$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveGrady, Aniya
42$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveDonnelly, Raleigh
47$236.00$0.00$0.00$236.00$0.00Sep 02 2015ActiveMoore-Cummerata (DuBuque, Russ)
49$212.00$0.00$0.00$212.00$0.00Sep 02 2015ActiveMante LLC (Kessler, Enid)
48$212.00$0.00$0.00$212.00$0.00Sep 02 2015ActiveLuettgen-Jacobs (Hintz, Junior)
50$209.00$0.00$0.00$209.00$0.00Sep 03 2015ActiveHoeger-Brown (Shields, Serenity)
51$228.00$0.00$0.00$228.00$0.00Sep 04 2015ActiveLind-Bahringer (Ratke, Roma)
53$200.00$0.00$0.00$200.00$0.00Sep 06 2015ActivePfeffer, Shanahan and Cruickshank (Kutch, Rosario)
52$185.00$0.00$0.00$185.00$0.00Sep 06 2015ActiveMcKenzie, Kareem
54$60.00$0.00$0.00$60.00$60.00Sep 07 2015ActiveRuecker, Lucious
56$60.00$0.00$0.00$60.00$0.00Sep 08 2015ActiveRoberts-Schinner (Flatley, Amelia)
55$212.00$0.00$0.00$212.00$212.00Sep 08 2015ActiveO'Reilly-Mraz (Pagac, Kennedi)
57$60.00$0.00$0.00$60.00$60.00Sep 09 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
58$188.00$0.00$0.00$188.00$0.00Sep 10 2015ActiveFlatley-Hagenes (Donnelly, Odessa)
59$182.00$0.00$0.00$182.00$182.00Sep 12 2015ActiveSimonis Inc (Runolfsson, Kareem)
60$179.00$0.00$0.00$179.00$179.00Sep 13 2015ActiveConn-McLaughlin (O'Connell, Gayle)
61$153.00$0.00$0.00$153.00$0.00Sep 14 2015ActiveKunde, Noemi
63$188.00$0.00$0.00$188.00$188.00Sep 14 2015ActiveO'Keefe Inc (Schamberger, Felix)
62$60.00$0.00$0.00$60.00$0.00Sep 14 2015ActiveWeimann Inc (Cartwright, Judah)
64$60.00$0.00$0.00$60.00$0.00Sep 15 2015ActiveKozey and Sons (Vandervort, Harmon)
66$60.00$0.00$0.00$60.00$0.00Sep 18 2015ActiveBraun, Rath and Gutkowski (Wilderman, Reyes)
65$60.00$0.00$0.00$60.00$60.00Sep 18 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
70$117.00$0.00$0.00$117.00$0.00Sep 19 2015ActiveTurcotte, Janessa
69$161.00$0.00$0.00$161.00$161.00Sep 19 2015ActiveBoyle-Schmeler (Maggio, Fay)
67$60.00$0.00$0.00$60.00$0.00Sep 19 2015ActiveKuhlman-Huels (Parisian, Cristopher)
68$60.00$0.00$0.00$60.00$0.00Sep 19 2015ActiveFay and Sons (Gerhold, Thora)
71$60.00$0.00$0.00$60.00$0.00Sep 20 2015ActiveLeannon-Pfannerstill (O'Keefe, Bernie)
74$129.00$0.00$0.00$129.00$129.00Sep 20 2015ActiveStokes, Janelle
72$164.00$0.00$0.00$164.00$164.00Sep 20 2015ActiveBalistreri-Schoen (Schultz, Jaylan)
73$164.00$0.00$0.00$164.00$164.00Sep 20 2015ActiveFlatley, Yundt and Pacocha (Volkman, Tabitha)
75$115.00$0.00$0.00$115.00$115.00Sep 21 2015ActiveSwaniawski, Adrienne
78$155.00$0.00$0.00$155.00$0.00Sep 21 2015ActiveMcLaughlin-Luettgen (Berge, Houston)
77$115.00$0.00$0.00$115.00$0.00Sep 21 2015ActiveFrami, Gayle
76$60.00$0.00$0.00$60.00$0.00Sep 21 2015ActiveMedhurst Group (Medhurst, Rafaela)
79$60.00$0.00$0.00$60.00$60.00Sep 22 2015ActiveLehner-Klein (Smitham, Pansy)
80$114.00$0.00$0.00$114.00$0.00Sep 22 2015ActiveGleason, Ahmed
81$113.00$0.00$0.00$113.00$113.00Sep 23 2015ActiveBernhard, Kris
82$113.00$0.00$0.00$113.00$0.00Sep 24 2015ActiveAbbott, Addison
83$143.00$0.00$0.00$143.00$143.00Sep 25 2015ActiveWolff Inc (Hessel, Brianne)
85$110.00$0.00$0.00$110.00$0.00Sep 26 2015ActiveRowe, Amara
84$105.00$0.00$0.00$105.00$0.00Sep 26 2015ActiveCarter, Cathy
86$101.00$0.00$0.00$101.00$101.00Sep 27 2015ActiveGleichner, Delmer
87$136.00$0.00$0.00$136.00$0.00Sep 27 2015ActiveRodriguez-Ebert (Bergstrom, Cecilia)
92$60.00$0.00$0.00$60.00$60.00Sep 28 2015ActiveWaters, Godfrey
88$108.00$0.00$0.00$108.00$108.00Sep 28 2015ActiveDouglas, Willow
89$97.00$0.00$0.00$97.00$0.00Sep 28 2015ActiveToy, Bethany
90$132.00$0.00$0.00$132.00$0.00Sep 28 2015ActiveQuitzon Group (Davis, Jeffery)
91$97.00$0.00$0.00$97.00$0.00Sep 28 2015ActiveKunze, Michale
95$128.00$0.00$0.00$128.00$0.00Sep 29 2015ActiveCole, Graham and Towne (Hickle, Javier)
93$131.00$0.00$0.00$131.00$0.00Sep 29 2015ActiveToy-Gerlach (Zulauf, Sharon)
94$131.00$0.00$0.00$131.00$131.00Sep 29 2015ActiveBernhard-Treutel (Shanahan, Kevin)
96$128.00$0.00$0.00$128.00$128.00Sep 30 2015ActiveTorp, Sawayn and Friesen (Pollich, Maritza)
121$120.00$0.00$0.00$120.00$0.00Oct 01 2015ActiveMoore-Cummerata (DuBuque, Russ)
124$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
147$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveBoyle-Schmeler (Maggio, Fay)
153$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveConn-McLaughlin (O'Connell, Gayle)
- - - - 1 - - - 2 - - - 3 - - - 4 - - - 5 - - - 6 - - ... - - 8 - - - 9 - - ... - - 11 - - ... - - 13 - - - 14 - - - 15 - - - 16 - - Next - - - -
- - - - -
- - - - - - - - diff --git a/FS-Test/share/output/search/cust_bill.html/keywords=OPEN90_date:order_by=invnum b/FS-Test/share/output/search/cust_bill.html/keywords=OPEN90_date:order_by=invnum new file mode 100644 index 000000000..cb6d637f3 --- /dev/null +++ b/FS-Test/share/output/search/cust_bill.html/keywords=OPEN90_date:order_by=invnum @@ -0,0 +1,6044 @@ + + + + + + Invoice Search Results + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
freeside + Freeside Test 5.0.1 + Logged in as test  logout
Preferences +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ Adv + +
+ + + + + +
+ +
+
+ Advanced + +
+ + + + + +
+ + + + + +
+ + Adv
+ +
+ + + + + +
+ +
+
+ Advanced + +
+ + + + + +
+ +
+
+ Advanced + +
+ + + + + +
+ + + + + + + +
+ + +
+ +

+ Invoice Search Results +

+ +
+ + + Print these invoices | Email these invoices + +

+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+ +
+ + 1584 total invoices + + + ( show per page ) + + +
+ + $141620.74 gross sales
+ − $0.00 discounted
+ − $0.00 credited
+ = $141620.74 net sales
+ $48352.49 outstanding balance
+
+ +
+ + Download full results
+ + as Excel spreadsheet
+ + as CSV file
+ + + as printable copy + +
+ + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + ... + + 8 + + + 9 + + ... + + 11 + + ... + + 13 + + + 14 + + + 15 + + + 16 + + Next + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Invoice # + + Gross Amount + + Discount + + Credits + + Net Amount + + Balance + + Date + + Cust. Status + + Customer +
1$129.19$0.00$0.00$129.19$129.19Aug 07 2015ActiveRuecker, Lucious
3$194.68$0.00$0.00$194.68$0.00Aug 08 2015ActiveRoberts-Schinner (Flatley, Amelia)
2$177.91$0.00$0.00$177.91$0.00Aug 08 2015ActiveZemlak, Asia
4$191.77$0.00$0.00$191.77$191.77Aug 09 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
5$162.41$0.00$0.00$162.41$0.00Aug 12 2015ActiveSteuber, Ryley
6$158.55$0.00$0.00$158.55$0.00Aug 13 2015ActiveLeuschke, Edd
7$154.68$0.00$0.00$154.68$0.00Aug 14 2015ActiveGrady, Aniya
8$189.68$0.00$0.00$189.68$0.00Aug 14 2015ActiveWeimann Inc (Cartwright, Judah)
9$174.35$0.00$0.00$174.35$0.00Aug 15 2015ActiveKozey and Sons (Vandervort, Harmon)
10$143.07$0.00$0.00$143.07$143.07Aug 17 2015ActiveBoyer, Lamont
11$143.07$0.00$0.00$143.07$0.00Aug 17 2015ActiveDonnelly, Raleigh
13$174.20$0.00$0.00$174.20$0.00Aug 18 2015ActiveBraun, Rath and Gutkowski (Wilderman, Reyes)
12$165.65$0.00$0.00$165.65$165.65Aug 18 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
16$162.74$0.00$0.00$162.74$0.00Aug 19 2015ActiveFay and Sons (Gerhold, Thora)
14$170.32$0.00$0.00$170.32$0.00Aug 19 2015ActiveKuhlman-Huels (Parisian, Cristopher)
15$135.32$0.00$0.00$135.32$0.00Aug 19 2015ActiveBrown, Danial
17$159.84$0.00$0.00$159.84$0.00Aug 20 2015ActiveLeannon-Pfannerstill (O'Keefe, Bernie)
18$156.94$0.00$0.00$156.94$0.00Aug 21 2015ActiveMedhurst Group (Medhurst, Rafaela)
19$158.71$0.00$0.00$158.71$158.71Aug 22 2015ActiveLehner-Klein (Smitham, Pansy)
20$108.23$0.00$0.00$108.23$108.23Aug 26 2015ActiveChristiansen, Leone
21$92.75$0.00$0.00$92.75$0.00Aug 30 2015ActiveKessler, Dana
22$88.87$0.00$0.00$88.87$0.00Aug 31 2015ActiveSchultz, Colten
23$105.97$0.00$0.00$105.97$105.97Aug 31 2015ActiveWaters, Godfrey
25$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveZemlak, Asia
33$120.00$0.00$0.00$120.00$120.00Sep 01 2015ActiveBoyer, Lamont
36$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveKessler, Dana
45$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveWeimann Inc (Cartwright, Judah)
35$90.00$0.00$0.00$90.00$90.00Sep 01 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
24$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveGrady, Aniya
43$90.00$0.00$0.00$90.00$90.00Sep 01 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
26$120.00$0.00$0.00$120.00$120.00Sep 01 2015ActiveLehner-Klein (Smitham, Pansy)
42$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveDonnelly, Raleigh
29$120.00$0.00$0.00$120.00$120.00Sep 01 2015ActiveChristiansen, Leone
27$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveKuhlman-Huels (Parisian, Cristopher)
38$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveFay and Sons (Gerhold, Thora)
32$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveSchultz, Colten
37$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveKozey and Sons (Vandervort, Harmon)
40$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveBraun, Rath and Gutkowski (Wilderman, Reyes)
34$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveBrown, Danial
46$30.00$0.00$0.00$30.00$30.00Sep 01 2015ActiveWaters, Godfrey
30$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveSteuber, Ryley
31$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveMedhurst Group (Medhurst, Rafaela)
39$30.00$0.00$0.00$30.00$30.00Sep 01 2015ActiveRuecker, Lucious
44$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveLeuschke, Edd
28$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveLeannon-Pfannerstill (O'Keefe, Bernie)
41$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveRoberts-Schinner (Flatley, Amelia)
47$236.00$0.00$0.00$236.00$0.00Sep 02 2015ActiveMoore-Cummerata (DuBuque, Russ)
49$212.00$0.00$0.00$212.00$0.00Sep 02 2015ActiveMante LLC (Kessler, Enid)
48$212.00$0.00$0.00$212.00$0.00Sep 02 2015ActiveLuettgen-Jacobs (Hintz, Junior)
50$209.00$0.00$0.00$209.00$0.00Sep 03 2015ActiveHoeger-Brown (Shields, Serenity)
51$228.00$0.00$0.00$228.00$0.00Sep 04 2015ActiveLind-Bahringer (Ratke, Roma)
53$200.00$0.00$0.00$200.00$0.00Sep 06 2015ActivePfeffer, Shanahan and Cruickshank (Kutch, Rosario)
52$185.00$0.00$0.00$185.00$0.00Sep 06 2015ActiveMcKenzie, Kareem
54$60.00$0.00$0.00$60.00$60.00Sep 07 2015ActiveRuecker, Lucious
56$60.00$0.00$0.00$60.00$0.00Sep 08 2015ActiveRoberts-Schinner (Flatley, Amelia)
55$212.00$0.00$0.00$212.00$212.00Sep 08 2015ActiveO'Reilly-Mraz (Pagac, Kennedi)
57$60.00$0.00$0.00$60.00$60.00Sep 09 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
58$188.00$0.00$0.00$188.00$0.00Sep 10 2015ActiveFlatley-Hagenes (Donnelly, Odessa)
59$182.00$0.00$0.00$182.00$182.00Sep 12 2015ActiveSimonis Inc (Runolfsson, Kareem)
60$179.00$0.00$0.00$179.00$179.00Sep 13 2015ActiveConn-McLaughlin (O'Connell, Gayle)
61$153.00$0.00$0.00$153.00$0.00Sep 14 2015ActiveKunde, Noemi
63$188.00$0.00$0.00$188.00$188.00Sep 14 2015ActiveO'Keefe Inc (Schamberger, Felix)
62$60.00$0.00$0.00$60.00$0.00Sep 14 2015ActiveWeimann Inc (Cartwright, Judah)
64$60.00$0.00$0.00$60.00$0.00Sep 15 2015ActiveKozey and Sons (Vandervort, Harmon)
65$60.00$0.00$0.00$60.00$60.00Sep 18 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
66$60.00$0.00$0.00$60.00$0.00Sep 18 2015ActiveBraun, Rath and Gutkowski (Wilderman, Reyes)
70$117.00$0.00$0.00$117.00$0.00Sep 19 2015ActiveTurcotte, Janessa
69$161.00$0.00$0.00$161.00$161.00Sep 19 2015ActiveBoyle-Schmeler (Maggio, Fay)
67$60.00$0.00$0.00$60.00$0.00Sep 19 2015ActiveKuhlman-Huels (Parisian, Cristopher)
68$60.00$0.00$0.00$60.00$0.00Sep 19 2015ActiveFay and Sons (Gerhold, Thora)
71$60.00$0.00$0.00$60.00$0.00Sep 20 2015ActiveLeannon-Pfannerstill (O'Keefe, Bernie)
74$129.00$0.00$0.00$129.00$129.00Sep 20 2015ActiveStokes, Janelle
72$164.00$0.00$0.00$164.00$164.00Sep 20 2015ActiveBalistreri-Schoen (Schultz, Jaylan)
73$164.00$0.00$0.00$164.00$164.00Sep 20 2015ActiveFlatley, Yundt and Pacocha (Volkman, Tabitha)
76$60.00$0.00$0.00$60.00$0.00Sep 21 2015ActiveMedhurst Group (Medhurst, Rafaela)
75$115.00$0.00$0.00$115.00$115.00Sep 21 2015ActiveSwaniawski, Adrienne
78$155.00$0.00$0.00$155.00$0.00Sep 21 2015ActiveMcLaughlin-Luettgen (Berge, Houston)
77$115.00$0.00$0.00$115.00$0.00Sep 21 2015ActiveFrami, Gayle
79$60.00$0.00$0.00$60.00$60.00Sep 22 2015ActiveLehner-Klein (Smitham, Pansy)
80$114.00$0.00$0.00$114.00$0.00Sep 22 2015ActiveGleason, Ahmed
81$113.00$0.00$0.00$113.00$113.00Sep 23 2015ActiveBernhard, Kris
82$113.00$0.00$0.00$113.00$0.00Sep 24 2015ActiveAbbott, Addison
83$143.00$0.00$0.00$143.00$143.00Sep 25 2015ActiveWolff Inc (Hessel, Brianne)
85$110.00$0.00$0.00$110.00$0.00Sep 26 2015ActiveRowe, Amara
84$105.00$0.00$0.00$105.00$0.00Sep 26 2015ActiveCarter, Cathy
87$136.00$0.00$0.00$136.00$0.00Sep 27 2015ActiveRodriguez-Ebert (Bergstrom, Cecilia)
86$101.00$0.00$0.00$101.00$101.00Sep 27 2015ActiveGleichner, Delmer
92$60.00$0.00$0.00$60.00$60.00Sep 28 2015ActiveWaters, Godfrey
89$97.00$0.00$0.00$97.00$0.00Sep 28 2015ActiveToy, Bethany
88$108.00$0.00$0.00$108.00$108.00Sep 28 2015ActiveDouglas, Willow
90$132.00$0.00$0.00$132.00$0.00Sep 28 2015ActiveQuitzon Group (Davis, Jeffery)
91$97.00$0.00$0.00$97.00$0.00Sep 28 2015ActiveKunze, Michale
95$128.00$0.00$0.00$128.00$0.00Sep 29 2015ActiveCole, Graham and Towne (Hickle, Javier)
93$131.00$0.00$0.00$131.00$0.00Sep 29 2015ActiveToy-Gerlach (Zulauf, Sharon)
94$131.00$0.00$0.00$131.00$131.00Sep 29 2015ActiveBernhard-Treutel (Shanahan, Kevin)
96$128.00$0.00$0.00$128.00$128.00Sep 30 2015ActiveTorp, Sawayn and Friesen (Pollich, Maritza)
153$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveConn-McLaughlin (O'Connell, Gayle)
151$30.00$0.00$0.00$30.00$30.00Oct 01 2015ActiveWaters, Godfrey
124$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
121$120.00$0.00$0.00$120.00$0.00Oct 01 2015ActiveMoore-Cummerata (DuBuque, Russ)
+ + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + ... + + 8 + + + 9 + + ... + + 11 + + ... + + 13 + + + 14 + + + 15 + + + 16 + + Next + + + +
+ + + + +
+ + + + + + + + diff --git a/FS-Test/share/output/search/cust_bill.html/keywords=date:order_by=invnum b/FS-Test/share/output/search/cust_bill.html/keywords=date:order_by=invnum new file mode 100644 index 000000000..0fbf9aa3e --- /dev/null +++ b/FS-Test/share/output/search/cust_bill.html/keywords=date:order_by=invnum @@ -0,0 +1,6044 @@ + + + + + + Invoice Search Results + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
freeside + Freeside Test 5.0.1 + Logged in as test  logout
Preferences +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ Adv + +
+ + + + + +
+ +
+
+ Advanced + +
+ + + + + +
+ + + + + +
+ + Adv
+ +
+ + + + + +
+ +
+
+ Advanced + +
+ + + + + +
+ +
+
+ Advanced + +
+ + + + + +
+ + + + + + + +
+ + +
+ +

+ Invoice Search Results +

+ +
+ + + Print these invoices | Email these invoices + +

+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+ +
+ + 1584 total invoices + + + ( show per page ) + + +
+ + $141620.74 gross sales
+ − $0.00 discounted
+ − $0.00 credited
+ = $141620.74 net sales
+ $48352.49 outstanding balance
+
+ +
+ + Download full results
+ + as Excel spreadsheet
+ + as CSV file
+ + + as printable copy + +
+ + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + ... + + 8 + + + 9 + + ... + + 11 + + ... + + 13 + + + 14 + + + 15 + + + 16 + + Next + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Invoice # + + Gross Amount + + Discount + + Credits + + Net Amount + + Balance + + Date + + Cust. Status + + Customer +
1$129.19$0.00$0.00$129.19$129.19Aug 07 2015ActiveRuecker, Lucious
3$194.68$0.00$0.00$194.68$0.00Aug 08 2015ActiveRoberts-Schinner (Flatley, Amelia)
2$177.91$0.00$0.00$177.91$0.00Aug 08 2015ActiveZemlak, Asia
4$191.77$0.00$0.00$191.77$191.77Aug 09 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
5$162.41$0.00$0.00$162.41$0.00Aug 12 2015ActiveSteuber, Ryley
6$158.55$0.00$0.00$158.55$0.00Aug 13 2015ActiveLeuschke, Edd
7$154.68$0.00$0.00$154.68$0.00Aug 14 2015ActiveGrady, Aniya
8$189.68$0.00$0.00$189.68$0.00Aug 14 2015ActiveWeimann Inc (Cartwright, Judah)
9$174.35$0.00$0.00$174.35$0.00Aug 15 2015ActiveKozey and Sons (Vandervort, Harmon)
10$143.07$0.00$0.00$143.07$143.07Aug 17 2015ActiveBoyer, Lamont
11$143.07$0.00$0.00$143.07$0.00Aug 17 2015ActiveDonnelly, Raleigh
13$174.20$0.00$0.00$174.20$0.00Aug 18 2015ActiveBraun, Rath and Gutkowski (Wilderman, Reyes)
12$165.65$0.00$0.00$165.65$165.65Aug 18 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
16$162.74$0.00$0.00$162.74$0.00Aug 19 2015ActiveFay and Sons (Gerhold, Thora)
14$170.32$0.00$0.00$170.32$0.00Aug 19 2015ActiveKuhlman-Huels (Parisian, Cristopher)
15$135.32$0.00$0.00$135.32$0.00Aug 19 2015ActiveBrown, Danial
17$159.84$0.00$0.00$159.84$0.00Aug 20 2015ActiveLeannon-Pfannerstill (O'Keefe, Bernie)
18$156.94$0.00$0.00$156.94$0.00Aug 21 2015ActiveMedhurst Group (Medhurst, Rafaela)
19$158.71$0.00$0.00$158.71$158.71Aug 22 2015ActiveLehner-Klein (Smitham, Pansy)
20$108.23$0.00$0.00$108.23$108.23Aug 26 2015ActiveChristiansen, Leone
21$92.75$0.00$0.00$92.75$0.00Aug 30 2015ActiveKessler, Dana
22$88.87$0.00$0.00$88.87$0.00Aug 31 2015ActiveSchultz, Colten
23$105.97$0.00$0.00$105.97$105.97Aug 31 2015ActiveWaters, Godfrey
25$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveZemlak, Asia
33$120.00$0.00$0.00$120.00$120.00Sep 01 2015ActiveBoyer, Lamont
36$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveKessler, Dana
45$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveWeimann Inc (Cartwright, Judah)
35$90.00$0.00$0.00$90.00$90.00Sep 01 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
24$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveGrady, Aniya
43$90.00$0.00$0.00$90.00$90.00Sep 01 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
26$120.00$0.00$0.00$120.00$120.00Sep 01 2015ActiveLehner-Klein (Smitham, Pansy)
42$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveDonnelly, Raleigh
29$120.00$0.00$0.00$120.00$120.00Sep 01 2015ActiveChristiansen, Leone
27$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveKuhlman-Huels (Parisian, Cristopher)
38$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveFay and Sons (Gerhold, Thora)
32$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveSchultz, Colten
37$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveKozey and Sons (Vandervort, Harmon)
40$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveBraun, Rath and Gutkowski (Wilderman, Reyes)
34$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveBrown, Danial
46$30.00$0.00$0.00$30.00$30.00Sep 01 2015ActiveWaters, Godfrey
30$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveSteuber, Ryley
31$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveMedhurst Group (Medhurst, Rafaela)
39$30.00$0.00$0.00$30.00$30.00Sep 01 2015ActiveRuecker, Lucious
44$120.00$0.00$0.00$120.00$0.00Sep 01 2015ActiveLeuschke, Edd
28$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveLeannon-Pfannerstill (O'Keefe, Bernie)
41$90.00$0.00$0.00$90.00$0.00Sep 01 2015ActiveRoberts-Schinner (Flatley, Amelia)
47$236.00$0.00$0.00$236.00$0.00Sep 02 2015ActiveMoore-Cummerata (DuBuque, Russ)
49$212.00$0.00$0.00$212.00$0.00Sep 02 2015ActiveMante LLC (Kessler, Enid)
48$212.00$0.00$0.00$212.00$0.00Sep 02 2015ActiveLuettgen-Jacobs (Hintz, Junior)
50$209.00$0.00$0.00$209.00$0.00Sep 03 2015ActiveHoeger-Brown (Shields, Serenity)
51$228.00$0.00$0.00$228.00$0.00Sep 04 2015ActiveLind-Bahringer (Ratke, Roma)
53$200.00$0.00$0.00$200.00$0.00Sep 06 2015ActivePfeffer, Shanahan and Cruickshank (Kutch, Rosario)
52$185.00$0.00$0.00$185.00$0.00Sep 06 2015ActiveMcKenzie, Kareem
54$60.00$0.00$0.00$60.00$60.00Sep 07 2015ActiveRuecker, Lucious
56$60.00$0.00$0.00$60.00$0.00Sep 08 2015ActiveRoberts-Schinner (Flatley, Amelia)
55$212.00$0.00$0.00$212.00$212.00Sep 08 2015ActiveO'Reilly-Mraz (Pagac, Kennedi)
57$60.00$0.00$0.00$60.00$60.00Sep 09 2015ActiveRunolfsson, Roob and Hoppe (Bergstrom, Esteban)
58$188.00$0.00$0.00$188.00$0.00Sep 10 2015ActiveFlatley-Hagenes (Donnelly, Odessa)
59$182.00$0.00$0.00$182.00$182.00Sep 12 2015ActiveSimonis Inc (Runolfsson, Kareem)
60$179.00$0.00$0.00$179.00$179.00Sep 13 2015ActiveConn-McLaughlin (O'Connell, Gayle)
61$153.00$0.00$0.00$153.00$0.00Sep 14 2015ActiveKunde, Noemi
63$188.00$0.00$0.00$188.00$188.00Sep 14 2015ActiveO'Keefe Inc (Schamberger, Felix)
62$60.00$0.00$0.00$60.00$0.00Sep 14 2015ActiveWeimann Inc (Cartwright, Judah)
64$60.00$0.00$0.00$60.00$0.00Sep 15 2015ActiveKozey and Sons (Vandervort, Harmon)
65$60.00$0.00$0.00$60.00$60.00Sep 18 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
66$60.00$0.00$0.00$60.00$0.00Sep 18 2015ActiveBraun, Rath and Gutkowski (Wilderman, Reyes)
70$117.00$0.00$0.00$117.00$0.00Sep 19 2015ActiveTurcotte, Janessa
69$161.00$0.00$0.00$161.00$161.00Sep 19 2015ActiveBoyle-Schmeler (Maggio, Fay)
67$60.00$0.00$0.00$60.00$0.00Sep 19 2015ActiveKuhlman-Huels (Parisian, Cristopher)
68$60.00$0.00$0.00$60.00$0.00Sep 19 2015ActiveFay and Sons (Gerhold, Thora)
71$60.00$0.00$0.00$60.00$0.00Sep 20 2015ActiveLeannon-Pfannerstill (O'Keefe, Bernie)
74$129.00$0.00$0.00$129.00$129.00Sep 20 2015ActiveStokes, Janelle
72$164.00$0.00$0.00$164.00$164.00Sep 20 2015ActiveBalistreri-Schoen (Schultz, Jaylan)
73$164.00$0.00$0.00$164.00$164.00Sep 20 2015ActiveFlatley, Yundt and Pacocha (Volkman, Tabitha)
76$60.00$0.00$0.00$60.00$0.00Sep 21 2015ActiveMedhurst Group (Medhurst, Rafaela)
75$115.00$0.00$0.00$115.00$115.00Sep 21 2015ActiveSwaniawski, Adrienne
78$155.00$0.00$0.00$155.00$0.00Sep 21 2015ActiveMcLaughlin-Luettgen (Berge, Houston)
77$115.00$0.00$0.00$115.00$0.00Sep 21 2015ActiveFrami, Gayle
79$60.00$0.00$0.00$60.00$60.00Sep 22 2015ActiveLehner-Klein (Smitham, Pansy)
80$114.00$0.00$0.00$114.00$0.00Sep 22 2015ActiveGleason, Ahmed
81$113.00$0.00$0.00$113.00$113.00Sep 23 2015ActiveBernhard, Kris
82$113.00$0.00$0.00$113.00$0.00Sep 24 2015ActiveAbbott, Addison
83$143.00$0.00$0.00$143.00$143.00Sep 25 2015ActiveWolff Inc (Hessel, Brianne)
85$110.00$0.00$0.00$110.00$0.00Sep 26 2015ActiveRowe, Amara
84$105.00$0.00$0.00$105.00$0.00Sep 26 2015ActiveCarter, Cathy
87$136.00$0.00$0.00$136.00$0.00Sep 27 2015ActiveRodriguez-Ebert (Bergstrom, Cecilia)
86$101.00$0.00$0.00$101.00$101.00Sep 27 2015ActiveGleichner, Delmer
92$60.00$0.00$0.00$60.00$60.00Sep 28 2015ActiveWaters, Godfrey
89$97.00$0.00$0.00$97.00$0.00Sep 28 2015ActiveToy, Bethany
88$108.00$0.00$0.00$108.00$108.00Sep 28 2015ActiveDouglas, Willow
90$132.00$0.00$0.00$132.00$0.00Sep 28 2015ActiveQuitzon Group (Davis, Jeffery)
91$97.00$0.00$0.00$97.00$0.00Sep 28 2015ActiveKunze, Michale
95$128.00$0.00$0.00$128.00$0.00Sep 29 2015ActiveCole, Graham and Towne (Hickle, Javier)
93$131.00$0.00$0.00$131.00$0.00Sep 29 2015ActiveToy-Gerlach (Zulauf, Sharon)
94$131.00$0.00$0.00$131.00$131.00Sep 29 2015ActiveBernhard-Treutel (Shanahan, Kevin)
96$128.00$0.00$0.00$128.00$128.00Sep 30 2015ActiveTorp, Sawayn and Friesen (Pollich, Maritza)
153$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveConn-McLaughlin (O'Connell, Gayle)
151$30.00$0.00$0.00$30.00$30.00Oct 01 2015ActiveWaters, Godfrey
124$90.00$0.00$0.00$90.00$90.00Oct 01 2015ActiveJacobson-Gorczany (Vandervort, Kiley)
121$120.00$0.00$0.00$120.00$0.00Oct 01 2015ActiveMoore-Cummerata (DuBuque, Russ)
+ + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + ... + + 8 + + + 9 + + ... + + 11 + + ... + + 13 + + + 14 + + + 15 + + + 16 + + Next + + + +
+ + + + +
+ + + + + + + + diff --git a/FS-Test/share/output/search/cust_bill.html/magic=_date:agentnum=1:beginning=:ending=10%2F01%2F2015:charged_lt=:charged_gt=200.00:owed_lt=:owed_gt=:open=1 b/FS-Test/share/output/search/cust_bill.html/magic=_date:agentnum=1:beginning=:ending=10%2F01%2F2015:charged_lt=:charged_gt=200.00:owed_lt=:owed_gt=:open=1 index 0b7e3f23f..b040cdc17 100644 --- a/FS-Test/share/output/search/cust_bill.html/magic=_date:agentnum=1:beginning=:ending=10%2F01%2F2015:charged_lt=:charged_gt=200.00:owed_lt=:owed_gt=:open=1 +++ b/FS-Test/share/output/search/cust_bill.html/magic=_date:agentnum=1:beginning=:ending=10%2F01%2F2015:charged_lt=:charged_gt=200.00:owed_lt=:owed_gt=:open=1 @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/search/cust_bill_pkg.cgi/agentnum=1:status=:cust_classnum=:beginning=01%2F01%2F2016:ending=01%2F31%2F2016 b/FS-Test/share/output/search/cust_bill_pkg.cgi/agentnum=1:status=:cust_classnum=:beginning=01%2F01%2F2016:ending=01%2F31%2F2016 index f5274253f..ecff0d1aa 100644 --- a/FS-Test/share/output/search/cust_bill_pkg.cgi/agentnum=1:status=:cust_classnum=:beginning=01%2F01%2F2016:ending=01%2F31%2F2016 +++ b/FS-Test/share/output/search/cust_bill_pkg.cgi/agentnum=1:status=:cust_classnum=:beginning=01%2F01%2F2016:ending=01%2F31%2F2016 @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); @@ -1002,7 +1002,7 @@ myMenu68.width = 256; > - Paid + Paid - Credited + Credited - Paid + Paid - Credited + Credited - Paid + Paid - Credited + Credited - Paid + Paid - Credited + Credited
Domain:example.com
+
Domain:example.com
@@ -1375,7 +1375,7 @@ myMenu68.width = 256; -
Test svc_acct:berta@example.com
+
Test svc_acct:berta@example.com
@@ -1476,7 +1476,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.212.50.247, MAC:0000911C4815
+
Test svc_broadband:IP:10.212.50.247, MAC:0000911C4815
@@ -1577,7 +1577,7 @@ myMenu68.width = 256; -
Test svc_domain:waters-turner.com
+
Test svc_domain:waters-turner.com
@@ -1678,7 +1678,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.127.31.117, MAC:0000A3E013D7
+
Test svc_broadband:IP:10.127.31.117, MAC:0000A3E013D7
@@ -1779,7 +1779,7 @@ myMenu68.width = 256; -
Test svc_domain:ziemann-dietrich.com
+
Test svc_domain:ziemann-dietrich.com
@@ -1880,7 +1880,7 @@ myMenu68.width = 256; -
Test svc_phone:19671718037
+
Test svc_phone:19671718037
@@ -1981,7 +1981,7 @@ myMenu68.width = 256; -
Test svc_domain:watsica-llc.com
+
Test svc_domain:watsica-llc.com
@@ -2082,7 +2082,7 @@ myMenu68.width = 256; -
Test svc_phone:337448915280026
+
Test svc_phone:337448915280026
@@ -2183,7 +2183,7 @@ myMenu68.width = 256; -
Test svc_acct:sherwood@example.com
+
Test svc_acct:sherwood@example.com
@@ -2284,7 +2284,7 @@ myMenu68.width = 256; -
Test svc_phone:7941182146
+
Test svc_phone:7941182146
@@ -2385,7 +2385,7 @@ myMenu68.width = 256; -
Test svc_acct:rashad@example.com
+
Test svc_acct:rashad@example.com
@@ -2486,7 +2486,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.4.160.230, MAC:0000D12765F4
+
Test svc_broadband:IP:10.4.160.230, MAC:0000D12765F4
@@ -2587,7 +2587,7 @@ myMenu68.width = 256; -
Test svc_acct:doris@example.com
+
Test svc_acct:doris@example.com
@@ -2688,7 +2688,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.203.139.97, MAC:0000FA2C8FB1
+
Test svc_broadband:IP:10.203.139.97, MAC:0000FA2C8FB1
@@ -2789,7 +2789,7 @@ myMenu68.width = 256; -
Test svc_domain:hane-llc.com
+
Test svc_domain:hane-llc.com
@@ -2890,7 +2890,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.75.143.4, MAC:0000EE773AA3
+
Test svc_broadband:IP:10.75.143.4, MAC:0000EE773AA3
@@ -2991,7 +2991,7 @@ myMenu68.width = 256; -
Test svc_domain:will-schmitt-and-buckridge.com
+
Test svc_domain:will-schmitt-and-buckridge.com
@@ -3092,7 +3092,7 @@ myMenu68.width = 256; -
Test svc_phone:507789172836615
+
Test svc_phone:507789172836615
@@ -3193,7 +3193,7 @@ myMenu68.width = 256; -
Test svc_domain:hagenes-mclaughlin-and-fadel.com
+
Test svc_domain:hagenes-mclaughlin-and-fadel.com
@@ -3294,7 +3294,7 @@ myMenu68.width = 256; -
Test svc_phone:402545251883238
+
Test svc_phone:402545251883238
@@ -3395,7 +3395,7 @@ myMenu68.width = 256; -
Test svc_acct:hilario@example.com
+
Test svc_acct:hilario@example.com
@@ -3496,7 +3496,7 @@ myMenu68.width = 256; -
Test svc_phone:5204979036
+
Test svc_phone:5204979036
@@ -3597,7 +3597,7 @@ myMenu68.width = 256; -
Test svc_acct:lennie@example.com
+
Test svc_acct:lennie@example.com
@@ -3698,7 +3698,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.2.159.93, MAC:0000BEEA3257
+
Test svc_broadband:IP:10.2.159.93, MAC:0000BEEA3257
@@ -3799,7 +3799,7 @@ myMenu68.width = 256; -
Test svc_acct:travis@example.com
+
Test svc_acct:travis@example.com
@@ -3900,7 +3900,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.147.177.86, MAC:0000E5156362
+
Test svc_broadband:IP:10.147.177.86, MAC:0000E5156362
@@ -4001,7 +4001,7 @@ myMenu68.width = 256; -
Test svc_domain:watsica-sauer-and-braun.com
+
Test svc_domain:watsica-sauer-and-braun.com
@@ -4102,7 +4102,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.61.84.169, MAC:000069394AF2
+
Test svc_broadband:IP:10.61.84.169, MAC:000069394AF2
@@ -4203,7 +4203,7 @@ myMenu68.width = 256; -
Test svc_domain:greenholt-parisian.com
+
Test svc_domain:greenholt-parisian.com
@@ -4304,7 +4304,7 @@ myMenu68.width = 256; -
Test svc_phone:2103459718
+
Test svc_phone:2103459718
@@ -4405,7 +4405,7 @@ myMenu68.width = 256; -
Test svc_domain:corkery-hackett-and-franecki.com
+
Test svc_domain:corkery-hackett-and-franecki.com
@@ -4506,7 +4506,7 @@ myMenu68.width = 256; -
Test svc_phone:14745441565
+
Test svc_phone:14745441565
@@ -4607,7 +4607,7 @@ myMenu68.width = 256; -
Test svc_acct:william@example.com
+
Test svc_acct:william@example.com
@@ -4708,7 +4708,7 @@ myMenu68.width = 256; -
Test svc_phone:4609716945803
+
Test svc_phone:4609716945803
@@ -4809,7 +4809,7 @@ myMenu68.width = 256; -
Test svc_acct:richmond@example.com
+
Test svc_acct:richmond@example.com
@@ -4910,7 +4910,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.229.63.194, MAC:00004D3E8513
+
Test svc_broadband:IP:10.229.63.194, MAC:00004D3E8513
@@ -5011,7 +5011,7 @@ myMenu68.width = 256; -
Test svc_acct:samir@example.com
+
Test svc_acct:samir@example.com
@@ -5112,7 +5112,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.188.48.84, MAC:000036BEDE13
+
Test svc_broadband:IP:10.188.48.84, MAC:000036BEDE13
@@ -5213,7 +5213,7 @@ myMenu68.width = 256; -
Test svc_domain:ullrich-kilback.com
+
Test svc_domain:ullrich-kilback.com
@@ -5314,7 +5314,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.225.235.123, MAC:00003C2A1764
+
Test svc_broadband:IP:10.225.235.123, MAC:00003C2A1764
@@ -5415,7 +5415,7 @@ myMenu68.width = 256; -
Test svc_domain:crooks-collins.com
+
Test svc_domain:crooks-collins.com
@@ -5516,7 +5516,7 @@ myMenu68.width = 256; -
Test svc_phone:7315522562
+
Test svc_phone:7315522562
@@ -5617,7 +5617,7 @@ myMenu68.width = 256; -
Test svc_domain:kuhn-llc.com
+
Test svc_domain:kuhn-llc.com
@@ -5718,7 +5718,7 @@ myMenu68.width = 256; -
Test svc_phone:5473351513
+
Test svc_phone:5473351513
@@ -5819,7 +5819,7 @@ myMenu68.width = 256; -
Test svc_acct:walton@example.com
+
Test svc_acct:walton@example.com
@@ -5920,7 +5920,7 @@ myMenu68.width = 256; -
Test svc_phone:580124349433539
+
Test svc_phone:580124349433539
@@ -6021,7 +6021,7 @@ myMenu68.width = 256; -
Test svc_acct:nigel@example.com
+
Test svc_acct:nigel@example.com
@@ -6122,7 +6122,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.189.193.93, MAC:000043D6F9E3
+
Test svc_broadband:IP:10.189.193.93, MAC:000043D6F9E3
@@ -6223,7 +6223,7 @@ myMenu68.width = 256; -
Test svc_acct:sammy@example.com
+
Test svc_acct:sammy@example.com
@@ -6324,7 +6324,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.44.211.87, MAC:00008BCF4552
+
Test svc_broadband:IP:10.44.211.87, MAC:00008BCF4552
@@ -6425,7 +6425,7 @@ myMenu68.width = 256; -
Test svc_domain:hansen-kilback.com
+
Test svc_domain:hansen-kilback.com
@@ -6526,7 +6526,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.116.203.5, MAC:00005AFAF158
+
Test svc_broadband:IP:10.116.203.5, MAC:00005AFAF158
@@ -6627,7 +6627,7 @@ myMenu68.width = 256; -
Test svc_domain:klocko-inc.com
+
Test svc_domain:klocko-inc.com
@@ -6728,7 +6728,7 @@ myMenu68.width = 256; -
Test svc_phone:47925781188566
+
Test svc_phone:47925781188566
@@ -6829,7 +6829,7 @@ myMenu68.width = 256; -
Test svc_domain:ward-murphy-and-wisozk.com
+
Test svc_domain:ward-murphy-and-wisozk.com
@@ -6930,7 +6930,7 @@ myMenu68.width = 256; -
Test svc_phone:215984572910627
+
Test svc_phone:215984572910627
@@ -7031,7 +7031,7 @@ myMenu68.width = 256; -
Test svc_acct:linwood@example.com
+
Test svc_acct:linwood@example.com
@@ -7132,7 +7132,7 @@ myMenu68.width = 256; -
Test svc_phone:5055483796977
+
Test svc_phone:5055483796977
@@ -7233,7 +7233,7 @@ myMenu68.width = 256; -
Test svc_acct:esperanza@example.com
+
Test svc_acct:esperanza@example.com
@@ -7334,7 +7334,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.178.209.2, MAC:00009245E1CE
+
Test svc_broadband:IP:10.178.209.2, MAC:00009245E1CE
@@ -7435,7 +7435,7 @@ myMenu68.width = 256; -
Test svc_acct:branson@example.com
+
Test svc_acct:branson@example.com
@@ -7536,7 +7536,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.86.8.116, MAC:00009D2D385F
+
Test svc_broadband:IP:10.86.8.116, MAC:00009D2D385F
@@ -7637,7 +7637,7 @@ myMenu68.width = 256; -
Test svc_domain:okeefe-stracke.com
+
Test svc_domain:okeefe-stracke.com
@@ -7738,7 +7738,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.166.5.138, MAC:000020A5C9AB
+
Test svc_broadband:IP:10.166.5.138, MAC:000020A5C9AB
@@ -7839,7 +7839,7 @@ myMenu68.width = 256; -
Test svc_domain:murazik-padberg-and-dibbert.com
+
Test svc_domain:murazik-padberg-and-dibbert.com
@@ -7940,7 +7940,7 @@ myMenu68.width = 256; -
Test svc_phone:510207687452209
+
Test svc_phone:510207687452209
@@ -8041,7 +8041,7 @@ myMenu68.width = 256; -
Test svc_domain:rodriguez-llc.com
+
Test svc_domain:rodriguez-llc.com
@@ -8142,7 +8142,7 @@ myMenu68.width = 256; -
Test svc_phone:19732087174151
+
Test svc_phone:19732087174151
@@ -8243,7 +8243,7 @@ myMenu68.width = 256; -
Test svc_acct:victor@example.com
+
Test svc_acct:victor@example.com
@@ -8344,7 +8344,7 @@ myMenu68.width = 256; -
Test svc_phone:99158298078002
+
Test svc_phone:99158298078002
@@ -8445,7 +8445,7 @@ myMenu68.width = 256; -
Test svc_acct:ryleigh@example.com
+
Test svc_acct:ryleigh@example.com
@@ -8546,7 +8546,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.223.20.110, MAC:0000B618A255
+
Test svc_broadband:IP:10.223.20.110, MAC:0000B618A255
@@ -8647,7 +8647,7 @@ myMenu68.width = 256; -
Test svc_acct:citlalli@example.com
+
Test svc_acct:citlalli@example.com
@@ -8748,7 +8748,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.181.162.31, MAC:0000A7F64E79
+
Test svc_broadband:IP:10.181.162.31, MAC:0000A7F64E79
@@ -8849,7 +8849,7 @@ myMenu68.width = 256; -
Test svc_domain:gorczany-weimann.com
+
Test svc_domain:gorczany-weimann.com
@@ -8950,7 +8950,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.30.204.50, MAC:000097EB747A
+
Test svc_broadband:IP:10.30.204.50, MAC:000097EB747A
@@ -9051,7 +9051,7 @@ myMenu68.width = 256; -
Test svc_domain:ward-buckridge.com
+
Test svc_domain:ward-buckridge.com
@@ -9152,7 +9152,7 @@ myMenu68.width = 256; -
Test svc_phone:8935173249
+
Test svc_phone:8935173249
@@ -9253,7 +9253,7 @@ myMenu68.width = 256; -
Test svc_domain:treutel-llc.com
+
Test svc_domain:treutel-llc.com
@@ -9354,7 +9354,7 @@ myMenu68.width = 256; -
Test svc_phone:152553597965486
+
Test svc_phone:152553597965486
@@ -9455,7 +9455,7 @@ myMenu68.width = 256; -
Test svc_acct:lenora@example.com
+
Test svc_acct:lenora@example.com
@@ -9556,7 +9556,7 @@ myMenu68.width = 256; -
Test svc_phone:114180154577357
+
Test svc_phone:114180154577357
@@ -9657,7 +9657,7 @@ myMenu68.width = 256; -
Test svc_acct:nyasia@example.com
+
Test svc_acct:nyasia@example.com
@@ -9758,7 +9758,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.171.182.118, MAC:00003EE1A6D7
+
Test svc_broadband:IP:10.171.182.118, MAC:00003EE1A6D7
@@ -9859,7 +9859,7 @@ myMenu68.width = 256; -
Test svc_acct:alden@example.com
+
Test svc_acct:alden@example.com
@@ -9960,7 +9960,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.79.94.0, MAC:00008A11465E
+
Test svc_broadband:IP:10.79.94.0, MAC:00008A11465E
@@ -10061,7 +10061,7 @@ myMenu68.width = 256; -
Test svc_domain:baumbach-llc.com
+
Test svc_domain:baumbach-llc.com
@@ -10162,7 +10162,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.74.128.210, MAC:00009E471138
+
Test svc_broadband:IP:10.74.128.210, MAC:00009E471138
@@ -10263,7 +10263,7 @@ myMenu68.width = 256; -
Test svc_domain:kessler-larson-and-bauch.com
+
Test svc_domain:kessler-larson-and-bauch.com
@@ -10364,7 +10364,7 @@ myMenu68.width = 256; -
Test svc_phone:1535633738761521
+
Test svc_phone:1535633738761521
@@ -10465,7 +10465,7 @@ myMenu68.width = 256; -
Test svc_domain:moen-kovacek.com
+
Test svc_domain:moen-kovacek.com
@@ -10566,7 +10566,7 @@ myMenu68.width = 256; -
Test svc_phone:7091741436337
+
Test svc_phone:7091741436337
@@ -10667,7 +10667,7 @@ myMenu68.width = 256; -
Test svc_acct:sally@example.com
+
Test svc_acct:sally@example.com
@@ -10768,7 +10768,7 @@ myMenu68.width = 256; -
Test svc_phone:1076194311
+
Test svc_phone:1076194311
@@ -10869,7 +10869,7 @@ myMenu68.width = 256; -
Test svc_acct:drew@example.com
+
Test svc_acct:drew@example.com
@@ -10970,7 +10970,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.72.141.97, MAC:000086A32C19
+
Test svc_broadband:IP:10.72.141.97, MAC:000086A32C19
@@ -11071,7 +11071,7 @@ myMenu68.width = 256; -
Test svc_acct:nils@example.com
+
Test svc_acct:nils@example.com
@@ -11172,7 +11172,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.229.201.34, MAC:0000494CF06C
+
Test svc_broadband:IP:10.229.201.34, MAC:0000494CF06C
@@ -11273,7 +11273,7 @@ myMenu68.width = 256; -
Test svc_domain:marvin-and-sons.com
+
Test svc_domain:marvin-and-sons.com
diff --git a/FS-Test/share/output/search/cust_pkg.cgi/magic=bill:classnum=0:setup_ending=10%2F31%2F2015:pkgpart=5:pkgpart=2:order_by=pkgnum b/FS-Test/share/output/search/cust_pkg.cgi/magic=bill:classnum=0:setup_ending=10%2F31%2F2015:pkgpart=5:pkgpart=2:order_by=pkgnum index fc43d4200..8a748b993 100644 --- a/FS-Test/share/output/search/cust_pkg.cgi/magic=bill:classnum=0:setup_ending=10%2F31%2F2015:pkgpart=5:pkgpart=2:order_by=pkgnum +++ b/FS-Test/share/output/search/cust_pkg.cgi/magic=bill:classnum=0:setup_ending=10%2F31%2F2015:pkgpart=5:pkgpart=2:order_by=pkgnum @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); @@ -866,7 +866,7 @@ myMenu68.width = 256; ->Change these packages
Email a notice to these customers +>Change these packages
Email a notice to these customers

@@ -1259,7 +1259,7 @@ myMenu68.width = 256; -
Test svc_acct:berta@example.com
+
Test svc_acct:berta@example.com
@@ -1360,7 +1360,7 @@ myMenu68.width = 256; -
Test svc_phone:19671718037
+
Test svc_phone:19671718037
@@ -1461,7 +1461,7 @@ myMenu68.width = 256; -
Test svc_phone:402545251883238
+
Test svc_phone:402545251883238
@@ -1562,7 +1562,7 @@ myMenu68.width = 256; -
Test svc_acct:hilario@example.com
+
Test svc_acct:hilario@example.com
@@ -1663,7 +1663,7 @@ myMenu68.width = 256; -
Test svc_phone:5204979036
+
Test svc_phone:5204979036
@@ -1764,7 +1764,7 @@ myMenu68.width = 256; -
Test svc_acct:lennie@example.com
+
Test svc_acct:lennie@example.com
@@ -1865,7 +1865,7 @@ myMenu68.width = 256; -
Test svc_acct:travis@example.com
+
Test svc_acct:travis@example.com
@@ -1966,7 +1966,7 @@ myMenu68.width = 256; -
Test svc_phone:2103459718
+
Test svc_phone:2103459718
@@ -2067,7 +2067,7 @@ myMenu68.width = 256; -
Test svc_phone:4609716945803
+
Test svc_phone:4609716945803
@@ -2168,7 +2168,7 @@ myMenu68.width = 256; -
Test svc_acct:richmond@example.com
+
Test svc_acct:richmond@example.com
@@ -2269,7 +2269,7 @@ myMenu68.width = 256; -
Test svc_phone:7315522562
+
Test svc_phone:7315522562
@@ -2370,7 +2370,7 @@ myMenu68.width = 256; -
Test svc_acct:sammy@example.com
+
Test svc_acct:sammy@example.com
@@ -2471,7 +2471,7 @@ myMenu68.width = 256; -
Test svc_phone:47925781188566
+
Test svc_phone:47925781188566
@@ -2572,7 +2572,7 @@ myMenu68.width = 256; -
Test svc_phone:215984572910627
+
Test svc_phone:215984572910627
@@ -2673,7 +2673,7 @@ myMenu68.width = 256; -
Test svc_acct:linwood@example.com
+
Test svc_acct:linwood@example.com
@@ -2774,7 +2774,7 @@ myMenu68.width = 256; -
Test svc_phone:510207687452209
+
Test svc_phone:510207687452209
@@ -2875,7 +2875,7 @@ myMenu68.width = 256; -
Test svc_phone:99158298078002
+
Test svc_phone:99158298078002
@@ -2976,7 +2976,7 @@ myMenu68.width = 256; -
Test svc_acct:ryleigh@example.com
+
Test svc_acct:ryleigh@example.com
@@ -3077,7 +3077,7 @@ myMenu68.width = 256; -
Test svc_acct:citlalli@example.com
+
Test svc_acct:citlalli@example.com
@@ -3178,7 +3178,7 @@ myMenu68.width = 256; -
Test svc_phone:152553597965486
+
Test svc_phone:152553597965486
@@ -3279,7 +3279,7 @@ myMenu68.width = 256; -
Test svc_acct:lenora@example.com
+
Test svc_acct:lenora@example.com
@@ -3380,7 +3380,7 @@ myMenu68.width = 256; -
Test svc_phone:114180154577357
+
Test svc_phone:114180154577357
@@ -3481,7 +3481,7 @@ myMenu68.width = 256; -
Test svc_acct:nyasia@example.com
+
Test svc_acct:nyasia@example.com
@@ -3582,7 +3582,7 @@ myMenu68.width = 256; -
Test svc_acct:alden@example.com
+
Test svc_acct:alden@example.com
@@ -3683,7 +3683,7 @@ myMenu68.width = 256; -
Test svc_phone:7091741436337
+
Test svc_phone:7091741436337
@@ -3784,7 +3784,7 @@ myMenu68.width = 256; -
Test svc_acct:sally@example.com
+
Test svc_acct:sally@example.com
@@ -3885,7 +3885,7 @@ myMenu68.width = 256; -
Test svc_acct:nils@example.com
+
Test svc_acct:nils@example.com
@@ -3986,7 +3986,7 @@ myMenu68.width = 256; -
Test svc_phone:19242934458
+
Test svc_phone:19242934458
@@ -4087,7 +4087,7 @@ myMenu68.width = 256; -
Test svc_phone:6941312477183
+
Test svc_phone:6941312477183
@@ -4188,7 +4188,7 @@ myMenu68.width = 256; -
Test svc_acct:kolby@example.com
+
Test svc_acct:kolby@example.com
@@ -4289,7 +4289,7 @@ myMenu68.width = 256; -
Test svc_acct:donato@example.com
+
Test svc_acct:donato@example.com
@@ -4390,7 +4390,7 @@ myMenu68.width = 256; -
Test svc_phone:158607899401245
+
Test svc_phone:158607899401245
@@ -4491,7 +4491,7 @@ myMenu68.width = 256; -
Test svc_acct:hardy@example.com
+
Test svc_acct:hardy@example.com
@@ -4592,7 +4592,7 @@ myMenu68.width = 256; -
Test svc_phone:5260896063
+
Test svc_phone:5260896063
@@ -4693,7 +4693,7 @@ myMenu68.width = 256; -
Test svc_acct:hilbert@example.com
+
Test svc_acct:hilbert@example.com
@@ -4794,7 +4794,7 @@ myMenu68.width = 256; -
Test svc_phone:71877083088273
+
Test svc_phone:71877083088273
@@ -4895,7 +4895,7 @@ myMenu68.width = 256; -
Test svc_phone:95994707748468
+
Test svc_phone:95994707748468
@@ -4996,7 +4996,7 @@ myMenu68.width = 256; -
Test svc_acct:ebony@example.com
+
Test svc_acct:ebony@example.com
@@ -5097,7 +5097,7 @@ myMenu68.width = 256; -
Test svc_acct:edd@example.com
+
Test svc_acct:edd@example.com
@@ -5198,7 +5198,7 @@ myMenu68.width = 256; -
Test svc_phone:7511349049
+
Test svc_phone:7511349049
@@ -5299,7 +5299,7 @@ myMenu68.width = 256; -
Test svc_phone:0465059470
+
Test svc_phone:0465059470
@@ -5400,7 +5400,7 @@ myMenu68.width = 256; -
Test svc_acct:ettie@example.com
+
Test svc_acct:ettie@example.com
@@ -5501,7 +5501,7 @@ myMenu68.width = 256; -
Test svc_phone:261457560511658
+
Test svc_phone:261457560511658
@@ -5602,7 +5602,7 @@ myMenu68.width = 256; -
Test svc_acct:haley@example.com
+
Test svc_acct:haley@example.com
@@ -5703,7 +5703,7 @@ myMenu68.width = 256; -
Test svc_phone:1906003499937
+
Test svc_phone:1906003499937
@@ -5804,7 +5804,7 @@ myMenu68.width = 256; -
Test svc_acct:rodolfo@example.com
+
Test svc_acct:rodolfo@example.com
@@ -5905,7 +5905,7 @@ myMenu68.width = 256; -
Test svc_acct:anika@example.com
+
Test svc_acct:anika@example.com
@@ -6006,7 +6006,7 @@ myMenu68.width = 256; -
Test svc_phone:6741985321
+
Test svc_phone:6741985321
@@ -6107,7 +6107,7 @@ myMenu68.width = 256; -
Test svc_phone:078151255309299
+
Test svc_phone:078151255309299
@@ -6208,7 +6208,7 @@ myMenu68.width = 256; -
Test svc_acct:kaitlyn@example.com
+
Test svc_acct:kaitlyn@example.com
@@ -6309,7 +6309,7 @@ myMenu68.width = 256; -
Test svc_acct:marian@example.com
+
Test svc_acct:marian@example.com
@@ -6410,7 +6410,7 @@ myMenu68.width = 256; -
Test svc_phone:8632406717
+
Test svc_phone:8632406717
@@ -6511,7 +6511,7 @@ myMenu68.width = 256; -
Test svc_acct:kacey@example.com
+
Test svc_acct:kacey@example.com
@@ -6612,7 +6612,7 @@ myMenu68.width = 256; -
Test svc_acct:kyla@example.com
+
Test svc_acct:kyla@example.com
@@ -6713,7 +6713,7 @@ myMenu68.width = 256; -
Test svc_phone:2636239939
+
Test svc_phone:2636239939
@@ -6814,7 +6814,7 @@ myMenu68.width = 256; -
Test svc_acct:jacky@example.com
+
Test svc_acct:jacky@example.com
@@ -6915,7 +6915,7 @@ myMenu68.width = 256; -
Test svc_acct:horace@example.com
+
Test svc_acct:horace@example.com
@@ -7016,7 +7016,7 @@ myMenu68.width = 256; -
Test svc_phone:68981950057600
+
Test svc_phone:68981950057600
@@ -7117,7 +7117,7 @@ myMenu68.width = 256; -
Test svc_phone:31674614863771
+
Test svc_phone:31674614863771
@@ -7218,7 +7218,7 @@ myMenu68.width = 256; -
Test svc_acct:alexa@example.com
+
Test svc_acct:alexa@example.com
@@ -7319,7 +7319,7 @@ myMenu68.width = 256; -
Test svc_phone:10163759294554
+
Test svc_phone:10163759294554
@@ -7420,7 +7420,7 @@ myMenu68.width = 256; -
Test svc_acct:lexie@example.com
+
Test svc_acct:lexie@example.com
@@ -7521,7 +7521,7 @@ myMenu68.width = 256; -
Test svc_acct:cleo@example.com
+
Test svc_acct:cleo@example.com
@@ -7622,7 +7622,7 @@ myMenu68.width = 256; -
Test svc_phone:2749371736
+
Test svc_phone:2749371736
@@ -7723,7 +7723,7 @@ myMenu68.width = 256; -
Test svc_phone:70045317269958
+
Test svc_phone:70045317269958
@@ -7824,7 +7824,7 @@ myMenu68.width = 256; -
Test svc_acct:clinton@example.com
+
Test svc_acct:clinton@example.com
@@ -7925,7 +7925,7 @@ myMenu68.width = 256; -
Test svc_phone:106766405260980
+
Test svc_phone:106766405260980
@@ -8026,7 +8026,7 @@ myMenu68.width = 256; -
Test svc_phone:9519625792
+
Test svc_phone:9519625792
@@ -8127,7 +8127,7 @@ myMenu68.width = 256; -
Test svc_phone:16095013569
+
Test svc_phone:16095013569
@@ -8228,7 +8228,7 @@ myMenu68.width = 256; -
Test svc_acct:karen@example.com
+
Test svc_acct:karen@example.com
@@ -8329,7 +8329,7 @@ myMenu68.width = 256; -
Test svc_phone:1732869050
+
Test svc_phone:1732869050
@@ -8430,7 +8430,7 @@ myMenu68.width = 256; -
Test svc_acct:antwan@example.com
+
Test svc_acct:antwan@example.com
@@ -8531,7 +8531,7 @@ myMenu68.width = 256; -
Test svc_acct:mitchell@example.com
+
Test svc_acct:mitchell@example.com
@@ -8632,7 +8632,7 @@ myMenu68.width = 256; -
Test svc_phone:5866817423
+
Test svc_phone:5866817423
@@ -8733,7 +8733,7 @@ myMenu68.width = 256; -
Test svc_phone:3521080416
+
Test svc_phone:3521080416
@@ -8834,7 +8834,7 @@ myMenu68.width = 256; -
Test svc_acct:rebeca@example.com
+
Test svc_acct:rebeca@example.com
@@ -8935,7 +8935,7 @@ myMenu68.width = 256; -
Test svc_phone:183790150181541
+
Test svc_phone:183790150181541
@@ -9036,7 +9036,7 @@ myMenu68.width = 256; -
Test svc_acct:ibrahim@example.com
+
Test svc_acct:ibrahim@example.com
@@ -9137,7 +9137,7 @@ myMenu68.width = 256; -
Test svc_acct:jadon@example.com
+
Test svc_acct:jadon@example.com
@@ -9238,7 +9238,7 @@ myMenu68.width = 256; -
Test svc_phone:14991580189167
+
Test svc_phone:14991580189167
@@ -9339,7 +9339,7 @@ myMenu68.width = 256; -
Test svc_phone:2964457155392
+
Test svc_phone:2964457155392
@@ -9440,7 +9440,7 @@ myMenu68.width = 256; -
Test svc_acct:coby@example.com
+
Test svc_acct:coby@example.com
@@ -9541,7 +9541,7 @@ myMenu68.width = 256; -
Test svc_phone:4593519604
+
Test svc_phone:4593519604
@@ -9642,7 +9642,7 @@ myMenu68.width = 256; -
Test svc_phone:4989851645
+
Test svc_phone:4989851645
@@ -9743,7 +9743,7 @@ myMenu68.width = 256; -
Test svc_acct:stanton@example.com
+
Test svc_acct:stanton@example.com
@@ -9844,7 +9844,7 @@ myMenu68.width = 256; -
Test svc_phone:3337658056
+
Test svc_phone:3337658056
@@ -9945,7 +9945,7 @@ myMenu68.width = 256; -
Test svc_acct:clarabelle@example.com
+
Test svc_acct:clarabelle@example.com
@@ -10046,7 +10046,7 @@ myMenu68.width = 256; -
Test svc_acct:stan@example.com
+
Test svc_acct:stan@example.com
@@ -10147,7 +10147,7 @@ myMenu68.width = 256; -
Test svc_phone:0783009535773
+
Test svc_phone:0783009535773
@@ -10248,7 +10248,7 @@ myMenu68.width = 256; -
Test svc_acct:marc@example.com
+
Test svc_acct:marc@example.com
@@ -10349,7 +10349,7 @@ myMenu68.width = 256; -
Test svc_phone:1894866195856273
+
Test svc_phone:1894866195856273
@@ -10450,7 +10450,7 @@ myMenu68.width = 256; -
Test svc_acct:newell@example.com
+
Test svc_acct:newell@example.com
@@ -10551,7 +10551,7 @@ myMenu68.width = 256; -
Test svc_phone:15790441533145
+
Test svc_phone:15790441533145
@@ -10652,7 +10652,7 @@ myMenu68.width = 256; -
Test svc_phone:3724022714296
+
Test svc_phone:3724022714296
@@ -10753,7 +10753,7 @@ myMenu68.width = 256; -
Test svc_acct:therese@example.com
+
Test svc_acct:therese@example.com
@@ -10854,7 +10854,7 @@ myMenu68.width = 256; -
Test svc_phone:4942001551
+
Test svc_phone:4942001551
@@ -10955,7 +10955,7 @@ myMenu68.width = 256; -
Test svc_acct:maida@example.com
+
Test svc_acct:maida@example.com
@@ -11056,7 +11056,7 @@ myMenu68.width = 256; -
Test svc_phone:3911632965
+
Test svc_phone:3911632965
@@ -11157,7 +11157,7 @@ myMenu68.width = 256; -
Test svc_acct:eldridge@example.com
+
Test svc_acct:eldridge@example.com
@@ -11258,7 +11258,7 @@ myMenu68.width = 256; -
Test svc_phone:150342529271096
+
Test svc_phone:150342529271096
diff --git a/FS-Test/share/output/search/cust_pkg.cgi/magic=bill:custnum=135 b/FS-Test/share/output/search/cust_pkg.cgi/magic=bill:custnum=135 index 2ccc71f9d..7963b90dd 100644 --- a/FS-Test/share/output/search/cust_pkg.cgi/magic=bill:custnum=135 +++ b/FS-Test/share/output/search/cust_pkg.cgi/magic=bill:custnum=135 @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); @@ -1242,7 +1242,7 @@ myMenu68.width = 256; -
Test svc_phone:152300260278657
+
Test svc_phone:152300260278657
@@ -1343,7 +1343,7 @@ myMenu68.width = 256; -
Test svc_broadband:IP:10.98.22.188, MAC:00007E436BF5
+
Test svc_broadband:IP:10.98.22.188, MAC:00007E436BF5
@@ -1444,7 +1444,7 @@ myMenu68.width = 256; -
Test svc_domain:botsford-mueller.com
+
Test svc_domain:botsford-mueller.com
diff --git a/FS-Test/share/output/search/cust_pkg_churn.html/agentnum=:status=setup:begin=1438412400:end=1441090800:order_by=cust_pkg.pkgnum b/FS-Test/share/output/search/cust_pkg_churn.html/agentnum=:status=setup:begin=1438412400:end=1441090800:order_by=cust_pkg.pkgnum index 92493adbb..b9e53fa57 100644 --- a/FS-Test/share/output/search/cust_pkg_churn.html/agentnum=:status=setup:begin=1438412400:end=1441090800:order_by=cust_pkg.pkgnum +++ b/FS-Test/share/output/search/cust_pkg_churn.html/agentnum=:status=setup:begin=1438412400:end=1441090800:order_by=cust_pkg.pkgnum @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/search/cust_pkg_summary.cgi/beginning=02%2F01%2F2016:ending=02%2F28%2F2016:classnum=0 b/FS-Test/share/output/search/cust_pkg_summary.cgi/beginning=02%2F01%2F2016:ending=02%2F28%2F2016:classnum=0 index 5df716b94..f003efdf8 100644 --- a/FS-Test/share/output/search/cust_pkg_summary.cgi/beginning=02%2F01%2F2016:ending=02%2F28%2F2016:classnum=0 +++ b/FS-Test/share/output/search/cust_pkg_summary.cgi/beginning=02%2F01%2F2016:ending=02%2F28%2F2016:classnum=0 @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/search/h_cust_pkg.html/classnum=0:status=active,suspended:date=1454313600:pkgpart=2 b/FS-Test/share/output/search/h_cust_pkg.html/classnum=0:status=active,suspended:date=1454313600:pkgpart=2 index 404e26af0..1a8264147 100644 --- a/FS-Test/share/output/search/h_cust_pkg.html/classnum=0:status=active,suspended:date=1454313600:pkgpart=2 +++ b/FS-Test/share/output/search/h_cust_pkg.html/classnum=0:status=active,suspended:date=1454313600:pkgpart=2 @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/search/report_receivables.cgi/days=0:as_of=03%2F01%2F2016 b/FS-Test/share/output/search/report_receivables.cgi/days=0:as_of=03%2F01%2F2016 index dc51c8826..413100a94 100644 --- a/FS-Test/share/output/search/report_receivables.cgi/days=0:as_of=03%2F01%2F2016 +++ b/FS-Test/share/output/search/report_receivables.cgi/days=0:as_of=03%2F01%2F2016 @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/search/svc_acct.cgi/magic=all:sortby=username b/FS-Test/share/output/search/svc_acct.cgi/magic=all:sortby=username index 88ac10dde..64f89ba0c 100644 --- a/FS-Test/share/output/search/svc_acct.cgi/magic=all:sortby=username +++ b/FS-Test/share/output/search/svc_acct.cgi/magic=all:sortby=username @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/search/svc_broadband.cgi/magic=advanced:custnum=:agentnum=1:routernum=none:cust_pkg_fields=:cust_fields=:_dummy=1:maxrecords=100:_type=html:offset=0:order_by=ip_addr b/FS-Test/share/output/search/svc_broadband.cgi/magic=advanced:custnum=:agentnum=1:routernum=none:cust_pkg_fields=:cust_fields=:_dummy=1:maxrecords=100:_type=html:offset=0:order_by=ip_addr index 896f7201f..22ffa22b8 100644 --- a/FS-Test/share/output/search/svc_broadband.cgi/magic=advanced:custnum=:agentnum=1:routernum=none:cust_pkg_fields=:cust_fields=:_dummy=1:maxrecords=100:_type=html:offset=0:order_by=ip_addr +++ b/FS-Test/share/output/search/svc_broadband.cgi/magic=advanced:custnum=:agentnum=1:routernum=none:cust_pkg_fields=:cust_fields=:_dummy=1:maxrecords=100:_type=html:offset=0:order_by=ip_addr @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/search/svc_domain.cgi/magic=all:sortby=domain b/FS-Test/share/output/search/svc_domain.cgi/magic=all:sortby=domain index b7df3545a..c38527045 100644 --- a/FS-Test/share/output/search/svc_domain.cgi/magic=all:sortby=domain +++ b/FS-Test/share/output/search/svc_domain.cgi/magic=all:sortby=domain @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/search/svc_phone.cgi/magic=all:sortby=phonenum b/FS-Test/share/output/search/svc_phone.cgi/magic=all:sortby=phonenum index 915073f3f..01ca7a521 100644 --- a/FS-Test/share/output/search/svc_phone.cgi/magic=all:sortby=phonenum +++ b/FS-Test/share/output/search/svc_phone.cgi/magic=all:sortby=phonenum @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/search/unprovisioned_services.html b/FS-Test/share/output/search/unprovisioned_services.html index e0c8d1dc6..af2ca4cea 100644 --- a/FS-Test/share/output/search/unprovisioned_services.html +++ b/FS-Test/share/output/search/unprovisioned_services.html @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/view/cust_bill-tex.cgi/invnum=681:notice_name=Invoice b/FS-Test/share/output/view/cust_bill-tex.cgi/invnum=681:notice_name=Invoice index 7d616f6af..fc9294b20 100644 --- a/FS-Test/share/output/view/cust_bill-tex.cgi/invnum=681:notice_name=Invoice +++ b/FS-Test/share/output/view/cust_bill-tex.cgi/invnum=681:notice_name=Invoice @@ -81,7 +81,7 @@ \begin{tabular}{ll} \returninset \begin{tabular}{ll} - \makebox{ \includegraphics{/usr/local/etc/freeside/cache.DBI:Pg:dbname=freeside/cust_bill.681.aSksR2dX.eps}} & + \makebox{ \includegraphics{/usr/local/etc/freeside/cache.DBI:Pg:dbname=freeside/cust_bill.681.XoeuvJBe.eps}} & \begin{minipage}[b]{5.5cm} Freeside Test 5.0.1\\* 1234 Example Lane\\* @@ -142,7 +142,7 @@ Freeside Test 5.0.1 \returninset \makebox{ \begin{tabular}{ll} - \includegraphics{/usr/local/etc/freeside/cache.DBI:Pg:dbname=freeside/cust_bill.681.aSksR2dX.eps} & + \includegraphics{/usr/local/etc/freeside/cache.DBI:Pg:dbname=freeside/cust_bill.681.XoeuvJBe.eps} & \begin{minipage}[b]{5.5cm} Freeside Test 5.0.1\\* 1234 Example Lane\\* @@ -152,7 +152,7 @@ Exampleton, CA~~54321\\* } } { % ... pages - %\includegraphics{/usr/local/etc/freeside/cache.DBI:Pg:dbname=freeside/cust_bill.681.aSksR2dX.eps} % Uncomment if you want the logo on all pages. + %\includegraphics{/usr/local/etc/freeside/cache.DBI:Pg:dbname=freeside/cust_bill.681.XoeuvJBe.eps} % Uncomment if you want the logo on all pages. } } diff --git a/FS-Test/share/output/view/cust_bill.cgi/681 b/FS-Test/share/output/view/cust_bill.cgi/681 index 3ffad465f..53e49287f 100644 --- a/FS-Test/share/output/view/cust_bill.cgi/681 +++ b/FS-Test/share/output/view/cust_bill.cgi/681 @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/view/cust_main.cgi/135 b/FS-Test/share/output/view/cust_main.cgi/135 index 011c96f0b..5787fca15 100644 --- a/FS-Test/share/output/view/cust_main.cgi/135 +++ b/FS-Test/share/output/view/cust_main.cgi/135 @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/view/cust_main.cgi/custnum=135:show=change_history b/FS-Test/share/output/view/cust_main.cgi/custnum=135:show=change_history index 6d7fd40e1..0b800d686 100644 --- a/FS-Test/share/output/view/cust_main.cgi/custnum=135:show=change_history +++ b/FS-Test/share/output/view/cust_main.cgi/custnum=135:show=change_history @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/view/cust_main.cgi/custnum=135:show=packages b/FS-Test/share/output/view/cust_main.cgi/custnum=135:show=packages index 132d5f6d0..20a969b89 100644 --- a/FS-Test/share/output/view/cust_main.cgi/custnum=135:show=packages +++ b/FS-Test/share/output/view/cust_main.cgi/custnum=135:show=packages @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/view/cust_main.cgi/custnum=135:show=payment_history b/FS-Test/share/output/view/cust_main.cgi/custnum=135:show=payment_history index 58e3b142c..e1e75c58e 100644 --- a/FS-Test/share/output/view/cust_main.cgi/custnum=135:show=payment_history +++ b/FS-Test/share/output/view/cust_main.cgi/custnum=135:show=payment_history @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/view/cust_main.cgi/custnum=2:show=packages b/FS-Test/share/output/view/cust_main.cgi/custnum=2:show=packages index ba5791957..757aa17fe 100644 --- a/FS-Test/share/output/view/cust_main.cgi/custnum=2:show=packages +++ b/FS-Test/share/output/view/cust_main.cgi/custnum=2:show=packages @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); @@ -2371,6 +2371,216 @@ function clearhint_search_cust_svc(obj, str) { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Test one-time charge + - + $100.00 - $100.00 one-time +
+ + + ( Modify one-time charge ) + ( Discount ) +
+ +
+
+ + + +
+ + + + ( Change sales person ) + + +
+ + ( Add invoice details ) + + + + ( Add comments ) + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Ordered Mar 2nd, 2016
One-time charge
Billed Mar 2nd, 2016
+ + + + + ( Add contact ) + +
+ +
+ + Default service location
24866 VonRueden Roads
Floor 94 X
Crown Point CT 59629-7714 + +
+ + 38.5000000, -121.5000000 + + +map + + directions + +earth + + + + +
+ +
+ + ( Change location ) + ( Edit location ) + + + + + + + + + +
+ + + diff --git a/FS-Test/share/output/view/cust_main.cgi/custnum=2:show=payment_history b/FS-Test/share/output/view/cust_main.cgi/custnum=2:show=payment_history index 36a01b077..8d15d4190 100644 --- a/FS-Test/share/output/view/cust_main.cgi/custnum=2:show=payment_history +++ b/FS-Test/share/output/view/cust_main.cgi/custnum=2:show=payment_history @@ -617,7 +617,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); @@ -1620,6 +1620,35 @@ function areyousure(href, message) { + + + + 03/02/2016 + + + + + Open Invoice #1585 (Balance 100.00) + + + + $100.00 + + + + + + + + + + + + $874.03 + + + + diff --git a/FS-Test/share/output/view/svc_acct.cgi/406 b/FS-Test/share/output/view/svc_acct.cgi/406 index 32d9a568a..60867f8d4 100644 --- a/FS-Test/share/output/view/svc_acct.cgi/406 +++ b/FS-Test/share/output/view/svc_acct.cgi/406 @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); @@ -1015,7 +1015,7 @@ function updateTicketLink() { } Tickets -Create new ticket +Create new ticket in queue diff --git a/FS-Test/share/output/view/svc_broadband.cgi/401 b/FS-Test/share/output/view/svc_broadband.cgi/401 index 31877d80e..8bf4026f9 100644 --- a/FS-Test/share/output/view/svc_broadband.cgi/401 +++ b/FS-Test/share/output/view/svc_broadband.cgi/401 @@ -634,7 +634,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); diff --git a/FS-Test/share/output/view/svc_domain.cgi/402 b/FS-Test/share/output/view/svc_domain.cgi/402 index 71977b5d2..b930c3c19 100644 --- a/FS-Test/share/output/view/svc_domain.cgi/402 +++ b/FS-Test/share/output/view/svc_domain.cgi/402 @@ -618,7 +618,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); @@ -1025,11 +1025,11 @@ Service #402 function updateTicketLink() { var link = document.getElementById('CreateTicketLink'); var selector = document.getElementById('Queue') - link.href = "http://localhost/freeside//rt/Ticket/Create.html?new-MemberOf=freeside://freeside/cust_svc/402;Requestors=;Queue=" + selector.options[selector.selectedIndex].value; + link.href = "http://localhost/freeside//rt/Ticket/Create.html?Requestors=;new-MemberOf=freeside://freeside/cust_svc/402;Queue=" + selector.options[selector.selectedIndex].value; } Tickets -Create new ticket +Create new ticket in queue diff --git a/FS-Test/share/output/view/svc_phone.cgi/403 b/FS-Test/share/output/view/svc_phone.cgi/403 index 113aaf715..1389fd7eb 100644 --- a/FS-Test/share/output/view/svc_phone.cgi/403 +++ b/FS-Test/share/output/view/svc_phone.cgi/403 @@ -634,7 +634,7 @@ myMenu48.add(new WebFXMenuItem("Billing", null, "", myMenu65 )); myMenu48.add(new WebFXMenuSeparator()); var myMenu67 = new WebFXMenu; myMenu67.emptyText = ''; -myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template.html", "Templates for customer notices" )); +myMenu67.add(new WebFXMenuItem("Message templates", "http://localhost/freeside/browse/msg_template/email.html", "Templates for customer notices" )); myMenu67.add(new WebFXMenuItem("Advertising sources", "http://localhost/freeside/browse/part_referral.html", "Where a customer heard about your service." )); myMenu67.add(new WebFXMenuItem("Custom fields", "http://localhost/freeside/browse/part_virtual_field.html", "Locally defined fields" )); myMenu67.add(new WebFXMenuItem("Translation strings", "http://localhost/freeside/browse/msgcat.html", "Translations and other customizable labels for each locale" )); @@ -977,7 +977,7 @@ function areyousure_delete() { function updateTicketLink() { var link = document.getElementById('CreateTicketLink'); var selector = document.getElementById('Queue') - link.href = "http://localhost/freeside//rt/Ticket/Create.html?new-MemberOf=freeside://freeside/cust_svc/403;Requestors=;Queue=" + selector.options[selector.selectedIndex].value; + link.href = "http://localhost/freeside//rt/Ticket/Create.html?Requestors=;new-MemberOf=freeside://freeside/cust_svc/403;Queue=" + selector.options[selector.selectedIndex].value; } Tickets -- cgit v1.2.1 From 653b350d0f8cc69e66e265537a3775f512fd5dda Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 31 Aug 2015 14:55:34 -0700 Subject: one more repeatability fix + documentation, #37340 --- FS-Test/README | 38 ++++++++++++++------------------ FS-Test/share/output/edit/part_pkg.cgi/2 | 18 +++++++-------- FS/FS/cdr.pm | 2 +- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/FS-Test/README b/FS-Test/README index b1518bd6f..8e9681cef 100644 --- a/FS-Test/README +++ b/FS-Test/README @@ -2,42 +2,36 @@ FS-Test INSTALLATION -To install this module, install Freeside as usual. Then run the following -commands: - - perl Makefile.PL - make - make install - -INITIALIZATION - -Run "freeside-test-start" from an account that can sudo to root. This will -create a database with the test image and set the system clock to one day -after the last bill. If there's an existing Freeside database, it will be -renamed to "freeside_YYYYMMDD" (the current date). - -To restore the system clock and put the existing database back in place, -run "freeside-test-stop". +This module no longer needs to be installed. Run it directly from the source +tree. RUNNING TESTS -"freeside-test-run" is the main test script. Currently there's only one -test plan, "ui_tests". freeside-test-run will: +"freeside-test-run" is the main test script. Currently there's only one test +plan, "ui_tests". freeside-test-run will: - download all the URLs listed in the test plan into a directory in /tmp - compare them to the reference versions with "diff -ur" - write the output to "freeside_test.YYYYMMDD.diff" -- display the results of "diffstat" on that diff The raw output directory will not be deleted, so you can examine the results -yourself. This is recommended for files that don't diff nicely like Excel -versions of reports and PDF invoices. +yourself. + +If you want to do anything with the database besides compare the test results +to reference, run "freeside-test-start" by hand first. This will create a +database with the test image and start Apache with a fake time of one day after +the last bill. If there's an existing Freeside database, it will be renamed to +"freeside_YYYYMMDD" (the current date). + +To put the existing database back in place, run "freeside-test-stop", then +restart Apache and any Freeside services. UPDATING THE REFERENCE PAGES The simplest way to update the reference copies of the test pages is -freeside-test-fetch -d ./share/output +bin/freeside-test-start +bin/freeside-test-fetch -d ./share/output (from the FS-Test source directory). If you're installing from a git repo, this will overwrite the working tree with the newly downloaded test pages. diff --git a/FS-Test/share/output/edit/part_pkg.cgi/2 b/FS-Test/share/output/edit/part_pkg.cgi/2 index 9aba1583e..798351f2d 100644 --- a/FS-Test/share/output/edit/part_pkg.cgi/2 +++ b/FS-Test/share/output/edit/part_pkg.cgi/2 @@ -4720,9 +4720,9 @@ spawn_supp_dst_pkgpart(this);" Calculate usage based on the duration field instead of the billsec field Rewrite these (comma-separated) destination numbers to 411 for rating purposes (also ignore any carrierid check): CDR display format for invoices - CDR display format for selfservice - Inbound CDR display format for selfservice - Always put usage details in separate section. The section is defined in the next option. + CDR display format for selfservice + Inbound CDR display format for selfservice + Always put usage details in separate section. The section is defined in the next option. Section in which to place usage charges (whether separated or not): Include usage summary with recurring charges when usage is in separate section Show details for included / no-charge calls. @@ -4775,7 +4775,7 @@ spawn_supp_dst_pkgpart(this);" Do not charge for CDRs where the lastapp matches this value Calculate usage based on the duration field instead of the billsec field CDR invoice display format - Always put usage details in separate section + Always put usage details in separate section Include usage summary with recurring charges when usage is in separate section Section in which to place usage charges (whether separated or not) Generate an invoice immediately for every call. Useful for prepaid. @@ -4834,9 +4834,9 @@ spawn_supp_dst_pkgpart(this);" Calculate usage based on the duration field instead of the billsec field Rewrite these (comma-separated) destination numbers to 411 for rating purposes (also ignore any carrierid check): CDR display format for invoices - CDR display format for selfservice - Inbound CDR display format for selfservice - Always put usage details in separate section. The section is defined in the next option. + CDR display format for selfservice + Inbound CDR display format for selfservice + Always put usage details in separate section. The section is defined in the next option. Section in which to place usage charges (whether separated or not): Include usage summary with recurring charges when usage is in separate section Show details for included / no-charge calls. @@ -4872,7 +4872,7 @@ spawn_supp_dst_pkgpart(this);" When prorating first month, also bill for one full period after that Show prorate details on the invoice CDR invoice display format - Section in which to place separate usage charges + Section in which to place separate usage charges Include usage summary with recurring charges when usage is in separate section Always put usage details in separate section Credit the customer for the unused portion of service at cancellation @@ -4992,7 +4992,7 @@ spawn_supp_dst_pkgpart(this);" When prorating first month, also bill for one full period after that Show prorate details on the invoice CDR invoice display format - Section in which to place separate usage charges + Section in which to place separate usage charges Include usage summary with recurring charges when usage is in separate section Always put usage details in separate section Credit the customer for the unused portion of service at cancellation diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index 1a3666099..775c79114 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -1463,7 +1463,7 @@ as keys (for use with part_pkg::voip_cdr) and "pretty" format names as values. sub invoice_formats { map { ($_ => $export_names{$_}->{'name'}) } grep { $export_names{$_}->{'invoice_header'} } - keys %export_names; + sort keys %export_names; } =item invoice_header FORMAT -- cgit v1.2.1 From 6163b943f45e083a87cc03344eb775a9edd553ce Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 31 Aug 2015 22:16:37 -0700 Subject: allow services with a tower but no sector to appear in search results, #33056 --- FS/FS/svc_Tower_Mixin.pm | 8 +++++++- httemplate/elements/select-tower_sector.html | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/FS/FS/svc_Tower_Mixin.pm b/FS/FS/svc_Tower_Mixin.pm index 2555b9e50..d6776791c 100644 --- a/FS/FS/svc_Tower_Mixin.pm +++ b/FS/FS/svc_Tower_Mixin.pm @@ -27,7 +27,13 @@ sub tower_sector_sql { my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$value); my @orwhere; push @orwhere, "tower_sector.$field IN ($in)" if $in; - push @orwhere, "tower_sector.$field IS NULL" if grep /^none$/, @$value; + if ( grep /^none$/, @$value ) { + # then allow this field to be null + push @orwhere, "tower_sector.$field IS NULL"; + # and if this field is the sector, also allow the default sector + # on the tower + push @orwhere, "sectorname = '_default'" if $field eq 'sectornum'; + } push @where, '( '.join(' OR ', @orwhere).' )'; } elsif ( $value =~ /^(\d+)$/ ) { diff --git a/httemplate/elements/select-tower_sector.html b/httemplate/elements/select-tower_sector.html index a16d3bfa0..59b016359 100644 --- a/httemplate/elements/select-tower_sector.html +++ b/httemplate/elements/select-tower_sector.html @@ -12,7 +12,7 @@ table => 'tower', name_col => 'towername', id => 'towernum', - field => 'dummy_towernum', + field => 'towernum', onchange => 'change_towernum(this.value);', element_etc => 'STYLE="vertical-align:top"', &> @@ -63,5 +63,5 @@ foreach my $towernum (keys %sectors_of) { } } -my $empty_label = $opt{'empty_label'} || 'Include services with no tower/sector'; +my $empty_label = $opt{'empty_label'} || 'Include services with no sector'; -- cgit v1.2.1 From e0e76b55a2f83c19e4114eefe4dabcab092808b4 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 1 Sep 2015 23:11:05 -0500 Subject: RT#32892: Monthly Sales Tax Report --- FS/FS/Report/Table.pm | 27 ++++++ httemplate/search/report_tax_sales.cgi | 158 ++++++++++++++++++++++++++++++++ httemplate/search/report_tax_sales.html | 35 +++++++ 3 files changed, 220 insertions(+) create mode 100644 httemplate/search/report_tax_sales.cgi create mode 100755 httemplate/search/report_tax_sales.html diff --git a/FS/FS/Report/Table.pm b/FS/FS/Report/Table.pm index 4b1ad05d6..4b22b60b8 100644 --- a/FS/FS/Report/Table.pm +++ b/FS/FS/Report/Table.pm @@ -753,6 +753,33 @@ sub cust_bill_pkg_taxes { $self->scalar_sql($total_sql); } +#all credits applied to matching pkg line items (ie not taxes or fees) + +sub cust_bill_pkg_credits { + my $self = shift; + my ($speriod, $eperiod, $agentnum, %opt) = @_; + + $agentnum ||= $opt{'agentnum'}; + + my @where = ( + '(cust_bill_pkg.pkgnum != 0 OR feepart IS NOT NULL)', + $self->with_classnum($opt{'classnum'}, $opt{'use_override'}), + $self->with_report_option(%opt), + $self->in_time_period_and_agent($speriod, $eperiod, $agentnum), + $self->with_refnum(%opt), + $self->with_cust_classnum(%opt) + ); + + my $total_sql = "SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0) + FROM cust_bill_pkg + $cust_bill_pkg_join + LEFT JOIN cust_credit_bill_pkg + USING ( billpkgnum ) + WHERE " . join(' AND ', grep $_, @where); + + $self->scalar_sql($total_sql); +} + ##### package churn report ##### =item active_pkg: The number of packages that were active at the start of diff --git a/httemplate/search/report_tax_sales.cgi b/httemplate/search/report_tax_sales.cgi new file mode 100644 index 000000000..5c531c343 --- /dev/null +++ b/httemplate/search/report_tax_sales.cgi @@ -0,0 +1,158 @@ + +<% include('/graph/elements/report.html', + 'title' => 'Monthly Sales Tax Report', + 'items' => \@row_labels, + 'data' => \@rowdata, + 'row_labels' => \@row_labels, + 'colors' => [], + 'col_labels' => \@col_labels, + ) %> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +# validate cgi input +my $start_month = $cgi->param('start_month'); +die "Bad start month" unless $start_month =~ /^\d*$/; +my $start_year = $cgi->param('start_year'); +die "Bad start year" unless $start_year =~ /^\d*$/; +my $end_month = $cgi->param('end_month'); +die "Bad end month" unless $end_month =~ /^\d*$/; +my $end_year = $cgi->param('end_year'); +die "Bad end year" unless $end_year =~ /^\d*$/; +die "End year before start year" if $end_year < $start_year; +die "End month before start month" if ($start_year == $end_year) && ($end_month < $start_month); +my $country = $cgi->param('country'); +die "Bad country code" unless $country =~ /^\w\w$/; + +# Data structure for building final table +# row order will be calculated separately +# +# $data->{$rowlabel} = \@rowvalues +# + +my $data = {}; + +### Calculate package values + +my @pkg_class = qsearch('pkg_class'); +my @pkg_classnum = map { $_->classnum } @pkg_class; +unshift(@pkg_classnum,0); +my @pkg_classname = map { $_->classname } @pkg_class; +unshift(@pkg_classname,'(empty class)'); + +# some false laziness with graph/elements/monthly.html +my %reportopts = ( + 'items' => [ qw( cust_bill_pkg cust_bill_pkg_credits ) ], + 'cross_params' => [ map { [ 'classnum', $_ ] } @pkg_classnum ], + 'start_month' => $start_month, + 'start_year' => $start_year, + 'end_month' => $end_month, + 'end_year' => $end_year, +); +my $pkgreport = new FS::Report::Table::Monthly(%reportopts); +my $pkgdata = $pkgreport->data; + +# assuming every month/year combo is included in results, +# just use this list for the final table +my @col_labels = @{$pkgdata->{'label'}}; + +# unpack report data into a more manageable format +foreach my $item ( qw( invoiced credited ) ) { # invoiced, credited + my $itemref = shift @{$pkgdata->{'data'}}; + foreach my $label (@{$pkgdata->{'label'}}) { # month/year + my $labelref = shift @$itemref; + foreach my $classname (@pkg_classname) { # pkg class + my $value = shift @$labelref; + my $rowlabel = $classname.' '.$item; + $data->{$rowlabel} ||= []; + push(@{$data->{$rowlabel}},$value); + } + } +} + +### Calculate tax values + +# false laziness w report_tax.html, put this in FS::Report::Tax? +my $sth = dbh->prepare('SELECT DISTINCT(COALESCE(taxname, \'Tax\')) FROM cust_main_county'); +$sth->execute or die $sth->errstr; +my @taxnames = map { $_->[0] } @{ $sth->fetchall_arrayref }; +$sth->finish; + +# get DateTime objects for start & end +my $startdate = DateTime->new( + year => $start_year, + month => $start_month, + day => 1 + ); +my $enddate = DateTime->new( + year => $end_year, + month => $end_month, + day => 1 + ); +$enddate->add( months => 1 )->subtract( seconds => 1 ); # the last second of the month + +# common to all tax reports +my %params = ( + 'country' => $country, + 'credit_date' => 'cust_bill', +); + +# run a report for each month, for each tax +my $countdate = $startdate->clone; +while ($countdate < $enddate) { + + # set report start date, iterate to end of this month, set report end date + $params{'beginning'} = $countdate->epoch; + $params{'ending'} = $countdate->add( months => 1 )->subtract( seconds => 1 )->epoch; + + # run a report for each tax name + foreach my $taxname (@taxnames) { + $params{'taxname'} = $taxname; + my $report = FS::Report::Tax->report_internal(%params); + + # extract totals from report, kinda awkward + my $pkgclass = ''; # this will get more complicated if we breakdown by pkgclass + my @values = (0,0); + if ($report->{'total'}->{$pkgclass}) { + my %totals = map { $$_[0] => $$_[2] } @{$report->{'total'}->{$pkgclass}}; + $values[0] = $totals{'tax'}; + $values[1] = $totals{'credit'}; + } + + # treat each tax class like it's an additional pkg class + foreach my $item ( qw ( invoiced credited ) ) { + my $rowlabel = $taxname . ' ' . $item; + my $value = shift @values; + $data->{$rowlabel} ||= []; + push(@{$data->{$rowlabel}},$value); + } + + } + + # iterate to next month + $countdate->add( seconds => 1 ); +} + +# put the data in the order we want it +my @row_labels; +my @rowdata; +foreach my $classname (@pkg_classname,@taxnames) { + my @classlabels = (); + my @classdata = (); + my $hasdata = 0; + foreach my $item ( qw( invoiced credited ) ) { + my $rowlabel = $classname . ' ' . $item; + my $rowdata = $data->{$rowlabel}; + $hasdata = 1 if grep { $_ } @$rowdata; + push(@classlabels,$rowlabel); + push(@classdata,$rowdata); + } + next unless $hasdata; # don't include class if it has no data in time range + push(@row_labels,@classlabels); + push(@rowdata,@classdata); +} + + diff --git a/httemplate/search/report_tax_sales.html b/httemplate/search/report_tax_sales.html new file mode 100755 index 000000000..374a15601 --- /dev/null +++ b/httemplate/search/report_tax_sales.html @@ -0,0 +1,35 @@ +<% include('/elements/header.html', 'Monthly Sales Tax Report' ) %> + +
+ + + + <% include('/elements/tr-select-from_to.html') %> + + <% include('/elements/tr-select.html', + 'label' => 'Country', + 'field' => 'country', + 'options' => \@countries, + 'curr_value' => ($conf->config('countrydefault') || 'US'), + ) %> + +
+ +
+ +
+ +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; + +# false laziness w report_tax.html, put this in FS::Report::Tax? +my $sth = dbh->prepare('SELECT DISTINCT(country) FROM cust_location'); +$sth->execute or die $sth->errstr; +my @countries = map { $_->[0] } @{ $sth->fetchall_arrayref }; + + -- cgit v1.2.1 From 5cbb1285d26ffe2f7fbf8aed14b5b3d7c037fe83 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 2 Sep 2015 21:05:28 -0500 Subject: RT#32892: Monthly Sales Tax Report [fixed names and colors] --- FS/FS/Report/Table.pm | 2 +- httemplate/elements/menu.html | 2 + httemplate/graph/elements/report.html | 6 +- httemplate/search/report_tax_sales.cgi | 158 ----------------------------- httemplate/search/report_tax_sales.html | 35 ------- httemplate/search/tax_sales.cgi | 172 ++++++++++++++++++++++++++++++++ httemplate/search/tax_sales.html | 35 +++++++ 7 files changed, 214 insertions(+), 196 deletions(-) delete mode 100644 httemplate/search/report_tax_sales.cgi delete mode 100755 httemplate/search/report_tax_sales.html create mode 100644 httemplate/search/tax_sales.cgi create mode 100755 httemplate/search/tax_sales.html diff --git a/FS/FS/Report/Table.pm b/FS/FS/Report/Table.pm index 4b22b60b8..eeb99bac5 100644 --- a/FS/FS/Report/Table.pm +++ b/FS/FS/Report/Table.pm @@ -753,7 +753,7 @@ sub cust_bill_pkg_taxes { $self->scalar_sql($total_sql); } -#all credits applied to matching pkg line items (ie not taxes or fees) +#all credits applied to matching pkg line items (ie not taxes) sub cust_bill_pkg_credits { my $self = shift; diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 55645cf31..ea6933198 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -381,6 +381,8 @@ if( $curuser->access_right('Financial reports') ) { $report_financial{'Tax Liability (vendor tax data)'} = [ $fsurl.'search/report_newtax.html', 'Tax liability report (vendor tax data)' ] if $taxproducts; + $report_financial{'Monthly Sales and Taxes'} = [$fsurl.'search/tax_sales.html', 'Monthly sales and taxes report']; + # most sites don't need this but there isn't really a config to enable it $report_financial{'E911 Fee Summary'} = [ $fsurl.'search/report_e911.html', 'E911 fee summary' ]; diff --git a/httemplate/graph/elements/report.html b/httemplate/graph/elements/report.html index f1b0d166d..b5d214816 100644 --- a/httemplate/graph/elements/report.html +++ b/httemplate/graph/elements/report.html @@ -11,6 +11,7 @@ Example: #these run parallel to items, and can be given as hashes 'row_labels' => \@row_labels, #required 'colors' => \@colors, #required + 'bgcolors' => \@bgcolors, #optional 'graph_labels' => \@graph_labels, #defaults to row_labels 'links' => \@links, #optional @@ -22,7 +23,7 @@ Example: #optional 'nototal' => 1, - 'graph_type' => 'LinesPoints', + 'graph_type' => 'LinesPoints', #can be 'none' for no graph 'bottom_total' => 1, 'sprintf' => '%u', #sprintf format, overrides default %.2f 'disable_money' => 1, @@ -231,7 +232,8 @@ any delimiter and linked from the elements in @data. % foreach my $row ( @items ) { % #make a style % my $color = shift @{ $opt{'colors'} }; -% push @styles, ".i$i { text-align: right; color: #$color; }"; +% my $bgcolor = $opt{'bgcolors'} ? (shift @{ $opt{'bgcolors'} }) : 'ffffff'; +% push @styles, ".i$i { text-align: right; color: #$color; background: #$bgcolor; }"; % #create the data row % my $links = shift @{$opt{'links'}} || ['']; % my $link_prefix = shift @$links; diff --git a/httemplate/search/report_tax_sales.cgi b/httemplate/search/report_tax_sales.cgi deleted file mode 100644 index 5c531c343..000000000 --- a/httemplate/search/report_tax_sales.cgi +++ /dev/null @@ -1,158 +0,0 @@ - -<% include('/graph/elements/report.html', - 'title' => 'Monthly Sales Tax Report', - 'items' => \@row_labels, - 'data' => \@rowdata, - 'row_labels' => \@row_labels, - 'colors' => [], - 'col_labels' => \@col_labels, - ) %> - -<%init> - -die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); - -# validate cgi input -my $start_month = $cgi->param('start_month'); -die "Bad start month" unless $start_month =~ /^\d*$/; -my $start_year = $cgi->param('start_year'); -die "Bad start year" unless $start_year =~ /^\d*$/; -my $end_month = $cgi->param('end_month'); -die "Bad end month" unless $end_month =~ /^\d*$/; -my $end_year = $cgi->param('end_year'); -die "Bad end year" unless $end_year =~ /^\d*$/; -die "End year before start year" if $end_year < $start_year; -die "End month before start month" if ($start_year == $end_year) && ($end_month < $start_month); -my $country = $cgi->param('country'); -die "Bad country code" unless $country =~ /^\w\w$/; - -# Data structure for building final table -# row order will be calculated separately -# -# $data->{$rowlabel} = \@rowvalues -# - -my $data = {}; - -### Calculate package values - -my @pkg_class = qsearch('pkg_class'); -my @pkg_classnum = map { $_->classnum } @pkg_class; -unshift(@pkg_classnum,0); -my @pkg_classname = map { $_->classname } @pkg_class; -unshift(@pkg_classname,'(empty class)'); - -# some false laziness with graph/elements/monthly.html -my %reportopts = ( - 'items' => [ qw( cust_bill_pkg cust_bill_pkg_credits ) ], - 'cross_params' => [ map { [ 'classnum', $_ ] } @pkg_classnum ], - 'start_month' => $start_month, - 'start_year' => $start_year, - 'end_month' => $end_month, - 'end_year' => $end_year, -); -my $pkgreport = new FS::Report::Table::Monthly(%reportopts); -my $pkgdata = $pkgreport->data; - -# assuming every month/year combo is included in results, -# just use this list for the final table -my @col_labels = @{$pkgdata->{'label'}}; - -# unpack report data into a more manageable format -foreach my $item ( qw( invoiced credited ) ) { # invoiced, credited - my $itemref = shift @{$pkgdata->{'data'}}; - foreach my $label (@{$pkgdata->{'label'}}) { # month/year - my $labelref = shift @$itemref; - foreach my $classname (@pkg_classname) { # pkg class - my $value = shift @$labelref; - my $rowlabel = $classname.' '.$item; - $data->{$rowlabel} ||= []; - push(@{$data->{$rowlabel}},$value); - } - } -} - -### Calculate tax values - -# false laziness w report_tax.html, put this in FS::Report::Tax? -my $sth = dbh->prepare('SELECT DISTINCT(COALESCE(taxname, \'Tax\')) FROM cust_main_county'); -$sth->execute or die $sth->errstr; -my @taxnames = map { $_->[0] } @{ $sth->fetchall_arrayref }; -$sth->finish; - -# get DateTime objects for start & end -my $startdate = DateTime->new( - year => $start_year, - month => $start_month, - day => 1 - ); -my $enddate = DateTime->new( - year => $end_year, - month => $end_month, - day => 1 - ); -$enddate->add( months => 1 )->subtract( seconds => 1 ); # the last second of the month - -# common to all tax reports -my %params = ( - 'country' => $country, - 'credit_date' => 'cust_bill', -); - -# run a report for each month, for each tax -my $countdate = $startdate->clone; -while ($countdate < $enddate) { - - # set report start date, iterate to end of this month, set report end date - $params{'beginning'} = $countdate->epoch; - $params{'ending'} = $countdate->add( months => 1 )->subtract( seconds => 1 )->epoch; - - # run a report for each tax name - foreach my $taxname (@taxnames) { - $params{'taxname'} = $taxname; - my $report = FS::Report::Tax->report_internal(%params); - - # extract totals from report, kinda awkward - my $pkgclass = ''; # this will get more complicated if we breakdown by pkgclass - my @values = (0,0); - if ($report->{'total'}->{$pkgclass}) { - my %totals = map { $$_[0] => $$_[2] } @{$report->{'total'}->{$pkgclass}}; - $values[0] = $totals{'tax'}; - $values[1] = $totals{'credit'}; - } - - # treat each tax class like it's an additional pkg class - foreach my $item ( qw ( invoiced credited ) ) { - my $rowlabel = $taxname . ' ' . $item; - my $value = shift @values; - $data->{$rowlabel} ||= []; - push(@{$data->{$rowlabel}},$value); - } - - } - - # iterate to next month - $countdate->add( seconds => 1 ); -} - -# put the data in the order we want it -my @row_labels; -my @rowdata; -foreach my $classname (@pkg_classname,@taxnames) { - my @classlabels = (); - my @classdata = (); - my $hasdata = 0; - foreach my $item ( qw( invoiced credited ) ) { - my $rowlabel = $classname . ' ' . $item; - my $rowdata = $data->{$rowlabel}; - $hasdata = 1 if grep { $_ } @$rowdata; - push(@classlabels,$rowlabel); - push(@classdata,$rowdata); - } - next unless $hasdata; # don't include class if it has no data in time range - push(@row_labels,@classlabels); - push(@rowdata,@classdata); -} - - diff --git a/httemplate/search/report_tax_sales.html b/httemplate/search/report_tax_sales.html deleted file mode 100755 index 374a15601..000000000 --- a/httemplate/search/report_tax_sales.html +++ /dev/null @@ -1,35 +0,0 @@ -<% include('/elements/header.html', 'Monthly Sales Tax Report' ) %> - -
- - - - <% include('/elements/tr-select-from_to.html') %> - - <% include('/elements/tr-select.html', - 'label' => 'Country', - 'field' => 'country', - 'options' => \@countries, - 'curr_value' => ($conf->config('countrydefault') || 'US'), - ) %> - -
- -
- -
- -<% include('/elements/footer.html') %> -<%init> - -die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); - -my $conf = new FS::Conf; - -# false laziness w report_tax.html, put this in FS::Report::Tax? -my $sth = dbh->prepare('SELECT DISTINCT(country) FROM cust_location'); -$sth->execute or die $sth->errstr; -my @countries = map { $_->[0] } @{ $sth->fetchall_arrayref }; - - diff --git a/httemplate/search/tax_sales.cgi b/httemplate/search/tax_sales.cgi new file mode 100644 index 000000000..4b28c934a --- /dev/null +++ b/httemplate/search/tax_sales.cgi @@ -0,0 +1,172 @@ + +<% include('/graph/elements/report.html', + 'title' => 'Monthly Sales and Taxes Report', + 'items' => \@row_labels, + 'data' => \@rowdata, + 'row_labels' => \@row_labels, + 'colors' => \@rowcolors, + 'bgcolors' => \@rowbgcolors, + 'col_labels' => \@col_labels, + 'graph_type' => 'none', + ) %> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +# validate cgi input +my $start_month = $cgi->param('start_month'); +die "Bad start month" unless $start_month =~ /^\d*$/; +my $start_year = $cgi->param('start_year'); +die "Bad start year" unless $start_year =~ /^\d*$/; +my $end_month = $cgi->param('end_month'); +die "Bad end month" unless $end_month =~ /^\d*$/; +my $end_year = $cgi->param('end_year'); +die "Bad end year" unless $end_year =~ /^\d*$/; +die "End year before start year" if $end_year < $start_year; +die "End month before start month" if ($start_year == $end_year) && ($end_month < $start_month); +my $country = $cgi->param('country'); +die "Bad country code" unless $country =~ /^\w\w$/; + +# Data structure for building final table +# row order will be calculated separately +# +# $data->{$rowlabel} = \@rowvalues +# + +my $data = {}; + +### Calculate package values + +my @pkg_class = qsearch('pkg_class'); +my @pkg_classnum = map { $_->classnum } @pkg_class; +unshift(@pkg_classnum,0); +my @pkg_classname = map { $_->classname } @pkg_class; +unshift(@pkg_classname,'(empty class)'); + +# some false laziness with graph/elements/monthly.html +my %reportopts = ( + 'items' => [ qw( cust_bill_pkg cust_bill_pkg_credits ) ], + 'cross_params' => [ map { [ 'classnum', $_ ] } @pkg_classnum ], + 'start_month' => $start_month, + 'start_year' => $start_year, + 'end_month' => $end_month, + 'end_year' => $end_year, +); +my $pkgreport = new FS::Report::Table::Monthly(%reportopts); +my $pkgdata = $pkgreport->data; + +# assuming every month/year combo is included in results, +# just use this list for the final table +my @col_labels = @{$pkgdata->{'label'}}; + +# unpack report data into a more manageable format +foreach my $item ( qw( invoiced credited ) ) { # invoiced, credited + my $itemref = shift @{$pkgdata->{'data'}}; + foreach my $label (@{$pkgdata->{'label'}}) { # month/year + my $labelref = shift @$itemref; + foreach my $classname (@pkg_classname) { # pkg class + my $value = shift @$labelref; + my $rowlabel = $classname.' '.$item; + $data->{$rowlabel} ||= []; + push(@{$data->{$rowlabel}},$value); + } + } +} + +### Calculate tax values + +# false laziness w report_tax.html, put this in FS::Report::Tax? +my $sth = dbh->prepare('SELECT DISTINCT(COALESCE(taxname, \'Tax\')) FROM cust_main_county'); +$sth->execute or die $sth->errstr; +my @taxnames = map { $_->[0] } @{ $sth->fetchall_arrayref }; +$sth->finish; + +# get DateTime objects for start & end +my $startdate = DateTime->new( + year => $start_year, + month => $start_month, + day => 1 + ); +my $enddate = DateTime->new( + year => $end_year, + month => $end_month, + day => 1 + ); +$enddate->add( months => 1 )->subtract( seconds => 1 ); # the last second of the month + +# common to all tax reports +my %params = ( + 'country' => $country, + 'credit_date' => 'cust_bill', +); + +# run a report for each month, for each tax +my $countdate = $startdate->clone; +while ($countdate < $enddate) { + + # set report start date, iterate to end of this month, set report end date + $params{'beginning'} = $countdate->epoch; + $params{'ending'} = $countdate->add( months => 1 )->subtract( seconds => 1 )->epoch; + + # run a report for each tax name + foreach my $taxname (@taxnames) { + $params{'taxname'} = $taxname; + my $report = FS::Report::Tax->report_internal(%params); + + # extract totals from report, kinda awkward + my $pkgclass = ''; # this will get more complicated if we breakdown by pkgclass + my @values = (0,0); + if ($report->{'total'}->{$pkgclass}) { + my %totals = map { $$_[0] => $$_[2] } @{$report->{'total'}->{$pkgclass}}; + $values[0] = $totals{'tax'}; + $values[1] = $totals{'credit'}; + } + + # treat each tax class like it's an additional pkg class + foreach my $item ( qw ( invoiced credited ) ) { + my $rowlabel = $taxname . ' ' . $item; + my $value = shift @values; + $data->{$rowlabel} ||= []; + push(@{$data->{$rowlabel}},$value); + } + + } + + # iterate to next month + $countdate->add( seconds => 1 ); +} + +# put the data in the order we want it +my @row_labels; +my @rowdata; +my @rowcolors; +my @rowbgcolors; +my $pkgcount = 0; #for colors +foreach my $classname (@pkg_classname,@taxnames) { + my $istax = ($pkgcount++ < @pkg_classname) ? 0 : 1; + my @classlabels = (); + my @classdata = (); + my @classcolors = (); + my @classbgcolors = (); + my $hasdata = 0; + foreach my $item ( qw( invoiced credited ) ) { + my $rowlabel = $classname . ' ' . $item; + my $rowdata = $data->{$rowlabel}; + my $rowcolor = $istax ? '0000ff' : '000000'; + my $rowbgcolor = ($item eq 'credited') ? 'cccccc' : 'ffffff'; + $hasdata = 1 if grep { $_ } @$rowdata; + push(@classlabels,$rowlabel); + push(@classdata,$rowdata); + push(@classcolors,$rowcolor); + push(@classbgcolors,$rowbgcolor); + } + next unless $hasdata; # don't include class if it has no data in time range + push(@row_labels,@classlabels); + push(@rowdata,@classdata); + push(@rowcolors,@classcolors); + push(@rowbgcolors,@classbgcolors); +} + + diff --git a/httemplate/search/tax_sales.html b/httemplate/search/tax_sales.html new file mode 100755 index 000000000..61cf86e2e --- /dev/null +++ b/httemplate/search/tax_sales.html @@ -0,0 +1,35 @@ +<% include('/elements/header.html', 'Monthly Sales and Taxes Report' ) %> + +
+ + + + <% include('/elements/tr-select-from_to.html') %> + + <% include('/elements/tr-select.html', + 'label' => 'Country', + 'field' => 'country', + 'options' => \@countries, + 'curr_value' => ($conf->config('countrydefault') || 'US'), + ) %> + +
+ +
+ +
+ +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; + +# false laziness w report_tax.html, put this in FS::Report::Tax? +my $sth = dbh->prepare('SELECT DISTINCT(country) FROM cust_location'); +$sth->execute or die $sth->errstr; +my @countries = map { $_->[0] } @{ $sth->fetchall_arrayref }; + + -- cgit v1.2.1 From bb70ee978959d0489e6a049aedbb18250ee2e594 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 3 Sep 2015 13:42:45 -0700 Subject: quick payment entry: fix preloading of rows when some of them contain bad amounts, #15861 --- httemplate/misc/batch-cust_pay.html | 97 +++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 26 deletions(-) diff --git a/httemplate/misc/batch-cust_pay.html b/httemplate/misc/batch-cust_pay.html index 9f2540cc7..197ade14f 100644 --- a/httemplate/misc/batch-cust_pay.html +++ b/httemplate/misc/batch-cust_pay.html @@ -101,6 +101,10 @@ function select_discount_term(row) { var invoices_for_row = new Object; +var preloading = 0; // the number of preloading threads currently running + +// callback from toggle_application_row: we've received a list of +// the customer's open invoices. store them. function update_invoices(rownum, invoices) { invoices_for_row[rownum] = new Object; // only called before create_application_row @@ -113,6 +117,12 @@ function toggle_application_row(ev, next) { if (!next) next = function(){}; //optional continuation var rownum = this.getAttribute('rownum'); if ( this.checked ) { + // the user has opted to apply the payment to specific invoices. + // - lock the customer + // - fetch the list of open invoices + // - create a row to select an invoice + // - then optionally call "next", with this as the invocant + // and the rownum as argument; we use this to preload rows. var custnum = document.getElementById('custnum'+rownum).value; if (!custnum) return; lock_payment_row(rownum, true); @@ -124,6 +134,9 @@ function toggle_application_row(ev, next) { } ); } else { + // the user has opted not to do that. + // - remove all application rows + // - unlock the customer var row = document.getElementById('row'+rownum); var table_rows = row.parentNode.rows; for (i = row.sectionRowIndex; i < table_rows.count; i++) { @@ -183,6 +196,16 @@ function amount_unapplied(rownum) { var change_app_amount; +// the user has chosen an invoice. the previously chosen invoice is still +// in curr_invoice +// - if there is a value there, put it back on the invoices_for_row list for +// this customer. +// - then _remove_ the newly chosen invoice from that list. +// - find the "owed" element for this application row and set its value to the +// amount owed on that invoice. +// - find the "amount" element for this application row and set its value to +// either "owed" or the remaining payment amount, whichever is less. +// - call change_app_amount() on that element. function choose_app_invnum() { var rownum = this.getAttribute('rownum'); var appnum = this.getAttribute('appnum'); @@ -210,8 +233,10 @@ function choose_app_invnum() { } } +// the invoice selector has gained focus. clear its list of options, and +// replace them with the list of open invoices (from invoices_for_row). +// if there's already a selected invoice, prepend that to the list. function focus_app_invnum() { -% # invoice numbers just display as invoice numbers var rownum = this.getAttribute('rownum'); var add_opt = function(obj, value, label) { var o = document.createElement('OPTION'); @@ -233,14 +258,15 @@ function focus_app_invnum() { } } +// an application amount has been changed. if there's any unapplied payment +// amount, and any remaining invoices_for_row, add a blank application row. +// (but don't do this while preloading; it will unconditionally add enough +// rows to show all the attempted applications) function change_app_amount() { var rownum = this.getAttribute('rownum'); var appnum = this.getAttribute('appnum'); -%# maybe some kind of warning if amount_unapplied < 0? -%# only spawn a new application row if there are open invoices left, -%# and this is the highest-numbered application row for the customer, -%# and the sum of the applied amounts is < the amount of the payment, - if ( Object.keys(invoices_for_row[rownum]).length > 0 + if ( preloading == 0 + && Object.keys(invoices_for_row[rownum]).length > 0 && !document.getElementById( 'row'+rownum+'.'+(parseInt(appnum) + 1) ) && amount_unapplied(rownum) > 0 ) { @@ -248,6 +274,9 @@ function change_app_amount() { } } +// we're creating a payment application row. +// create the following elements: , s, "Apply to invoice" caption, +// invnum selector, "owed" display, amount input box, delete button. function create_application_row(rownum, appnum) { var payment_row = document.getElementById('row'+rownum); var tr_app = document.createElement('TR'); @@ -341,29 +370,45 @@ function preload() { var enable = document.getElementById('enable_app'+rownum); enable.checked = true; var preload_row = function(r) {//continuation from toggle_application_row - for (appnum=0; appnum < row_obj[r].length; appnum++) { - this_app = row_obj[r][appnum]; - var x = r + '.' + appnum; - //set invnum - var select_invnum = document.getElementById('invnum'+x); - focus_app_invnum.call(select_invnum); - for (i=0; i Date: Mon, 7 Sep 2015 17:40:25 -0700 Subject: "1 months", eww --- FS/FS/discount.pm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/FS/FS/discount.pm b/FS/FS/discount.pm index 361e0b4b2..e11335741 100644 --- a/FS/FS/discount.pm +++ b/FS/FS/discount.pm @@ -196,7 +196,13 @@ sub description { ( my $months = $self->months ) =~ s/\.0+$//; $months =~ s/(\.\d*[1-9])0+$/$1/; - $desc .= " for $months months" if $months; + if ($months) { + if ($months == 1) { + $desc .= " for 1 month"; + } else { + $desc .= " for $months months"; + } + } $desc .= ', applies to setup' if $self->setup; -- cgit v1.2.1 From 1813f9f4ff4d48ad6bf76d70c01edd67c5a4bfa4 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 7 Sep 2015 17:40:30 -0700 Subject: rework discount calculation, #20613, #19173, #19354 --- FS/FS/part_pkg/discount_Mixin.pm | 196 +++++++++++++++++++++++---------------- 1 file changed, 115 insertions(+), 81 deletions(-) diff --git a/FS/FS/part_pkg/discount_Mixin.pm b/FS/FS/part_pkg/discount_Mixin.pm index abde93f8f..5de7d8ea5 100644 --- a/FS/FS/part_pkg/discount_Mixin.pm +++ b/FS/FS/part_pkg/discount_Mixin.pm @@ -28,11 +28,15 @@ sub calc_recur { =head METHODS -=item calc_discount +=item calc_discount CUST_PKG, SDATE, DETAILS_ARRAYREF, PARAM_HASHREF -Takes all the arguments of calc_recur. Calculates and returns the amount -by which to reduce the recurring fee; also increments months used on the -discount. +Takes all the arguments of calc_recur. Calculates and returns the amount +by which to reduce the charge; also increments months used on the discount. + +If there is a setup fee, this will be called once with 'setup_charge' => the +setup fee amount (and should return the discount to be applied to the setup +charge, if any), and again without it (for the recurring fee discount). +PARAM_HASHREF carries over between the two invocations. =cut @@ -40,9 +44,6 @@ sub calc_discount { my($self, $cust_pkg, $sdate, $details, $param ) = @_; my $conf = new FS::Conf; - my $br_permonth = $self->base_recur_permonth($cust_pkg, $sdate); - $br_permonth += $param->{'override_charges'} if $param->{'override_charges'}; - my $br = $self->base_recur($cust_pkg, $sdate); $br += $param->{'override_charges'} * ($cust_pkg->part_pkg->freq || 0) if $param->{'override_charges'}; @@ -83,52 +84,125 @@ sub calc_discount { my $discount_left; my $discount = $cust_pkg_discount->discount; #UI enforces one or the other (for now? probably for good) + # $chg_months: the number of months we are charging recur for + # $months: $chg_months or the months left on the discount, whchever is less + + my $chg_months = $cust_pkg->part_pkg->freq || 1; + if ( defined($param->{'months'}) ) { # then override + $chg_months = $param->{'months'}; + } + + my $months = $chg_months; + if ( $discount->months ) { + $months = min( $chg_months, + $discount->months - $cust_pkg_discount->months_used ); + } + + # $amount is now the (estimated) discount amount on the recurring charge. + # if it's a percent discount, that's base recur * percentage. + my $amount = 0; - $amount += $discount->amount - if $cust_pkg->pkgpart == $param->{'real_pkgpart'}; - $amount += sprintf('%.2f', $discount->percent * $br_permonth / 100 ); # FIXME: should this use $br / $freq to avoid rounding errors? - my $chg_months = defined($param->{'months'}) ? - $param->{'months'} : - $cust_pkg->part_pkg->freq; - - my $months = $discount->months - ? min( $chg_months, - $discount->months - $cust_pkg_discount->months_used ) - : $chg_months; if (defined $param->{'setup_charge'}) { + + # we are calculating the setup discount. + # if this discount doesn't apply to setup fees, skip it. + # if it's a percent discount, set $amount = percent * setup_charge. + # if it's a flat amount discount for one month: + # - if the discount amount > setup_charge, then set it to setup_charge, + # and set 'discount_left_recur' to the difference. + # - otherwise set it to just the discount amount. + # if it's a flat amount discount for other than one month: + # - skip the discount. unsure, leaving it alone for now. + next unless $discount->setup; + $months = 0; # never count a setup discount as a month of discount + # (the recur discount in the same month should do it) + if ( $discount->percent > 0 ) { - $amount = sprintf('%.2f', $discount->percent * $param->{'setup_charge'} / 100 ); - $months = 1; - } elsif ( $discount->amount > 0 && $discount->months == 1) { - $discount_left = $param->{'setup_charge'} - $discount->amount; - $amount = $param->{'setup_charge'} if $discount_left < 0; - $amount = $discount->amount if $discount_left >= 0; - $months = 1; - + $amount = $discount->percent * $param->{'setup_charge'} / 100; + } elsif ( $discount->amount > 0 && ($discount->months || 0) == 1) { + # apply the discount amount, up to a maximum of the setup charge + $amount = min($discount->amount, $param->{'setup_charge'}); + $discount_left = sprintf('%.2f', $discount->amount - $amount); # transfer remainder of discount, if any, to recur - $param->{'discount_left_recur'}{$discount->discountnum} = - 0 - $discount_left if $discount_left < 0; + $param->{'discount_left_recur'}{$discount->discountnum} = $discount_left; } else { + # I guess we don't allow multiple-month flat amount discounts to + # apply to setup? next; } - } elsif ( defined $param->{'discount_left_recur'}{$discount->discountnum} - && $param->{'discount_left_recur'}{$discount->discountnum} > 0 - ) { - # use up transferred remainder of discount from setup + + } else { + + # we are calculating a recurring fee discount. estimate the recurring + # fee: + # XXX it would be more accurate for calc_recur to just _tell us_ what + # it's going to charge + + my $recur_charge = $br * ($cust_pkg->quantity || 1) * $chg_months / $self->freq; + # round this, because the real recur charge is rounded + $recur_charge = sprintf('%.2f', $recur_charge); + + # if it's a percentage discount, calculate it based on that estimate. + # otherwise use the flat amount. + + if ( $discount->percent > 0 ) { + $amount = $recur_charge * $discount->percent / 100; + } elsif ( $discount->amount > 0 + and $cust_pkg->pkgpart == $param->{'real_pkgpart'} ) { + $amount = $discount->amount * $months; + } + + if ( exists $param->{'discount_left_recur'}{$discount->discountnum} ) { + # there is a discount_left_recur entry for this discountnum, so this + # is the second (recur) pass on the discount. use up transferred + # remainder of discount from setup. + # + # note that discount_left_recur can now be zero. $amount = $param->{'discount_left_recur'}{$discount->discountnum}; $param->{'discount_left_recur'}{$discount->discountnum} = 0; - $months = 1; - } elsif ( $discount->setup - && $discount->months == 1 - && $discount->amount > 0 - ) { - next; - } + $months = 1; # XXX really? not $chg_months? + } + #elsif ( $discount->setup + # && ($discount->months || 0) == 1 + # && $discount->amount > 0 + # ) { + # next; + # + # RT #11512: bugfix to prevent applying flat discount to both setup + # and recur. The original implementation ignored discount_left_recur + # if it was zero, so if the setup fee used up the entire flat + # discount, the recurring charge would get to use the entire flat + # discount also. This bugfix was a kludge. Instead, we now allow + # discount_left_recur to be zero in that case, and then the available + # recur discount is zero. + #} + + # transfer remainder of discount, if any, to setup + # this is used when the recur phase wants to add a setup fee + # (prorate_defer_bill): the "discount_left_setup" amount will + # be subtracted in _make_lines. + if ( $discount->setup && $discount->amount > 0 + && ($discount->months || 0) != 1 + ) + { + # $amount is no longer permonth at this point! correct. very good. + $discount_left = $amount - $recur_charge; # backward, as above + if ( $discount_left > 0 ) { + $amount = $recur_charge; + $param->{'discount_left_setup'}{$discount->discountnum} = + 0 - $discount_left; + } + } - if ( ! defined $param->{'setup_charge'} ) { + # cap the discount amount at the recur charge + $amount = min($amount, $recur_charge); + + # if this is the base pkgpart, schedule increment_months_used to run at + # the end of billing. (addon packages haven't been calculated yet, so + # don't let the discount expire during the billing process. RT#17045.) if ( $cust_pkg->pkgpart == $param->{'real_pkgpart'} ) { push @{ $param->{precommit_hooks} }, sub { my $error = $cust_pkg_discount->increment_months_used($months); @@ -136,62 +210,22 @@ sub calc_discount { }; } - $amount = min($amount * $months, $br); } $amount = sprintf('%.2f', $amount + 0.00000001 ); #so 1.005 rounds to 1.01 next unless $amount > 0; - # transfer remainder of discount, if any, to setup - if ( $discount->setup && $discount->amount > 0 - && (!$discount->months || $discount->months != 1) - && !defined $param->{'setup_charge'} - ) - { - $discount_left = $br_permonth - $amount; # FIXME: $amount is no longer permonth at this point! - if ( $discount_left < 0 ) { - $amount = $br_permonth; # FIXME: seems like this should *= $months - $param->{'discount_left_setup'}{$discount->discountnum} = - 0 - $discount_left; - } - } - #record details in cust_bill_pkg_discount my $cust_bill_pkg_discount = new FS::cust_bill_pkg_discount { 'pkgdiscountnum' => $cust_pkg_discount->pkgdiscountnum, 'amount' => $amount, 'months' => $months, + # XXX should have a 'setuprecur' }; push @{ $param->{'discounts'} }, $cust_bill_pkg_discount; $tot_discount += $amount; - #add details on discount to invoice - # no longer! this is now done during rendering based on the existence - # of the cust_bill_pkg_discount record - # - #my $money_char = $conf->config('money_char') || '$'; - #$months = sprintf('%.2f', $months) if $months =~ /\./; - - #my $d = 'Includes '; - #my $format; - - #if ( $months eq '1' ) { - # $d .= "discount of $money_char$amount"; - # $d .= " each" if $cust_pkg->quantity > 1; - # $format = 'Undiscounted amount: %s%.2f'; - #} else { - # $d .= 'setup ' if defined $param->{'setup_charge'}; - # $d .= 'discount of '. $discount->description_short; - # $d .= " for $months months" - # unless defined $param->{'setup_charge'}; - # $d .= ": $money_char$amount" if $discount->percent; - # $format = 'Undiscounted monthly amount: %s%.2f'; - #} - - #push @$details, $d; - #push @$details, sprintf( $format, $money_char, $br_permonth ); - } sprintf('%.2f', $tot_discount); -- cgit v1.2.1 From a4245323f5dad7d8e9d19f2be4e3f5b036274276 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 9 Sep 2015 00:18:16 -0700 Subject: fix weird behavior with bundles where base package has zero recur, #32460 --- FS/FS/cust_main/Billing.pm | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 5c10c639a..2d7b690df 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -880,6 +880,7 @@ sub bill { } #discard bundled packages of 0 value +# XXX we should reconsider whether we even need this sub _omit_zero_value_bundles { my @in = @_; @@ -888,11 +889,20 @@ sub _omit_zero_value_bundles { my $discount_show_always = $conf->exists('discount-show-always'); my $show_this = 0; + # Sort @in the same way we do during invoice rendering, so we can identify + # bundles. See FS::Template_Mixin::_items_nontax. + @in = sort { $a->pkgnum <=> $b->pkgnum or + $a->sdate <=> $b->sdate or + ($a->pkgpart_override ? 0 : -1) or + ($b->pkgpart_override ? 0 : 1) or + $b->hidden cmp $a->hidden or + $a->pkgpart_override <=> $b->pkgpart_override + } @in; + # this is a pack-and-deliver pattern. every time there's a cust_bill_pkg # _without_ pkgpart_override, that's the start of the new bundle. if there's # an existing bundle, and it contains a nonzero amount (or a zero amount # that's displayable anyway), push all line items in the bundle. - foreach my $cust_bill_pkg ( @in ) { if (scalar(@bundle) and !$cust_bill_pkg->pkgpart_override) { -- cgit v1.2.1 From e75f08d15a25977a68948399350f6e13303d560a Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 9 Sep 2015 19:18:45 -0700 Subject: fix itemdesc filter, #38020, from #26770 --- httemplate/search/cust_bill_pkg.cgi | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index 4fb9b662b..278382feb 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -601,6 +601,16 @@ if ( $cgi->param('nottax') ) { } } + # itemdesc, for breakdown from the vendor tax report + # (this is definitely used) + if ( $cgi->param('itemdesc') ) { + if ( $cgi->param('itemdesc') eq 'Tax' ) { + push @where, "($itemdesc = 'Tax' OR $itemdesc is null)"; + } else { + push @where, "$itemdesc = ". dbh->quote($cgi->param('itemdesc')); + } + } + } else { # the internal-tax case my $tax_select = 'SELECT tax.billpkgnum, SUM(tax.amount) as tax_total'; -- cgit v1.2.1 From 34eed3944f194c9a1c20420801c05fd134cf6147 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 9 Sep 2015 19:20:18 -0700 Subject: fix bad internal link --- FS/FS/tax_rate.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm index 1094968c6..c6fe243d4 100644 --- a/FS/FS/tax_rate.pm +++ b/FS/FS/tax_rate.pm @@ -2328,7 +2328,7 @@ EOF my $dropstring = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/report.'; $reportname =~ s/^$dropstring//; - my $reporturl = "%%%ROOTURL%%%/misc/queued_report?report=$reportname"; + my $reporturl = "%%%ROOTURL%%%/misc/queued_report.html?report=$reportname"; die "view\n"; } -- cgit v1.2.1 From 2602299667c4c548ba52d6cc301082bd34a3c707 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Wed, 9 Sep 2015 21:22:04 -0700 Subject: torrux 4.x compat --- torrus/perllib/Torrus/Renderer/Freeside.pm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/torrus/perllib/Torrus/Renderer/Freeside.pm b/torrus/perllib/Torrus/Renderer/Freeside.pm index 9a7c023be..d97920e15 100644 --- a/torrus/perllib/Torrus/Renderer/Freeside.pm +++ b/torrus/perllib/Torrus/Renderer/Freeside.pm @@ -3,7 +3,7 @@ package Torrus::Renderer::Freeside; use strict; use warnings; use base 'Torrus::Freeside'; -use FS::UID qw(cgisuidsetup); +use FS::UID qw(setcgi adminsuidsetup); use FS::TicketSystem; our $cgi = ''; @@ -15,7 +15,9 @@ sub freesideSetup { $cgi = $Torrus::CGI::q; - cgisuidsetup($cgi); + setcgi($cgi); + + adminsuidsetup; FS::TicketSystem->init(); } -- cgit v1.2.1 From 6d72fff8b002cafb90395159c8bea3717d620dba Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Wed, 9 Sep 2015 22:12:53 -0700 Subject: default torrus to Pg --- torrus/configs/torrus-siteconfig.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torrus/configs/torrus-siteconfig.pl b/torrus/configs/torrus-siteconfig.pl index 504c0f37d..0ecec1c92 100644 --- a/torrus/configs/torrus-siteconfig.pl +++ b/torrus/configs/torrus-siteconfig.pl @@ -25,7 +25,7 @@ $Torrus::Freeside::FSURL = '%%%FREESIDE_URL%%%'; $Torrus::Renderer::displayReports = 1; push (@Torrus::Collector::loadModules, 'Torrus::Collector::ExternalStorage'); $Torrus::SQL::connections{'Default'}{'dsn'} = - 'DBI:mysql:dbname=freeside'; #XXX sub in DATASOURCE + 'DBI:Pg:dbname=freeside'; #XXX sub in DATASOURCE $Torrus::SQL::connections{'Default'}{'username'} = 'freeside'; #DB_USER $Torrus::SQL::connections{'Default'}{'password'} = ''; #DB_PASSWORD -- cgit v1.2.1 From 8d76610b4329151076c7e2b81891406df36d18bb Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 10 Sep 2015 12:23:31 -0700 Subject: correct regex for importing with id 50, RT#37472 --- FS/FS/cdr/aapt.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/cdr/aapt.pm b/FS/FS/cdr/aapt.pm index 3c4964317..934608c72 100644 --- a/FS/FS/cdr/aapt.pm +++ b/FS/FS/cdr/aapt.pm @@ -77,7 +77,7 @@ my %UNIT_SCALE = ( #Table 2.1.4 'calltypenum', # usage ID (CUSG) sub { # ID type my ($cdr, $data, $conf, $param) = @_; - if ($data !~ /(1|50)/) { + if ($data !~ /^(1|50)$/) { warn "AAPT: service ID type is not telephone number.\n"; $param->{skiprow} = 1; } -- cgit v1.2.1 From 9896275b96170e2a97e313e64c7aa5bfaf12a087 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 10 Sep 2015 16:27:43 -0700 Subject: improve usage_class_summary with number of calls and total minutes, #37122 --- FS/FS/Template_Mixin.pm | 2 +- FS/FS/cust_bill.pm | 12 +++++++++--- conf/invoice_html | 42 +++++++++++++++++++++++++++++++++--------- conf/invoice_latex | 36 +++++++++++++++++++++++++++++++++++- 4 files changed, 78 insertions(+), 14 deletions(-) diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index e9b60a86c..206c03cde 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -1519,7 +1519,7 @@ sub print_generic { # usage subtotals if ( $conf->exists('usage_class_summary') and $self->can('_items_usage_class_summary') ) { - my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function); + my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function, 'money_char' => $other_money_char); if ( @usage_subtotals ) { unshift @sections, $usage_subtotals[0]->{section}; # do not summarize unshift @detail_items, @usage_subtotals; diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 410fa7bf7..09424ba52 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -2661,10 +2661,12 @@ sub _items_usage_class_summary { my %opt = @_; my $escape = $opt{escape} || sub { $_[0] }; + my $money_char = $opt{money_char}; my $invnum = $self->invnum; my @classes = qsearch({ 'table' => 'usage_class', - 'select' => 'classnum, classname, SUM(amount) AS amount', + 'select' => 'classnum, classname, SUM(amount) AS amount,'. + ' COUNT(*) AS calls, SUM(duration) AS duration', 'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' . ' LEFT JOIN cust_bill_pkg USING (billpkgnum)', 'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum". @@ -2675,17 +2677,21 @@ sub _items_usage_class_summary { my @l; my $section = { description => &{$escape}($self->mt('Usage Summary')), - no_subtotal => 1, usage_section => 1, + subtotal => 0, }; foreach my $class (@classes) { + $section->{subtotal} += $class->get('amount'); push @l, { 'description' => &{$escape}($class->classname), - 'amount' => sprintf('%.2f', $class->amount), + 'amount' => $money_char.sprintf('%.2f', $class->get('amount')), + 'quantity' => $class->get('calls'), + 'duration' => $class->get('duration'), 'usage_classnum' => $class->classnum, 'section' => $section, }; } + $section->{subtotal} = $money_char.sprintf('%.2f', $section->{subtotal}); return @l; } diff --git a/conf/invoice_html b/conf/invoice_html index dfd87c79b..e1af70703 100644 --- a/conf/invoice_html +++ b/conf/invoice_html @@ -176,15 +176,28 @@ $OUT .= $header; $columncount = scalar(my @array = split /<\/th>' . emt('Description') . ''. - ( $unitprices - ? '' . emt('Unit Price') . ''. - '' . emt('Quantity') . '' - : '' ). - '' . emt('Amount') . ''; + my @headings = ( '', 'Description', 'Amount' ); + my @aligns = ( 'center', 'left', 'right' ); + if ( $unitprices ) { + splice @headings, 2, 0, 'Unit Price', 'Quantity'; + splice @aligns, 2, 0, 'right', 'right'; + } + if ( $section->{usage_section} ) { + @headings = ( '', 'Description', 'Calls', 'Duration', 'Amount' ); + @aligns = ( '', 'left', 'right', 'right', 'right' ); + $columncount = 5; + } + + while ( @headings ) { + my $heading = shift @headings; + $heading = emt($heading) if $heading; + my $align = shift @aligns; + $OUT .= ' + ' . $heading . ''; + } } - $OUT .= ''; + + $OUT .= ''; my $lastref = 0; foreach my $line ( @@ -197,6 +210,17 @@ if ( $section->{description_generator} ) { $OUT .= ' + + ' . $line->{'description'} . ' + ' . $line->{'quantity'} . ' + ' . $minutes . 'm ' . $seconds . 's' . ' + ' . $line->{'amount'} . ' + '; } else { my $class = 'invoice_desc_more'; if ( ($line->{'ref'} || 0) ne $lastref ) { @@ -257,7 +281,7 @@ } $OUT .= ''; } - } + } # if !$section->{summarized} if ($section->{'posttotal'}) { $OUT .= ''; $OUT .= diff --git a/conf/invoice_latex b/conf/invoice_latex index c7c696b5d..4df858d22 100644 --- a/conf/invoice_latex +++ b/conf/invoice_latex @@ -196,7 +196,20 @@ \hline } -% ...description... +\newcommand{\FSusagehead}{ + \hline + \rule{0pt}{2.5ex} + \makebox[1.4cm]{} & + \multicolumn{4}{l}{ + \makebox[\FSdescriptionlength][l]{\textbf{[@-- emt('Description') --@]}} + } & + \textbf{~~[@-- emt('Calls') --@]} & + \textbf{~~[@-- emt('Duration') --@]} & + \textbf{~~[@-- emt('Amount') --@]} \\ + \hline +} + +}% ...description... \newcommand{\FSdesc}[5]{ \multicolumn{1}{c}{\rule{0pt}{2.5ex}\textbf{#1}} & \multicolumn{[@-- $unitprices ? '4' : '6' --@]}{l}{\textbf{#2}} & @@ -217,6 +230,15 @@ & \multicolumn{6}{l}{#1} & #2\\ } +% ...usage class summary +\newcommand{\FSusagedesc}[4]{ + \multicolumn{1}{c}{\rule{0pt}{2.5ex}} & + \multicolumn{4}{l}{\textbf{#1}} & + \multicolumn{1}{r}{\textbf{#2}} & + \multicolumn{1}{r}{\textbf{#3}} & + \multicolumn{1}{r}{\textbf{#4}} + \\ +} \begin{document} % Headers and footers defined for the first page @@ -289,6 +311,8 @@ $OUT .= '}\\\\'; if ($section->{header_generator}) { $OUT .= &{$section->{header_generator}}(); + } elsif ( $section->{usage_section} ) { + $OUT .= '\FSusagehead'; } else { $OUT .= '\FShead'; } @@ -296,6 +320,8 @@ $OUT .= '\multicolumn{7}{r}{\rule{0pt}{2.5ex}'.emt('Continued from previous page').'}\\\\'; if ($section->{header_generator}) { $OUT .= &{$section->{header_generator}}(); + } elsif ( $section->{usage_section} ) { + $OUT .= '\FSusagehead'; } else { $OUT .= '\FShead'; } @@ -343,6 +369,14 @@ $OUT .= "\\hline\n" if (($line->{'ref'} || 0) ne $lastref); if ($section->{description_generator}) { $OUT .= &{$section->{description_generator}}($line); + } elsif ($section->{usage_section}) { + my $minutes = sprintf('%d', $line->{'duration'} / 60); + my $seconds = $line->{'duration'} % 60; + $OUT .= '\FSusagedesc + {' . $line->{'description'} . '} + {' . $line->{'quantity'} . '} + {' . $minutes . 'm ' . $seconds . 's' . '} + {' . '\dollar' . $line->{'amount'} . '}'; } else { $OUT .= '\FSdesc'. '{}'. -- cgit v1.2.1 From ffa5a5ba5f1e75ad5e7545f5c8e303e079014f2f Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 10 Sep 2015 16:36:28 -0700 Subject: fix double "$", #37122 --- conf/invoice_latex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/invoice_latex b/conf/invoice_latex index 4df858d22..ceff84bd2 100644 --- a/conf/invoice_latex +++ b/conf/invoice_latex @@ -376,7 +376,7 @@ {' . $line->{'description'} . '} {' . $line->{'quantity'} . '} {' . $minutes . 'm ' . $seconds . 's' . '} - {' . '\dollar' . $line->{'amount'} . '}'; + {' . $line->{'amount'} . '}'; } else { $OUT .= '\FSdesc'. '{}'. -- cgit v1.2.1 From 8eeac13d3a8b231efd786eca0555087de5dbb17e Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Thu, 10 Sep 2015 21:33:37 -0500 Subject: RT#33410: Package GB add-ons --- FS/FS/cust_pkg.pm | 12 ++- FS/FS/cust_pkg_usageprice.pm | 5 + FS/FS/part_pkg/sqlradacct_hour.pm | 15 ++- FS/FS/part_pkg_usageprice.pm | 44 ++++++++- httemplate/edit/process/change-cust_pkg.html | 39 +++++++- httemplate/elements/cust_pkg_usageprice.html | 9 +- httemplate/elements/order_pkg.js | 68 +------------ httemplate/misc/change_pkg.cgi | 23 ++++- httemplate/misc/cust_pkg_usageprice.html | 121 +++++++++++++++++++++++ httemplate/misc/order_pkg.html | 21 +--- httemplate/misc/xmlhttp-part_pkg_usageprice.html | 18 +++- 11 files changed, 267 insertions(+), 108 deletions(-) create mode 100644 httemplate/misc/cust_pkg_usageprice.html diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index c5a3d2e58..0ef7aa0fa 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -2296,9 +2296,15 @@ sub change { } } - # transfer usage pricing add-ons, if we're not changing pkgpart - if ( $same_pkgpart ) { - foreach my $old_cust_pkg_usageprice ($self->cust_pkg_usageprice) { + # transfer usage pricing add-ons, if we're not changing pkgpart or if they were specified + if ( $same_pkgpart || $opt->{'cust_pkg_usageprice'}) { + my @old_cust_pkg_usageprice; + if ($opt->{'cust_pkg_usageprice'}) { + @old_cust_pkg_usageprice = @{ $opt->{'cust_pkg_usageprice'} }; + } else { + @old_cust_pkg_usageprice = $self->cust_pkg_usageprice; + } + foreach my $old_cust_pkg_usageprice (@old_cust_pkg_usageprice) { my $new_cust_pkg_usageprice = new FS::cust_pkg_usageprice { 'pkgnum' => $cust_pkg->pkgnum, 'usagepricepart' => $old_cust_pkg_usageprice->usagepricepart, diff --git a/FS/FS/cust_pkg_usageprice.pm b/FS/FS/cust_pkg_usageprice.pm index 5b6b18c70..29e627882 100644 --- a/FS/FS/cust_pkg_usageprice.pm +++ b/FS/FS/cust_pkg_usageprice.pm @@ -163,6 +163,11 @@ sub apply { #this has no multiplication involved, its just a set only #} elsif ( $target eq 'svc_conferencing.confqualitynum' ) { + + } elsif ( $target eq 'sqlradacct_hour.recur_included_total' ) { + + $error = "Cannot call apply on target $target"; + } if ( $error ) { diff --git a/FS/FS/part_pkg/sqlradacct_hour.pm b/FS/FS/part_pkg/sqlradacct_hour.pm index 79e64fbab..206bea0d0 100644 --- a/FS/FS/part_pkg/sqlradacct_hour.pm +++ b/FS/FS/part_pkg/sqlradacct_hour.pm @@ -105,7 +105,18 @@ sub calc_recur { 'AcctOutputOctets' ) / BU; - my $total = $input + $output - $self->option('recur_included_total'); + my $included_total = $self->option('recur_included_total') || 0; + my $addoncharge = 0; + foreach my $cust_pkg_usageprice ($cust_pkg->cust_pkg_usageprice) { + my $part_pkg_usageprice = $cust_pkg_usageprice->part_pkg_usageprice; + $included_total += $cust_pkg_usageprice->quantity * $part_pkg_usageprice->amount; + $addoncharge += $cust_pkg_usageprice->price; + } + my $raw_total = $input + $output; + push(@$details,sprintf( "%.3f %ss included, %.3f %ss used", $included_total, BA, $raw_total, BA )) + if $included_total; + + my $total = $input + $output - $included_total; $total = 0 if $total < 0; $input = $input - $self->option('recur_included_input'); $input = 0 if $input < 0; @@ -153,7 +164,7 @@ sub calc_recur { sprintf('%.1f', $hours). " hours: $hourscharge"; } - my $charges = $hourscharge + $inputcharge + $outputcharge + $totalcharge; + my $charges = $hourscharge + $inputcharge + $outputcharge + $totalcharge + $addoncharge; if ( $self->option('global_cap') && $charges > $self->option('global_cap') ) { $charges = $self->option('global_cap'); push @$details, "Usage charges capped at: $charges"; diff --git a/FS/FS/part_pkg_usageprice.pm b/FS/FS/part_pkg_usageprice.pm index 9c3b1be87..b33904e9e 100644 --- a/FS/FS/part_pkg_usageprice.pm +++ b/FS/FS/part_pkg_usageprice.pm @@ -111,13 +111,46 @@ sub check { || $self->ut_enum('action', [ 'increment', 'set' ]) || $self->ut_enum('target', [ 'svc_acct.totalbytes', 'svc_acct.seconds', 'svc_conferencing.participants', - 'svc_conferencing.confqualitynum' +# 'svc_conferencing.confqualitynum', + 'sqlradacct_hour.recur_included_total' ] ) || $self->ut_text('amount') ; return $error if $error; + #Check target against package + #UI doesn't currently prevent these from happing, + #so keep error messages informative + my $part_pkg = $self->part_pkg; + my $target = $self->target; + my $label = $self->target_info->{'label'}; + my ($needs_svcdb, $needs_plan); + if ( $target =~ /^svc_acct.(\w+)$/ ) { + $needs_svcdb = 'svc_acct'; + } elsif ( $target eq 'svc_conferencing.participants' ) { + $needs_svcdb = 'svc_conferencing'; + } elsif ( $target =~ /^sqlradacct_hour.(\w+)$/ ) { + $needs_plan = 'sqlradacct_hour'; + } + if ($needs_svcdb) { + my $has_svcdb = 0; + foreach my $pkg_svc ($part_pkg->pkg_svc) { + next unless $pkg_svc->quantity; + my $svcdb = $pkg_svc->part_svc->svcdb; + $has_svcdb = 1 + if $svcdb eq $needs_svcdb; + last if $has_svcdb; + } + return "Usage pricing add-on \'$label\' can only be used on packages with at least one $needs_svcdb service.\n" + unless $has_svcdb; + } + if ($needs_plan) { + return "Usage pricing add-on \'$label\' can only be used on packages with pricing plan \'" . + FS::part_pkg->plan_info->{$needs_plan}->{'shortname'} . "\'\n" + unless ref($part_pkg) eq 'FS::part_pkg::' . $needs_plan; + } + $self->SUPER::check; } @@ -147,10 +180,10 @@ sub targets { #'svc_acct.totalbytes' => { label => 'Megabytes', # multiplier => 1048576, # }, - 'svc_acct.totalbytes' => { label => 'Gigabytes', + 'svc_acct.totalbytes' => { label => 'Total Gigabytes', multiplier => 1073741824, }, - 'svc_acct.seconds' => { label => 'Hours', + 'svc_acct.seconds' => { label => 'Total Hours', multiplier => 3600, }, 'svc_conferencing.participants' => { label => 'Conference Participants', @@ -160,6 +193,11 @@ sub targets { # and then value comes from a select, not a text field # 'svc_conferencing.confqualitynum' => { label => 'Conference Quality', # }, + + # this bypasses usual apply methods, handled entirely in sqlradacct_hour + 'sqlradacct_hour.recur_included_total' => { label => 'Included Gigabytes', + multiplier => 1 }, #recur_included_total is stored in GB + ; \%targets; diff --git a/httemplate/edit/process/change-cust_pkg.html b/httemplate/edit/process/change-cust_pkg.html index 046a9795c..308ea8ffd 100644 --- a/httemplate/edit/process/change-cust_pkg.html +++ b/httemplate/edit/process/change-cust_pkg.html @@ -59,6 +59,40 @@ unless ($error) { $error = $cust_pkg->change_later(\%change); } } else { + + # for now, can't change usageprice with change_later + my @old_cust_pkg_usageprice = $cust_pkg->cust_pkg_usageprice; + + # build new usageprice array + # false laziness with /edit/process/quick-cust_pkg.cgi + my @cust_pkg_usageprice = (); + foreach my $quantity_param ( grep { $cgi->param($_) && $cgi->param($_) > 0 } + grep /^usagepricenum(\d+)_quantity$/, + $cgi->param + ) + { + $quantity_param =~ /^usagepricenum(\d+)_quantity$/ or die 'unpossible'; + my $num = $1; + push @cust_pkg_usageprice, new FS::cust_pkg_usageprice { + usagepricepart => scalar($cgi->param("usagepricenum${num}_usagepricepart")), + quantity => scalar($cgi->param($quantity_param)), + }; + } + + # Need to figure out if usagepricepart quantities changed + my %oldup = map { $_->usagepricepart, $_->quantity } @old_cust_pkg_usageprice; + my %newup = map { $_->usagepricepart, $_->quantity } @cust_pkg_usageprice; + my $usagechanged = 0; + foreach my $up (keys %oldup) { + last if $usagechanged; + $usagechanged = 1 unless $oldup{$up} == $newup{$up}; + } + foreach my $up (keys %newup) { + last if $usagechanged; + $usagechanged = 1 unless $oldup{$up} == $newup{$up}; + } + $change{'cust_pkg_usageprice'} = \@cust_pkg_usageprice; + # special case: if there's a package change scheduled, and it matches # the parameters the user requested this time, then change to the existing # future package. @@ -68,12 +102,13 @@ unless ($error) { $change_to->pkgpart == $change{'pkgpart'} and $change_to->locationnum == $change{'locationnum'} and $change_to->quantity == $change{'quantity'} and - $change_to->contract_end == $change{'contract_end'} + $change_to->contract_end == $change{'contract_end'} and + !$usagechanged ) { %change = ( 'cust_pkg' => $change_to ); } } - + # do a package change right now my $pkg_or_error = $cust_pkg->change( \%change ); $error = ref($pkg_or_error) ? '' : $pkg_or_error; diff --git a/httemplate/elements/cust_pkg_usageprice.html b/httemplate/elements/cust_pkg_usageprice.html index 729099320..74b7842be 100644 --- a/httemplate/elements/cust_pkg_usageprice.html +++ b/httemplate/elements/cust_pkg_usageprice.html @@ -23,15 +23,16 @@ > % my $info = $part_pkg_usageprice->target_info; % my $amount = $part_pkg_usageprice->amount / ($info->{multiplier}||1); - % for (1..100) { #100? arbitrary. - % } @@ -42,8 +43,6 @@ % } <%init> -#my $targets = FS::part_pkg_usageprice->targets; - my( %opt ) = @_; my $conf = new FS::Conf; diff --git a/httemplate/elements/order_pkg.js b/httemplate/elements/order_pkg.js index 3586a54cb..a850d2193 100644 --- a/httemplate/elements/order_pkg.js +++ b/httemplate/elements/order_pkg.js @@ -1,10 +1,12 @@ function pkg_changed () { var form = document.OrderPkgForm; var discountnum = form.discountnum; + var opt = form.pkgpart.options[form.pkgpart.selectedIndex]; + + usageprice_pkg_changed( opt.value ); if ( form.pkgpart.selectedIndex > 0 ) { - var opt = form.pkgpart.options[form.pkgpart.selectedIndex]; var date_button = document.getElementById('start_date_button'); var date_button_disabled = document.getElementById('start_date_disabled'); var date_text = document.getElementById('start_date_text'); @@ -68,78 +70,14 @@ function pkg_changed () { } } - get_part_pkg_usageprice( opt.value, update_part_pkg_usageprice ); - } else { form.submitButton.disabled = true; if ( discountnum ) { form.discountnum.disabled = true; } discountnum_changed(form.discountnum); } -} - -function update_part_pkg_usageprice(part_pkg_usageprice) { - - var table = document.getElementById('cust_pkg_usageprice_table'); - - // black the current usage price rows - for ( var r = table.rows.length - 1; r >= 0; r-- ) { - table.deleteRow(r); - } - - // add the new usage price rows - var rownum = 0; - var usagepriceArray = eval('(' + part_pkg_usageprice + ')' ); - for ( var s = 0; s < usagepriceArray.length; s=s+2 ) { - //surely this should be some kind of JSON structure - var html = usagepriceArray[s+0]; - var javascript = usagepriceArray[s+1]; - - // a lot like ("inspiried by") edit/elements/edit.html function spawn_<%$field%> - - // XXX evaluate the javascript - //if (window.ActiveXObject) { - // window.execScript(newfunc); - //} else { /* (window.XMLHttpRequest) */ - // //window.eval(newfunc); - // setTimeout(newfunc, 0); - //} - - var row = table.insertRow(rownum++); - - //var label_cell = document.createElement('TD'); - - //label_cell.id = '<% $field %>_label' + <%$field%>_fieldnum; - - //label_cell.style.textAlign = "right"; - //label_cell.style.verticalAlign = "top"; - //label_cell.style.borderTop = "1px solid black"; - //label_cell.style.paddingTop = "5px"; - - //label_cell.innerHTML = '<% $label %>'; - - //row.appendChild(label_cell); - - var widget_cell = document.createElement('TD'); - - //widget_cell.style.borderTop = "1px solid black"; - widget_cell.style.paddingTop = "3px"; - widget_cell.colSpan = "2"; - - widget_cell.innerHTML = html; - - row.appendChild(widget_cell); - - } - - if ( rownum > 0 ) { - document.getElementById('cust_pkg_usageprice_title').style.display = ''; - } else { - document.getElementById('cust_pkg_usageprice_title').style.display = 'none'; - } } - function standardize_new_location() { var form = document.OrderPkgForm; var loc = form.locationnum; diff --git a/httemplate/misc/change_pkg.cgi b/httemplate/misc/change_pkg.cgi index e74747e82..b562d24cd 100755 --- a/httemplate/misc/change_pkg.cgi +++ b/httemplate/misc/change_pkg.cgi @@ -19,13 +19,13 @@ <& /elements/tr-select-cust-part_pkg.html, 'pre_label' => emt('New'), - 'curr_value' => scalar($cgi->param('pkgpart')), + 'curr_value' => scalar($cgi->param('pkgpart')) || $cust_pkg->pkgpart, 'classnum' => $part_pkg->classnum, 'cust_main' => $cust_main, &> <& /elements/tr-input-pkg-quantity.html, - 'curr_value' => $cust_pkg->quantity + 'curr_value' => scalar($cgi->param('quantity')) || $cust_pkg->quantity &> % if ($use_contract_end) { @@ -39,6 +39,11 @@
+<% include('/misc/cust_pkg_usageprice.html', + 'pkgpart' => (scalar($cgi->param('pkgpart')) || $cust_pkg->pkgpart), + 'pkgnum' => ($cust_pkg->change_to_pkgnum || $pkgnum), + ) %> +
<% mt('Change') |h %> <% ntable('#cccccc') %> @@ -49,8 +54,16 @@ document.getElementById('start_date_text').disabled = !enable; document.getElementById('start_date_button').style.display = (enable ? '' : 'none'); - document.getElementById('start_date_button_disabled').style.display = - (enable ? 'none' : ''); + if (document.getElementById('start_date_button_disabled')) { // does this ever exist anymore? + document.getElementById('start_date_button_disabled').style.display = + (enable ? 'none' : ''); + } + if (enable) { + usageprice_disable(1); + } else { + var form = document.OrderPkgForm; + usageprice_disable(0,form.pkgpart.options[form.pkgpart.selectedIndex].value); + } } <&| /elements/onload.js &> delay_changed(); @@ -96,7 +109,7 @@ TYPE = "button" VALUE = "<% mt("Change package") |h %>" onClick = "this.disabled=true; standardize_new_location();" - <% scalar($cgi->param('pkgpart')) ? '' : 'DISABLED' %> + <% #scalar($cgi->param('pkgpart')) ? '' : 'DISABLED' %> > diff --git a/httemplate/misc/cust_pkg_usageprice.html b/httemplate/misc/cust_pkg_usageprice.html new file mode 100644 index 000000000..f2e0f57e6 --- /dev/null +++ b/httemplate/misc/cust_pkg_usageprice.html @@ -0,0 +1,121 @@ +<%doc> +Sets up the xmlhttp, javascript and initial (empty) table for selecting cust_pkg_usageprice. +Available values are based on pkgpart, and can be updated when changing pkgpart +by passing the new pkgpart to the following javascript: + + usageprice_pkg_changed( pkgpart, pkgnum ) + +The pkgnum input is optional, and will be used to set initial selected values. + +If pkgpart is passed as an option to this element, will run usageprice_pkg_changed +once to initialize table; pkgnum can be passed as an option along with this. + +You can disable usageprice selection temporarily (remove the fields from the form) +with the javascript usageprice_disable(1), and restore it with usageprice_disable(0,pkgnum). +While disabled, calling usageprice_pkg_changed will have no effect. + + +<& /elements/xmlhttp.html, + 'url' => $p.'misc/xmlhttp-part_pkg_usageprice.html', + 'subs' => [ 'get_part_pkg_usageprice' ], +&> + + + + +
+ + + +<%init> +my %opt = @_; + + + diff --git a/httemplate/misc/order_pkg.html b/httemplate/misc/order_pkg.html index 799165fe0..cb2bd4832 100644 --- a/httemplate/misc/order_pkg.html +++ b/httemplate/misc/order_pkg.html @@ -5,11 +5,6 @@ } &> -<& /elements/xmlhttp.html, - 'url' => $p.'misc/xmlhttp-part_pkg_usageprice.html', - 'subs' => [ 'get_part_pkg_usageprice' ], -&> - <& /elements/init_calendar.html &> @@ -121,19 +116,9 @@
-%#so: -%# - hide until you selecdt a pacakge with add-ons -%# -lookup and display the available add-ons when -%# -add them to the (recur if there is one, otherwise setup) price and display magically like processing fees do on edit/cust_pay.cgi - -%# better label? - - - -
+<% include('/misc/cust_pkg_usageprice.html', + 'pkgpart' => $pkgpart + ) %>
% my $discount_cust_pkg = $curuser->access_right('Discount customer package'); diff --git a/httemplate/misc/xmlhttp-part_pkg_usageprice.html b/httemplate/misc/xmlhttp-part_pkg_usageprice.html index d4e2d8469..9decdeff9 100644 --- a/httemplate/misc/xmlhttp-part_pkg_usageprice.html +++ b/httemplate/misc/xmlhttp-part_pkg_usageprice.html @@ -1,24 +1,32 @@ <% encode_json( \@return ) %>\ <%init> -my( $pkgpart ) = $cgi->param('arg'); +my( $pkgpart, $pkgnum ) = $cgi->param('arg'); #could worry about agent-virting this so you can't see the add-on pricing of # other agents, but not a real-world big worry my $part_pkg = qsearchs( 'part_pkg', { pkgpart=>$pkgpart } ); +my %curr_quantity; +if ($pkgnum) { + my $cust_pkg = qsearchs( 'cust_pkg', { pkgnum=>$pkgnum } ); + %curr_quantity = map { $_->usagepricepart, $_->quantity } $cust_pkg->cust_pkg_usageprice; +} + my $num = 0; -my @return = map { +# probably don't need to be returning js_only anymore? +my @return = ($pkgpart, map { + my $usagepricepart = $_->usagepricepart; my @inc = ('/elements/cust_pkg_usageprice.html', - 'usagepricepart' => $_->usagepricepart, + 'usagepricepart' => $usagepricepart, ); - + push(@inc,'curr_quantity',($curr_quantity{$usagepricepart} || 0)); ( include(@inc, field=>'usagepricenum'.$num, html_only=>1 ), include(@inc, field=>'usagepricenum'.$num++, js_only=>1 ), ); } - $part_pkg->part_pkg_usageprice; + $part_pkg->part_pkg_usageprice); -- cgit v1.2.1 From 58d388d4354f18477e9863ad95c78e73772d1768 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 14 Sep 2015 13:01:38 -0700 Subject: don't show disabled towers in dropdown, RT#37488 --- httemplate/elements/select-tower_sector.html | 1 + 1 file changed, 1 insertion(+) diff --git a/httemplate/elements/select-tower_sector.html b/httemplate/elements/select-tower_sector.html index 59b016359..458bcddcf 100644 --- a/httemplate/elements/select-tower_sector.html +++ b/httemplate/elements/select-tower_sector.html @@ -11,6 +11,7 @@ <& /elements/select-table.html, table => 'tower', name_col => 'towername', + hashref => { 'disabled' => '', }, id => 'towernum', field => 'towernum', onchange => 'change_towernum(this.value);', -- cgit v1.2.1 From 8927f51c07dce9dd1090d4825f6f3b5bffb6d4e5 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 15 Sep 2015 11:38:39 -0700 Subject: add cancel option, RT#38145 --- bin/cust_main-bulk_change | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/bin/cust_main-bulk_change b/bin/cust_main-bulk_change index 32a6d7bd6..e03901272 100755 --- a/bin/cust_main-bulk_change +++ b/bin/cust_main-bulk_change @@ -1,7 +1,7 @@ #!/usr/bin/perl use strict; -use vars qw( $opt_a $opt_p $opt_t $opt_k ); +use vars qw( $opt_a $opt_p $opt_t $opt_k $opt_c ); use Getopt::Std; use FS::UID qw(adminsuidsetup); use FS::Record qw(qsearch qsearchs); @@ -9,7 +9,7 @@ use FS::cust_main; use FS::cust_tag; use FS::cust_pkg; -getopts('a:p:t:k:'); +getopts('a:p:t:k:c:'); my $user = shift or &usage; adminsuidsetup $user; @@ -64,6 +64,11 @@ while () { } } + if ( $opt_c ) { + my @error = $cust_main->cancel( 'reason' => $opt_c ); + die join(' / ', @error). "\n" if @error; + } + } sub usage { @@ -76,7 +81,7 @@ cust_main-bulk_change =head1 SYNOPSIS - cust_main-bulk_change [ -a agentnum ] [ -p NEW_PAYBY ] [ -t tagnum ] [ -k old_pkgpart:new_pkgpart,... ] username . Multiple entries can be comma-separated. +-c: Cancel customer + user: Employee username =head1 BUGS -- cgit v1.2.1 From 62bfe13409a16e04599f52edb3ce00ee3031a0f9 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 16 Sep 2015 01:59:07 -0500 Subject: RT#36813: Monthly Recurring Total [change_to_pkgnum handling] --- httemplate/view/cust_main/billing.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html index c031ce929..f7ea68a65 100644 --- a/httemplate/view/cust_main/billing.html +++ b/httemplate/view/cust_main/billing.html @@ -44,11 +44,13 @@ % 'hashref' => { 'custnum' => $cust_main->custnum, }, % 'extra_sql' => 'AND ( cancel IS NULL OR cancel = 0 ) % AND freq = '. dbh->quote($freq), +% 'order_by' => 'ORDER BY pkgnum', # to ensure old pkgs come before change_to_pkg % }) or next; % % my $freq_pretty = $cust_pkg[0]->part_pkg->freq_pretty; % % my $amount = 0; +% my $skip_pkg = {}; % foreach my $cust_pkg (@cust_pkg) { % my $part_pkg = $cust_pkg->part_pkg; % next if $cust_pkg->susp @@ -57,6 +59,15 @@ % || $cust_pkg->option('no_suspend_bill') % ); % +% #pkg change handling +% next if $skip_pkg->{$cust_pkg->pkgnum}; +% if ($cust_pkg->change_to_pkgnum) { +% #if change is on or before next bill date, use new pkg +% next if $cust_pkg->expire <= $cust_pkg->bill; +% #if change is after next bill date, use old (this) pkg +% $skip_pkg->{$cust_pkg->change_to_pkgnum} = 1; +% } +% % my $pkg_amount = 0; % % #add recurring amounts for this package and its billing add-ons -- cgit v1.2.1 From 7ee96ef046f8e5167a4dda7c4322485549ec29c3 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Thu, 17 Sep 2015 19:27:10 -0500 Subject: RT#35197: Apply changes button in Edit rate plan screen clears the global default --- FS/FS/rate.pm | 7 +++++-- httemplate/edit/process/rate_detail.html | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/FS/FS/rate.pm b/FS/FS/rate.pm index 8ee9a83be..03dde041b 100644 --- a/FS/FS/rate.pm +++ b/FS/FS/rate.pm @@ -469,8 +469,11 @@ sub process { warn "$rate replacing $old (". $param->{'ratenum'}. ")\n" if $DEBUG; my @param = ( 'job'=>$job ); - push @param, 'rate_detail'=>\@rate_detail - unless $param->{'preserve_rate_detail'}; + if ($param->{'preserve_rate_detail'}) { + $rate->default_detailnum($old->default_detailnum); + } else { + push @param, 'rate_detail'=>\@rate_detail; + } $error = $rate->replace( $old, @param ); diff --git a/httemplate/edit/process/rate_detail.html b/httemplate/edit/process/rate_detail.html index 0709d5079..f8a744418 100644 --- a/httemplate/edit/process/rate_detail.html +++ b/httemplate/edit/process/rate_detail.html @@ -12,7 +12,6 @@ die "access denied" my $set_default_detail = sub { my ($cgi, $rate_detail) = @_; -warn Dumper $rate_detail; if (!$rate_detail->dest_regionnum) { # then this is a global default rate my $rate = $rate_detail->rate; -- cgit v1.2.1 From 5aafaf2e4efe3a7b57ec9afd563ced32e70b581f Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 18 Sep 2015 00:40:10 -0700 Subject: fix HTML --- httemplate/elements/tr-td-label.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httemplate/elements/tr-td-label.html b/httemplate/elements/tr-td-label.html index 78690f1fb..f7067221b 100644 --- a/httemplate/elements/tr-td-label.html +++ b/httemplate/elements/tr-td-label.html @@ -9,7 +9,7 @@ Actually $label VALIGN = "<% $opt{'valign'} || 'top' %>" STYLE = "<% $style %>" ID = "<% $opt{label_id} || $opt{id}. '_label0' %>" - ><% $required %><% $opt{label} %> + ><% $required %><% $opt{label} %> <%init> -- cgit v1.2.1 From c0c5709fb022b83a482d0b35f7094505766d5868 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 18 Sep 2015 10:18:43 -0700 Subject: send commission reports by email, #33101 --- FS/FS/AccessRight.pm | 1 + FS/FS/Mason.pm | 4 + FS/FS/Schema.pm | 31 ++ FS/FS/cust_bill_pkg.pm | 10 +- FS/FS/cust_msg.pm | 1 + FS/FS/msg_template/email.pm | 12 + FS/FS/msg_template/http.pm | 4 + FS/FS/report_batch.pm | 321 +++++++++++++++++++++ FS/MANIFEST | 8 + FS/t/report_batch.t | 5 + .../elements/popup_link-send_report_batch.html | 28 ++ httemplate/misc/process/send-report.html | 7 + httemplate/misc/send-report.html | 166 +++++++++++ httemplate/search/cust_msg.html | 3 +- httemplate/search/report_sales_commission_pkg.html | 10 + httemplate/search/sales_commission_pkg.html | 7 + 16 files changed, 615 insertions(+), 3 deletions(-) create mode 100644 FS/FS/report_batch.pm create mode 100644 FS/t/report_batch.t create mode 100644 httemplate/elements/popup_link-send_report_batch.html create mode 100644 httemplate/misc/process/send-report.html create mode 100644 httemplate/misc/send-report.html diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 9274ad858..53c7cf622 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -288,6 +288,7 @@ tie my %rights, 'Tie::IxHash', 'Billing event reports', 'Receivables report', 'Financial reports', + { rightname=>'Send reports to customers', global=>1 }, { rightname=> 'List inventory', global=>1 }, { rightname=>'View email logs', global=>1 }, { rightname=>'View system logs' }, diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index ae4f07cdb..98a75c8df 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -405,6 +405,10 @@ if ( -e $addl_handler_use_file ) { use FS::cust_pkg_reason_fee; use FS::part_svc_link; use FS::access_user_log; + use FS::report_batch; + use FS::report_batch; + use FS::report_batch; + use FS::report_batch; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 12211d1e1..85fbbeb8a 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -7124,6 +7124,37 @@ sub tables_hashref { ], }, + 'report_batch' => { + 'columns' => [ + 'reportbatchnum', 'serial', '', '', '', '', + 'reportname', 'varchar', '', 255, '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'send_date', @date_type, '', '', + 'sdate', @date_type, '', '', + 'edate', @date_type, '', '', + 'usernum', 'int', 'NULL', '', '', '', + 'msgnum', 'int', 'NULL', '', '', '', + # add report params here as necessary + ], + 'primary_key' => 'reportbatchnum', + 'unique' => [], + 'index' => [], + 'foreign_keys' => [ + { columns => [ 'agentnum' ], + table => 'agent', + references => [ 'agentnum' ], + }, + { columns => [ 'usernum' ], + table => 'access_user', + references => [ 'usernum' ], + }, + { columns => [ 'msgnum' ], + table => 'msg_template', + references => [ 'msgnum' ], + }, + ], + }, + # name type nullability length default local #'new_table' => { diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 8233ce0d6..178042666 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -1124,7 +1124,10 @@ sub tax_locationnum { if ( $self->pkgnum ) { # normal sales return $self->cust_pkg->tax_locationnum; } elsif ( $self->feepart ) { # fees - return $self->cust_bill->cust_main->ship_locationnum; + my $custnum = $self->fee_origin->custnum; + if ( $custnum ) { + return FS::cust_main->by_key($custnum)->ship_locationnum; + } } else { # taxes return ''; } @@ -1135,7 +1138,10 @@ sub tax_location { if ( $self->pkgnum ) { # normal sales return $self->cust_pkg->tax_location; } elsif ( $self->feepart ) { # fees - return $self->cust_bill->cust_main->ship_location; + my $custnum = $self->fee_origin->custnum; + if ( $custnum ) { + return FS::cust_main->by_key($custnum)->ship_location; + } } else { # taxes return; } diff --git a/FS/FS/cust_msg.pm b/FS/FS/cust_msg.pm index db026808c..27272b8a3 100644 --- a/FS/FS/cust_msg.pm +++ b/FS/FS/cust_msg.pm @@ -148,6 +148,7 @@ sub check { 'invoice', 'receipt', 'admin', + 'report', ]) ; return $error if $error; diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index e6d5a5a99..377dbb17b 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -200,6 +200,12 @@ A hash reference of additional substitutions A string identifying the kind of message this is. Currently can be "invoice", "receipt", "admin", or null. Expand this list as necessary. +=item override_content + +A string to use as the HTML body; if specified, replaces the entire +body of the message. This should be used ONLY by L and may +go away in the future. + =back =cut @@ -265,6 +271,12 @@ sub prepare { warn "$me filling in body template\n" if $DEBUG; $body = $body_tmpl->fill_in( HASH => $hashref ); + # override $body if requested + if ( $opt{'override_content'} ) { + warn "$me overriding template body with requested content" if $DEBUG; + $body = $opt{'override_content'}; + } + ### # and email ### diff --git a/FS/FS/msg_template/http.pm b/FS/FS/msg_template/http.pm index 51dfcffc2..a2b0986ea 100644 --- a/FS/FS/msg_template/http.pm +++ b/FS/FS/msg_template/http.pm @@ -59,6 +59,10 @@ sub prepare { %$document, %$hashref }; + # put override content _somewhere_ so it can be used + if ( $opt{'override_content'} ) { + $document{'content'} = $opt{'override_content'}; + } my $request_content = $json->encode($document); warn "$me ".$self->prepare_url."\n" if $DEBUG; diff --git a/FS/FS/report_batch.pm b/FS/FS/report_batch.pm new file mode 100644 index 000000000..64412dfba --- /dev/null +++ b/FS/FS/report_batch.pm @@ -0,0 +1,321 @@ +package FS::report_batch; +use base qw( FS::Record ); + +use strict; +use FS::Record qw( qsearch qsearchs dbdef ); +use FS::msg_template; +use FS::cust_main; +use FS::Misc::DateTime qw(parse_datetime); +use FS::Mason qw(mason_interps); +use URI::Escape; +use HTML::Defang; + +our $DEBUG = 0; + +=head1 NAME + +FS::report_batch - Object methods for report_batch records + +=head1 SYNOPSIS + + use FS::report_batch; + + $record = new FS::report_batch \%hash; + $record = new FS::report_batch { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::report_batch object represents an order to send a batch of reports to +their respective customers or other contacts. FS::report_batch inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item reportbatchnum + +primary key + +=item reportname + +The name of the report, which will be the same as the file name (minus any +directory names). There's an enumerated set of these; you can't use just any +report. + +=item send_date + +The date the report was sent. + +=item agentnum + +The agentnum to limit the report to, if any. + +=item sdate + +The start date of the report period. + +=item edate + +The end date of the report period. + +=item usernum + +The user who ordered the report. + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new report batch. To add the record to the database, see L<"insert">. + +=cut + +sub table { 'report_batch'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=item delete + +Deletes this record from the database. + +=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. + +=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_numbern('reportbatchnum') + || $self->ut_text('reportname') + || $self->ut_numbern('agentnum') + || $self->ut_numbern('sdate') + || $self->ut_numbern('edate') + || $self->ut_numbern('usernum') + ; + return $error if $error; + + $self->set('send_date', time); + + $self->SUPER::check; +} + +=back + +=head1 SUBROUTINES + +=over 4 + +=item process_send_report JOB, PARAMS + +Takes a hash of PARAMS, determines all contacts who need to receive a report, +and sends it to them. On completion, creates and stores a report_batch record. +JOB is a queue job to receive status messages. + +PARAMS can include: + +- reportname: the name of the report (listed in the C<%sendable_reports> hash). +Required. +- msgnum: the L to use for this report. Currently the +content of the template is ignored, but the subject line and From/Bcc addresses +are still used. Required. +- agentnum: the agent to limit the report to. +- beginning, ending: the date range to run the report, as human-readable +dates (I unix timestamps). + +=cut + +# trying to keep this data-driven, with parameters that tell how the report is +# to be handled rather than callbacks. +# - path: where under the document root the report is located +# - domain: which table to query for objects on which the report is run. +# Each record in that table produces one report. +# - cust_main: the method on that object that returns its linked customer (to +# which the report will be sent). If the table has a 'custnum' field, this +# can be omitted. +our %sendable_reports = ( + 'sales_commission_pkg' => { + 'name' => 'Sales commission per package', + 'path' => '/search/sales_commission_pkg.html', + 'domain' => 'sales', + 'cust_main' => 'sales_cust_main', + }, +); + +sub process_send_report { + my $job = shift; + my $param = shift; + + my $msgnum = $param->{'msgnum'}; + my $template = FS::msg_template->by_key($msgnum) + or die "msg_template $msgnum not found\n"; + + my $reportname = $param->{'reportname'}; + my $info = $sendable_reports{$reportname} + or die "don't know how to send report '$reportname'\n"; + + # the most important thing: which report is it? + my $path = $info->{'path'}; + + # find all targets for the report: + # - those matching the agentnum if there is one. + # - those that aren't disabled. + my $domain = $info->{domain}; + my $dbt = dbdef->table($domain); + my $hashref = {}; + if ( $param->{'agentnum'} and $dbt->column('agentnum') ) { + $hashref->{'agentnum'} = $param->{'agentnum'}; + } + if ( $dbt->column('disabled') ) { + $hashref->{'disabled'} = ''; + } + my @records = qsearch($domain, $hashref); + my $num_targets = scalar(@records); + return if $num_targets == 0; + my $sent = 0; + + my $outbuf; + my ($fs_interp) = mason_interps('standalone', 'outbuf' => \$outbuf); + # if generating the report fails, we want to capture the error and exit, + # not send it. + $fs_interp->error_mode('fatal'); + $fs_interp->error_format('brief'); + + # we have to at least have an RT::Handle + require RT; + RT::LoadConfig(); + RT::Init(); + + # hold onto all the reports until we're sure they generated correctly. + my %cust_main; + my %report_content; + + # grab the stylesheet + ### note: if we need the ability to support different stylesheets, this + ### is the place to put it in + eval { $fs_interp->exec('/elements/freeside.css') }; + die "couldn't load stylesheet via Mason: $@\n" if $@; + my $stylesheet = $outbuf; + + my $pkey = $dbt->primary_key; + foreach my $rec (@records) { + + $job->update_statustext(int( 100 * $sent / $num_targets )); + my $pkey_val = $rec->get($pkey); # e.g. sales.salesnum + + # find the customer we're sending to, and their email + my $cust_main; + if ( $info->{'cust_main'} ) { + my $cust_method = $info->{'cust_main'}; + $cust_main = $rec->$cust_method; + } elsif ( $rec->custnum ) { + $cust_main = FS::cust_main->by_key($rec->custnum); + } else { + warn "$pkey = $pkey_val has no custnum; not sending report\n"; + next; + } + my @email = $cust_main->invoicing_list_emailonly; + if (!@email) { + warn "$pkey = $pkey_val has no email destinations\n" if $DEBUG; + next; + } + + # params to send to the report (as if from the user's browser) + my @report_param = ( # maybe list these in $info + agentnum => $param->{'agentnum'}, + beginning => $param->{'beginning'}, + ending => $param->{'ending'}, + $pkey => $pkey_val, + _type => 'html-print', + ); + + # build a query string + my $query_string = ''; + while (@report_param) { + $query_string .= uri_escape(shift @report_param) + . '=' + . uri_escape(shift @report_param); + $query_string .= ';' if @report_param; + } + warn "$path?$query_string\n\n" if $DEBUG; + + # run the report! + $FS::Mason::Request::QUERY_STRING = $query_string; + $FS::Mason::Request::FSURL = ''; + $outbuf = ''; + eval { $fs_interp->exec($path) }; + die "creating report for $pkey = $pkey_val: $@" if $@; + + # make some adjustments to the report + my $html_defang; + $html_defang = HTML::Defang->new( + url_callback => sub { 1 }, # strip all URLs (they're not accessible) + tags_to_callback => [ 'body' ], # and after the BODY tag... + tags_callback => sub { + my $isEndTag = $_[4]; + $html_defang->add_to_output("\n\n") + unless $isEndTag; + }, + ); + $outbuf = $html_defang->defang($outbuf); + + $cust_main{ $cust_main->custnum } = $cust_main; + $report_content{ $cust_main->custnum } = $outbuf; + } # foreach $rec + + $job->update_statustext('Sending reports...'); + foreach my $custnum (keys %cust_main) { + # create an email message with the report as body + # change this when backporting to 3.x + $template->send( + cust_main => $cust_main{$custnum}, + object => $cust_main{$custnum}, + msgtype => 'report', + override_content => $report_content{$custnum}, + ); + } + + my $self = FS::report_batch->new({ + reportname => $param->{'reportname'}, + agentnum => $param->{'agentnum'}, + sdate => parse_datetime($param->{'beginning'}), + edate => parse_datetime($param->{'ending'}), + usernum => $job->usernum, + msgnum => $param->{'msgnum'}, + }); + my $error = $self->insert; + warn "error recording completion of report: $error\n" if $error; + +} + +=head1 SEE ALSO + +L + +=cut + +1; + diff --git a/FS/MANIFEST b/FS/MANIFEST index 5b73b728c..5041ccd68 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -850,3 +850,11 @@ FS/part_svc_link.pm t/part_svc_link.t FS/access_user_log.pm t/access_user_log.t +FS/report_batch.pm +t/report_batch.t +FS/report_batch.pm +t/report_batch.t +FS/report_batch.pm +t/report_batch.t +FS/report_batch.pm +t/report_batch.t diff --git a/FS/t/report_batch.t b/FS/t/report_batch.t new file mode 100644 index 000000000..42fc8936a --- /dev/null +++ b/FS/t/report_batch.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::report_batch; +$loaded=1; +print "ok 1\n"; diff --git a/httemplate/elements/popup_link-send_report_batch.html b/httemplate/elements/popup_link-send_report_batch.html new file mode 100644 index 000000000..5f4471054 --- /dev/null +++ b/httemplate/elements/popup_link-send_report_batch.html @@ -0,0 +1,28 @@ +<%doc> + +Example: + +<& /elements/popup_link-send_report_batch.html, + reportname => 'sales_commission_pkg', + label => 'Click here to send reports by email', +&> + +<& /elements/popup_link.html, $params &>\ +<%init> + +my $params = { 'closetext' => emt('Close') }; + +if (ref($_[0]) eq 'HASH') { + $params = { %$params, %{ $_[0] } }; +} else { + $params = { %$params, @_ }; +} + +$params->{'label'} ||= emt('Send reports by email'); +$params->{'actionlabel'} ||= emt('Send reports'); +#$params->{'width'} ||= 350; +$params->{'height'} ||= 650; + +$params->{'action'} = $fsurl. 'misc/send-report.html?reportname='. $params->{'reportname'}; + + diff --git a/httemplate/misc/process/send-report.html b/httemplate/misc/process/send-report.html new file mode 100644 index 000000000..3bceebc0c --- /dev/null +++ b/httemplate/misc/process/send-report.html @@ -0,0 +1,7 @@ +<%init> +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Send reports to customers'); + +my $server = FS::UI::Web::JSRPC->new('FS::report_batch::process_send_report', $cgi); + +<% $server->process %> diff --git a/httemplate/misc/send-report.html b/httemplate/misc/send-report.html new file mode 100644 index 000000000..557767a57 --- /dev/null +++ b/httemplate/misc/send-report.html @@ -0,0 +1,166 @@ +<%doc> + +Parameters: + +- reportname: the report name (per FS::report_batch) + + +<& /elements/header-popup.html, { title => $report_info->{name} } &> + +
+ + + + + <& /elements/tr-select-agent.html &> + + <& /elements/tr-td-label.html, label => emt('Message template') &> + + + + <& /elements/tr-input-beginning_ending.html &> + + <& /elements/progress-init.html, + 'OneTrueForm', + [ qw( reportname msgnum agentnum beginning ending ) ], + $p.'misc/process/send-report.html', + { message => 'Reports sent', + url => $cgi->referer } + &> + +
+ <& /elements/select-msg_template.html, field => 'msgnum' &> +
+ + +
+ + + +% if ( @report_history ) { +
+ + + + + + + + + +% my $row = 0; +% foreach my $report (@report_history) { +% my $agent = ($report->agentnum ? +% $report->agent->agent : 'All agents'); + + + + + + + +% $row++; +% } + +
<% emt('Report history') %>
AgentSent onDate rangeUser
<% $agent %><% time2str($date_format, $report->send_date) %><% time2str($date_format, $report->sdate) %><% time2str($date_format, $report->edate) %><% $report->access_user->username %>
+% } + +<& /elements/footer.html &> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Send reports to customers'); + +$cgi->param('reportname') =~ /^(\w+)$/ + or die "bad reportname"; +my $reportname = $1; +my $report_info = $FS::report_batch::sendable_reports{$reportname} + or die "bad reportname"; + +my $date_format = FS::Conf->new->config('date_format') || '%x'; + +my @report_history = qsearch({ + table => 'report_batch', + hashref => { reportname => $reportname }, + order_by => ' ORDER BY send_date DESC', +}); + +# defaults per agent that could be selected for the report +my %agent; + +foreach my $report ( @report_history ) { + my $agentnum = $report->agentnum; + next if $agent{$agentnum}; + + # estimate the width of the report period, in months + my $last_sdate = DateTime->from_epoch( epoch => $report->sdate ); + my $last_edate = DateTime->from_epoch( epoch => $report->edate ); + + my $days = $last_sdate->delta_days( $last_edate )->delta_days; + my $months = sprintf('%.0f', $days / 6) / 5; + + my $next_sdate = $last_edate->clone->add(days => 1); + my $next_edate = $next_sdate->clone; + if ( $months >= 1 ) { # then treat as an interval in months + $next_edate->add( months => sprintf('%.0f', $months) ); + $next_edate->subtract(days => 1); + } else { # treat as a number of days + $next_edate->add( days => $days ); + } + + my $name = $agentnum ? FS::agent->by_key($agentnum)->agent : 'All agents'; + $agent{$agentnum} = { + name => $name, + beginning => $next_sdate->strftime($date_format), + ending => $next_edate->strftime($date_format), + msgnum => $report->msgnum, + }; +} + + diff --git a/httemplate/search/cust_msg.html b/httemplate/search/cust_msg.html index 401f52ebb..e9aece202 100644 --- a/httemplate/search/cust_msg.html +++ b/httemplate/search/cust_msg.html @@ -144,11 +144,12 @@ include('/elements/select.html', include('/elements/select.html', 'field' => 'msgtype', 'curr_value' => $cgi->param('msgtype') || '', - 'options' => [ '', 'invoice', 'receipt', 'admin' ], + 'options' => [ '', 'invoice', 'receipt', 'admin', 'report' ], 'labels' => { '' => '(any)', 'invoice' => 'Invoices', 'receipt' => 'Receipts', 'admin' => 'Admin notices', + 'report' => 'Reports', }, ) . ' diff --git a/httemplate/search/report_sales_commission_pkg.html b/httemplate/search/report_sales_commission_pkg.html index 6adf090e9..27906e0c3 100644 --- a/httemplate/search/report_sales_commission_pkg.html +++ b/httemplate/search/report_sales_commission_pkg.html @@ -1,5 +1,15 @@ <& /elements/header.html, 'Sales commission report per package' &> +% if ($FS::CurrentUser::CurrentUser->access_right('Send reports to customers')) +% { +

+<& /elements/popup_link-send_report_batch.html, + reportname => 'sales_commission_pkg', + label => emt('Send these reports by email'), +&> +

+% } +
diff --git a/httemplate/search/sales_commission_pkg.html b/httemplate/search/sales_commission_pkg.html index 2b5f2bb0a..9fbe22eca 100644 --- a/httemplate/search/sales_commission_pkg.html +++ b/httemplate/search/sales_commission_pkg.html @@ -1,12 +1,17 @@ %# still not a good way to do rows grouped by some field in a search.html %# report +%# (there is now, but we're not yet sponsored to switch this over to it) % if ( $type eq 'xls' ) { <% $data %>\ % } else { +% if ( $type eq 'html-print' ) { +<& /elements/header-popup.html, $title &> +% } else { <& /elements/header.html, $title &>

Download full results
as Excel spreadsheet

+% }
<& /elements/table-grid.html &> + + % my ($custnum, $sales, $commission, $row, $bgcolor) = (0, 0, 0, 0); % foreach my $cust_pkg ( @cust_pkg ) { % if ( $custnum ne $cust_pkg->custnum ) { -- cgit v1.2.1 From 15278bb4dcfaf4bdb79c7f8781320e24ef8f1e7d Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Sat, 19 Sep 2015 18:11:54 -0500 Subject: RT#35197: Apply changes button in Edit rate plan screen clears the global default [removed preserve_rate_detail, always true] --- FS/FS/rate.pm | 7 ++----- httemplate/edit/rate.cgi | 4 +--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/FS/FS/rate.pm b/FS/FS/rate.pm index 03dde041b..d26d11697 100644 --- a/FS/FS/rate.pm +++ b/FS/FS/rate.pm @@ -469,11 +469,8 @@ sub process { warn "$rate replacing $old (". $param->{'ratenum'}. ")\n" if $DEBUG; my @param = ( 'job'=>$job ); - if ($param->{'preserve_rate_detail'}) { - $rate->default_detailnum($old->default_detailnum); - } else { - push @param, 'rate_detail'=>\@rate_detail; - } + + $rate->default_detailnum($old->default_detailnum); $error = $rate->replace( $old, @param ); diff --git a/httemplate/edit/rate.cgi b/httemplate/edit/rate.cgi index 183ea8a42..1b052d62d 100644 --- a/httemplate/edit/rate.cgi +++ b/httemplate/edit/rate.cgi @@ -5,7 +5,7 @@ <% include('/elements/progress-init.html', 'OneTrueForm', - [ 'rate', 'agentnum', 'preserve_rate_detail' ], # 'rate', 'min_', 'sec_' ], + [ 'rate', 'agentnum' ], 'process/rate.cgi', $p.'browse/rate.cgi', ) @@ -27,8 +27,6 @@
Package Sales Percentage Commission

- - " onClick="document.OneTrueForm.submit.disabled=true; process();"> -- cgit v1.2.1 From 0cf5ea1f85245f80e595f853b86aee03e3438042 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Sat, 19 Sep 2015 19:36:55 -0500 Subject: RT#36813: Monthly Recurring Total [fixed order by] --- httemplate/view/cust_main/billing.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html index f7ea68a65..0c9f74a7c 100644 --- a/httemplate/view/cust_main/billing.html +++ b/httemplate/view/cust_main/billing.html @@ -44,7 +44,7 @@ % 'hashref' => { 'custnum' => $cust_main->custnum, }, % 'extra_sql' => 'AND ( cancel IS NULL OR cancel = 0 ) % AND freq = '. dbh->quote($freq), -% 'order_by' => 'ORDER BY pkgnum', # to ensure old pkgs come before change_to_pkg +% 'order_by' => 'ORDER BY COALESCE(start_date,0), pkgnum', # to ensure old pkgs come before change_to_pkg % }) or next; % % my $freq_pretty = $cust_pkg[0]->part_pkg->freq_pretty; -- cgit v1.2.1 From 1f688eba76414dfb81882d3ac800a6710312202a Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Sat, 19 Sep 2015 20:32:00 -0500 Subject: RT#37488: Tower/sector selection box is showing disabled towers [removed disabled when using tower_sector] --- httemplate/elements/select-tower_sector.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/httemplate/elements/select-tower_sector.html b/httemplate/elements/select-tower_sector.html index 458bcddcf..76ff25223 100644 --- a/httemplate/elements/select-tower_sector.html +++ b/httemplate/elements/select-tower_sector.html @@ -2,6 +2,8 @@ <& /elements/select-table.html, table => 'tower_sector', name_col => 'description', + addl_from => 'JOIN tower USING (towernum)', + extra_sql => q(WHERE disabled = '' OR disabled IS NULL), order_by => 'ORDER BY towernum,sectorname', empty_label => ' ', @_ -- cgit v1.2.1 From 01698260f2624212ab71be26bb4c644c0aeea2e4 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 19 Sep 2015 10:56:59 -0700 Subject: add credited sales column to tax report, #37088 --- FS/FS/Report/Tax.pm | 110 +++++++++++++++++++++++----- httemplate/search/cust_credit_bill_pkg.html | 24 ++++-- httemplate/search/report_tax.cgi | 31 ++++++-- 3 files changed, 136 insertions(+), 29 deletions(-) diff --git a/FS/FS/Report/Tax.pm b/FS/FS/Report/Tax.pm index 0923d55cf..f114c1c6b 100644 --- a/FS/FS/Report/Tax.pm +++ b/FS/FS/Report/Tax.pm @@ -182,26 +182,92 @@ sub report_internal { $all_sql{exempt_monthly} = $all_exempt; $all_sql{exempt_monthly} =~ s/EXEMPT_WHERE/exempt_monthly = 'Y'/; + # credits applied to taxable sales + # Note that negative exemptions (from exempt sales being credited) are NOT + # counted when calculating the exempt amount. (See above.) Therefore we need + # to NOT include any credits against exempt sales in this amount, either. + # These two subqueries implement that. They have joins to cust_credit_bill + # and cust_bill so that credits can be filtered by application date if + # requested. + + # Each row here is the sum of credits applied to a line item. + my $sales_credit = + "SELECT billpkgnum, SUM(cust_credit_bill_pkg.amount) AS credited + FROM cust_credit_bill_pkg + JOIN cust_credit_bill USING (creditbillnum) + JOIN cust_bill USING (invnum) + WHERE cust_bill._date >= $beginning AND cust_bill._date <= $ending + GROUP BY billpkgnum + "; + + # Each row here is the sum of negative exemptions applied to a combination + # of line item and tax definition. + my $exempt_credit = + "SELECT cust_credit_bill_pkg.billpkgnum, taxnum, + 0 - SUM(cust_tax_exempt_pkg.amount) AS exempt_credited + FROM cust_credit_bill_pkg + LEFT JOIN cust_tax_exempt_pkg USING (creditbillpkgnum) + JOIN cust_credit_bill USING (creditbillnum) + JOIN cust_bill USING (invnum) + WHERE cust_bill._date >= $beginning AND cust_bill._date <= $ending + GROUP BY cust_credit_bill_pkg.billpkgnum, taxnum + "; + + if ( $opt{credit_date} eq 'cust_credit_bill' ) { + $sales_credit =~ s/cust_bill._date/cust_credit_bill._date/g; + $exempt_credit =~ s/cust_bill._date/cust_credit_bill._date/g; + } + + $sql{sales_credited} = "$select + SUM(COALESCE(credited, 0) - COALESCE(exempt_credited, 0)) + FROM cust_main_county + JOIN ($pkg_tax) AS pkg_tax USING (taxnum) + JOIN cust_bill_pkg USING (billpkgnum) + LEFT JOIN ($sales_credit) AS sales_credit USING (billpkgnum) + LEFT JOIN ($exempt_credit) AS exempt_credit USING (billpkgnum, taxnum) + $join_cust_pkg $where AND $nottax + $group + "; + + $all_sql{sales_credited} = "$select_all + SUM(COALESCE(credited, 0) - COALESCE(exempt_credited, 0)) + FROM cust_main_county + JOIN ($pkg_tax) AS pkg_tax USING (taxnum) + JOIN cust_bill_pkg USING (billpkgnum) + LEFT JOIN ($sales_credit) AS sales_credit USING (billpkgnum) + LEFT JOIN ($exempt_credit) AS exempt_credit USING (billpkgnum, taxnum) + $join_cust_pkg $where AND $nottax + $group + "; + # taxable sales $sql{taxable} = "$select - SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(exempt_charged, 0)) + SUM(cust_bill_pkg.setup + cust_bill_pkg.recur + - COALESCE(exempt_charged, 0) + - COALESCE(credited, 0) + + COALESCE(exempt_credited, 0) + ) FROM cust_main_county JOIN ($pkg_tax) AS pkg_tax USING (taxnum) JOIN cust_bill_pkg USING (billpkgnum) - LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt - ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum - AND pkg_tax_exempt.taxnum = cust_main_county.taxnum) + LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt USING (billpkgnum, taxnum) + LEFT JOIN ($sales_credit) AS sales_credit USING (billpkgnum) + LEFT JOIN ($exempt_credit) AS exempt_credit USING (billpkgnum, taxnum) $join_cust_pkg $where AND $nottax $group"; $all_sql{taxable} = "$select_all - SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(exempt_charged, 0)) + SUM(cust_bill_pkg.setup + cust_bill_pkg.recur + - COALESCE(exempt_charged, 0) + - COALESCE(credited, 0) + + COALESCE(exempt_credited, 0) + ) FROM cust_main_county JOIN ($pkg_tax) AS pkg_tax USING (taxnum) JOIN cust_bill_pkg USING (billpkgnum) - LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt - ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum - AND pkg_tax_exempt.taxnum = cust_main_county.taxnum) + LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt USING (billpkgnum, taxnum) + LEFT JOIN ($sales_credit) AS sales_credit USING (billpkgnum) + LEFT JOIN ($exempt_credit) AS exempt_credit USING (billpkgnum, taxnum) $join_cust_pkg $where AND $nottax $group_all"; @@ -211,27 +277,35 @@ sub report_internal { # estimated tax (taxable * rate) $sql{estimated} = "$select SUM(cust_main_county.tax / 100 * - (cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(exempt_charged, 0)) + (cust_bill_pkg.setup + cust_bill_pkg.recur + - COALESCE(exempt_charged, 0) + - COALESCE(credited, 0) + + COALESCE(exempt_credited, 0) + ) ) FROM cust_main_county JOIN ($pkg_tax) AS pkg_tax USING (taxnum) JOIN cust_bill_pkg USING (billpkgnum) - LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt - ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum - AND pkg_tax_exempt.taxnum = cust_main_county.taxnum) + LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt USING (billpkgnum, taxnum) + LEFT JOIN ($sales_credit) AS sales_credit USING (billpkgnum) + LEFT JOIN ($exempt_credit) AS exempt_credit USING (billpkgnum, taxnum) $join_cust_pkg $where AND $nottax $group"; $all_sql{estimated} = "$select_all SUM(cust_main_county.tax / 100 * - (cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(exempt_charged, 0)) + (cust_bill_pkg.setup + cust_bill_pkg.recur + - COALESCE(exempt_charged, 0) + - COALESCE(credited, 0) + + COALESCE(exempt_credited, 0) + ) ) FROM cust_main_county JOIN ($pkg_tax) AS pkg_tax USING (taxnum) JOIN cust_bill_pkg USING (billpkgnum) - LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt - ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum - AND pkg_tax_exempt.taxnum = cust_main_county.taxnum) + LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt USING (billpkgnum, taxnum) + LEFT JOIN ($sales_credit) AS sales_credit USING (billpkgnum) + LEFT JOIN ($exempt_credit) AS exempt_credit USING (billpkgnum, taxnum) $join_cust_pkg $where AND $nottax $group_all"; @@ -290,12 +364,12 @@ sub report_internal { $creditwhere =~ s/cust_bill._date/cust_credit_bill._date/g; } - $sql{credit} = "$select SUM(cust_credit_bill_pkg.amount) + $sql{tax_credited} = "$select SUM(cust_credit_bill_pkg.amount) $creditfrom $creditwhere AND $istax $group"; - $all_sql{credit} = "$select_all SUM(cust_credit_bill_pkg.amount) + $all_sql{tax_credited} = "$select_all SUM(cust_credit_bill_pkg.amount) $creditfrom $creditwhere AND $istax $group_all"; diff --git a/httemplate/search/cust_credit_bill_pkg.html b/httemplate/search/cust_credit_bill_pkg.html index 63d70c27e..b5a0ee9f6 100644 --- a/httemplate/search/cust_credit_bill_pkg.html +++ b/httemplate/search/cust_credit_bill_pkg.html @@ -203,6 +203,7 @@ if ( $cgi->param('taxclass') my @loc_param = qw( district city county state country ); if ( $cgi->param('out') ) { + # probably don't need this part my ( $loc_sql, @param ) = FS::cust_pkg->location_sql( 'ornull' => 1 ); while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution @@ -242,6 +243,8 @@ if ( $cgi->param('out') ) { #hacky, could be more efficient. care if it is ever used for more than the # tax-report_groups filtering kludge + # (does that even still exist? if so, correct this (or location_sql itself) + # to work with modern cust_location links) my $locs_sql = ' ( '. join(' OR ', map { @@ -266,15 +269,24 @@ if ( $cgi->param('out') ) { } else { - my %ph = map { $_ => dbh->quote( scalar($cgi->param($_)) ) } @loc_param; - - my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; - while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution - $loc_sql =~ s/\?/$ph{shift(@param)}/e; + my @loc_where; + foreach (@loc_param) { + if ( length($cgi->param($_)) ) { + my $quoted = dbh->quote($cgi->param($_)); + push @loc_where, "(COALESCE(cust_location.$_, '') = $quoted)"; + } } + my $loc_sql = join(' AND ', @loc_where); - push @where, $loc_sql; + #my %ph = map { $_ => dbh->quote( scalar($cgi->param($_)) ) } @loc_param; + # + #my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; + #while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution + # $loc_sql =~ s/\?/$ph{shift(@param)}/e; + #} + push @where, $loc_sql; +warn $loc_sql; } my($title, $name); diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi index 1d906473e..6d0e95d2a 100644 --- a/httemplate/search/report_tax.cgi +++ b/httemplate/search/report_tax.cgi @@ -22,7 +22,7 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } - Sales + Sales Rate @@ -41,6 +41,7 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } Non-taxable Non-taxable Non-taxable + Credited Taxable @@ -73,10 +74,19 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } % } # if $row->{pkgclass} ne ... % # construct base links that limit to the tax rates described by this row +% # cust_bill_pkg.cgi wants a list of specific taxnums (and package class) +% # cust_credit_bill_pkg.html wants a geographic scope (and package class) % my $rowlink = ';taxnum=' . $row->{taxnums}; +% my $rowregion = ''; +% foreach my $loc (qw(state county city district)) { +% if ( $row->{$loc} ) { +% $rowregion .= ";$loc=" . uri_escape($row->{$loc}); +% } +% } % # and also the package class, if we're limiting package class % if ( $params{breakdown}->{pkgclass} ) { % $rowlink .= ';classnum=' . ($row->{pkgclass} || 0); +% $rowregion .= ';classnum=' . ($row->{pkgclass} || 0); % } % % if ( $row->{total} ) { @@ -109,14 +119,20 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } <% $money_sprintf->( $row->{exempt_monthly} ) %> +% # credited sales + + + <% $money_sprintf->( $row->{sales_credited} ) %> + + + × + <% $row->{rate} %> % # taxable sales "> <% $money_sprintf->( $row->{taxable} ) %> - × - <% $row->{rate} %> % # estimated tax = @@ -134,12 +150,12 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } − %# currently broken - <% $money_sprintf->( $row->{credit} ) %> + <% $money_sprintf->( $row->{tax_credited} ) %> %# % # net tax due = - <% $money_sprintf->( $row->{tax} - $row->{credit} ) %> + <% $money_sprintf->( $row->{tax} - $row->{tax_credited} ) %> % # tax collected   <% $money_sprintf->( $row->{tax_paid} ) %> @@ -223,6 +239,11 @@ if ( $params{agentnum} ) { my $saleslink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;nottax=1"; my $taxlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;istax=1"; my $exemptlink = $p. "search/cust_tax_exempt_pkg.cgi?$dateagentlink"; +my $salescreditlink = $p. "search/cust_credit_bill_pkg.html?$dateagentlink;nottax=1"; +if ( $params{'credit_date'} eq 'cust_credit_bill' ) { + $salescreditlink =~ s/begin/credit_begin/; + $salescreditlink =~ s/end/credit_end/; +} #my $creditlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;credit=1;istax=1"; #if ( $params{'credit_date'} eq 'cust_credit_bill' ) { # $creditlink =~ s/begin/credit_begin/; -- cgit v1.2.1 From b38fc0b849b21ed4e2a83bab885b63223914edd5 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sun, 20 Sep 2015 10:00:37 -1000 Subject: fix creation of negative exemption records, #37088, from #13971 --- FS/FS/cust_tax_exempt_pkg.pm | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/FS/FS/cust_tax_exempt_pkg.pm b/FS/FS/cust_tax_exempt_pkg.pm index b64ef515d..5057781f4 100644 --- a/FS/FS/cust_tax_exempt_pkg.pm +++ b/FS/FS/cust_tax_exempt_pkg.pm @@ -3,6 +3,7 @@ use base qw( FS::cust_main_Mixin FS::Record ); use strict; use FS::UID qw(dbh); +use FS::cust_main_county; use FS::upgrade_journal; # some kind of common ancestor with cust_bill_pkg_tax_location would make sense @@ -176,6 +177,16 @@ Otherwise returns false. =cut +# do not remove; this can't be autogenerated + +sub cust_main_county { + my $self = shift; + if ( $self->taxtype eq 'FS::cust_main_county' ) { + return FS::cust_main_county->by_key($self->taxnum); + } + ''; +} + sub _upgrade_data { my $class = shift; -- cgit v1.2.1 From 94ab54acc7f6941d9ed02f2ad5d7b63f1d2f6b60 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sun, 20 Sep 2015 10:09:53 -1000 Subject: fix total sales column, #37088 --- FS/FS/Report/Tax.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/FS/FS/Report/Tax.pm b/FS/FS/Report/Tax.pm index f114c1c6b..2480a45b9 100644 --- a/FS/FS/Report/Tax.pm +++ b/FS/FS/Report/Tax.pm @@ -557,6 +557,7 @@ sub table { # and calculate row totals $this_row{sales} = sprintf('%.2f', $this_row{taxable} + + $this_row{sales_credited} + $this_row{exempt_cust} + $this_row{exempt_pkg} + $this_row{exempt_monthly} -- cgit v1.2.1 From 376794a00e837317e35fefd61a29ab58c0303b35 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 21 Sep 2015 02:02:13 -0700 Subject: billing event to call web services, RT#35167 --- FS/FS/Misc/DateTime.pm | 22 ++++++++--- FS/FS/cust_pay.pm | 16 ++++++++ FS/FS/part_event.pm | 2 + FS/FS/part_event/Action/http.pm | 85 +++++++++++++++++++++++++++++++++++++++++ FS/FS/part_event/Condition.pm | 1 + FS/FS/part_event_option.pm | 3 +- 6 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 FS/FS/part_event/Action/http.pm diff --git a/FS/FS/Misc/DateTime.pm b/FS/FS/Misc/DateTime.pm index 2fff90647..56baec3ed 100644 --- a/FS/FS/Misc/DateTime.pm +++ b/FS/FS/Misc/DateTime.pm @@ -6,9 +6,10 @@ use Carp; use Time::Local; use Date::Parse; use DateTime::Format::Natural; +use Date::Format; use FS::Conf; -@EXPORT_OK = qw( parse_datetime day_end ); +@EXPORT_OK = qw( parse_datetime day_end iso8601 ); =head1 NAME @@ -65,11 +66,22 @@ same date but 23:59:59 for the time. =cut sub day_end { - my $time = shift; + my $time = shift; - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = - localtime($time); - timelocal(59,59,23,$mday,$mon,$year); + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = + localtime($time); + timelocal(59,59,23,$mday,$mon,$year); +} + +=item iso8601 TIME + +Parses time as an integer UNIX timestamp and returns the ISO 8601 formatted +date and time. + +=cut + +sub iso8601 { + time2str('%Y-%m-%dT%T', @_); } =back diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index 59d77742c..cb39d4391 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -409,6 +409,22 @@ sub insert { warn "can't send payment receipt/statement: $error" if $error; } + #run payment events immediately + my $due_cust_event = $self->cust_main->due_cust_event( + 'eventtable' => 'cust_pay', + 'objects' => [ $self ], + ); + if ( !ref($due_cust_event) ) { + warn "Error searching for cust_pay billing events: $due_cust_event\n"; + } else { + foreach my $cust_event (@$due_cust_event) { + next unless $cust_event->test_conditions; + if ( my $error = $cust_event->do_event() ) { + warn "Error running cust_pay billing event: $error\n"; + } + } + } + ''; } diff --git a/FS/FS/part_event.pm b/FS/FS/part_event.pm index d15f35b7d..9a1144c85 100644 --- a/FS/FS/part_event.pm +++ b/FS/FS/part_event.pm @@ -369,6 +369,7 @@ sub eventtable_labels { 'cust_pkg' => 'Package', 'cust_bill' => 'Invoice', 'cust_main' => 'Customer', + 'cust_pay' => 'Payment', 'cust_pay_batch' => 'Batch payment', 'cust_statement' => 'Statement', #too general a name here? "Invoice group"? 'svc_acct' => 'Login service', @@ -408,6 +409,7 @@ sub eventtable_pkey { 'cust_main' => 'custnum', 'cust_bill' => 'invnum', 'cust_pkg' => 'pkgnum', + 'cust_pay' => 'paynum', 'cust_pay_batch' => 'paybatchnum', 'cust_statement' => 'statementnum', 'svc_acct' => 'svcnum', diff --git a/FS/FS/part_event/Action/http.pm b/FS/FS/part_event/Action/http.pm new file mode 100644 index 000000000..b8715a714 --- /dev/null +++ b/FS/FS/part_event/Action/http.pm @@ -0,0 +1,85 @@ +package FS::part_event::Action::http; + +use strict; +use base qw( FS::part_event::Action ); +use LWP::UserAgent; +use HTTP::Request::Common; +use JSON::XS; +use FS::Misc::DateTime qw( iso8601 ); + +#sub description { 'Send an HTTP or HTTPS GET or POST request'; } +sub description { 'Send an HTTP or HTTPS POST request'; } + +sub eventtable_hashref { + { 'cust_bill' => 1, + 'cust_pay' => 1, + }, +} + +sub option_fields { + ( + 'method' => { label => 'Method', + type => 'select', + options => [qw( POST )], #GET )], + }, + 'url' => { label => 'URL', + type => 'text', + size => 120, + }, + 'ssl_no_verify' => { label => 'Skip SSL certificate validation', + type => 'checkbox', + }, + 'encoding' => { label => 'Encoding', + type => 'select', + options => [qw( JSON )], #XML, Form, etc. + }, + 'content' => { label => 'Content', #nneed better inline docs on format + type => 'textarea', + }, + #'response_error_param' => 'Response error parameter', + ); +} + +sub default_weight { 57; } + +our %content_type = ( + 'JSON' => 'application/json', +); + +sub do_action { + my( $self, $object ) = @_; + + my $cust_main = $self->cust_main($object); + + my %content = + map { + /^\s*(\S+)\s+(.*)$/ or /()()/; + my( $field, $value_expression ) = ( $1, $2 ); + my $value = eval $value_expression; + die $@ if $@; + ( $field, $value ); + } split(/\n/, $self->option('content') ); + + my $content = encode_json( \%content ); + + my @lwp_opts = (); + push @lwp_opts, 'ssl_opts'=>{ 'verify_hostname'=>0 } + if $self->option('ssl_no_verify'); + my $ua = LWP::UserAgent->new(@lwp_opts); + + my $req = HTTP::Request::Common::POST( + $self->option('url'), + Content_Type => $content_type{ $self->option('encoding') }, + Content => $content, + ); + + my $response = $ua->request($req); + + die $response->status_line if $response->is_error; + + my $response_json = decode_json( $response->content ); + die $response_json->{error} if $response_json->{error}; #XXX response_error_param + +} + +1; diff --git a/FS/FS/part_event/Condition.pm b/FS/FS/part_event/Condition.pm index 60697c196..36fbe9a0d 100644 --- a/FS/FS/part_event/Condition.pm +++ b/FS/FS/part_event/Condition.pm @@ -52,6 +52,7 @@ sub eventtable_hashref { { 'cust_main' => 1, 'cust_bill' => 1, 'cust_pkg' => 1, + 'cust_pay' => 1, 'cust_pay_batch' => 1, 'cust_statement' => 1, 'svc_acct' => 1, diff --git a/FS/FS/part_event_option.pm b/FS/FS/part_event_option.pm index 09b775609..6df9e84c1 100644 --- a/FS/FS/part_event_option.pm +++ b/FS/FS/part_event_option.pm @@ -183,7 +183,8 @@ sub check { $self->ut_numbern('optionnum') || $self->ut_foreign_key('eventpart', 'part_event', 'eventpart' ) || $self->ut_text('optionname') - || $self->ut_textn('optionvalue') + #|| $self->ut_textn('optionvalue') + || $self->ut_anything('optionvalue') #http.pm content has \n ; return $error if $error; -- cgit v1.2.1 From ab5177ddab29e7fca9f64144a0c1ed104ead3ead Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 21 Sep 2015 18:25:57 -0700 Subject: import BWGroupNumber as charged_party when accountcode is empty, RT#27946 --- FS/FS/cdr/amcom.pm | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/FS/FS/cdr/amcom.pm b/FS/FS/cdr/amcom.pm index 36be8d8c3..97ab402ca 100644 --- a/FS/FS/cdr/amcom.pm +++ b/FS/FS/cdr/amcom.pm @@ -22,8 +22,12 @@ my ($tmp_mday, $tmp_mon, $tmp_year); my ($cdr, $field, $conf, $hashref) = @_; $hashref->{skiprow} = 1 unless $field eq 'DCR'; }, - '', # 2. BWGroupID (centrex group) - '', # 3. BWGroupNumber + 'accountcode',# 2. BWGroupID (centrex group) + sub { # 3. BWGroupNumber + my ($cdr, $field) = @_; #, $conf, $hashref) = @_; + $cdr->charged_party($field) + if $cdr->accountcode eq '' && $field =~ /^(1800|1300)/; + }, 'uniqueid', # 4. Record ID 'dcontext', # 5. Call Category (LOCAL, NATIONAL, FREECALL, MOBILE) sub { # 6. Start Date (DDMMYYYY -- cgit v1.2.1 From 0c759132a02d9403f391c6a997cbe754a4dba407 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 21 Sep 2015 20:40:36 -0700 Subject: import a2billing username as charged_party, RT#32909 --- FS/bin/freeside-cdr-a2billing-import | 208 +++++++++++++++++++++++++++++++++++ bin/cdr-a2billing.import | 164 --------------------------- 2 files changed, 208 insertions(+), 164 deletions(-) create mode 100755 FS/bin/freeside-cdr-a2billing-import delete mode 100755 bin/cdr-a2billing.import diff --git a/FS/bin/freeside-cdr-a2billing-import b/FS/bin/freeside-cdr-a2billing-import new file mode 100755 index 000000000..923f5fbb1 --- /dev/null +++ b/FS/bin/freeside-cdr-a2billing-import @@ -0,0 +1,208 @@ +#!/usr/bin/perl + +use strict; +use vars qw( $DEBUG ); +use Date::Parse 'str2time'; +use Date::Format 'time2str'; +use FS::UID qw(adminsuidsetup dbh); +use FS::cdr; +use DBI; +use Getopt::Std; + +my %opt; +getopts('H:U:P:D:T:s:e:c:', \%opt); +my $user = shift or die &usage; + +my $dsn = 'dbi:mysql'; +$dsn .= ":database=$opt{D}" if $opt{D}; +$dsn .= ":host=$opt{H}" if $opt{H}; + +my $mysql = DBI->connect($dsn, $opt{U}, $opt{P}) + or die $DBI::errstr; + +my ($start, $end) = ('', ''); +if ( $opt{s} ) { + $start = str2time($opt{s}) or die "can't parse start date $opt{s}\n"; + $start = time2str('%Y-%m-%d', $start); +} +if ( $opt{e} ) { + $end = str2time($opt{e}) or die "can't parse end date $opt{e}\n"; + $end = time2str('%Y-%m-%d', $end); +} + +adminsuidsetup $user; + +my $fsdbh = FS::UID::dbh; + +# check for existence of freesidestatus +my $table = $opt{T} || 'cc_call'; +my $status = $mysql->selectall_arrayref("SHOW COLUMNS FROM $table WHERE Field = 'freesidestatus'"); +if( ! @$status ) { + print "Adding freesidestatus column...\n"; + $mysql->do("ALTER TABLE $table ADD COLUMN freesidestatus varchar(32)") + or die $mysql->errstr; +} +else { + print "freesidestatus column present\n"; +} + +# Fields: +# id - primary key, sequential +# session_id - Local/- or SIP/- +# uniqueid - a decimal number, seems to be close to the unix timestamp +# card_id - probably the equipment port, 1 - 10 +# nasipaddress - we don't care +# starttime, stoptime - timestamps +# sessiontime - duration, seconds +# calledstation - dst +# sessionbill - upstream_price +# id_tariffgroup - null, 0, 1 +# id_tariffplan - null, 0, 3, 4, 5, 6, 7, 8, 9 +# id_ratecard - larger numbers +# (all of the id_* fields are foreign keys: cc_tariffgroup, cc_ratecard, etc.) +# id_trunk - we don't care +# sipiax - probably don't care +# src - src. Usually a phone number, but not always. +# id_did - always null +# buycost - wholesale price? correlated with sessionbill +# id_card_package_offer - no idea +# real_sessiontime - close to sessiontime, except when it's null +# (When sessiontime = 0, real_sessiontime is either 0 or null, and +# sessionbill is 0. When sessiontime > 0, but real_sessiontime is null, +# sessionbill is 0. So real_sessiontime seems to be the billable time, and +# is null when the call is non-billable.) +# dnid - sometimes equals calledstation, or calledstation without the leading +# "1". But not always. +# terminatecauseid - integer, 0 - 7 +# destination - seems to be the NPA or NPA+NXX sometimes, or "0". + +# terminatecauseid values: +my %disposition = ( + 0 => '', + 1 => 'ANSWER', #the only one that's billable + 2 => 'BUSY', + 3 => 'NOANSWER', + 4 => 'CANCEL', + 5 => 'CONGESTION', + 6 => 'CHANUNAVAIL', + 7 => 'DONTCALL', + 8 => 'TORTURE', #??? + 9 => 'INVALIDARGS', +); + +my @cols = ( + 'cc_call.id as id', 'cc_card.username as username', + qw( sessionid + starttime stoptime sessiontime real_sessiontime + terminatecauseid + calledstation src + id_tariffplan id_ratecard sessionbill + ) +); + +my $sql = 'SELECT '.join(',', @cols). " FROM $table". + ' WHERE freesidestatus IS NULL' . + ($start && " AND starttime >= '$start'") . + ($end && " AND starttime < '$end'") ; +my $sth = $mysql->prepare($sql); +$sth->execute; +print "Importing ".$sth->rows." records...\n"; + +my $cdr_batch = new FS::cdr_batch({ + 'cdrbatch' => 'mysql-import-'. time2str('%Y/%m/%d-%T',time), + }); +my $error = $cdr_batch->insert; +die $error if $error; +my $cdrbatchnum = $cdr_batch->cdrbatchnum; +my $imports = 0; +my $updates = 0; + +my $row; +while ( $row = $sth->fetchrow_hashref ) { + $row->{calledstation} =~ s/^1//; + $row->{src} =~ s/^1//; + my $cdr = FS::cdr->new ({ + uniqueid => $row->{sessionid}, + cdrbatchnum => $cdrbatchnum, + startdate => time2str($row->{starttime}), + enddate => time2str($row->{stoptime}), + duration => $row->{sessiontime}, + billsec => $row->{real_sessiontime}, + dst => $row->{calledstation}, + src => $row->{src}, + charged_party => $row->{username}, + upstream_rateplanid => $row->{id_tariffplan}, + upstream_rateid => $row->{id_ratecard}, # I think? + upstream_price => $row->{sessionbill}, + }); + $cdr->cdrtypenum($opt{c}) if $opt{c}; + + my $error = $cdr->insert; + if($error) { + print "failed import: $error\n"; + } else { + $imports++; + my $updated = $mysql->do( + "UPDATE $table SET freesidestatus = 'done' WHERE id = ?", + undef, + $row->{'id'} + ); + $updates += $updated; + print "failed to set status: ".$mysql->errstr."\n" unless $updated; + } +} +print "Done.\nImported $imports CDRs, marked $updates as done in source database.\n"; +$mysql->disconnect; + +sub usage { + "Usage: + freeside-cdr-a2billing-import + [ -H host ] + -D database + -U user + -P password + [ -s start ] [ -e end ] [ -c cdrtypenum ] + freesideuser +"; +} + +=head1 NAME + +freeside-cdr-a2billing-import - Download CDRs from an A2Billing MySQL database + +=head1 SYNOPSIS + + freeside-cdr-a2billing-import [ -H host ] -D database -U user -P password + [ -T tablename ] + [ -s start ] [ -e end ] [ -c cdrtypenum ] + freesideuser + +-H: database hostname + +-D: database name + +-U: database username + +-P: database password + +-T: table to import, defaults to cc_call + +-s: start date, e.g. 4/20/2015 + +-e: end date, e.g. 12/25/2015 + +-c: cdrtypenum to set, defaults to none + +freesideuser: freeside username + +=head1 DESCRIPTION + +=head1 BUGS + +=head1 SEE ALSO + +L + +=cut + +1; diff --git a/bin/cdr-a2billing.import b/bin/cdr-a2billing.import deleted file mode 100755 index 6677fa0a0..000000000 --- a/bin/cdr-a2billing.import +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/perl - -use strict; -use vars qw( $DEBUG ); -use Date::Parse 'str2time'; -use Date::Format 'time2str'; -use FS::UID qw(adminsuidsetup dbh); -use FS::cdr; -use DBI; -use Getopt::Std; - -my %opt; -getopts('H:U:P:D:T:s:e:c:', \%opt); -my $user = shift or die &usage; - -my $dsn = 'dbi:mysql'; -$dsn .= ":database=$opt{D}" if $opt{D}; -$dsn .= ":host=$opt{H}" if $opt{H}; - -my $mysql = DBI->connect($dsn, $opt{U}, $opt{P}) - or die $DBI::errstr; - -my ($start, $end) = ('', ''); -if ( $opt{s} ) { - $start = str2time($opt{s}) or die "can't parse start date $opt{s}\n"; - $start = time2str('%Y-%m-%d', $start); -} -if ( $opt{e} ) { - $end = str2time($opt{e}) or die "can't parse end date $opt{e}\n"; - $end = time2str('%Y-%m-%d', $end); -} - -adminsuidsetup $user; - -my $fsdbh = FS::UID::dbh; - -# check for existence of freesidestatus -my $table = $opt{T} || 'cc_call'; -my $status = $mysql->selectall_arrayref("SHOW COLUMNS FROM $table WHERE Field = 'freesidestatus'"); -if( ! @$status ) { - print "Adding freesidestatus column...\n"; - $mysql->do("ALTER TABLE $table ADD COLUMN freesidestatus varchar(32)") - or die $mysql->errstr; -} -else { - print "freesidestatus column present\n"; -} - -# Fields: -# id - primary key, sequential -# session_id - Local/- or SIP/- -# uniqueid - a decimal number, seems to be close to the unix timestamp -# card_id - probably the equipment port, 1 - 10 -# nasipaddress - we don't care -# starttime, stoptime - timestamps -# sessiontime - duration, seconds -# calledstation - dst -# sessionbill - upstream_price -# id_tariffgroup - null, 0, 1 -# id_tariffplan - null, 0, 3, 4, 5, 6, 7, 8, 9 -# id_ratecard - larger numbers -# (all of the id_* fields are foreign keys: cc_tariffgroup, cc_ratecard, etc.) -# id_trunk - we don't care -# sipiax - probably don't care -# src - src. Usually a phone number, but not always. -# id_did - always null -# buycost - wholesale price? correlated with sessionbill -# id_card_package_offer - no idea -# real_sessiontime - close to sessiontime, except when it's null -# (When sessiontime = 0, real_sessiontime is either 0 or null, and -# sessionbill is 0. When sessiontime > 0, but real_sessiontime is null, -# sessionbill is 0. So real_sessiontime seems to be the billable time, and -# is null when the call is non-billable.) -# dnid - sometimes equals calledstation, or calledstation without the leading -# "1". But not always. -# terminatecauseid - integer, 0 - 7 -# destination - seems to be the NPA or NPA+NXX sometimes, or "0". - -# terminatecauseid values: -my %disposition = ( - 0 => '', - 1 => 'ANSWER', #the only one that's billable - 2 => 'BUSY', - 3 => 'NOANSWER', - 4 => 'CANCEL', - 5 => 'CONGESTION', - 6 => 'CHANUNAVAIL', - 7 => 'DONTCALL', - 8 => 'TORTURE', #??? - 9 => 'INVALIDARGS', -); - -my @cols = ( qw( - id sessionid - starttime stoptime sessiontime real_sessiontime - terminatecauseid - calledstation src - id_tariffplan id_ratecard sessionbill -) ); - -my $sql = 'SELECT '.join(',', @cols). " FROM $table". - ' WHERE freesidestatus IS NULL' . - ($start && " AND starttime >= '$start'") . - ($end && " AND starttime < '$end'") ; -my $sth = $mysql->prepare($sql); -$sth->execute; -print "Importing ".$sth->rows." records...\n"; - -my $cdr_batch = new FS::cdr_batch({ - 'cdrbatch' => 'mysql-import-'. time2str('%Y/%m/%d-%T',time), - }); -my $error = $cdr_batch->insert; -die $error if $error; -my $cdrbatchnum = $cdr_batch->cdrbatchnum; -my $imports = 0; -my $updates = 0; - -my $row; -while ( $row = $sth->fetchrow_hashref ) { - $row->{calledstation} =~ s/^1//; - $row->{src} =~ s/^1//; - my $cdr = FS::cdr->new ({ - uniqueid => $row->{sessionid}, - cdrbatchnum => $cdrbatchnum, - startdate => time2str($row->{starttime}), - enddate => time2str($row->{stoptime}), - duration => $row->{sessiontime}, - billsec => $row->{real_sessiontime}, - dst => $row->{calledstation}, - src => $row->{src}, - upstream_rateplanid => $row->{id_tariffplan}, - upstream_rateid => $row->{id_ratecard}, # I think? - upstream_price => $row->{sessionbill}, - }); - $cdr->cdrtypenum($opt{c}) if $opt{c}; - - my $error = $cdr->insert; - if($error) { - print "failed import: $error\n"; - } else { - $imports++; - my $updated = $mysql->do( - "UPDATE $table SET freesidestatus = 'done' WHERE id = ?", - undef, - $row->{'id'} - ); - $updates += $updated; - print "failed to set status: ".$mysql->errstr."\n" unless $updated; - } -} -print "Done.\nImported $imports CDRs, marked $updates as done in source database.\n"; -$mysql->disconnect; - -sub usage { - "Usage: - cdr-a2billing.import - [ -H host ] - -D database - -U user - -P password - [ -s start ] [ -e end ] [ -c cdrtypenum ] - freesideuser -"; -} -- cgit v1.2.1 From 15a4e1674694b76ecc2af87de479aabe370ac03d Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 22 Sep 2015 01:08:04 -0500 Subject: RT#37908: Convert existing email-sending code to use common interface [removals and switches to FS::Log] --- FS/FS/AccessRight.pm | 2 +- FS/FS/Conf.pm | 39 +-------- FS/FS/Cron/agent_email.pm | 79 ------------------ FS/FS/Cron/backup.pm | 59 ++++++-------- FS/FS/Upgrade.pm | 6 ++ FS/FS/cust_credit.pm | 32 +------- FS/FS/log_context.pm | 2 + FS/FS/pay_batch.pm | 19 ++--- FS/bin/freeside-daily | 4 - FS/bin/freeside-fetch | 93 ---------------------- bin/agent_email | 30 ------- httemplate/misc/delete-cust_credit.cgi | 21 ----- httemplate/view/cust_main/payment_history.html | 2 +- .../view/cust_main/payment_history/credit.html | 11 +-- 14 files changed, 48 insertions(+), 351 deletions(-) delete mode 100644 FS/FS/Cron/agent_email.pm delete mode 100755 FS/bin/freeside-fetch delete mode 100755 bin/agent_email delete mode 100755 httemplate/misc/delete-cust_credit.cgi diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 53c7cf622..3f2c0f35d 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -223,7 +223,7 @@ tie my %rights, 'Tie::IxHash', 'Void credit', #NEWER than things marked NEWNEWNEW 'Unvoid credit', #NEWER than things marked NEWNEWNEW { rightname=>'Unapply credit', desc=>'Enable "unapplication" of unclosed credits.' }, #aka unapplycredits - { rightname=>'Delete credit', desc=>'Enable deletion of unclosed credits. Be very careful! Only delete credits that were data-entry errors, not adjustments.' }, #aka. deletecredits Optionally specify one or more comma-separated email addresses to be notified when a credit is deleted. + { rightname=>'Delete credit', desc=>'Enable deletion of unclosed credits. Be very careful! Only delete credits that were data-entry errors, not adjustments.' }, 'View refunds', { rightname=>'Post refund', desc=>'Enable posting of check and cash refunds.' }, 'Post check refund', diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 1714c575a..5c4774ab5 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -1038,16 +1038,6 @@ my $validate_email = sub { $_[0] =~ 'per_locale' => 1, }, - { - 'key' => 'deletecredits', - #not actually deprecated yet - #'section' => 'deprecated', - #'description' => 'DEPRECATED, now controlled by ACLs. Used to enable deletion of unclosed credits. Be very careful! Only delete credits that were data-entry errors, not adjustments. Optionally specify one or more comma-separated email addresses to be notified when a credit is deleted.', - 'section' => '', - 'description' => 'One or more comma-separated email addresses to be notified when a credit is deleted.', - 'type' => [qw( checkbox text )], - }, - { 'key' => 'deleterefunds', 'section' => 'billing', @@ -2710,14 +2700,6 @@ and customer address. Include units.', 'type' => 'text', }, - { - 'key' => 'dump-email_to', - 'section' => '', - 'description' => "Optional email address to send success/failure message for database dumps.", - 'type' => 'text', - 'validate' => $validate_email, - }, - { 'key' => 'credit_card-recurring_billing_flag', 'section' => 'billing', @@ -3778,11 +3760,12 @@ and customer address. Include units.', 'select_enum' => [ 'approve', 'decline' ], }, + # replaces batch-errors_to (sent email on error) { - 'key' => 'batch-errors_to', + 'key' => 'batch-errors_not_fatal', 'section' => 'billing', - 'description' => 'Email errors when processing batches to this address. If unspecified, batch processing will stop immediately on error.', - 'type' => 'text', + 'description' => 'If checked, when importing batches from a gateway, item errors will be recorded in the system log without aborting processing. If unchecked, batch processing will fail on error.', + 'type' => 'checkbox', }, #lists could be auto-generated from pay_batch info @@ -4647,13 +4630,6 @@ and customer address. Include units.', 'type' => 'text', }, - { - 'key' => 'email_report-subject', - 'section' => '', - 'description' => 'Subject for reports emailed by freeside-fetch. Defaults to "Freeside report".', - 'type' => 'text', - }, - { 'key' => 'selfservice-head', 'section' => 'self-service', @@ -5780,13 +5756,6 @@ and customer address. Include units.', ], }, - { - 'key' => 'agent-email_day', - 'section' => '', - 'description' => 'On this day of each month, agents with master customer records containing email addresses will be emailed a list of their customers and balances.', - 'type' => 'text', - }, - { 'key' => 'report-cust_pay-select_time', 'section' => 'UI', diff --git a/FS/FS/Cron/agent_email.pm b/FS/FS/Cron/agent_email.pm deleted file mode 100644 index 6bc1cc643..000000000 --- a/FS/FS/Cron/agent_email.pm +++ /dev/null @@ -1,79 +0,0 @@ -package FS::Cron::agent_email; -use base qw( Exporter ); - -use strict; -use vars qw( @EXPORT_OK $DEBUG ); -use Date::Simple qw(today); -use URI::Escape; -use FS::Mason qw( mason_interps ); -use FS::Conf; -use FS::Misc qw(send_email); -use FS::Record qw(qsearch);# qsearchs); -use FS::agent; - -@EXPORT_OK = qw ( agent_email ); -$DEBUG = 0; - -sub agent_email { - my %opt = @_; - - my $conf = new FS::Conf; - - my $day = $conf->config('agent-email_day') or return; - return unless $day == today->day; - - if ( 1 ) { #XXX if ( %%%RT_ENABLED%%% ) { - require RT; - RT::LoadConfig(); - RT::Init(); - RT::ConnectToDatabase(); - } - - my $from = $conf->invoice_from_full(); - - my $outbuf = '';; - my( $fs_interp, $rt_interp ) = mason_interps('standalone', 'outbuf'=>\$outbuf); - - my $comp = '/search/cust_main.html'; - my %args = ( - 'cust_fields' => 'Cust# | Cust. Status | Customer | Current Balance', - '_type' => 'html-print', - ); - my $query = join('&', map "$_=".uri_escape($args{$_}), keys %args ); - - my $extra_sql = $opt{a} ? " AND agentnum IN ( $opt{a} ) " : ''; - - foreach my $agent ( qsearch({ - 'table' => 'agent', - 'hashref' => { - 'disabled' => '', - 'agent_custnum' => { op=>'!=', value=>'' }, - }, - 'extra_sql' => $extra_sql, - }) - ) - { - - $FS::Mason::Request::QUERY_STRING = $query. '&agentnum='. $agent->agentnum; - $fs_interp->exec($comp); - - my @email = $agent->agent_cust_main->invoicing_list or next; - - warn "emailing ". join(',',@email). " for agent ". $agent->agent. "\n" - if $DEBUG; - send_email( - 'from' => $from, - 'to' => \@email, - 'subject' => 'Customer report', - 'body' => $outbuf, - 'content-type' => 'text/html', - #'content-encoding' - ); - - $outbuf = ''; - - } - -} - -1; diff --git a/FS/FS/Cron/backup.pm b/FS/FS/Cron/backup.pm index cfc8e3624..a192ca90e 100644 --- a/FS/FS/Cron/backup.pm +++ b/FS/FS/Cron/backup.pm @@ -6,7 +6,7 @@ use Exporter; use File::Copy; use Date::Format; use FS::UID qw(driver_name datasrc); -use FS::Misc qw( send_email ); +use FS::Log @ISA = qw( Exporter ); @EXPORT_OK = qw( backup ); @@ -20,7 +20,7 @@ sub backup { my $filename = time2str('%Y%m%d%H%M%S',time); datasrc =~ /dbname=([\w\.]+)$/ - or backup_email_and_die($conf,$filename,"unparsable datasrc ". datasrc); + or backup_log_and_die($filename,"unparsable datasrc ". datasrc); my $database = $1; my $ext; @@ -31,70 +31,61 @@ sub backup { system("mysqldump $database >/var/tmp/$database.sql"); $ext = 'sql'; } else { - backup_email_and_die($conf,$filename,"database dumps not yet supported for ". driver_name); + backup_log_and_die($filename,"database dumps not yet supported for ". driver_name); } chmod 0600, "/var/tmp/$database.$ext"; if ( $conf->config('dump-pgpid') ) { eval 'use GnuPG;'; - backup_email_and_die($conf,$filename,$@) if $@; + backup_log_and_die($filename,$@) if $@; my $gpg = new GnuPG; $gpg->encrypt( plaintext => "/var/tmp/$database.$ext", output => "/var/tmp/$database.gpg", recipient => $conf->config('dump-pgpid'), ); unlink "/var/tmp/$database.$ext" - or backup_email_and_die($conf,$filename,$!); + or backup_log_and_die($filename,$!); chmod 0600, "/var/tmp/$database.gpg"; $ext = 'gpg'; } if ( $localdest ) { copy("/var/tmp/$database.$ext", "$localdest/$filename.$ext") - or backup_email_and_die($conf,$filename,$!); + or backup_log_and_die($filename,$!); chmod 0600, "$localdest/$filename.$ext"; } if ( $scpdest ) { eval "use Net::SCP qw(scp);"; - backup_email_and_die($conf,$filename,$@) if $@; + backup_log_and_die($filename,$@) if $@; scp("/var/tmp/$database.$ext", "$scpdest/$filename.$ext"); } - unlink "/var/tmp/$database.$ext" or backup_email_and_die($conf,$filename,$!); #or just warn? + unlink "/var/tmp/$database.$ext" or backup_log_and_die($filename,$!); #or just warn? - backup_email($conf,$filename); + backup_log($filename); } -#runs backup_email and dies with same error message -sub backup_email_and_die { - my ($conf,$filename,$error) = @_; - backup_email($conf,$filename,$error); - warn "backup_email_and_die called without error message" unless $error; +#runs backup_log and dies with same error message +sub backup_log_and_die { + my ($filename,$error) = @_; + $error = "backup_log_and_die called without error message" unless $error; + backup_log($filename,$error); die $error; } -#checks if email should be sent, sends it -sub backup_email { - my ($conf,$filename,$error) = @_; - my $to = $conf->config('dump-email_to'); - return unless $to; - my $result = $error ? 'FAILED' : 'succeeded'; - my $email_error = send_email( - 'from' => $conf->config('invoice_from'), #or whatever, don't think it matters - 'to' => $to, - 'subject' => 'FREESIDE NOTIFICATION: Backup ' . $result, - 'body' => [ - "This is an automatic message from your Freeside installation.\n", - "Freeside backup $filename $result", - ($error ? " with the following error:\n\n" : "\n"), - ($error || ''), - "\n", - ], - 'msgtype' => 'admin', - ); - warn $email_error if $email_error; +#logs result +sub backup_log { + my ($filename,$error) = @_; + my $result = $error ? "FAILED: $error" : 'succeeded'; + my $message = "backup $filename $result\n"; + my $log = FS::Log->new('Cron::backup'); + if ($error) { + $log->error($message); + } else { + $log->info($message); + } return; } diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index ffc04bab7..263230b34 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -154,6 +154,12 @@ If you need to continue using the old Form 477 report, turn on the $conf->set('previous_balance-exclude_from_total', ''); } + # switch from specifying an email address to boolean check + if ( $conf->exists('batch-errors_to') ) { + $conf->touch('batch-errors_not_fatal'); + $conf->delete('batch-errors_to'); + } + enable_banned_pay_pad() unless length($conf->config('banned_pay-pad')); } diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 544a0e83d..31adebec1 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -9,7 +9,6 @@ use vars qw( $conf $unsuspendauto $me $DEBUG use List::Util qw( min ); use Date::Format; use FS::UID qw( dbh ); -use FS::Misc qw(send_email); use FS::Record qw( qsearch qsearchs dbdef ); use FS::CurrentUser; use FS::cust_pkg; @@ -277,35 +276,6 @@ sub delete { return $error; } - if ( !$opt{void} and $conf->config('deletecredits') ne '' ) { - - my $cust_main = $self->cust_main; - - my $error = send_email( - 'from' => $conf->invoice_from_full($self->cust_main->agentnum), - #invoice_from??? well as good as any - 'to' => $conf->config('deletecredits'), - 'subject' => 'FREESIDE NOTIFICATION: Credit deleted', - 'body' => [ - "This is an automatic message from your Freeside installation\n", - "informing you that the following credit has been deleted:\n", - "\n", - 'crednum: '. $self->crednum. "\n", - 'custnum: '. $self->custnum. - " (". $cust_main->last. ", ". $cust_main->first. ")\n", - 'amount: $'. sprintf("%.2f", $self->amount). "\n", - 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n", - 'reason: '. $self->reason. "\n", - ], - ); - - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "can't send credit deletion notification: $error"; - } - - } - $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -415,7 +385,7 @@ sub void { return $error; } - $error = $self->delete(void => 1); # suppress deletecredits warning + $error = $self->delete(); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; diff --git a/FS/FS/log_context.pm b/FS/FS/log_context.pm index 403829ac2..bd142471c 100644 --- a/FS/FS/log_context.pm +++ b/FS/FS/log_context.pm @@ -9,7 +9,9 @@ my @contexts = ( qw( bill_and_collect FS::cust_main::Billing::bill_and_collect FS::cust_main::Billing::bill + FS::pay_batch::import_from_gateway Cron::bill + Cron::backup Cron::upload spool_upload daily diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm index 2a522b46e..d7dd7bbe4 100644 --- a/FS/FS/pay_batch.pm +++ b/FS/FS/pay_batch.pm @@ -10,10 +10,10 @@ use Time::Local; use Text::CSV_XS; use Date::Parse qw(str2time); use Business::CreditCard qw(cardtype); -use FS::Misc qw(send_email); # for error notification use FS::Record qw( dbh qsearch qsearchs ); use FS::Conf; use FS::cust_pay; +use FS::Log; =head1 NAME @@ -567,8 +567,8 @@ sub import_from_gateway { ); my @item_errors; - my $mail_on_error = $conf->config('batch-errors_to'); - if ( $mail_on_error ) { + my $errors_not_fatal = $conf->config('batch-errors_not_fatal'); + if ( $errors_not_fatal ) { # construct error trap $proc_opt{'on_parse_error'} = sub { my ($self, $line, $error) = @_; @@ -801,15 +801,10 @@ sub import_from_gateway { "Errors during batch import: ".scalar(@item_errors), @item_errors ); - if ( $mail_on_error ) { - my $subject = "Batch import errors"; #? - my $body = "Import from gateway ".$gateway->label."\n".$error_text; - send_email( - to => $mail_on_error, - from => $conf->invoice_from_full(), - subject => $subject, - body => $body, - ); + if ( $errors_not_fatal ) { + my $message = "Import from gateway ".$gateway->label." errors: ".$error_text; + my $log = FS::Log->new('FS::pay_batch::import_from_gateway'); + $log->error($message); } else { # Bail out. $dbh->rollback if $oldAutoCommit; diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily index cb018d1df..6a2daf934 100755 --- a/FS/bin/freeside-daily +++ b/FS/bin/freeside-daily @@ -79,10 +79,6 @@ pay_batch_receive(%opt); use FS::Cron::export_batch qw(export_batch_submit); export_batch_submit(%opt); -#you can skip this by not having the config -use FS::Cron::agent_email qw(agent_email); -agent_email(%opt); - #clears out cacti imports & deletes select database cache files use FS::Cron::cleanup qw( cleanup cleanup_before_backup ); cleanup_before_backup(); diff --git a/FS/bin/freeside-fetch b/FS/bin/freeside-fetch deleted file mode 100755 index c1ab78373..000000000 --- a/FS/bin/freeside-fetch +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/perl -w - -use strict; -use LWP::UserAgent; -use FS::UID qw(adminsuidsetup); -use FS::Record qw(qsearchs); -use FS::Misc qw(send_email); - -my $user = shift or die &usage; -my $employeelist = shift or die &usage; -my $url = shift or die &usage; -adminsuidsetup $user; - -my @employees = split ',', $employeelist; - -foreach my $employee (@employees) { - - $employee =~ /^(\w+)$/; - - my $access_user = qsearchs( 'access_user', { 'username' => $1 } ); - unless ($access_user) { - warn "Can't find employee $employee... skipping"; - next; - } - - my $email_address = $access_user->option('email_address'); - unless ($email_address) { - warn "No email address for $employee... skipping"; - next; - } - - no warnings 'redefine'; - local *LWP::UserAgent::get_basic_credentials = sub { - return ($access_user->username, $access_user->_password); - }; - - my $ua = new LWP::UserAgent; - $ua->timeout(1800); #30m, some reports can take a while - $ua->agent("FreesideFetcher/0.1 " . $ua->agent); - - my $req = new HTTP::Request GET => $url; - my $res = $ua->request($req); - - my $conf = new FS::Conf; - my $subject = $conf->config('email_report-subject') || 'Freeside report'; - - my %options = ( 'from' => $email_address, - 'to' => $email_address, - 'subject' => $subject, - 'body' => $res->content, - ); - - $options{'content-type'} = $res->content_type - if $res->content_type; - $options{'content-encoding'} = $res->content_encoding - if $res->content_encoding; - - if ($res->is_success) { - send_email %options; - }else{ - warn "fetching $url failed for $employee: " . $res->status_line; - } -} - -sub usage { - die "Usage:\n\n freeside-fetch user employee[,employee ...] url\n\n"; -} - -=head1 NAME - -freeside-fetch - Send a freeside page to a list of employees. - -=head1 SYNOPSIS - - freeside-fetch user employee[,employee ...] url - -=head1 DESCRIPTION - - Fetches a web page specified by url as if employee and emails it to - employee. Useful when run out of cron to send freeside web pages. - - user: Freeside user - - employee: the username of an employee to receive the emailed page. May be a comma separated list - - url: the web page to be received - -=head1 BUGS - - Can leak employee usernames and passwords if requested to access inappropriate urls. - -=cut - diff --git a/bin/agent_email b/bin/agent_email deleted file mode 100755 index 2fe47c4ba..000000000 --- a/bin/agent_email +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/perl - -use strict; -use Getopt::Std; -use FS::UID qw(adminsuidsetup); - -&untaint_argv; #what it sounds like (eww) -use vars qw(%opt); -getopts("a:", \%opt); - -my $user = shift or die &usage; -adminsuidsetup $user; - -use FS::Cron::agent_email qw(agent_email); -agent_email(%opt); - -### -# subroutines -### - -sub untaint_argv { - foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV - #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\""; - # Date::Parse - $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\""; - $ARGV[$_]=$1; - } -} - -1; diff --git a/httemplate/misc/delete-cust_credit.cgi b/httemplate/misc/delete-cust_credit.cgi deleted file mode 100755 index 03eb47299..000000000 --- a/httemplate/misc/delete-cust_credit.cgi +++ /dev/null @@ -1,21 +0,0 @@ -% if ( $error ) { -% errorpage($error); -% } else { -<% $cgi->redirect($p. "view/cust_main.cgi?". $custnum) %> -% } -<%init> - -die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Delete credit'); - -#untaint crednum -my($query) = $cgi->keywords; -$query =~ /^(\d+)$/ || die "Illegal crednum"; -my $crednum = $1; - -my $cust_credit = qsearchs('cust_credit',{'crednum'=>$crednum}); -my $custnum = $cust_credit->custnum; - -my $error = $cust_credit->delete; - - diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html index 1525e9314..c85559543 100644 --- a/httemplate/view/cust_main/payment_history.html +++ b/httemplate/view/cust_main/payment_history.html @@ -231,7 +231,7 @@ my %opt = ( 'Post refund', 'Post check refund', 'Post cash refund ', 'Refund payment', 'Credit card void', 'Echeck void', 'Void payments', 'Unvoid payments', 'Delete payment', 'Unapply payment', - 'Apply credit', 'Delete credit', 'Unapply credit', 'Void credit', 'Unvoid credit', + 'Apply credit', 'Unapply credit', 'Void credit', 'Unvoid credit', 'Delete refund', 'Billing event reports', 'View customer billing events', ) diff --git a/httemplate/view/cust_main/payment_history/credit.html b/httemplate/view/cust_main/payment_history/credit.html index 3eed833d3..db2e5e582 100644 --- a/httemplate/view/cust_main/payment_history/credit.html +++ b/httemplate/view/cust_main/payment_history/credit.html @@ -1,4 +1,4 @@ -<% $credit. ' '. $reason. $desc. $change_pkg. $apply. $delete. $unapply. $void %> +<% $credit. ' '. $reason. $desc. $change_pkg. $apply . $unapply. $void %> <%init> my( $cust_credit, %opt ) = @_; @@ -138,15 +138,6 @@ $void = ' ('. if $cust_credit->closed !~ /^Y/i && $opt{'Void credit'}; -my $delete = ''; -$delete = areyousure_link("${p}misc/delete-cust_credit.cgi?".$cust_credit->crednum, - emt('Are you sure you want to delete this credit?'), - '', - emt('delete') - ) - if $cust_credit->closed !~ /^Y/i - && $opt{'Delete credit'}; - my $unapply = ''; $unapply = areyousure_link("${p}misc/unapply-cust_credit.cgi?".$cust_credit->crednum, emt('Are you sure you want to unapply this credit?'), -- cgit v1.2.1 From 7427404751de534a767b44541f93915b35477116 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Wed, 23 Sep 2015 10:50:24 -0700 Subject: fix searches for cust_pay events, RT#35167 --- FS/FS/cust_event.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/cust_event.pm b/FS/FS/cust_event.pm index f299f9377..c35e1185b 100644 --- a/FS/FS/cust_event.pm +++ b/FS/FS/cust_event.pm @@ -302,7 +302,7 @@ sub join_sql { JOIN part_event USING ( eventpart ) LEFT JOIN cust_bill ON ( eventtable = 'cust_bill' AND tablenum = invnum ) LEFT JOIN cust_pkg ON ( eventtable = 'cust_pkg' AND tablenum = pkgnum ) - + LEFT JOIN cust_pay ON ( eventtable = 'cust_pay' AND tablenum = paynum ) LEFT JOIN cust_svc ON ( eventtable = 'svc_acct' AND tablenum = svcnum ) LEFT JOIN cust_pkg AS cust_pkg_for_svc ON ( cust_svc.pkgnum = cust_pkg_for_svc.pkgnum ) LEFT JOIN cust_main ON ( ( eventtable = 'cust_main' AND tablenum = cust_main.custnum ) -- cgit v1.2.1 From 60ca6141ee3efd2479dc89615504433a0d950356 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Wed, 23 Sep 2015 10:51:29 -0700 Subject: fix searches for cust_pay events, RT#35167 --- FS/FS/cust_event.pm | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/FS/FS/cust_event.pm b/FS/FS/cust_event.pm index c35e1185b..1d8af1e6e 100644 --- a/FS/FS/cust_event.pm +++ b/FS/FS/cust_event.pm @@ -9,6 +9,7 @@ use FS::Record qw( qsearch qsearchs dbdef ); use FS::cust_main; use FS::cust_pkg; use FS::cust_bill; +use FS::cust_pay; use FS::svc_acct; $DEBUG = 0; @@ -305,11 +306,13 @@ sub join_sql { LEFT JOIN cust_pay ON ( eventtable = 'cust_pay' AND tablenum = paynum ) LEFT JOIN cust_svc ON ( eventtable = 'svc_acct' AND tablenum = svcnum ) LEFT JOIN cust_pkg AS cust_pkg_for_svc ON ( cust_svc.pkgnum = cust_pkg_for_svc.pkgnum ) - LEFT JOIN cust_main ON ( ( eventtable = 'cust_main' AND tablenum = cust_main.custnum ) - OR ( eventtable = 'cust_bill' AND cust_bill.custnum = cust_main.custnum ) - OR ( eventtable = 'cust_pkg' AND cust_pkg.custnum = cust_main.custnum ) - OR ( eventtable = 'svc_acct' AND cust_pkg_for_svc.custnum = cust_main.custnum ) - ) + LEFT JOIN cust_main ON ( + ( eventtable = 'cust_main' AND tablenum = cust_main.custnum ) + OR ( eventtable = 'cust_bill' AND cust_bill.custnum = cust_main.custnum ) + OR ( eventtable = 'cust_pkg' AND cust_pkg.custnum = cust_main.custnum ) + OR ( eventtable = 'cust_pay' AND cust_pay.custnum = cust_main.custnum ) + OR ( eventtable = 'svc_acct' AND cust_pkg_for_svc.custnum = cust_main.custnum ) + ) "; } @@ -389,6 +392,11 @@ sub search_sql_where { "tablenum = '$1'"; } + if ( $param->{'paynum'} =~ /^(\d+)$/ ) { + push @search, "part_event.eventtable = 'cust_pay'", + "tablenum = '$1'"; + } + if ( $param->{'svcnum'} =~ /^(\d+)$/ ) { push @search, "part_event.eventtable = 'svc_acct'", "tablenum = '$1'"; -- cgit v1.2.1 From 1c83c4c02ba6d35ffbabe71bfd4cf6e70afbb894 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Wed, 23 Sep 2015 13:16:34 -0700 Subject: import a2billing username as charged_party, RT#32909 --- FS/bin/freeside-cdr-a2billing-import | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FS/bin/freeside-cdr-a2billing-import b/FS/bin/freeside-cdr-a2billing-import index 923f5fbb1..a8469e744 100755 --- a/FS/bin/freeside-cdr-a2billing-import +++ b/FS/bin/freeside-cdr-a2billing-import @@ -91,7 +91,7 @@ my %disposition = ( ); my @cols = ( - 'cc_call.id as id', 'cc_card.username as username', + "$table.id as id", 'cc_card.username as username', qw( sessionid starttime stoptime sessiontime real_sessiontime terminatecauseid @@ -101,6 +101,7 @@ my @cols = ( ); my $sql = 'SELECT '.join(',', @cols). " FROM $table". + " LEFT JOIN cc_card ON ( $table.card_id = cc_card.id ) ". ' WHERE freesidestatus IS NULL' . ($start && " AND starttime >= '$start'") . ($end && " AND starttime < '$end'") ; -- cgit v1.2.1 From c34a48fd2107adbc7ea08cf3aae007d70ec60b61 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 23 Sep 2015 23:56:32 -0500 Subject: RT#37908: Convert existing email-sending code to use common interface [removed template confs] --- FS/FS/Conf.pm | 104 +------------------ FS/FS/Cron/notify.pm | 9 -- FS/FS/cust_main.pm | 170 +++++++++++++++---------------- FS/FS/cust_main/Billing_Realtime.pm | 27 +---- FS/FS/cust_pkg.pm | 11 -- FS/FS/msg_template.pm | 13 ++- FS/FS/svc_acct.pm | 194 ++++++------------------------------ 7 files changed, 127 insertions(+), 401 deletions(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 5c4774ab5..db7dbd04d 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2354,13 +2354,6 @@ and customer address. Include units.', %msg_template_options, }, - { - 'key' => 'declinetemplate', - 'section' => 'deprecated', - 'description' => 'Template file for credit card and electronic check decline emails.', - 'type' => 'textarea', - }, - { 'key' => 'emaildecline', 'section' => 'notification', @@ -2384,20 +2377,6 @@ and customer address. Include units.', %msg_template_options, }, - { - 'key' => 'cancelmessage', - 'section' => 'deprecated', - 'description' => 'Template file for cancellation emails.', - 'type' => 'textarea', - }, - - { - 'key' => 'cancelsubject', - 'section' => 'deprecated', - 'description' => 'Subject line for cancellation emails.', - 'type' => 'text', - }, - { 'key' => 'emailcancel', 'section' => 'notification', @@ -2521,39 +2500,6 @@ and customer address. Include units.', 'multiple' => 1, }, - { - 'key' => 'welcome_email', - 'section' => 'deprecated', - 'description' => 'Template file for welcome email. Welcome emails are sent to the customer email invoice destination(s) each time a svc_acct record is created.', - 'type' => 'textarea', - 'per_agent' => 1, - }, - - { - 'key' => 'welcome_email-from', - 'section' => 'deprecated', - 'description' => 'From: address header for welcome email', - 'type' => 'text', - 'per_agent' => 1, - }, - - { - 'key' => 'welcome_email-subject', - 'section' => 'deprecated', - 'description' => 'Subject: header for welcome email', - 'type' => 'text', - 'per_agent' => 1, - }, - - { - 'key' => 'welcome_email-mimetype', - 'section' => 'deprecated', - 'description' => 'MIME type for welcome email', - 'type' => 'select', - 'select_enum' => [ 'text/plain', 'text/html' ], - 'per_agent' => 1, - }, - { 'key' => 'welcome_letter', 'section' => '', @@ -2561,47 +2507,11 @@ and customer address. Include units.', 'type' => 'textarea', }, -# { -# 'key' => 'warning_msgnum', -# 'section' => 'notification', -# 'description' => 'Template to use for warning messages, sent to the customer email invoice destination(s) when a svc_acct record has its usage drop below a threshold.', -# %msg_template_options, -# }, - - { - 'key' => 'warning_email', - 'section' => 'notification', - 'description' => 'Template file for warning email. Warning emails are sent to the customer email invoice destination(s) each time a svc_acct record has its usage drop below a threshold or 0. See the Text::Template documentation for details on the template substitution language. The following variables are available
  • $username
  • $password
  • $first
  • $last
  • $pkg
  • $column
  • $amount
  • $threshold
', - 'type' => 'textarea', - }, - - { - 'key' => 'warning_email-from', - 'section' => 'notification', - 'description' => 'From: address header for warning email', - 'type' => 'text', - }, - { - 'key' => 'warning_email-cc', + 'key' => 'threshold_warning_msgnum', 'section' => 'notification', - 'description' => 'Additional recipient(s) (comma separated) for warning email when remaining usage reaches zero.', - 'type' => 'text', - }, - - { - 'key' => 'warning_email-subject', - 'section' => 'notification', - 'description' => 'Subject: header for warning email', - 'type' => 'text', - }, - - { - 'key' => 'warning_email-mimetype', - 'section' => 'notification', - 'description' => 'MIME type for warning email', - 'type' => 'select', - 'select_enum' => [ 'text/plain', 'text/html' ], + 'description' => 'Template to use for warning messages sent to the customer email invoice destination(s) when a svc_acct record has its usage drop below a threshold. Extra substitutions available: $column, $amount, $threshold', + %msg_template_options, }, { @@ -4064,14 +3974,6 @@ and customer address. Include units.', %msg_template_options, }, - { - 'key' => 'impending_recur_template', - 'section' => 'deprecated', - 'description' => 'Template file for alerts about looming first time recurrant billing. See the Text::Template documentation for details on the template substitition language. Also see packages with a flat price plan The following variables are available
  • $packages allowing $packages->[0] thru $packages->[n]
  • $package the first package, same as $packages->[0]
  • $recurdates allowing $recurdates->[0] thru $recurdates->[n]
  • $recurdate the first recurdate, same as $recurdate->[0]
  • $first
  • $last
', -#
  • $payby
  • $expdate most likely only confuse - 'type' => 'textarea', - }, - { 'key' => 'logo.png', 'section' => 'UI', #'invoicing' ? diff --git a/FS/FS/Cron/notify.pm b/FS/FS/Cron/notify.pm index 6d7065429..34977c8e6 100644 --- a/FS/FS/Cron/notify.pm +++ b/FS/FS/Cron/notify.pm @@ -111,15 +111,6 @@ END $error = $msg_template->send('cust_main' => $cust_main, 'object' => $cust_main); } - else { - $error = $cust_main->notify( 'impending_recur_template', - 'extra_fields' => { 'packages' => \@packages, - 'recurdates' => \@recurdates, - 'package' => $packages[0], - 'recurdate' => $recurdates[0], - }, - ); - } #if $msgnum warn "Error notifying, custnum ". $cust_main->custnum. ": $error" if $error; unless ($error) { diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index c636408d8..6afbd1cf5 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -32,7 +32,7 @@ use Locale::Country; use FS::UID qw( dbh driver_name ); use FS::Record qw( qsearchs qsearch dbdef regexp_sql ); use FS::Cursor; -use FS::Misc qw( generate_email send_email generate_ps do_print money_pretty ); +use FS::Misc qw( generate_ps do_print money_pretty ); use FS::Msgcat qw(gettext); use FS::CurrentUser; use FS::TicketSystem; @@ -4574,102 +4574,102 @@ sub search { =over 4 -=item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS +#=item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS -Deprecated. Use event notification and message templates -(L) instead. +#Deprecated. Use event notification and message templates +#(L) instead. -Sends a templated email notification to the customer (see L). +#Sends a templated email notification to the customer (see L). -OPTIONS is a hash and may include - -I - the email sender (default is invoice_from) +#OPTIONS is a hash and may include -I - comma-separated scalar or arrayref of recipients - (default is invoicing_list) +#I - the email sender (default is invoice_from) -I - The subject line of the sent email notification - (default is "Notice from company_name") +#I - comma-separated scalar or arrayref of recipients +# (default is invoicing_list) -I - a hashref of name/value pairs which will be substituted - into the template +#I - The subject line of the sent email notification +# (default is "Notice from company_name") -The following variables are vavailable in the template. +#I - a hashref of name/value pairs which will be substituted +# into the template -I<$first> - the customer first name -I<$last> - the customer last name -I<$company> - the customer company -I<$payby> - a description of the method of payment for the customer - # would be nice to use FS::payby::shortname -I<$payinfo> - the account information used to collect for this customer -I<$expdate> - the expiration of the customer payment in seconds from epoch - -=cut - -sub notify { - my ($self, $template, %options) = @_; +#The following variables are vavailable in the template. - return unless $conf->exists($template); +#I<$first> - the customer first name +#I<$last> - the customer last name +#I<$company> - the customer company +#I<$payby> - a description of the method of payment for the customer +# # would be nice to use FS::payby::shortname +#I<$payinfo> - the account information used to collect for this customer +#I<$expdate> - the expiration of the customer payment in seconds from epoch - my $from = $conf->invoice_from_full($self->agentnum) - if $conf->exists('invoice_from', $self->agentnum); - $from = $options{from} if exists($options{from}); - - my $to = join(',', $self->invoicing_list_emailonly); - $to = $options{to} if exists($options{to}); - - my $subject = "Notice from " . $conf->config('company_name', $self->agentnum) - if $conf->exists('company_name', $self->agentnum); - $subject = $options{subject} if exists($options{subject}); - - my $notify_template = new Text::Template (TYPE => 'ARRAY', - SOURCE => [ map "$_\n", - $conf->config($template)] - ) - or die "can't create new Text::Template object: Text::Template::ERROR"; - $notify_template->compile() - or die "can't compile template: Text::Template::ERROR"; - - $FS::notify_template::_template::company_name = - $conf->config('company_name', $self->agentnum); - $FS::notify_template::_template::company_address = - join("\n", $conf->config('company_address', $self->agentnum) ). "\n"; - - my $paydate = $self->paydate || '2037-12-31'; - $FS::notify_template::_template::first = $self->first; - $FS::notify_template::_template::last = $self->last; - $FS::notify_template::_template::company = $self->company; - $FS::notify_template::_template::payinfo = $self->mask_payinfo; - my $payby = $self->payby; - my ($payyear,$paymonth,$payday) = split (/-/,$paydate); - my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear); - - #credit cards expire at the end of the month/year of their exp date - if ($payby eq 'CARD' || $payby eq 'DCRD') { - $FS::notify_template::_template::payby = 'credit card'; - ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++); - $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear); - $expire_time--; - }elsif ($payby eq 'COMP') { - $FS::notify_template::_template::payby = 'complimentary account'; - }else{ - $FS::notify_template::_template::payby = 'current method'; - } - $FS::notify_template::_template::expdate = $expire_time; - - for (keys %{$options{extra_fields}}){ - no strict "refs"; - ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_}; - } +#=cut - send_email(from => $from, - to => $to, - subject => $subject, - body => $notify_template->fill_in( PACKAGE => - 'FS::notify_template::_template' ), - ); +#sub notify { +# my ($self, $template, %options) = @_; + +# return unless $conf->exists($template); + +# my $from = $conf->invoice_from_full($self->agentnum) +# if $conf->exists('invoice_from', $self->agentnum); +# $from = $options{from} if exists($options{from}); + +# my $to = join(',', $self->invoicing_list_emailonly); +# $to = $options{to} if exists($options{to}); +# +# my $subject = "Notice from " . $conf->config('company_name', $self->agentnum) +# if $conf->exists('company_name', $self->agentnum); +# $subject = $options{subject} if exists($options{subject}); + +# my $notify_template = new Text::Template (TYPE => 'ARRAY', +# SOURCE => [ map "$_\n", +# $conf->config($template)] +# ) +# or die "can't create new Text::Template object: Text::Template::ERROR"; +# $notify_template->compile() +# or die "can't compile template: Text::Template::ERROR"; + +# $FS::notify_template::_template::company_name = +# $conf->config('company_name', $self->agentnum); +# $FS::notify_template::_template::company_address = +# join("\n", $conf->config('company_address', $self->agentnum) ). "\n"; + +# my $paydate = $self->paydate || '2037-12-31'; +# $FS::notify_template::_template::first = $self->first; +# $FS::notify_template::_template::last = $self->last; +# $FS::notify_template::_template::company = $self->company; +# $FS::notify_template::_template::payinfo = $self->mask_payinfo; +# my $payby = $self->payby; +# my ($payyear,$paymonth,$payday) = split (/-/,$paydate); +# my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear); + +# #credit cards expire at the end of the month/year of their exp date +# if ($payby eq 'CARD' || $payby eq 'DCRD') { +# $FS::notify_template::_template::payby = 'credit card'; +# ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++); +# $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear); +# $expire_time--; +# }elsif ($payby eq 'COMP') { +# $FS::notify_template::_template::payby = 'complimentary account'; +# }else{ +# $FS::notify_template::_template::payby = 'current method'; +# } +# $FS::notify_template::_template::expdate = $expire_time; + +# for (keys %{$options{extra_fields}}){ +# no strict "refs"; +# ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_}; +# } + +# send_email(from => $from, +# to => $to, +# subject => $subject, +# body => $notify_template->fill_in( PACKAGE => +# 'FS::notify_template::_template' ), +# ); -} +#} =item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index fda3ae040..c6b3b3180 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -8,7 +8,6 @@ use Data::Dumper; use Business::CreditCard 0.28; use FS::UID qw( dbh ); use FS::Record qw( qsearch qsearchs ); -use FS::Misc qw( send_email ); use FS::payby; use FS::cust_pay; use FS::cust_pay_pending; @@ -1121,31 +1120,7 @@ sub _realtime_bop_result { $error = $msg_template->send( 'cust_main' => $self, 'object' => $cust_pay_pending ); } - else { #!$msgnum - - my @templ = $conf->config('declinetemplate'); - my $template = new Text::Template ( - TYPE => 'ARRAY', - SOURCE => [ map "$_\n", @templ ], - ) or return "($perror) can't create template: $Text::Template::ERROR"; - $template->compile() - or return "($perror) can't compile template: $Text::Template::ERROR"; - - my $templ_hash = { - 'company_name' => - scalar( $conf->config('company_name', $self->agentnum ) ), - 'company_address' => - join("\n", $conf->config('company_address', $self->agentnum ) ), - 'error' => $transaction->error_message, - }; - - my $error = send_email( - 'from' => $conf->invoice_from_full( $self->agentnum ), - 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ], - 'subject' => 'Your payment could not be processed', - 'body' => [ $template->fill_in(HASH => $templ_hash) ], - ); - } + $perror .= " (also received error sending decline notification: $error)" if $error; diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 0ef7aa0fa..279205b19 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -13,7 +13,6 @@ use Tie::IxHash; use Time::Local qw( timelocal timelocal_nocheck ); use MIME::Entity; use FS::UID qw( dbh driver_name ); -use FS::Misc qw( send_email ); use FS::Record qw( qsearch qsearchs fields ); use FS::CurrentUser; use FS::cust_svc; @@ -1057,16 +1056,6 @@ sub cancel { $error = $msg_template->send( 'cust_main' => $self->cust_main, 'object' => $self ); } - else { - $error = send_email( - 'from' => $conf->invoice_from_full( $self->cust_main->agentnum ), - 'to' => \@invoicing_list, - 'subject' => ( $conf->config('cancelsubject') || 'Cancellation Notice' ), - 'body' => [ map "$_\n", $conf->config('cancelmessage') ], - 'custnum' => $self->custnum, - 'msgtype' => '', #admin? - ); - } #should this do something on errors? } diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index 4c2ac4bd4..49403889c 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -94,6 +94,7 @@ sub _rebless { my $class = 'FS::msg_template::' . $self->msgclass; eval "use $class;"; bless($self, $class) unless $@; + warn "Error loading msg_template msgclass: " . $@ if $@; #or die? # merge in the extension fields (but let fields in $self override them) # except don't ever override the extension's primary key, it's immutable @@ -657,20 +658,22 @@ sub _upgrade_data { [ 'decline_msgnum', 'declinetemplate', '', '', '' ], [ 'impending_recur_msgnum', 'impending_recur_template', '', '', 'impending_recur_bcc' ], [ 'payment_receipt_msgnum', 'payment_receipt_email', '', '', '' ], - [ 'welcome_msgnum', 'welcome_email', 'welcome_email-subject', 'welcome_email-from', '' ], - [ 'warning_msgnum', 'warning_email', 'warning_email-subject', 'warning_email-from', '' ], + [ 'welcome_msgnum', 'welcome_email', 'welcome_email-subject', 'welcome_email-from', '', 'welcome_email-mimetype' ], + [ 'threshold_warning_msgnum', 'warning_email', 'warning_email-subject', 'warning_email-from', 'warning_email-cc', 'warning_email-mimetype' ], ); my @agentnums = ('', map {$_->agentnum} qsearch('agent', {})); foreach my $agentnum (@agentnums) { foreach (@fixes) { - my ($newname, $oldname, $subject, $from, $bcc) = @$_; + my ($newname, $oldname, $subject, $from, $bcc, $mimetype) = @$_; + if ($conf->exists($oldname, $agentnum)) { my $new = new FS::msg_template({ + 'msgclass' => 'email', 'msgname' => $oldname, 'agentnum' => $agentnum, 'from_addr' => ($from && $conf->config($from, $agentnum)) || '', - 'bcc_addr' => ($bcc && $conf->config($from, $agentnum)) || '', + 'bcc_addr' => ($bcc && $conf->config($bcc, $agentnum)) || '', 'subject' => ($subject && $conf->config($subject, $agentnum)) || '', 'mime_type' => 'text/html', 'body' => join('
    ',$conf->config($oldname, $agentnum)), @@ -681,6 +684,8 @@ sub _upgrade_data { $conf->delete($oldname, $agentnum); $conf->delete($from, $agentnum) if $from; $conf->delete($subject, $agentnum) if $subject; + $conf->delete($bcc, $agentnum) if $bcc; + $conf->delete($mimetype, $agentnum) if $mimetype; } } diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm index 0181b1e0e..f3070338b 100644 --- a/FS/FS/svc_acct.pm +++ b/FS/FS/svc_acct.pm @@ -17,8 +17,7 @@ use vars qw( $DEBUG $me $conf $skip_fuzzyfiles $username_slash $username_equals $username_pound $username_exclamation $password_noampersand $password_noexclamation - $warning_template $warning_from $warning_subject $warning_mimetype - $warning_cc + $warning_msgnum $smtpmachine $radius_password $radius_ip $dirhash @@ -90,22 +89,7 @@ FS::UID->install_callback( sub { $password_noampersand = $conf->exists('password-noexclamation'); $password_noexclamation = $conf->exists('password-noexclamation'); $dirhash = $conf->config('dirhash') || 0; - if ( $conf->exists('warning_email') ) { - $warning_template = new Text::Template ( - TYPE => 'ARRAY', - SOURCE => [ map "$_\n", $conf->config('warning_email') ] - ) or warn "can't create warning email template: $Text::Template::ERROR"; - $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum' - $warning_subject = $conf->config('warning_email-subject') || 'Warning'; - $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain'; - $warning_cc = $conf->config('warning_email-cc'); - } else { - $warning_template = ''; - $warning_from = ''; - $warning_subject = ''; - $warning_mimetype = ''; - $warning_cc = ''; - } + $warning_msgnum = $conf->config('threshold_warning_msgnum'); $smtpmachine = $conf->config('smtpmachine'); $radius_password = $conf->config('radius-password') || 'Password'; $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address'; @@ -737,83 +721,8 @@ sub insert { my $msg_template = qsearchs('msg_template', { msgnum => $msgnum }); $error = $msg_template->send('cust_main' => $cust_main, 'object' => $self); + #should this do something on error? } - else { #!$msgnum - my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype) - = ('','','','','',''); - - if ( $conf->exists('welcome_email', $agentnum) ) { - $welcome_template = new Text::Template ( - TYPE => 'ARRAY', - SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ] - ) or warn "can't create welcome email template: $Text::Template::ERROR"; - $welcome_from = $conf->config('welcome_email-from', $agentnum); - # || 'your-isp-is-dum' - $welcome_subject = $conf->config('welcome_email-subject', $agentnum) - || 'Welcome'; - $welcome_subject_template = new Text::Template ( - TYPE => 'STRING', - SOURCE => $welcome_subject, - ) or warn "can't create welcome email subject template: $Text::Template::ERROR"; - $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum) - || 'text/plain'; - } - if ( $welcome_template ) { - my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list ); - if ( $to ) { - - my %hash = ( - 'custnum' => $self->custnum, - 'username' => $self->username, - 'password' => $self->_password, - 'first' => $cust_main->first, - 'last' => $cust_main->getfield('last'), - 'pkg' => $cust_pkg->part_pkg->pkg, - ); - my $wqueue = new FS::queue { - 'svcnum' => $self->svcnum, - 'job' => 'FS::svc_acct::send_email' - }; - my $error = $wqueue->insert( - 'to' => $to, - 'from' => $welcome_from, - 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ), - 'mimetype' => $welcome_mimetype, - 'body' => $welcome_template->fill_in( HASH => \%hash, ), - ); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "error queuing welcome email: $error"; - } - - if ( $options{'depend_jobnum'} ) { - warn "$me depend_jobnum found; adding to welcome email dependancies" - if $DEBUG; - if ( ref($options{'depend_jobnum'}) ) { - warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ). - "to welcome email dependancies" - if $DEBUG; - push @jobnums, @{ $options{'depend_jobnum'} }; - } else { - warn "$me adding job $options{'depend_jobnum'} ". - "to welcome email dependancies" - if $DEBUG; - push @jobnums, $options{'depend_jobnum'}; - } - } - - foreach my $jobnum ( @jobnums ) { - my $error = $wqueue->depend_insert($jobnum); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "error queuing welcome email job dependancy: $error"; - } - } - - } - - } # if $welcome_template - } # if !$msgnum } } # if $cust_pkg @@ -2119,23 +2028,17 @@ sub _op_usage { } } - if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) { + if ($warning_msgnum && &{$op2warncondition{$op}}($self, $column, $amount)) { my $wqueue = new FS::queue { 'svcnum' => $self->svcnum, 'job' => 'FS::svc_acct::reached_threshold', }; - my $to = ''; - if ($op eq '-'){ - $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount); - } - # x_threshold race my $error = $wqueue->insert( 'svcnum' => $self->svcnum, 'op' => $op, - 'column' => $column, - 'to' => $to, + 'column' => $column ); if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -2834,32 +2737,6 @@ sub _search_svc { =over 4 -=item send_email - -This is the FS::svc_acct job-queue-able version. It still uses -FS::Misc::send_email under-the-hood. - -=cut - -sub send_email { - my %opt = @_; - - eval "use FS::Misc qw(send_email)"; - die $@ if $@; - - $opt{mimetype} ||= 'text/plain'; - $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/; - - my $error = send_email( - 'from' => $opt{from}, - 'to' => $opt{to}, - 'subject' => $opt{subject}, - 'content-type' => $opt{mimetype}, - 'body' => [ map "$_\n", split("\n", $opt{body}) ], - ); - die $error if $error; -} - =item check_and_rebuild_fuzzyfiles =cut @@ -2973,46 +2850,33 @@ sub reached_threshold { my $error = $svc_acct->replace; die $error if $error; # email next time, i guess - if ( $warning_template ) { - eval "use FS::Misc qw(send_email)"; - die $@ if $@; + if ( $warning_msgnum ) { - my $cust_pkg = $svc_acct->cust_svc->cust_pkg; - my $cust_main = $cust_pkg->cust_main; + my $msg_template = qsearchs('msg_template',{ msgnum => $warning_msgnum }); + die "Could not load template for threshold_warning_msgnum ($warning_msgnum)" unless $msg_template; - my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } - $cust_main->invoicing_list, - ($opt{'to'} ? $opt{'to'} : ()) - ); - - my $mimetype = $warning_mimetype; - $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/; - - my $body = $warning_template->fill_in( HASH => { - 'custnum' => $cust_main->custnum, - 'username' => $svc_acct->username, - 'password' => $svc_acct->_password, - 'first' => $cust_main->first, - 'last' => $cust_main->getfield('last'), - 'pkg' => $cust_pkg->part_pkg->pkg, - 'column' => $opt{'column'}, - 'amount' => $opt{'column'} =~/bytes/ - ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'})) - : $svc_acct->getfield($opt{'column'}), - 'threshold' => $opt{'column'} =~/bytes/ - ? FS::UI::bytecount::display_bytecount($threshold) - : $threshold, - } ); - - - my $error = send_email( - 'from' => $warning_from, - 'to' => $to, - 'subject' => $warning_subject, - 'content-type' => $mimetype, - 'body' => [ map "$_\n", split("\n", $body) ], + my $cust_main = $svc_acct->cust_svc->cust_pkg->cust_main; + + my $to = join(', ', $cust_main->invoicing_list_emailonly ); + + my $error = $msg_template->send( + cust_main => $cust_main, + object => $svc_acct, + to => $to, + substitutions => { + # have to override these, because we changed threshold above + 'column' => $opt{'column'}, + 'amount' => $opt{'column'} =~/bytes/ + ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'})) + : $svc_acct->getfield($opt{'column'}), + 'threshold' => $opt{'column'} =~/bytes/ + ? FS::UI::bytecount::display_bytecount($threshold) + : $threshold, + }, ); - die $error if $error; + + die "Error sending threshold warning email: $error" if $error; + } }else{ die "unknown op: " . $opt{'op'}; -- cgit v1.2.1 From 1cfc3ea3efb8c75388ad344ea9481f6f8df072b9 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 25 Sep 2015 17:06:44 -0500 Subject: RT#37908: Convert existing email-sending code to use common interface [switched jobs to use cust_msg::process_send, bug fix to http] --- FS/FS/Conf.pm | 7 ----- FS/FS/Misc.pm | 28 ------------------- FS/FS/contact.pm | 8 ++++-- FS/FS/cust_pay.pm | 69 ++++++++++------------------------------------ FS/FS/msg_template/http.pm | 2 +- 5 files changed, 21 insertions(+), 93 deletions(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index db7dbd04d..26dbbcd23 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -1662,13 +1662,6 @@ and customer address. Include units.', 'per_agent' => 1, }, - { - 'key' => 'payment_receipt_email', - 'section' => 'deprecated', - 'description' => 'Template file for payment receipts. Payment receipts are sent to the customer email invoice destination(s) when a payment is received.', - 'type' => [qw( checkbox textarea )], - }, - { 'key' => 'payment_receipt-trigger', 'section' => 'notification', diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm index e1f654c34..d06653edd 100644 --- a/FS/FS/Misc.pm +++ b/FS/FS/Misc.pm @@ -413,34 +413,6 @@ sub generate_email { } -=item process_send_email OPTION => VALUE ... - -Takes arguments as per generate_email() and sends the message. This -will die on any error and can be used in the job queue. - -=cut - -sub process_send_email { - my %message = @_; - my $error = send_email(generate_email(%message)); - die "$error\n" if $error; - ''; -} - -=item process_send_generated_email OPTION => VALUE ... - -Takes arguments as per send_email() and sends the message. This -will die on any error and can be used in the job queue. - -=cut - -sub process_send_generated_email { - my %args = @_; - my $error = send_email(%args); - die "$error\n" if $error; - ''; -} - =item send_fax OPTION => VALUE ... Options: diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm index 38b7fd7b7..612048022 100644 --- a/FS/FS/contact.pm +++ b/FS/FS/contact.pm @@ -837,6 +837,7 @@ sub send_reset_email { #die "selfservice-password_reset_msgnum unset" unless $msgnum; return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum; my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } ); + return { 'error' => "selfservice-password_reset_msgnum cannot be loaded" } unless $msg_template; my %msg_template = ( 'to' => join(',', map $_->emailaddress, @contact_email ), 'cust_main' => $cust_main, @@ -846,11 +847,14 @@ sub send_reset_email { if ( $opt{'queue'} ) { #or should queueing just be the default? + my $cust_msg = $msg_template->prepare( %msg_template ); + my $error = $cust_msg->insert; + return { 'error' => $error } if $error; my $queue = new FS::queue { - 'job' => 'FS::Misc::process_send_email', + 'job' => 'FS::cust_msg::process_send', 'custnum' => $cust_main ? $cust_main->custnum : '', }; - $queue->insert( $msg_template->prepare( %msg_template ) ); + $queue->insert( $cust_msg->custmsgnum ); } else { diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index cb39d4391..89bb193d2 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -661,72 +661,31 @@ sub send_receipt { my %substitutions = (); $substitutions{invnum} = $opt->{cust_bill}->invnum if $opt->{cust_bill}; - my $queue = new FS::queue { - 'job' => 'FS::Misc::process_send_email', - 'paynum' => $self->paynum, - 'custnum' => $cust_main->custnum, - }; - $error = $queue->insert( - FS::msg_template->by_key($msgnum)->prepare( + my $msg_template = qsearchs('msg_template',{ msgnum => $msgnum}); + unless ($msg_template) { + warn "send_receipt could not load msg_template"; + return; + } + + my $cust_msg = $msg_template->prepare( 'cust_main' => $cust_main, 'object' => $self, 'from_config' => 'payment_receipt_from', 'substitutions' => \%substitutions, - ), - 'msgtype' => 'receipt', # override msg_template's default - ); - - } elsif ( $conf->exists('payment_receipt_email') ) { - - my $receipt_template = new Text::Template ( - TYPE => 'ARRAY', - SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ], - ) or do { - warn "can't create payment receipt template: $Text::Template::ERROR"; - return ''; - }; - - my $payby = $self->payby; - my $payinfo = $self->payinfo; - $payby =~ s/^BILL$/Check/ if $payinfo; - if ( $payby eq 'CARD' || $payby eq 'CHEK' ) { - $payinfo = $self->paymask - } else { - $payinfo = $self->decrypt($payinfo); - } - $payby =~ s/^CHEK$/Electronic check/; - - my %fill_in = ( - 'date' => time2str("%a %B %o, %Y", $self->_date), - 'name' => $cust_main->name, - 'paynum' => $self->paynum, - 'paid' => sprintf("%.2f", $self->paid), - 'payby' => ucfirst(lc($payby)), - 'payinfo' => $payinfo, - 'balance' => $cust_main->balance, - 'company_name' => $conf->config('company_name', $cust_main->agentnum), + 'msgtype' => 'receipt', ); - - $fill_in{'invnum'} = $opt->{cust_bill}->invnum if $opt->{cust_bill}; - - if ( $opt->{'cust_pkg'} ) { - $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg; - #setup date, other things? + $error = $cust_msg ? $cust_msg->insert : 'error preparing msg_template'; + if ($error) { + warn "send_receipt: $error"; + return; } my $queue = new FS::queue { - 'job' => 'FS::Misc::process_send_generated_email', + 'job' => 'FS::cust_msg::process_send', 'paynum' => $self->paynum, 'custnum' => $cust_main->custnum, - 'msgtype' => 'receipt', }; - $error = $queue->insert( - 'from' => $conf->invoice_from_full( $cust_main->agentnum ), - #invoice_from??? well as good as any - 'to' => \@invoicing_list, - 'subject' => 'Payment receipt', - 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ], - ); + $error = $queue->insert( $cust_msg->custmsgnum ); } else { diff --git a/FS/FS/msg_template/http.pm b/FS/FS/msg_template/http.pm index a2b0986ea..9c4e68bd7 100644 --- a/FS/FS/msg_template/http.pm +++ b/FS/FS/msg_template/http.pm @@ -61,7 +61,7 @@ sub prepare { }; # put override content _somewhere_ so it can be used if ( $opt{'override_content'} ) { - $document{'content'} = $opt{'override_content'}; + $document->{'content'} = $opt{'override_content'}; } my $request_content = $json->encode($document); -- cgit v1.2.1 From 4c8c839f65491c9ec41e78fce02ab5c91a5f4595 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Mon, 28 Sep 2015 10:08:02 -0400 Subject: 37669 Additional back-office disclaimers --- FS/FS/API.pm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/FS/FS/API.pm b/FS/FS/API.pm index f848361ac..7ee080257 100644 --- a/FS/FS/API.pm +++ b/FS/FS/API.pm @@ -24,7 +24,9 @@ This module implements a backend API for advanced back-office integration. In contrast to the self-service API, which authenticates an end-user and offers functionality to that end user, the backend API performs a simple shared-secret authentication and offers full, administrator functionality, enabling -integration with other back-office systems. +integration with other back-office systems. Only ccess this API from a secure +network from other backoffice machines. DON'T use this API to create customer +portal functionality. If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block the port by default, only allow access from back-office servers with the same -- cgit v1.2.1 From 076f8cdad3dea2c56859df36479be398074c4807 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 28 Sep 2015 20:17:40 -0700 Subject: remove credit voiding ACL, RT#37908 --- httemplate/edit/process/cust_credit_bill.cgi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/httemplate/edit/process/cust_credit_bill.cgi b/httemplate/edit/process/cust_credit_bill.cgi index d3847dc40..db15eac18 100755 --- a/httemplate/edit/process/cust_credit_bill.cgi +++ b/httemplate/edit/process/cust_credit_bill.cgi @@ -12,8 +12,7 @@ die "access denied" if ( $cgi->param('src_amount') ) { die "access denied" - unless ( $FS::CurrentUser::CurrentUser->access_right('Post credit') && - $FS::CurrentUser::CurrentUser->access_right('Delete credit') ); + unless $FS::CurrentUser::CurrentUser->access_right('Post credit') } -- cgit v1.2.1 From 727d620374a9798dd2fe630d57e707fe16a63e49 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 28 Sep 2015 20:21:12 -0700 Subject: remove payment deletion, RT#37908 --- FS/FS/AccessRight.pm | 4 ---- httemplate/view/cust_main/payment_history.html | 2 +- httemplate/view/cust_main/payment_history/payment.html | 12 +----------- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 3f2c0f35d..95cf29a8b 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -209,7 +209,6 @@ tie my %rights, 'Tie::IxHash', { rightname=>'Process payment', desc=>'Process credit card or e-check payments' }, 'Process credit card payment', 'Process Echeck payment', - { rightname=>'Delete payment', desc=>'Enable deletion of unclosed payments. Be very careful! Only delete payments that were data-entry errors, not adjustments.' }, #aka. deletepayments Optionally specify one or more comma-separated email addresses to be notified when a payment is deleted. ], ### @@ -223,7 +222,6 @@ tie my %rights, 'Tie::IxHash', 'Void credit', #NEWER than things marked NEWNEWNEW 'Unvoid credit', #NEWER than things marked NEWNEWNEW { rightname=>'Unapply credit', desc=>'Enable "unapplication" of unclosed credits.' }, #aka unapplycredits - { rightname=>'Delete credit', desc=>'Enable deletion of unclosed credits. Be very careful! Only delete credits that were data-entry errors, not adjustments.' }, 'View refunds', { rightname=>'Post refund', desc=>'Enable posting of check and cash refunds.' }, 'Post check refund', @@ -441,8 +439,6 @@ Most (but not all) right names. sub default_superuser_rights { my $class = shift; my %omit = map { $_=>1 } ( - 'Delete payment', - 'Delete credit', #? 'Delete refund', #? 'Edit customer package dates', 'Time queue', diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html index c85559543..d81fe9935 100644 --- a/httemplate/view/cust_main/payment_history.html +++ b/httemplate/view/cust_main/payment_history.html @@ -230,7 +230,7 @@ my %opt = ( 'Apply payment', 'Refund credit card payment', 'Refund Echeck payment', 'Post refund', 'Post check refund', 'Post cash refund ', 'Refund payment', 'Credit card void', 'Echeck void', 'Void payments', 'Unvoid payments', - 'Delete payment', 'Unapply payment', + 'Unapply payment', 'Apply credit', 'Unapply credit', 'Void credit', 'Unvoid credit', 'Delete refund', 'Billing event reports', 'View customer billing events', diff --git a/httemplate/view/cust_main/payment_history/payment.html b/httemplate/view/cust_main/payment_history/payment.html index 0ed2f8003..fd336b86c 100644 --- a/httemplate/view/cust_main/payment_history/payment.html +++ b/httemplate/view/cust_main/payment_history/payment.html @@ -1,5 +1,5 @@ <% $payment. ' '. $info. $desc. - $view. $change_pkg. $apply. $refund. $void. $delete. $unapply + $view. $change_pkg. $apply. $refund. $void. $unapply %> <%init> @@ -185,16 +185,6 @@ $void = areyousure_link("${p}misc/void-cust_pay.cgi?".$cust_pay->paynum, || ( $cust_pay->payby !~ /^(CARD|CHEK)$/ && $opt{'Void payments'} ) ); -my $delete = ''; -$delete = areyousure_link("${p}misc/delete-cust_pay.cgi?".$cust_pay->paynum, - emt('Are you sure you want to delete this payment?'), - emt('Delete this payment from the database completely - not recommended'), - emt('delete') - ) - if $cust_pay->closed !~ /^Y/i - && $opt{'deletepayments'} - && $opt{'Delete payment'}; - my $unapply = ''; $unapply = areyousure_link("${p}misc/unapply-cust_pay.cgi?".$cust_pay->paynum, emt('Are you sure you want to unapply this payment?'), -- cgit v1.2.1 From d3880a1402fd46454ab448d59c2e4ff861094981 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 28 Sep 2015 20:21:27 -0700 Subject: remove payment deletion, RT#37908 --- httemplate/misc/delete-cust_pay.cgi | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100755 httemplate/misc/delete-cust_pay.cgi diff --git a/httemplate/misc/delete-cust_pay.cgi b/httemplate/misc/delete-cust_pay.cgi deleted file mode 100755 index 38e7e4ba1..000000000 --- a/httemplate/misc/delete-cust_pay.cgi +++ /dev/null @@ -1,21 +0,0 @@ -% if ( $error ) { -% errorpage($error); -% } else { -<% $cgi->redirect($p. "view/cust_main.cgi?". $custnum) %> -% } -<%init> - -die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Delete payment'); - -#untaint paynum -my($query) = $cgi->keywords; -$query =~ /^(\d+)$/ || die "Illegal paynum"; -my $paynum = $1; - -my $cust_pay = qsearchs('cust_pay',{'paynum'=>$paynum}); -my $custnum = $cust_pay->custnum; - -my $error = $cust_pay->delete; - - -- cgit v1.2.1 From c74ee632580cc2b90175bb266e442bce54d4b400 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 29 Sep 2015 00:00:42 -0500 Subject: RT#38048 not storing credit card #s --- httemplate/misc/process/payment.cgi | 77 +++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/httemplate/misc/process/payment.cgi b/httemplate/misc/process/payment.cgi index 27b818660..d9299e5bd 100644 --- a/httemplate/misc/process/payment.cgi +++ b/httemplate/misc/process/payment.cgi @@ -135,6 +135,45 @@ $cgi->param('discount_term') =~ /^(\d*)$/ or errorpage("illegal discount_term"); my $discount_term = $1; +# save first, for proper tokenization later +if ( $cgi->param('save') ) { + my $new = new FS::cust_main { $cust_main->hash }; + if ( $payby eq 'CARD' ) { + $new->set( 'payby' => ( $cgi->param('auto') ? 'CARD' : 'DCRD' ) ); + } elsif ( $payby eq 'CHEK' ) { + $new->set( 'payby' => ( $cgi->param('auto') ? 'CHEK' : 'DCHK' ) ); + } else { + die "unknown payby $payby"; + } + $new->payinfo($payinfo); #to properly set paymask + $new->set( 'paydate' => "$year-$month-01" ); + $new->set( 'payname' => $payname ); + + #false laziness w/FS:;cust_main::realtime_bop - check both to make sure + # working correctly + if ( $payby eq 'CARD' && + grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save') ) { + $new->set( 'paycvv' => $paycvv ); + } else { + $new->set( 'paycvv' => ''); + } + + if ( $payby eq 'CARD' ) { + my $bill_location = FS::cust_location->new; + $bill_location->set( $_ => $cgi->param($_) ) + foreach @{$payby2fields{$payby}}; + $new->set('bill_location' => $bill_location); + # will do nothing if the fields are all unchanged + } else { + $new->set( $_ => $cgi->param($_) ) foreach @{$payby2fields{$payby}}; + } + + my $error = $new->replace($cust_main); + errorpage("error saving info, payment not processed: $error") + if $error; + $cust_main = $new; +} + my $error = ''; my $paynum = ''; if ( $cgi->param('batch') ) { @@ -190,44 +229,6 @@ if ( $cgi->param('batch') ) { } -if ( $cgi->param('save') ) { - my $new = new FS::cust_main { $cust_main->hash }; - if ( $payby eq 'CARD' ) { - $new->set( 'payby' => ( $cgi->param('auto') ? 'CARD' : 'DCRD' ) ); - } elsif ( $payby eq 'CHEK' ) { - $new->set( 'payby' => ( $cgi->param('auto') ? 'CHEK' : 'DCHK' ) ); - } else { - die "unknown payby $payby"; - } - $new->set( 'payinfo' => $cust_main->card_token || $payinfo ); - $new->set( 'paydate' => "$year-$month-01" ); - $new->set( 'payname' => $payname ); - - #false laziness w/FS:;cust_main::realtime_bop - check both to make sure - # working correctly - if ( $payby eq 'CARD' && - grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save') ) { - $new->set( 'paycvv' => $paycvv ); - } else { - $new->set( 'paycvv' => ''); - } - - if ( $payby eq 'CARD' ) { - my $bill_location = FS::cust_location->new; - $bill_location->set( $_ => $cgi->param($_) ) - foreach @{$payby2fields{$payby}}; - $new->set('bill_location' => $bill_location); - # will do nothing if the fields are all unchanged - } else { - $new->set( $_ => $cgi->param($_) ) foreach @{$payby2fields{$payby}}; - } - - my $error = $new->replace($cust_main); - errorpage("payment processed successfully, but error saving info: $error") - if $error; - $cust_main = $new; -} - #success! -- cgit v1.2.1 From f474f79841172f506370814b14a7efe80545f472 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Tue, 29 Sep 2015 17:12:26 -0400 Subject: 37669 Fix typo --- FS/FS/API.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/API.pm b/FS/FS/API.pm index 7ee080257..9dbbc3c4f 100644 --- a/FS/FS/API.pm +++ b/FS/FS/API.pm @@ -24,7 +24,7 @@ This module implements a backend API for advanced back-office integration. In contrast to the self-service API, which authenticates an end-user and offers functionality to that end user, the backend API performs a simple shared-secret authentication and offers full, administrator functionality, enabling -integration with other back-office systems. Only ccess this API from a secure +integration with other back-office systems. Only access this API from a secure network from other backoffice machines. DON'T use this API to create customer portal functionality. -- cgit v1.2.1 From eb439974e7aa85bb7ee31ed1e3f432bc2a7a250b Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 29 Sep 2015 21:43:23 -0500 Subject: RT#38048: not storing credit card #s [save-first fix for selfservice] --- FS/FS/ClientAPI/MyAccount.pm | 71 ++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 6332dd75b..98b87ad55 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -1142,37 +1142,6 @@ sub do_process_payment { my $payby = delete $validate->{'payby'}; - my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount, - 'quiet' => 1, - 'manual' => 1, - 'selfservice' => 1, - 'paynum_ref' => \$paynum, - %$validate, - ); - 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', $cust_main->agentnum); - 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 }; - - $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'} ) { my $new = new FS::cust_main { $cust_main->hash }; if ($payby eq 'CARD' || $payby eq 'DCRD') { @@ -1193,7 +1162,7 @@ sub do_process_payment { stateid stateid_state ); $new->set( 'payby' => $validate->{'auto'} ? 'CHEK' : 'DCHK' ); } - $new->set( 'payinfo' => $cust_main->card_token || $validate->{'payinfo'} ); + $new->payinfo( $validate->{'payinfo'} ); #to properly set paymask $new->set( 'paydate' => $validate->{'paydate'} ); my $error = $new->replace($cust_main); if ( $error ) { @@ -1201,18 +1170,48 @@ sub do_process_payment { #return { 'error' => $error }; #XXX just warn verosely for now so i can figure out how these happen in # the first place, eventually should redirect them to the "change - #address" page but indicate the payment did process?? + #address" page but indicate if the payment processed? delete($validate->{'payinfo'}); #don't want to log this! warn "WARNING: error changing customer info when processing payment (not returning to customer as a processing error): $error\n". "NEW: ". Dumper($new)."\n". "OLD: ". Dumper($cust_main)."\n". "PACKET: ". Dumper($validate)."\n"; - #} else { - #not needed... - #$cust_main = $new; + } else { + $cust_main = $new; } } + my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount, + 'quiet' => 1, + 'manual' => 1, + 'selfservice' => 1, + 'paynum_ref' => \$paynum, + %$validate, + ); + 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', $cust_main->agentnum); + 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 }; + + $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; + my $cust_pay = ''; my $receipt_html = ''; if ($paynum) { -- cgit v1.2.1 From 8662aff8e73ac76b2c419a17a5ee0711a681e669 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 30 Sep 2015 01:43:44 -0500 Subject: RT#37547: Voice Network FS reexport --- FS/bin/freeside-reexport | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/FS/bin/freeside-reexport b/FS/bin/freeside-reexport index 54af9dd80..6b689178d 100644 --- a/FS/bin/freeside-reexport +++ b/FS/bin/freeside-reexport @@ -1,7 +1,7 @@ #!/usr/bin/perl -w use strict; -use vars qw($opt_s $opt_u $opt_p); +use vars qw($opt_s $opt_u $opt_p $opt_e); use Getopt::Std; use FS::UID qw(adminsuidsetup); use FS::Record qw(qsearch qsearchs); @@ -22,7 +22,7 @@ if ( $export_x =~ /^(\d+)$/ ) { or die "no exports of type $export_x found\n"; } -getopts('s:u:p:'); +getopts('s:u:p:e:'); my @svc_x = (); if ( $opt_s ) { @@ -38,16 +38,20 @@ if ( $opt_s ) { die "no services with svcpart $opt_p found\n" unless @svc_x; } +$opt_e ||= 'insert'; +die &usage unless grep { $_ eq $opt_e } qw( insert replace delete suspend unsuspend ); +my $method = 'export_' . $opt_e; + foreach my $part_export ( @part_export ) { foreach my $svc_x ( @svc_x ) { - my $error = $part_export->export_insert($svc_x); + my $error = $part_export->$method($svc_x,$svc_x); die $error if $error; } } sub usage { - die "Usage:\n\n freeside-reexport user exportnum|exporttype [ -s svcnum | -u username | -p svcpart ]\n"; + return "Usage:\n\n freeside-reexport user exportnum|exporttype [ -s svcnum | -u username | -p svcpart ] [ -e insert|replace|delete|suspend|unsuspend ]\n"; } =head1 NAME @@ -56,12 +60,13 @@ freeside-reexport - Command line tool to re-trigger export jobs for existing ser =head1 SYNOPSIS - freeside-reexport user exportnum|exporttype [ -s svcnum | -u username | -p svcpart ] + freeside-reexport user exportnum|exporttype [ -s svcnum | -u username | -p svcpart ] [ -e insert|replace|delete|suspend|unsuspend ] =head1 DESCRIPTION Re-queues the export job for the specified exportnum or exporttype(s) and - specified service (selected by svcnum or username). + specified service (selected by svcnum, username or svcpart). Optionally + specify the phase of export using the -e flag (default is insert.) =head1 SEE ALSO -- cgit v1.2.1 From 21b519eb5313ebe09242a2d90e1e615c56c64739 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 30 Sep 2015 20:32:20 -0500 Subject: RT#38048: not storing credit card #s [no longer setting cust_main->card_token] --- FS/FS/cust_main/Billing_Realtime.pm | 2 -- 1 file changed, 2 deletions(-) diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index c6b3b3180..2a920e074 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -765,8 +765,6 @@ sub realtime_bop { if ( $transaction->can('card_token') && $transaction->card_token ) { - $self->card_token($transaction->card_token); - if ( $options{'payinfo'} eq $self->payinfo ) { $self->payinfo($transaction->card_token); my $error = $self->replace; -- cgit v1.2.1 From d07c72046444319e0811c6a00b504885da091992 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 30 Sep 2015 22:49:38 -0700 Subject: graphical selection of deployment zones and automatic block lookup, #30260 --- FS/FS/Schema.pm | 3 +- FS/FS/deploy_zone.pm | 187 ++++++++++++++++++--- FS/FS/deploy_zone_block.pm | 5 - FS/FS/o2m_Common.pm | 18 +- FS/FS/part_pkg_fcc_option.pm | 2 +- httemplate/browse/deploy_zone.html | 6 +- httemplate/edit/deploy_zone-fixed.html | 85 +++++----- httemplate/edit/deploy_zone-mobile.html | 21 ++- httemplate/edit/process/deploy_zone-fixed.html | 35 +++- httemplate/edit/process/deploy_zone-mobile.html | 19 ++- httemplate/edit/process/elements/process.html | 21 ++- httemplate/elements/polygon.html | 127 ++++++++++++++ httemplate/elements/tr-polygon.html | 5 + .../misc/process/deploy_zone-block_lookup.cgi | 13 ++ 14 files changed, 450 insertions(+), 97 deletions(-) create mode 100644 httemplate/elements/polygon.html create mode 100644 httemplate/elements/tr-polygon.html create mode 100644 httemplate/misc/process/deploy_zone-block_lookup.cgi diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 85fbbeb8a..486860ff6 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -7038,6 +7038,7 @@ sub tables_hashref { 'zonenum', 'serial', '', '', '', '', 'description', 'char', 'NULL', $char_d, '', '', 'agentnum', 'int', '', '', '', '', + 'censusyear', 'char', 'NULL', 4, '', '', 'dbaname', 'char', 'NULL', $char_d, '', '', 'zonetype', 'char', '', 1, '', '', 'technology', 'int', '', '', '', '', @@ -7069,7 +7070,7 @@ sub tables_hashref { 'blocknum', 'serial', '', '', '', '', 'zonenum', 'int', '', '', '', '', 'censusblock', 'char', '', 15, '', '', - 'censusyear', 'char', '', 4, '', '', + 'censusyear', 'char','NULL', 4, '', '', ], 'primary_key' => 'blocknum', 'unique' => [], diff --git a/FS/FS/deploy_zone.pm b/FS/FS/deploy_zone.pm index 38dd7dc2d..71129cf44 100644 --- a/FS/FS/deploy_zone.pm +++ b/FS/FS/deploy_zone.pm @@ -6,6 +6,13 @@ use FS::Record qw( qsearch qsearchs dbh ); use Storable qw(thaw); use MIME::Base64; +use JSON qw(encode_json decode_json) ; +use LWP::UserAgent; +use HTTP::Request::Common; + +# update this in 2020, along with the URL for the TIGERweb service +our $CENSUS_YEAR = 2010; + =head1 NAME FS::deploy_zone - Object methods for deploy_zone records @@ -48,6 +55,12 @@ Optional text describing the zone. The agent that serves this zone. +=item censusyear + +The census map year for which this zone was last updated. May be null for +zones that contain no census blocks (mobile zones, or fixed zones that haven't +had their block lists filled in yet). + =item dbaname The name under which service is marketed in this zone. If null, will @@ -58,6 +71,8 @@ default to the agent name. The way the zone geography is defined: "B" for a list of census blocks (used by the FCC for fixed broadband service), "P" for a polygon (for mobile services). See L and L. +Note that block-type zones are still allowed to have a vertex list, for +use by the map editor. =item technology @@ -147,12 +162,16 @@ sub delete { local $FS::UID::AutoCommit = 0; # clean up linked records my $self = shift; - my $error = $self->process_o2m( - 'table' => $self->element_table, - 'num_col' => 'zonenum', - 'fields' => 'zonenum', - 'params' => {}, - ) || $self->SUPER::delete(@_); + my $error; + foreach (qw(deploy_zone_block deploy_zone_vertex)) { + $error ||= $self->process_o2m( + 'table' => $_, + 'num_col' => 'zonenum', + 'fields' => 'zonenum', + 'params' => {}, + ); + } + $error ||= $self->SUPER::delete(@_); if ($error) { dbh->rollback if $oldAutoCommit; @@ -185,6 +204,7 @@ sub check { $self->ut_numbern('zonenum') || $self->ut_text('description') || $self->ut_number('agentnum') + || $self->ut_numbern('censusyear') || $self->ut_foreign_key('agentnum', 'agent', 'agentnum') || $self->ut_textn('dbaname') || $self->ut_enum('zonetype', [ 'B', 'P' ]) @@ -219,24 +239,6 @@ sub check { $self->SUPER::check; } -=item element_table - -Returns the name of the table that contains the zone's elements (blocks or -vertices). - -=cut - -sub element_table { - my $self = shift; - if ($self->zonetype eq 'B') { - return 'deploy_zone_block'; - } elsif ( $self->zonetype eq 'P') { - return 'deploy_zone_vertex'; - } else { - die 'unknown zonetype'; - } -} - =item deploy_zone_block Returns the census block records in this zone, in order by census block @@ -244,8 +246,7 @@ number. Only appropriate to block-type zones. =item deploy_zone_vertex -Returns the vertex records for this zone, in order by sequence number. Only -appropriate to polygon-type zones. +Returns the vertex records for this zone, in order by sequence number. =cut @@ -267,7 +268,19 @@ sub deploy_zone_vertex { }); } -=back +=item vertices_json + +Returns the vertex list for this zone, as a JSON string of + +[ [ latitude0, longitude0 ], [ latitude1, longitude1 ] ... ] + +=cut + +sub vertices_json { + my $self = shift; + my @vertices = map { [ $_->latitude, $_->longitude ] } $self->deploy_zone_vertex; + encode_json(\@vertices); +} =head2 SUBROUTINES @@ -315,7 +328,125 @@ sub process_batch_import { FS::Record::process_batch_import( $job, $opt, $param ); } - + +=item process_block_lookup JOB, ZONENUM + +Look up all the census blocks in the zone's footprint, and insert them. +This will replace any existing block list. + +=cut + +sub process_block_lookup { + my $job = shift; + my $param = shift; + if (!ref($param)) { + $param = thaw(decode_base64($param)); + } + my $zonenum = $param->{zonenum}; + my $zone = FS::deploy_zone->by_key($zonenum) + or die "zone $zonenum not found\n"; + + # wipe the existing list of blocks + my $error = $zone->process_o2m( + 'table' => 'deploy_zone_block', + 'num_col' => 'zonenum', + 'fields' => 'zonenum', + 'params' => {}, + ); + die $error if $error; + + $job->update_statustext('0,querying census database') if $job; + + # negotiate the rugged jungle trails of the ArcGIS REST protocol: + # 1. unlike most places, longitude first. + my @zone_vertices = map { [ $_->longitude, $_->latitude ] } + $zone->deploy_zone_vertex; + + return if scalar(@zone_vertices) < 3; # then don't bother + + # 2. package this as "rings", inside a JSON geometry object + # 3. announce loudly and frequently that we are using spatial reference + # 4326, "true GPS coordinates" + my $geometry = encode_json({ + 'rings' => [ \@zone_vertices ], + 'wkid' => 4326, + }); + + my %query = ( + f => 'json', # duh + geometry => $geometry, + geometryType => 'esriGeometryPolygon', # as opposed to a bounding box + inSR => 4326, + outSR => 4326, + spatialRel => 'esriSpatialRelIntersects', # the test to perform + outFields => 'OID,GEOID', + returnGeometry => 'false', + orderByFields => 'OID', + ); + my $url = 'http://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/Tracts_Blocks/MapServer/12/query'; + my $ua = LWP::UserAgent->new; + + # first find out how many of these we're dealing with + my $response = $ua->request( + POST $url, Content => [ + %query, + returnCountOnly => 1, + ] + ); + die $response->status_line unless $response->is_success; + my $data = decode_json($response->content); + # their error messages are mostly useless, but don't just blindly continue + die $data->{error}{message} if $data->{error}; + + my $count = $data->{count}; + my $inserted = 0; + + #warn "Census block lookup: $count\n"; + + # we have to do our own pagination on this, because the census bureau + # doesn't support resultOffset (maybe they don't have ArcGIS 10.3 yet). + # that's why we're ordering by OID, it's globally unique + my $last_oid = 0; + my $done = 0; + while (!$done) { + $response = $ua->request( + POST $url, Content => [ + %query, + where => "OID>$last_oid", + ] + ); + die $response->status_line unless $response->is_success; + $data = decode_json($response->content); + die $data->{error}{message} if $data->{error}; + + foreach my $feature (@{ $data->{features} }) { + my $geoid = $feature->{attributes}{GEOID}; # the prize + my $block = FS::deploy_zone_block->new({ + zonenum => $zonenum, + censusblock => $geoid + }); + $error = $block->insert; + die "$error (inserting census block $geoid)" if $error; + + $inserted++; + if ($job and $inserted % 100 == 0) { + my $percent = sprintf('%.0f', $inserted / $count * 100); + $job->update_statustext("$percent,creating block records"); + } + } + + #warn "Inserted $inserted records\n"; + $last_oid = $data->{features}[-1]{attributes}{OID}; + $done = 1 unless $data->{exceededTransferLimit}; + } + + $zone->set('censusyear', $CENSUS_YEAR); + $error = $zone->replace; + warn "$error (updating zone census year)" if $error; # whatever, continue + + return; +} + =head1 BUGS =head1 SEE ALSO diff --git a/FS/FS/deploy_zone_block.pm b/FS/FS/deploy_zone_block.pm index 757af7e3d..2ac18e2fe 100644 --- a/FS/FS/deploy_zone_block.pm +++ b/FS/FS/deploy_zone_block.pm @@ -43,10 +43,6 @@ L foreign key for the zone. U.S. census block number (15 digits). -=item censusyear - -The year of the census map where the block appeared or was last verified. - =back =head1 METHODS @@ -107,7 +103,6 @@ sub check { $self->ut_numbern('blocknum') || $self->ut_number('zonenum') || $self->ut_number('censusblock') - || $self->ut_number('censusyear') ; return $error if $error; diff --git a/FS/FS/o2m_Common.pm b/FS/FS/o2m_Common.pm index 4f6d2e781..430f00bbb 100644 --- a/FS/FS/o2m_Common.pm +++ b/FS/FS/o2m_Common.pm @@ -35,11 +35,19 @@ Available options: table (required) - Table into which the records are inserted. -num_col (optional) - Column in table which links to the primary key of the base table. If not specified, it is assumed this has the same name. - -params (required) - Hashref of keys and values, often passed as CVars)> from a form. - -fields (required) - Arrayref of field names for each record in table. Pulled from params as "pkeyNN_field" where pkey is table's primary key and NN is the entry's numeric identifier. +fields (required) - Arrayref of the field names in the "many" table. + +params (required) - Hashref of keys and values, often passed as +CVars)> from a form. This will be scanned for keys of the form +"pkeyNN" (where pkey is the primary key column name, and NN is an integer). +Each of these designates one record in the "many" table. The contents of +that record will be taken from other parameters with the names +"pkeyNN_myfield" (where myfield is one of the fields in the 'fields' +array). + +num_col (optional) - Name of the foreign key column in the "many" table, which +links to the primary key of the base table. If not specified, it is assumed +this has the same name as in the base table. =cut diff --git a/FS/FS/part_pkg_fcc_option.pm b/FS/FS/part_pkg_fcc_option.pm index 5c78e5f9e..3d821f502 100644 --- a/FS/FS/part_pkg_fcc_option.pm +++ b/FS/FS/part_pkg_fcc_option.pm @@ -148,7 +148,7 @@ tie our %spectrum_labels, 'Tie::IxHash', ( 95 => 'Wireless Communications Service (WCS) Band', 96 => 'Broadband Radio Service/Educational Broadband Service Band', 97 => 'Satellite (e.g. L-band, Big LEO, Little LEO)', - 98 => 'Unlicensed (including broadcast television “white spaces”) Bands', + 98 => 'Unlicensed (including broadcast television "white spaces") Bands', 99 => '600 MHz', 100 => 'H Block', 101 => 'Advanced Wireless Services (AWS) 3 Band', diff --git a/httemplate/browse/deploy_zone.html b/httemplate/browse/deploy_zone.html index 3bd9d07dd..02ebb8b8c 100644 --- a/httemplate/browse/deploy_zone.html +++ b/httemplate/browse/deploy_zone.html @@ -17,6 +17,7 @@ 'Market', 'Advertised Mbps', 'Contractual Mbps', + 'Vertices', 'Census blocks', ], fields => [ 'zonenum', @@ -41,6 +42,9 @@ $self->cir_speed_up ) }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + }, sub { my $self = shift; FS::deploy_zone_block->count('zonenum = '.$self->zonenum) }, @@ -53,7 +57,7 @@ '(cir_speed_down, cir_speed_up)', ], links => [ $link_fixed, $link_fixed, ], - align => 'clllllr', + align => 'cllllrr', nohtmlheader => 1, disable_maxselect => 1, disable_total => 1, diff --git a/httemplate/edit/deploy_zone-fixed.html b/httemplate/edit/deploy_zone-fixed.html index 90d1b6667..b8d9f8bbc 100644 --- a/httemplate/edit/deploy_zone-fixed.html +++ b/httemplate/edit/deploy_zone-fixed.html @@ -54,29 +54,36 @@ value => 'Contractually guaranteed speed (Mbps)' }, 'cir_speed_down', 'cir_speed_up', - - { type => 'tablebreak-tr-title', value => 'Census blocks'}, - { field => 'file', - type => 'file-upload', - }, - { field => 'format', - type => 'hidden', - value => 'plain', - }, - { field => 'censusyear', - type => 'select', - options => [ '', qw( 2013 2012 2011 ) ], - }, - - { type => 'tablebreak-tr-title', value => '', }, - { field => 'blocknum', - type => 'deploy_zone_block', - o2m_table => 'deploy_zone_block', - m2_label => ' ', - m2_error_callback => $m2_error_callback, - }, + { type => 'tablebreak-tr-title', value => 'Footprint'}, + { field => 'vertices', + type => 'polygon', + curr_value_callback => sub { + my ($cgi, $object) = @_; + $cgi->param('vertices') || $object->vertices_json; + }, + } +# +# { type => 'tablebreak-tr-title', value => 'Census blocks'}, +# { field => 'file', +# type => 'file-upload', +# }, +# { field => 'format', +# type => 'hidden', +# value => 'plain', +# }, +# { field => 'censusyear', +# type => 'hidden', +# options => [ '', qw( 2013 2012 2011 ) ], +# }, +# +# { type => 'tablebreak-tr-title', value => '', }, +# { field => 'blocknum', +# type => 'deploy_zone_block', +# o2m_table => 'deploy_zone_block', +# m2_label => ' ', +# m2_error_callback => $m2_error_callback, +# }, ], - &> <%init> my $curuser = $FS::CurrentUser::CurrentUser; @@ -90,22 +97,22 @@ my $technology_labels = FS::part_pkg_fcc_option->technology_labels; my $media_types = FS::part_pkg_fcc_option->media_types; delete $media_types->{'Mobile Wireless'}; # cause this is the fixed zone page -my $m2_error_callback = sub { - my ($cgi, $deploy_zone) = @_; - my @blocknums = grep { - /^blocknum\d+/ and length($cgi->param($_.'_censusblock')) - } $cgi->param; - - sort { $a->censusblock <=> $b->censusblock } - map { - my $k = $_; - FS::deploy_zone_block->new({ - blocknum => scalar($cgi->param($k)), - zonenum => $deploy_zone->zonenum, - censusblock => scalar($cgi->param($k.'_censusblock')), - censusyear => scalar($cgi->param($k.'_censusyear')), - }) - } @blocknums; -}; +#my $m2_error_callback = sub { +# my ($cgi, $deploy_zone) = @_; +# my @blocknums = grep { +# /^blocknum\d+/ and length($cgi->param($_.'_censusblock')) +# } $cgi->param; +# +# sort { $a->censusblock <=> $b->censusblock } +# map { +# my $k = $_; +# FS::deploy_zone_block->new({ +# blocknum => scalar($cgi->param($k)), +# zonenum => $deploy_zone->zonenum, +# censusblock => scalar($cgi->param($k.'_censusblock')), +# censusyear => scalar($cgi->param($k.'_censusyear')), +# }) +# } @blocknums; +#}; diff --git a/httemplate/edit/deploy_zone-mobile.html b/httemplate/edit/deploy_zone-mobile.html index d049cb018..8cec298bf 100644 --- a/httemplate/edit/deploy_zone-mobile.html +++ b/httemplate/edit/deploy_zone-mobile.html @@ -49,14 +49,21 @@ 'adv_speed_down', 'adv_speed_up', { type => 'tablebreak-tr-title', value => 'Footprint'}, - { field => 'vertexnum', - type => 'deploy_zone_vertex', - o2m_table => 'deploy_zone_vertex', - m2_label => ' ', - m2_error_callback => $m2_error_callback, - }, - ], + { field => 'vertices', + type => 'polygon', + curr_value_callback => sub { + my ($cgi, $object) = @_; + $cgi->param('vertices') || $object->vertices_json; + }, + } +# { field => 'vertexnum', +# type => 'deploy_zone_vertex', +# o2m_table => 'deploy_zone_vertex', +# m2_label => ' ', +# m2_error_callback => $m2_error_callback, +# }, + ], &> <%init> my $curuser = $FS::CurrentUser::CurrentUser; diff --git a/httemplate/edit/process/deploy_zone-fixed.html b/httemplate/edit/process/deploy_zone-fixed.html index eae3a746d..0033bbe52 100644 --- a/httemplate/edit/process/deploy_zone-fixed.html +++ b/httemplate/edit/process/deploy_zone-fixed.html @@ -3,12 +3,31 @@ error_redirect => popurl(2).'deploy_zone-fixed.html', table => 'deploy_zone', viewall_dir => 'browse', - process_o2m => { - 'table' => 'deploy_zone_block', - 'fields' => [qw( censusblock censusyear )] - }, - process_upload => { - 'process' => 'misc/process/deploy_zone-import.html', - 'fields' => [qw( censusyear format )], - }, + precheck_callback => $precheck_callback, + process_o2m => + { 'table' => 'deploy_zone_vertex', + 'fields' => [qw( latitude longitude )] + }, + progress_init => [ + 'PostForm', + [ 'zonenum' ], + $fsurl.'misc/process/deploy_zone-block_lookup.cgi', + $fsurl.'browse/deploy_zone.html', + ], &> +<%init> +my $precheck_callback = sub { + # convert the vertex list into a process_o2m-style parameter list + if ( $cgi->param('vertices') ) { + my $vertices = decode_json($cgi->param('vertices')); + my $i = 0; + foreach (@$vertices) { + $cgi->param("vertexnum${i}", ''); + $cgi->param("vertexnum${i}_latitude", $_->[0]); + $cgi->param("vertexnum${i}_longitude", $_->[1]); + $i++; + } + } + ''; +}; + diff --git a/httemplate/edit/process/deploy_zone-mobile.html b/httemplate/edit/process/deploy_zone-mobile.html index 7b8f911ec..d36d5d448 100644 --- a/httemplate/edit/process/deploy_zone-mobile.html +++ b/httemplate/edit/process/deploy_zone-mobile.html @@ -2,8 +2,25 @@ error_redirect => popurl(2).'deploy_zone-mobile.html', table => 'deploy_zone', viewall_dir => 'browse', - process_o2m => + precheck_callback => $precheck_callback, + process_o2m => { 'table' => 'deploy_zone_vertex', 'fields' => [qw( latitude longitude )] }, &> +<%init> +my $precheck_callback = sub { + # convert the vertex list into a process_o2m-style parameter list + if ( $cgi->param('vertices') ) { + my $vertices = decode_json($cgi->param('vertices')); + my $i = 0; + foreach (@$vertices) { + $cgi->param("vertexnum${i}", ''); + $cgi->param("vertexnum${i}_latitude", $_->[0]); + $cgi->param("vertexnum${i}_longitude", $_->[1]); + $i++; + } + } + ''; +}; + diff --git a/httemplate/edit/process/elements/process.html b/httemplate/edit/process/elements/process.html index 69bd605f6..a76f4befb 100644 --- a/httemplate/edit/process/elements/process.html +++ b/httemplate/edit/process/elements/process.html @@ -160,7 +160,26 @@ process(); <& /elements/footer.html &> -%} elsif ( $opt{'popup_reload'} ) { +% } elsif ( $opt{'progress_init'} ) { +% # some false laziness with the above +% my ($form_name, $job_fields) = @{ $opt{'progress_init'} }; + +% foreach my $field (@$job_fields) { + +% } +<& /elements/progress-init.html, + @{ $opt{'progress_init'} } +&> + + + +<& /elements/footer.html &> + +% } elsif ( $opt{'popup_reload'} ) { <% include('/elements/header-popup.html', $opt{'popup_reload'} ) %> diff --git a/httemplate/elements/polygon.html b/httemplate/elements/polygon.html new file mode 100644 index 000000000..c26e98546 --- /dev/null +++ b/httemplate/elements/polygon.html @@ -0,0 +1,127 @@ +<%init> +my %opt = @_; +my $field = $opt{'field'}; +my $id = $opt{'id'} || $opt{'field'}; +my $div_id = "div_$id"; + +my $vertices_json = $opt{'curr_value'} || '[]'; + +<& hidden.html, %opt &> +
    + + + + + diff --git a/httemplate/elements/tr-polygon.html b/httemplate/elements/tr-polygon.html new file mode 100644 index 000000000..6990d3da6 --- /dev/null +++ b/httemplate/elements/tr-polygon.html @@ -0,0 +1,5 @@ + + +<& polygon.html, @_ &> + + diff --git a/httemplate/misc/process/deploy_zone-block_lookup.cgi b/httemplate/misc/process/deploy_zone-block_lookup.cgi new file mode 100644 index 000000000..8f4eac7e9 --- /dev/null +++ b/httemplate/misc/process/deploy_zone-block_lookup.cgi @@ -0,0 +1,13 @@ +<% $server->process %> +<%init> +my $curuser = $FS::CurrentUser::CurrentUser; +die "access denied" + unless $curuser->access_right([ + 'Edit FCC report configuration', + 'Edit FCC report configuration for all agents', + ]); + +my $server = FS::UI::Web::JSRPC->new( + 'FS::deploy_zone::process_block_lookup', $cgi +); + -- cgit v1.2.1 From 55de788f065cebc8273bcc52befb82cec9eff129 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Thu, 1 Oct 2015 10:25:37 -0400 Subject: 38406 Net 7 Terms --- FS/FS/Conf.pm | 2 +- httemplate/elements/select-terms.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 26dbbcd23..89080ceab 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -1537,7 +1537,7 @@ and customer address. Include units.', 'type' => 'select', 'per_agent' => 1, 'select_enum' => [ - '', 'Payable upon receipt', 'Net 0', 'Net 3', 'Net 5', 'Net 9', 'Net 10', 'Net 14', + '', 'Payable upon receipt', 'Net 0', 'Net 3', 'Net 5', 'Net 7', 'Net 9', 'Net 10', 'Net 14', 'Net 15', 'Net 18', 'Net 20', 'Net 21', 'Net 25', 'Net 30', 'Net 45', 'Net 60', 'Net 90' ], }, diff --git a/httemplate/elements/select-terms.html b/httemplate/elements/select-terms.html index 716832f52..a330df17c 100644 --- a/httemplate/elements/select-terms.html +++ b/httemplate/elements/select-terms.html @@ -36,7 +36,7 @@ my $empty_value = $opt{'empty_value'} || ''; my @terms = ( emt('Payable upon receipt'), ( map "Net $_", - 0, 3, 5, 9, 10, 14, 15, 18, 20, 21, 25, 30, 45, 60, 90 ), + 0, 3, 5, 7, 9, 10, 14, 15, 18, 20, 21, 25, 30, 45, 60, 90 ), ); my @pre_options = $opt{pre_options} ? @{ $opt{pre_options} } : (); -- cgit v1.2.1 From bf6c11bc520aa4e4e0fa75f0469c66a11cf11a31 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 1 Oct 2015 21:05:14 -0700 Subject: refine disable_previous_balance behavior to show new charges only, #35222, #37396 --- FS/FS/Conf.pm | 2 +- FS/FS/Template_Mixin.pm | 7 ++++++- FS/FS/cust_bill.pm | 16 +++++++++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 89080ceab..1e0d99928 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -4178,7 +4178,7 @@ and customer address. Include units.', { 'key' => 'disable_previous_balance', 'section' => 'invoicing', - 'description' => 'Disable inclusion of previous balance, payment, and credit lines on invoices.', + 'description' => 'Show new charges only; do not list previous invoices, payments, or credits on the invoice.', 'type' => 'checkbox', 'per_agent' => 1, }, diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index 206c03cde..1a3217c44 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -684,7 +684,12 @@ sub print_generic { my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits #my $balance_due = $self->owed + $pr_total - $cr_total; - my $balance_due = $self->owed + $pr_total; + my $balance_due = $self->owed; + if ( $self->enable_previous ) { + $balance_due += $pr_total; + } + # otherwise the previous balance is not shown, so including it in the + # balance due is just confusing # the sum of amount owed on all invoices # (this is used in the summary & on the payment coupon) diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 09424ba52..6546bfa95 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -2836,8 +2836,7 @@ sub _items_total { my ($previous_charges_desc, $new_charges_desc, $new_charges_amount); if ( $conf->exists('previous_balance-exclude_from_total') ) { - # can we do some caching on this stuff? it's going to change infrequently - # in production + # if enabled, specifically add a line for the previous balance total $previous_charges_desc = $self->mt( $conf->config('previous_balance-text') || 'Previous Balance' ); @@ -2849,6 +2848,12 @@ sub _items_total { total_amount => sprintf('%.2f',$pr_total) }; } + } + + if ( $conf->exists('previous_balance-exclude_from_total') + or !$self->enable_previous ) { + # show new charges only + $new_charges_desc = $self->mt( $conf->config('previous_balance-text-total_new_charges') || 'Total New Charges' @@ -2857,9 +2862,14 @@ sub _items_total { $new_charges_amount = $self->charged; } else { + # show new charges + previous invoice total $new_charges_desc = $self->mt('Total Charges'); - $new_charges_amount = sprintf('%.2f',$self->charged + $pr_total); + if ( $self->enable_previous ) { + $new_charges_amount = sprintf('%.2f', $self->charged + $pr_total); + } else { + $new_charges_amount = sprintf('%.2f', $self->charged); + } } -- cgit v1.2.1 From 61a0dc609fd2b7db3571f8f86672481d1e064331 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 2 Oct 2015 16:05:41 -0700 Subject: fix unit setup fee on prorate-deferred packages, #31276 --- FS/FS/cust_main/Billing.pm | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 2d7b690df..eee0958e0 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -1024,8 +1024,14 @@ sub _make_lines { return "$@ running calc_setup for $cust_pkg\n" if $@; - $unitsetup = $cust_pkg->base_setup() - || $setup; #XXX uuh + # Only increment unitsetup here if there IS a setup fee. + # prorate_defer_bill may cause calc_setup on a setup-stage package + # to return zero, and the setup fee to be charged later. (This happens + # when it's first billed on the prorate cutoff day. RT#31276.) + if ( $setup ) { + $unitsetup = $cust_pkg->base_setup() + || $setup; #XXX uuh + } if ( $setup_param{'billed_currency'} ) { $setup_billed_currency = delete $setup_param{'billed_currency'}; @@ -1196,7 +1202,7 @@ sub _make_lines { # Add an additional setup fee at the billing stage. # Used for prorate_defer_bill. $setup += $param{'setup_fee'}; - $unitsetup += $param{'setup_fee'}; + $unitsetup = $cust_pkg->base_setup(); $lineitems++; } -- cgit v1.2.1 From c62c90543fcd5de9e57b1d2ce442fa37c71358c8 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 2 Oct 2015 17:19:14 -0700 Subject: fix placement of "taxable" column, #37088 --- httemplate/search/report_tax.cgi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi index 6d0e95d2a..615abe5b2 100644 --- a/httemplate/search/report_tax.cgi +++ b/httemplate/search/report_tax.cgi @@ -125,14 +125,14 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } <% $money_sprintf->( $row->{sales_credited} ) %> - × - <% $row->{rate} %> % # taxable sales "> <% $money_sprintf->( $row->{taxable} ) %> + × + <% $row->{rate} %> % # estimated tax = -- cgit v1.2.1 From 323d6a0c3ee3d7752225b712f5bdcfbb1581d61f Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 3 Oct 2015 16:12:38 -0700 Subject: fix region-filtering of credit application search from tax report, #37088 --- httemplate/search/report_tax.cgi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi index 615abe5b2..0ad143f01 100644 --- a/httemplate/search/report_tax.cgi +++ b/httemplate/search/report_tax.cgi @@ -77,7 +77,7 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } % # cust_bill_pkg.cgi wants a list of specific taxnums (and package class) % # cust_credit_bill_pkg.html wants a geographic scope (and package class) % my $rowlink = ';taxnum=' . $row->{taxnums}; -% my $rowregion = ''; +% my $rowregion = ';country=' . $cgi->param('country'); % foreach my $loc (qw(state county city district)) { % if ( $row->{$loc} ) { % $rowregion .= ";$loc=" . uri_escape($row->{$loc}); @@ -88,6 +88,7 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } % $rowlink .= ';classnum=' . ($row->{pkgclass} || 0); % $rowregion .= ';classnum=' . ($row->{pkgclass} || 0); % } +%warn $rowregion; % % if ( $row->{total} ) { -- cgit v1.2.1 From d942f94119fdc54dc416e309f36d385652fb5272 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 5 Oct 2015 14:42:57 -0700 Subject: fix UTF-8 in ClientAPI, RT#38254 --- FS/FS/ClientAPI_XMLRPC.pm | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/FS/FS/ClientAPI_XMLRPC.pm b/FS/FS/ClientAPI_XMLRPC.pm index 435ee9835..dbcb565fa 100644 --- a/FS/FS/ClientAPI_XMLRPC.pm +++ b/FS/FS/ClientAPI_XMLRPC.pm @@ -30,6 +30,7 @@ L, L use strict; use vars qw($DEBUG $AUTOLOAD); +use Encode; use FS::XMLRPC_Lite; #XMLRPC::Lite, for XMLRPC::Data use FS::ClientAPI; @@ -67,12 +68,17 @@ sub AUTOLOAD { shift; #discard package name; + #$call = "FS::SelfService::$call"; #no strict 'refs'; #&{$call}(@_); #FS::ClientAPI->dispatch($autoload->{$call}, @_); - my $return = FS::ClientAPI->dispatch($autoload->{$call}, { @_ } ); + my %hash = @_; + #XXX doesn't handle multi-level data structs + $hash{$_} = decode(utf8=>$hash{$_}) foreach keys %hash; + + my $return = FS::ClientAPI->dispatch($autoload->{$call}, \%hash ); if ( exists($typefix{$call}) ) { my $typefix = $typefix{$call}; @@ -85,7 +91,7 @@ sub AUTOLOAD { $return; - }else{ + } else { die "No such procedure: $call"; } } -- cgit v1.2.1 From d0b3acc1efb65855d5e52d54c33bb035c9776e2d Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 5 Oct 2015 18:35:13 -0700 Subject: ticket_system-appointment-queueid config, RT#34237 --- FS/FS/Conf.pm | 30 +++++++++++++++++++++++++++++ rt/share/html/Elements/CalendarSlotSchedule | 6 +++++- rt/share/html/Search/Schedule.html | 9 +++++++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 1e0d99928..fa4ff41d3 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -3074,12 +3074,14 @@ and customer address. Include units.', } }, }, + { 'key' => 'ticket_system-force_default_queueid', 'section' => 'ticketing', 'description' => 'Disallow queue selection when creating new tickets from customer view.', 'type' => 'checkbox', }, + { 'key' => 'ticket_system-selfservice_queueid', 'section' => 'ticketing', @@ -3157,6 +3159,34 @@ and customer address. Include units.', 'type' => 'checkbox', }, + { + 'key' => 'ticket_system-appointment-queueid', + 'section' => 'ticketing', + 'description' => 'Custom field from the ticketing system to use as an appointment classification.', + #false laziness w/above + 'type' => 'select-sub', + 'options_sub' => sub { + my $conf = new FS::Conf; + if ( $conf->config('ticket_system') ) { + eval "use FS::TicketSystem;"; + die $@ if $@; + FS::TicketSystem->queues(); + } else { + (); + } + }, + 'option_sub' => sub { + my $conf = new FS::Conf; + if ( $conf->config('ticket_system') ) { + eval "use FS::TicketSystem;"; + die $@ if $@; + FS::TicketSystem->queue(shift); + } else { + ''; + } + }, + }, + { 'key' => 'ticket_system-escalation', 'section' => 'ticketing', diff --git a/rt/share/html/Elements/CalendarSlotSchedule b/rt/share/html/Elements/CalendarSlotSchedule index 045d6e436..4a9b3bcc3 100644 --- a/rt/share/html/Elements/CalendarSlotSchedule +++ b/rt/share/html/Elements/CalendarSlotSchedule @@ -87,7 +87,11 @@ % # (XXX and eventually, package) % my $cust_main = qsearchs('cust_main', { custnum=>$custnum } ) % or die "unknown custnum $custnum"; -% my $Queue = $cust_main->agent->ticketing_queueid || 1; # || $default_queueid;#XXX really, pick pkg_category queue +% +% my $conf = new FS::Conf; +% my $Queue = $conf->config('ticket_system-appointment-queueid') +% or die "ticket_system-appointment-queueid configuration not set"; +% % my $member = "freeside://freeside/cust_main/$custnum"; % %warn my $Starts = int($tod_row/60). ':'. sprintf('%02d',$tod_row%60). ':00'; diff --git a/rt/share/html/Search/Schedule.html b/rt/share/html/Search/Schedule.html index be5a140ef..c729ff068 100644 --- a/rt/share/html/Search/Schedule.html +++ b/rt/share/html/Search/Schedule.html @@ -201,8 +201,8 @@ <& /Search/Calendar.html, @_, Query => "( Status = 'new' OR Status = 'open' OR Status = 'stalled') - AND ( Type = 'reminder' OR 'Type' = 'ticket' )", - #XXX and we have the magic custom field + AND ( Type = 'reminder' OR 'Type' = 'ticket' ) + AND Queue.id = $queueid ", slots => scalar(@usernames), Embed => 'Schedule.html', DimPast => 1, @@ -222,6 +222,11 @@ my $timestep = RT->Config->Get('CalendarWeeklySizeMin') || 30; #1/2h <%init> +#abstraction-leaking +my $conf = new FS::Conf; +my $queueid = $conf->config('ticket_system-appointment-queueid') + or die "ticket_system-appointment-queueid configuration not set"; + my @files = (); #if ( ! $initialized ) { push @files, map "overlibmws$_", ( '', qw( _iframe _draggable _crossframe ) ); -- cgit v1.2.1 From 883e34abf9c9a59cf039c2c698b930b2f305be7f Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 5 Oct 2015 19:05:10 -0700 Subject: ticket_system-appointment-queueid config, RT#34237 --- rt/share/html/Search/Schedule.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rt/share/html/Search/Schedule.html b/rt/share/html/Search/Schedule.html index c729ff068..43680b47c 100644 --- a/rt/share/html/Search/Schedule.html +++ b/rt/share/html/Search/Schedule.html @@ -202,7 +202,7 @@ @_, Query => "( Status = 'new' OR Status = 'open' OR Status = 'stalled') AND ( Type = 'reminder' OR 'Type' = 'ticket' ) - AND Queue.id = $queueid ", + AND Queue = $queueid ", slots => scalar(@usernames), Embed => 'Schedule.html', DimPast => 1, -- cgit v1.2.1 From 5e6bfa1548ac370d2cf316e0db44785d83baa453 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 5 Oct 2015 19:09:33 -0700 Subject: ticket_system-appointment-custom_field, RT#34237 --- FS/FS/Conf.pm | 7 ++++++ FS/FS/TicketSystem/RT_Internal.pm | 4 +++- FS/FS/cust_main.pm | 24 +++++++++++++++++++ httemplate/view/cust_main/appointments.html | 36 +++++++++++++++++------------ 4 files changed, 55 insertions(+), 16 deletions(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index fa4ff41d3..647ae0bdf 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -3187,6 +3187,13 @@ and customer address. Include units.', }, }, + { + 'key' => 'ticket_system-appointment-custom_field', + 'section' => 'ticketing', + 'description' => 'Custom field from the ticketing system to use as an appointment classification.', + 'type' => 'text', + }, + { 'key' => 'ticket_system-escalation', 'section' => 'ticketing', diff --git a/FS/FS/TicketSystem/RT_Internal.pm b/FS/FS/TicketSystem/RT_Internal.pm index 6fb2c187d..b70ac5360 100644 --- a/FS/FS/TicketSystem/RT_Internal.pm +++ b/FS/FS/TicketSystem/RT_Internal.pm @@ -111,7 +111,7 @@ properly. # create an RT::Tickets object for a specified custnum or svcnum sub _tickets_search { - my( $self, $type, $number, $limit, $priority, $status ) = @_; + my( $self, $type, $number, $limit, $priority, $status, $queueid ) = @_; $type =~ /^Customer|Service$/ or die "invalid type: $type"; $number =~ /^\d+$/ or die "invalid custnum/svcnum: $number"; @@ -159,6 +159,8 @@ sub _tickets_search { join(' OR ', map { "Status = '$_'" } @statuses). ' ) '; + $rtql .= " AND Queue = $queueid " if $queueid; + warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG; $Tickets->FromSQL($rtql); diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 6afbd1cf5..2d6d45907 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -4053,6 +4053,30 @@ sub tickets { (@tickets); } +=item appointments [ STATUS ] + +Returns an array of hashes representing the customer's RT tickets which +are appointments. + +=cut + +sub appointments { + my $self = shift; + my $status = ( @_ && $_[0] ) ? shift : ''; + + return () unless $conf->config('ticket_system'); + + my $queueid = $conf->config('ticket_system-appointment-queueid'); + + @{ FS::TicketSystem->customer_tickets( $self->custnum, + 99, + undef, + $status, + $queueid, + ) + }; +} + # Return services representing svc_accts in customer support packages sub support_services { my $self = shift; diff --git a/httemplate/view/cust_main/appointments.html b/httemplate/view/cust_main/appointments.html index c907b25bb..9bf5be1d5 100644 --- a/httemplate/view/cust_main/appointments.html +++ b/httemplate/view/cust_main/appointments.html @@ -19,7 +19,9 @@ - <% mt('Type') |h %> +% if ( $custom_field ) { + <% mt('Type') |h %> +% } <% mt('Date') |h %> <% mt('Status') |h %> <% mt('Owner') |h %> @@ -41,10 +43,12 @@ % if $starts > 86400; - - - ><% 'custom field magic type' %> - + +% if ( $custom_field ) { + + ><% $ticket->{"CF.{$custom_field}"} |h %> + +% } ><% $starts_pretty %> @@ -78,20 +82,22 @@ return '' unless $conf->config('ticket_system'); #my $object = $opt{'object'}; #$object = $object->cust_svc if $object->isa('FS::svc_Common'); -my( @tickets ) = $object->tickets; #XXX but actually appointments... filter by presense of the necessary CF? RT::Appointment instead of RT::Ticket ? +my @tickets = $object->appointments; -my ($openlabel, $open_link, $res_link, $thing); -$openlabel = join('/', FS::TicketSystem->statuses ); +my $custom_field = $conf->config('ticket_system-appointment-custom_field'); + +# my ($openlabel, $open_link, $res_link, $thing); +# $openlabel = join('/', FS::TicketSystem->statuses ); # not the nicest way to do this--FS::has_tickets_Common? #if ( $object->isa('FS::cust_main') ) { - $thing = 'customer'; - $open_link = FS::TicketSystem->href_customer_tickets($object->custnum); - - $res_link = FS::TicketSystem->href_customer_tickets( - $object->custnum, - { 'statuses' => [ 'resolved' ] } - ); +# $thing = 'customer'; +# $open_link = FS::TicketSystem->href_customer_tickets($object->custnum); +# +# $res_link = FS::TicketSystem->href_customer_tickets( +# $object->custnum, +# { 'statuses' => [ 'resolved' ] } +# ); #} elsif ( $object->isa('FS::cust_svc') ) { # # return '' unless $object->pkgnum; -- cgit v1.2.1 From 755f6730e6bc4b59db2041db09403c31136c814d Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Mon, 5 Oct 2015 21:52:43 -0500 Subject: RT37465: RBC PAD error when calculating totals [W status is now approved] --- FS/FS/pay_batch/RBC.pm | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/FS/FS/pay_batch/RBC.pm b/FS/FS/pay_batch/RBC.pm index 644c73c8b..b0136786b 100644 --- a/FS/FS/pay_batch/RBC.pm +++ b/FS/FS/pay_batch/RBC.pm @@ -66,7 +66,7 @@ $name = 'RBC'; }, 'approved' => sub { my $hash = shift; - $hash->{'status'} eq ' ' + ($hash->{'status'} eq ' ') || ($hash->{'status'} eq 'W'); }, 'declined' => sub { my $hash = shift; @@ -127,12 +127,6 @@ $name = 'RBC'; if $hash->{'status'} eq ' '; #false laziness with 'approved' above return 1; } - #skipping W for now (maybe it should be declined?) - if ($hash->{'status'} eq 'W') { - #file counts this as part of total, but we skip - $totaloffset += sprintf("%.2f", $hash->{'paid'} / 100 ); - return 1; - } return ($hash->{'recordtype'} eq '3') || #Account Trailer Record, concludes returned items ($hash->{'subtype'} ne '0'); #error messages, etc, too late to apply to previous entry -- cgit v1.2.1 From 05c0b947ddba67a8ac2537c010a583277623ff3e Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 5 Oct 2015 20:12:09 -0700 Subject: show customer name on appointments, RT#34237 --- rt/share/html/Elements/CalendarDaySchedule | 7 ++++++- rt/share/html/Elements/CalendarSlotSchedule | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/rt/share/html/Elements/CalendarDaySchedule b/rt/share/html/Elements/CalendarDaySchedule index 5be5b06bc..bac9a78d4 100644 --- a/rt/share/html/Elements/CalendarDaySchedule +++ b/rt/share/html/Elements/CalendarDaySchedule @@ -31,8 +31,13 @@ $CurrentUser => undef % my( $starts, $due, $col, $t ) = @{ $schedule{'scheduled'}->{$id} }; % my $s = int(($starts-$stime)/10); % my $e = int(($due-$stime)/10)-1; + +% #false laziness w/misc/xmlhttp-ticket-update.html & CalendarSlotSchedule +% my %hash = $m->comp('/Ticket/Elements/Customers', Ticket => $t); +% my @cust_main = values( %{$hash{cust_main}} ); + = $tod_row ) { #first row +% +% #false laziness w/misc/xmlhttp-ticket-update.html & CalendarDaySchedule +% my %hash = $m->comp('/Ticket/Elements/Customers', Ticket => $t); +% my @cust_main = values( %{$hash{cust_main}} ); +% % $content .= ($content?', ':''). #$id. ': '. % #false laziness w/xmlhttp-ticket-update.html % FS::sched_avail::pretty_time($starts). '-'. -% FS::sched_avail::pretty_time($due); +% FS::sched_avail::pretty_time($due). +% ': '. encode_entities($cust_main[0]->_FreesideURILabel); % #'install for custname XX miles away'; #XXX placeholder/more % $draggable_ticketid = $id; % $draggable_length = $due - $starts; -- cgit v1.2.1 From ecc93e41eac485b6e84b844d9db8d25576dab3b9 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 5 Oct 2015 20:13:55 -0700 Subject: show customer name on appointments, RT#34237 --- httemplate/misc/xmlhttp-ticket-update.html | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 httemplate/misc/xmlhttp-ticket-update.html diff --git a/httemplate/misc/xmlhttp-ticket-update.html b/httemplate/misc/xmlhttp-ticket-update.html new file mode 100644 index 000000000..147fbef16 --- /dev/null +++ b/httemplate/misc/xmlhttp-ticket-update.html @@ -0,0 +1,66 @@ +<% encode_json($return) %>\ +<%init> + +my $id = $cgi->param('id'); +my $starts = $cgi->param('starts'); +my $due = $cgi->param('due'); +my $username = $cgi->param('username'); + +my $ticket = FS::TicketSystem->get_ticket_object( \%session, ticket_id=>$id ); + +#hmm, this should happen in a single transaction and either commit or rollback, +# but in reality failures "Don't Happen" so its not like a ticket gets +# half changed + +my $return; +if ( $ticket ) { + + my($orv, $omsg) = $ticket->SetOwner( $username, 'Steal' ); + $orv = 1 if ! $orv && $omsg =~ /already owns/i; + + if ( $orv ) { + + my $date = RT::Date->new( $session{CurrentUser} ); + $date->Set( Format=>'unix', Value=>$starts, ); + my($srv, $smsg) = $ticket->SetStarts( $date->ISO ); + + my $ddate; + unless ( ! $srv ) { + $ddate = RT::Date->new( $session{CurrentUser} ); + $ddate->Set( Format=>'unix', Value=>$due, ); + my($drv, $dmsg) = $ticket->SetDue( $ddate->ISO ); + #XXX lame + } + + if ( $srv ) { + + my ($sm, $sh) = ( $date->Localtime('user'))[1,2]; + my ($em, $eh) = ($ddate->Localtime('user'))[1,2]; + + #false laziness w/CalendarSlotSchedule and CalendarDaySchedule + my %hash = $m->comp('/rt/Ticket/Elements/Customers', Ticket => $ticket); + my @cust_main = values( %{$hash{cust_main}} ); + + $return = { 'error' => '', + #'starts' => $starts, + #'due' => $due, + #'username' => $username, + #false laziness w/CalendarSlotSchedule + 'sched_label' => + FS::sched_avail::pretty_time($sh*60+$sm). '-'. + FS::sched_avail::pretty_time($eh*60+$em). ': '. + $cust_main[0]->_FreesideURILabel, + }; + } else { + $return = { 'error' => $smsg }; + } + + } else { + $return = { 'error' => $omsg }; + } + +} else { + $return = { 'error' => 'Unknown ticket id' }; +} + + -- cgit v1.2.1 From a267a869ad2f2c9b6ba4e306aea6103e3a6bfe4e Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 6 Oct 2015 00:35:18 -0500 Subject: RT#38314: Declined payment shows card as tokenized after first attempt --- FS/FS/cust_main/Billing_Realtime.pm | 1 + httemplate/misc/process/payment.cgi | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 2a920e074..434815c16 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -622,6 +622,7 @@ sub realtime_bop { '_date' => '', 'payby' => $bop_method2payby{$options{method}}, 'payinfo' => $options{payinfo}, + 'paymask' => $options{paymask}, 'paydate' => $paydate, 'recurring_billing' => $content{recurring_billing}, 'pkgnum' => $options{'pkgnum'}, diff --git a/httemplate/misc/process/payment.cgi b/httemplate/misc/process/payment.cgi index d9299e5bd..efba9ed9a 100644 --- a/httemplate/misc/process/payment.cgi +++ b/httemplate/misc/process/payment.cgi @@ -74,11 +74,13 @@ $cgi->param('balance') =~ /^\s*(\-?\s*\d*(\.\d\d)?)\s*$/ my $balance = $1; my $payinfo; +my $paymask; # override only used by loaded cust payinfo, only implemented for realtime processing my $paycvv = ''; if ( $payby eq 'CHEK' ) { if ($cgi->param('payinfo1') =~ /xx/i || $cgi->param('payinfo2') =~ /xx/i ) { $payinfo = $cust_main->payinfo; + $paymask = $cust_main->paymask; } else { $cgi->param('payinfo1') =~ /^(\d+)$/ or errorpage("Illegal account number ". $cgi->param('payinfo1')); @@ -99,6 +101,7 @@ if ( $payby eq 'CHEK' ) { $payinfo = $cgi->param('payinfo'); if ($payinfo eq $cust_main->paymask) { $payinfo = $cust_main->payinfo; + $paymask = $cust_main->paymask; } $payinfo =~ s/\D//g; $payinfo =~ /^(\d{13,16}|\d{8,9})$/ @@ -145,7 +148,8 @@ if ( $cgi->param('save') ) { } else { die "unknown payby $payby"; } - $new->payinfo($payinfo); #to properly set paymask + $new->payinfo($payinfo); # sets default paymask, but not if it's already tokenized + $new->paymask($paymask) if $paymask; # in case it's been tokenized, override with loaded paymask $new->set( 'paydate' => "$year-$month-01" ); $new->set( 'payname' => $payname ); @@ -199,6 +203,7 @@ if ( $cgi->param('batch') ) { 'manual' => 1, 'balance' => $balance, 'payinfo' => $payinfo, + 'paymask' => $paymask, 'paydate' => "$year-$month-01", 'payname' => $payname, 'payunique' => $payunique, -- cgit v1.2.1 From cf512ab17435a0199ae13a8faefef94600a7a61b Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 6 Oct 2015 01:46:31 -0500 Subject: RT#37038 Add Card Type Name to Payment Report --- FS/FS/payinfo_Mixin.pm | 6 +++++- httemplate/search/cust_pay.html | 1 + httemplate/search/elements/cust_pay_or_refund.html | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm index c66e3bc37..6b96bbe27 100644 --- a/FS/FS/payinfo_Mixin.pm +++ b/FS/FS/payinfo_Mixin.pm @@ -239,7 +239,11 @@ sub payby_payinfo_pretty { my $locale = shift; my $lh = FS::L10N->get_handle($locale); if ( $self->payby eq 'CARD' ) { - $lh->maketext('Card #') . $self->paymask; + if ($self->paymask =~ /tokenized/) { + $lh->maketext('Tokenized Card'); + } else { + $lh->maketext('Card #') . $self->paymask; + } } elsif ( $self->payby eq 'CHEK' ) { #false laziness w/view/cust_main/payment_history.html::translate_payinfo diff --git a/httemplate/search/cust_pay.html b/httemplate/search/cust_pay.html index 03474c1ef..e466f6afa 100755 --- a/httemplate/search/cust_pay.html +++ b/httemplate/search/cust_pay.html @@ -3,4 +3,5 @@ 'amount_field' => 'paid', 'name_singular' => emt('payment'), 'name_verb' => emt('paid'), + 'show_card_type' => 1, &> diff --git a/httemplate/search/elements/cust_pay_or_refund.html b/httemplate/search/elements/cust_pay_or_refund.html index 5808e5f3e..1fea67c19 100755 --- a/httemplate/search/elements/cust_pay_or_refund.html +++ b/httemplate/search/elements/cust_pay_or_refund.html @@ -184,6 +184,16 @@ push @fields, 'payby_payinfo_pretty', push @link_onclicks, $sub_receipt, ''; push @sort_fields, 'paysort', $amount_field; +if ($opt{'show_card_type'}) { + push @header, emt('Card Type'); + $align .= 'r'; + push @links, ''; + push @fields, sub { + (($_[0]->payby eq 'CARD') && ($_[0]->paymask !~ /N\/A/)) ? cardtype($_[0]->paymask) : '' + }; + push @sort_fields, ''; +} + if ( $unapplied ) { push @header, emt('Unapplied'); $align .= 'r'; -- cgit v1.2.1 From 2b500be7e787a54eb005caa274406957728d8b1b Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 6 Oct 2015 03:58:39 -0700 Subject: consider "quick payment entry" payments manual for payment receipt purposes, RT#33681 --- FS/FS/cust_pay.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index 89bb193d2..d9ae0d39e 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -960,7 +960,7 @@ sub batch_insert { } } elsif ( !$error ) { #normal case: apply payments as usual - $cust_pay->cust_main->apply_payments; + $cust_pay->cust_main->apply_payments( 'manual'=>1 ); } } @@ -1311,7 +1311,7 @@ sub process_batch_import { my $cust_pay = shift; my $cust_main = $cust_pay->cust_main or return "can't find customer to which payments apply"; - my $error = $cust_main->apply_payments_and_credits; + my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 ); return $error ? "can't apply payments to customer ".$cust_pay->custnum."$error" : ''; -- cgit v1.2.1 From f27e48e51e2154468d960f1be656538373332ee5 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 6 Oct 2015 20:17:42 -0500 Subject: RT#38481: installer scheduling: redirect to basics (custom field edit) page instead of ticket view --- httemplate/elements/schedule-appointment.html | 3 ++- rt/share/html/Elements/CalendarSlotSchedule | 4 +++- rt/share/html/Search/Schedule.html | 1 + rt/share/html/Ticket/Display.html | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/httemplate/elements/schedule-appointment.html b/httemplate/elements/schedule-appointment.html index 45a8a5bab..69b9f422b 100644 --- a/httemplate/elements/schedule-appointment.html +++ b/httemplate/elements/schedule-appointment.html @@ -12,7 +12,8 @@ my $custnum = encode_entities( $cgi->param('custnum') ); my $query = join('&', map "username=$_", @username). "&LengthMin=$LengthMin". - "&custnum=$custnum"; + "&custnum=$custnum". + "&RedirectToBasics=1"; #XXX '&pkgnum=$pkgnum";need to be for specific package/location, not just for a customer... default to active(/ordered) package in a pkg_class w/ticketing_queueid, otherwise, a popup? we're getting complicated like form-creat_ticket.html my $url = $p. 'rt/Search/Schedule.html?'. $query; diff --git a/rt/share/html/Elements/CalendarSlotSchedule b/rt/share/html/Elements/CalendarSlotSchedule index 632fabba3..251347148 100644 --- a/rt/share/html/Elements/CalendarSlotSchedule +++ b/rt/share/html/Elements/CalendarSlotSchedule @@ -9,6 +9,7 @@ $LengthMin => $default_timestep $custnum => undef $pkgnum => undef + $RedirectToBasics => 0 % foreach my $username ( @username ) { % @@ -110,7 +111,8 @@ % '&Starts='. $Date->strftime('%F').'%20'. $Starts. % '&Due='. $Date->strftime('%F').'%20'. $Due. % '&new-MemberOf='. $member. #XXX uri_escape? -% '&Status=new'; +% '&Status=new'. +% '&RedirectToBasics='.$RedirectToBasics; % #'&Requestors='. #XXX Freeside customer requestor(s) (package? onmouseover = "boxon(this);" diff --git a/rt/share/html/Search/Schedule.html b/rt/share/html/Search/Schedule.html index 43680b47c..ccd844bf8 100644 --- a/rt/share/html/Search/Schedule.html +++ b/rt/share/html/Search/Schedule.html @@ -212,6 +212,7 @@ #oops, more freeside abstraction-leaking custnum => $ARGS{custnum}, pkgnum => $ARGS{pkgnum}, + RedirectToBasics => $ARGS{RedirectToBasics}, ], &> diff --git a/rt/share/html/Ticket/Display.html b/rt/share/html/Ticket/Display.html index 41684c5be..96a49d4e4 100755 --- a/rt/share/html/Ticket/Display.html +++ b/rt/share/html/Ticket/Display.html @@ -99,6 +99,7 @@ $id => undef $TicketObj => undef $ShowHeaders => 0 $ForceShowHistory => 0 +$RedirectToBasics => 0 <%INIT> @@ -217,6 +218,7 @@ $m->callback( ); # This code does automatic redirection if any updates happen. +$m->notes('RedirectToBasics' => 1) if $RedirectToBasics; my $path = '/Ticket/'. ( $m->notes('RedirectToBasics') ? 'Modify.html' : 'Display.html' ); MaybeRedirectForResults( -- cgit v1.2.1 From 956df0bc6383ed0513d4dd00668f3b24c42ba1e4 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Tue, 6 Oct 2015 13:37:15 -0400 Subject: cdr types for AMCom CDR's --- FS/FS/cdr/amcom.pm | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/FS/FS/cdr/amcom.pm b/FS/FS/cdr/amcom.pm index 97ab402ca..43e6afd60 100644 --- a/FS/FS/cdr/amcom.pm +++ b/FS/FS/cdr/amcom.pm @@ -4,6 +4,8 @@ use strict; use base qw( FS::cdr ); use vars qw( %info ); use DateTime; +use FS::Record qw( qsearchs ); +use FS::cdr_type; my ($tmp_mday, $tmp_mon, $tmp_year); @@ -29,7 +31,14 @@ my ($tmp_mday, $tmp_mon, $tmp_year); if $cdr->accountcode eq '' && $field =~ /^(1800|1300)/; }, 'uniqueid', # 4. Record ID - 'dcontext', # 5. Call Category (LOCAL, NATIONAL, FREECALL, MOBILE) + sub { # 5. Call Category (LOCAL, NATIONAL, FREECALL, MOBILE) + my ($cdr, $data) = @_; + $data ||= 'none'; + + my $cdr_type = qsearchs('cdr_type', { 'cdrtypename' => $data } ); + $cdr->set('cdrtypenum', $cdr_type->cdrtypenum) if $cdr_type; + $cdr->set('dcontext', $data); + }, sub { # 6. Start Date (DDMMYYYY my ($cdr, $date) = @_; $date =~ /^(\d{2})(\d{2})(\d{4})$/ -- cgit v1.2.1 From fc672686f119da0b3b34fd3c73acc3fea81262e6 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 7 Oct 2015 14:18:15 -0700 Subject: #37098: convert one-shot email notices to use message templates --- FS/FS/cust_main_Mixin.pm | 22 +++- FS/FS/msg_template.pm | 6 +- httemplate/misc/email-customers.html | 191 +++++++++++++++++++++-------------- 3 files changed, 140 insertions(+), 79 deletions(-) diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index 3d05f8473..867d43e60 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -426,6 +426,18 @@ sub email_search_result { if ( $msgnum ) { $msg_template = qsearchs('msg_template', { msgnum => $msgnum } ) or die "msgnum $msgnum not found\n"; + } else { + $msg_template = FS::msg_template->new({ + from_addr => $from, + msgname => $subject, # maybe a timestamp also? + disabled => 'D', # 'D'raft + # msgclass, maybe + }); + $error = $msg_template->insert( + subject => $subject, + body => $html_body, + ); + return "$error (when creating draft template)" if $error; } my $sql_query = $class->search($param->{'search'}); @@ -446,7 +458,7 @@ sub email_search_result { my %sent_to = (); if ( !$msg_template ) { - # XXX create on the fly + die "email_search_result now requires a msg_template"; } #eventually order+limit magic to reduce memory use? @@ -516,6 +528,14 @@ sub email_search_result { } } # foreach $obj + # if the message template was created as "draft", change its status to + # "completed" + if ($msg_template->disabled eq 'D') { + $msg_template->set('disabled' => 'C'); + my $error = $msg_template->replace; + warn "$error (setting draft message template status)" if $error; + } + if(@retry_jobs) { # fail the job, but with a status message that makes it clear # something was sent. diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index 49403889c..d17fd41cb 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -66,7 +66,9 @@ global template. =item bcc_addr - Bcc all mail to this address. -=item disabled - disabled ('Y' or NULL). +=item disabled - disabled (NULL for not-disabled and selectable, 'D' for a +draft of a one-time message, 'C' for a completed one-time message, 'Y' for a +normal template disabled by user action). =back @@ -247,7 +249,7 @@ sub check { || $self->ut_text('msgname') || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum') || $self->ut_textn('mime_type') - || $self->ut_enum('disabled', [ '', 'Y' ] ) + || $self->ut_enum('disabled', [ '', 'Y', 'D', 'S' ] ) || $self->ut_textn('from_addr') || $self->ut_textn('bcc_addr') # fine for now, but change this to some kind of dynamic check if we diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html index bffd0cf81..8e2863455 100644 --- a/httemplate/misc/email-customers.html +++ b/httemplate/misc/email-customers.html @@ -22,13 +22,13 @@ title - the title of the page no_search_fields - arrayref of additional fields that are not search parameters alternate_form - subroutine that returns alternate html for the initial form, -replaces msgnum/from/subject/html_body/action inputs and submit button, not +replaces msgnum/from/subject/body/action inputs and submit button, not used if an action is specified post_search_hook - sub hook for additional processing after search has been processed from cgi, gets passed options 'conf' and 'search' (a reference to the unfrozen %search hash), should be used to set msgnum or -from/subject/html_body cgi params +from/subject/body cgi params % if ($popup) { @@ -37,6 +37,7 @@ from/subject/html_body cgi params <& /elements/header.html, $title &> % } +<& /elements/error.html &>
    @@ -48,48 +49,40 @@ from/subject/html_body cgi params -% if ( $cgi->param('action') eq 'send' ) { - - Sending notice +% if ( $cgi->param('preview') ) { +% # preview mode: at this point we have a msg_template (either "real" or +% # draft) and $html_body and $text_body contain the preview message. +% # give the user a chance to back out (by going back to edit mode). + Preview notice <& /elements/progress-init.html, 'OneTrueForm', - [ qw( search table from subject html_body text_body msgnum ) ], + [ qw( search table msgnum ) ], $process_url, $pdest, &> -% } elsif ( $cgi->param('action') eq 'preview' ) { - - Preview notice - -% } - -% if ( $cgi->param('action') ) { - - - -% if ( $msg_template ) { - <& /elements/tr-fixed.html, - 'label' => 'Template:', - 'value' => $msg_template->msgname, - &> -% } + +% # kludge these through hidden inputs because they're not really part +% # of the template, but should be sticky during draft editing + + + +% if ( !$msg_template->disabled ) { + <& /elements/tr-td-label.html, 'label' => 'Template:' &> + + +% } - <& /elements/tr-fixed.html, - 'field' => 'from', - 'label' => 'From:', - 'value' => $from, - &> + <& /elements/tr-td-label.html, 'label' => 'From:' &> + + - <& /elements/tr-fixed.html, - 'field' => 'subject', - 'label' => 'Subject:', - 'value' => $subject, - &> + <& /elements/tr-td-label.html, 'label' => 'Subject:' &> + + -
    <% $msg_template->msgname |h %>
    <% $from |h %>
    <% $subject |h %>
     
    Message (HTML display): @@ -101,7 +94,6 @@ from/subject/html_body cgi params % $html_body % ) % ); -
     
    Message (Text display): @@ -114,38 +106,37 @@ from/subject/html_body cgi params
    -% if ( $cgi->param('action') eq 'preview' ) { + + } + -
    - - - -% } +
    + + % } elsif ($opt{'alternate_form'}) { <% &{$opt{'alternate_form'}}() %> % } else { +% # Edit mode. +% if ( $msg_template and $msg_template->disabled ) { +% # if we've already established a draft template, don't let msgnum be changed + <& /elements/hidden.html, + field => 'msgnum', + curr_value => ($cgi->param('msgnum') || ''), + &> +% } else { Template: <& /elements/select-msg_template.html, - onchange => 'toggle(this)', + onchange => 'toggle(this)', + curr_value => ($cgi->param('msgnum') || ''), &>
    +% } <& /elements/tr-td-label.html, 'label' => 'From:' &> <& /elements/tr-input-text.html, 'field' => 'subject', 'label' => 'Subject:', 'size' => 50, + 'curr_value' => $subject, &>
    <& /elements/input-text.html, @@ -165,46 +165,41 @@ Template: 'value' => $conf->config('invoice_from_name', $agent_virt_agentnum) || $conf->config('company_name', $agent_virt_agentnum), #? 'size' => 20, + 'curr_value' => $cgi->param('from_name'), &> <\ <& /elements/input-text.html, 'field' => 'from_addr', 'type' => 'email', # HTML5, woot 'value' => $conf->config('invoice_from', $agent_virt_agentnum), 'size' => 20, + 'curr_value' => $cgi->param('from_addr'), &>>
    Message: <& /elements/htmlarea.html, - 'field' => 'html_body', + 'field' => 'body', 'width' => 763, + 'curr_value' => $body, &>
    -%#Substitution vars: - - - + % } #end not action or alternate form
    -% if ( $cgi->param('action') eq 'send' ) { - -% } - <& /elements/footer.html &> <%init> @@ -217,7 +212,7 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right($opt{'acl'}); my $conf = FS::Conf->new; -my @no_search_fields = qw( action table from subject html_body text_body popup url ); +my @no_search_fields = qw( table from subject html_body text_body popup url ); my $form_action = $opt{'form_action'} || 'email-customers.html'; my $process_url = $opt{'process_url'} || 'process/email-customers.html'; @@ -261,12 +256,26 @@ if ( $cgi->param('from') ) { $from = $cgi->param('from_addr'); } -my $subject = $cgi->param('subject') || ''; -my $html_body = $cgi->param('html_body') || ''; - my $msg_template = ''; +if ( $cgi->param('msgnum') =~ /^(\d+)$/ ) { + $msg_template = FS::msg_template->by_key($1) + or die "template not found: ".$cgi->param('msgnum'); +} + +my $subject = $cgi->param('subject'); +my $body = $cgi->param('body'); +my ($html_body, $text_body); -if ( $cgi->param('action') eq 'preview' ) { +if ( !$cgi->param('preview') ) { + + # edit mode: initialize the fields from the saved draft, if there is one + if ( $msg_template and $msg_template->disabled eq 'D' ) { + my $content = $msg_template->content(''); # no localization on these yet + $subject ||= $content->subject; + $body ||= $content->body; + } + +} else { my $sql_query = "FS::$table"->search(\%search); my $count_query = delete($sql_query->{'count_query'}); @@ -277,10 +286,40 @@ if ( $cgi->param('action') eq 'preview' ) { my $count_arrayref = $count_sth->fetchrow_arrayref; $num_cust = $count_arrayref->[0]; - if ( $cgi->param('msgnum') ) { - $msg_template = qsearchs('msg_template', - { msgnum => scalar($cgi->param('msgnum')) } ) - or die "template not found: ".$cgi->param('msgnum'); + if ( !$msg_template or $msg_template->disabled eq 'D' ) { + # then this is a one-off template; edit it in place + my $subject = $cgi->param('subject') || ''; + my $body = $cgi->param('body') || ''; + + # create a draft template + $msg_template ||= FS::msg_template->new({ + msgclass => 'email', + disabled => 'D', + }); + # anyone have a better idea for msgname? + $msg_template->set('msgname' => "Notice " . DateTime->now->iso8601); + $msg_template->set('from_addr' => $from); + my %content = ( + subject => $subject, + body => $body, + ); + my $error; + if ( $msg_template->msgnum ) { + $error = $msg_template->replace(%content); + } else { + $error = $msg_template->insert(%content); + } + + if ( $error ) { + $cgi->param('error', $error); + $cgi->delete('preview'); # don't go on to preview stage yet + undef $msg_template; + } + } + # unless creating the msg_template failed, we now have one, so construct a + # preview message from the first customer/whatever in the search results + + if ( $msg_template ) { $sql_query->{'extra_sql'} .= ' LIMIT 1'; $sql_query->{'select'} = "$table.*"; $sql_query->{'order_by'} = ''; -- cgit v1.2.1 From 3b6d92312c10df349d91999f496ed2539b56c608 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 7 Oct 2015 16:21:39 -0700 Subject: add msgclass to initial msg_template, #38500 --- FS/FS/msg_template/InitialData.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/FS/FS/msg_template/InitialData.pm b/FS/FS/msg_template/InitialData.pm index a4e27fdc9..dbb9f4037 100644 --- a/FS/FS/msg_template/InitialData.pm +++ b/FS/FS/msg_template/InitialData.pm @@ -3,6 +3,7 @@ package FS::msg_template::InitialData; sub _initial_data { [ { msgname => 'Password reset', + msgclass => 'email', mime_type => 'text/html', #multipart/alternative with a text part? # cranky mutt/pine users like me are rare -- cgit v1.2.1 From 6b1a8c7316892165fa4dc8b66bb962bf113aa3d8 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 8 Oct 2015 13:46:55 -0700 Subject: remove fax from short contact info, RT#25536 --- httemplate/elements/contact.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httemplate/elements/contact.html b/httemplate/elements/contact.html index ef74481c0..87e15debe 100644 --- a/httemplate/elements/contact.html +++ b/httemplate/elements/contact.html @@ -135,7 +135,7 @@ tie my %label, 'Tie::IxHash', my $first = 0; foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { - next if $phone_type->typename eq 'Home'; + next if $phone_type->typename =~ /^(Home|Fax)$/; my $f = 'phonetypenum'.$phone_type->phonetypenum; $label{$f} = $phone_type->typename. ' phone'; $size{$f} = $first++ ? 10 : 15; -- cgit v1.2.1 From 294e2ce31d6bbd2784a812d20438f9b223de0490 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 8 Oct 2015 15:31:31 -0700 Subject: more detailed tax-credit report, #37088 --- FS/FS/Report/Tax.pm | 36 +++++++++-- httemplate/search/cust_credit_bill_pkg.html | 49 +++++++++++++-- httemplate/search/report_tax-xls.cgi | 94 +++++++++++++++++++++++++---- httemplate/search/report_tax.cgi | 86 +++++++++++++++++++++++++- 4 files changed, 240 insertions(+), 25 deletions(-) diff --git a/FS/FS/Report/Tax.pm b/FS/FS/Report/Tax.pm index 2480a45b9..a892a6b87 100644 --- a/FS/FS/Report/Tax.pm +++ b/FS/FS/Report/Tax.pm @@ -240,6 +240,25 @@ sub report_internal { $group "; + # also include the exempt-sales credit amount, for the credit report + $sql{exempt_credited} = "$select + SUM(COALESCE(exempt_credited, 0)) + FROM cust_main_county + LEFT JOIN ($exempt_credit) AS exempt_credit USING (taxnum) + JOIN cust_bill_pkg USING (billpkgnum) + $join_cust_pkg $where AND $nottax + $group + "; + + $all_sql{exempt_credited} = "$select_all + SUM(COALESCE(exempt_credited, 0)) + FROM cust_main_county + LEFT JOIN ($exempt_credit) AS exempt_credit USING (taxnum) + JOIN cust_bill_pkg USING (billpkgnum) + $join_cust_pkg $where AND $nottax + $group + "; + # taxable sales $sql{taxable} = "$select SUM(cust_bill_pkg.setup + cust_bill_pkg.recur @@ -339,12 +358,12 @@ sub report_internal { my $istax = "cust_bill_pkg.pkgnum = 0 and cust_bill_pkg.feepart is null"; - $sql{tax} = "$select SUM(cust_bill_pkg_tax_location.amount) + $sql{tax} = "$select COALESCE(SUM(cust_bill_pkg_tax_location.amount),0) $taxfrom $where AND $istax $group"; - $all_sql{tax} = "$select_all SUM(cust_bill_pkg_tax_location.amount) + $all_sql{tax} = "$select_all COALESCE(SUM(cust_bill_pkg_tax_location.amount),0) $taxfrom $where AND $istax $group_all"; @@ -364,12 +383,12 @@ sub report_internal { $creditwhere =~ s/cust_bill._date/cust_credit_bill._date/g; } - $sql{tax_credited} = "$select SUM(cust_credit_bill_pkg.amount) + $sql{tax_credited} = "$select COALESCE(SUM(cust_credit_bill_pkg.amount),0) $creditfrom $creditwhere AND $istax $group"; - $all_sql{tax_credited} = "$select_all SUM(cust_credit_bill_pkg.amount) + $all_sql{tax_credited} = "$select_all COALESCE(SUM(cust_credit_bill_pkg.amount),0) $creditfrom $creditwhere AND $istax $group_all"; @@ -385,12 +404,12 @@ sub report_internal { ' ON (cust_bill_pay_pkg.billpkgtaxlocationnum ='. ' cust_bill_pkg_tax_location.billpkgtaxlocationnum)'; - $sql{tax_paid} = "$select SUM(cust_bill_pay_pkg.amount) + $sql{tax_paid} = "$select COALESCE(SUM(cust_bill_pay_pkg.amount),0) $paidfrom $where AND $istax $group"; - $all_sql{tax_paid} = "$select_all SUM(cust_bill_pay_pkg.amount) + $all_sql{tax_paid} = "$select_all COALESCE(SUM(cust_bill_pay_pkg.amount),0) $paidfrom $where AND $istax $group_all"; @@ -562,6 +581,11 @@ sub table { $this_row{exempt_pkg} + $this_row{exempt_monthly} ); + $this_row{credits} = sprintf('%.2f', + $this_row{sales_credited} + + $this_row{exempt_credited} + + $this_row{tax_credited} + ); # and give it a label if ( $this_row{total} ) { $this_row{label} = 'Total'; diff --git a/httemplate/search/cust_credit_bill_pkg.html b/httemplate/search/cust_credit_bill_pkg.html index b5a0ee9f6..5e70c23bd 100644 --- a/httemplate/search/cust_credit_bill_pkg.html +++ b/httemplate/search/cust_credit_bill_pkg.html @@ -3,11 +3,12 @@ 'name_singular' => 'credit application', 'query' => $query, 'count_query' => $count_query, - 'count_addl' => [ $money_char. '%.2f total', ], + 'count_addl' => \@count_addl, 'header' => [ #'#', 'Amount', + 'Tax exempt', #credit 'Date', @@ -26,7 +27,9 @@ ], 'fields' => [ #'creditbillpkgnum', - sub { sprintf($money_char.'%.2f', shift->amount ) }, + sub { sprintf($money_char.'%.2f', shift->amount ) }, + + sub { sprintf($money_char.'%.2f', shift->get('exempt_credited') ) }, sub { time2str('%b %d %Y', shift->get('cust_credit_date') ) }, sub { shift->cust_credit_bill->cust_credit->otaker }, @@ -44,6 +47,7 @@ ], 'sort_fields' => [ 'amount', + 'exempt_credited', 'cust_credit_date', '', #'otaker', '', #reason @@ -55,6 +59,7 @@ #cust fields ], 'links' => [ + '', '', '', '', @@ -68,11 +73,12 @@ FS::UI::Web::cust_header() ), ], - 'align' => 'rrllll'. + 'align' => 'rrrllll'. $post_desc_align. 'rr'. FS::UI::Web::cust_aligns(), - 'color' => [ + 'color' => [ + '', '', '', '', @@ -85,6 +91,7 @@ FS::UI::Web::cust_colors(), ], 'style' => [ + '', '', '', '', @@ -286,7 +293,6 @@ if ( $cgi->param('out') ) { #} push @where, $loc_sql; -warn $loc_sql; } my($title, $name); @@ -393,6 +399,9 @@ if ( $cgi->param('cust_tax') ) { my $count_query = "SELECT COUNT(DISTINCT creditbillpkgnum), SUM(cust_credit_bill_pkg.amount)"; +if ( $cgi->param('nottax') ) { + $count_query .= ", SUM(exempt_credited)"; +} my $join_cust = ' JOIN cust_bill ON ( cust_bill_pkg.invnum = cust_bill.invnum )'. @@ -405,6 +414,21 @@ my $join_cust_bill_pkg = 'LEFT JOIN cust_bill_pkg USING ( billpkgnum )'; if ( $cgi->param('nottax') ) { + # There can be multiple cust_tax_exempt_pkg records with the same + # creditbillpkgnum iff the line item is exempt from multiple taxes. + # They will all have the same amount, except in the case where there are + # different exemption types and so the exemption amounts are different. + # In that case, show the amount of the largest exemption. + + $join_cust_bill_pkg .= ' + LEFT JOIN( + SELECT creditbillpkgnum, + MAX(0 - cust_tax_exempt_pkg.amount) AS exempt_credited + FROM cust_tax_exempt_pkg + WHERE creditbillpkgnum IS NOT NULL + GROUP BY creditbillpkgnum + ) AS exempt_credit USING (creditbillpkgnum) + '; $join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum ) LEFT JOIN part_pkg USING ( pkgpart ) LEFT JOIN part_pkg AS override @@ -472,6 +496,12 @@ push @select, 'part_pkg.pkg' unless $cgi->param('istax'); push @select, 'cust_main.custnum', FS::UI::Web::cust_sql_fields(); +if ( $cgi->param('istax') ) { + push @select, 'NULL AS exempt_credited'; # just display zero +} elsif ( $cgi->param('nottax') ) { + push @select, 'exempt_credited'; +} + my @post_desc_header = (); my @post_desc = (); my @post_desc_null = (); @@ -555,4 +585,13 @@ my $location_sub = sub { }; +my @count_addl = ( $money_char. '%.2f total', ); +if ( $cgi->param('nottax') ) { + push @count_addl, ( $money_char. '%.2f tax exempt' ); +} + +if ( $cgi->param('debug') ) { + warn "\nQUERY:\n" . Dumper($query) . "\nCOUNT_QUERY:\n$count_query\n\n"; +} + diff --git a/httemplate/search/report_tax-xls.cgi b/httemplate/search/report_tax-xls.cgi index 743f14788..773b403f1 100755 --- a/httemplate/search/report_tax-xls.cgi +++ b/httemplate/search/report_tax-xls.cgi @@ -122,7 +122,7 @@ my %default = ( border => 1, ); my @widths = ( #ick - 30, (13) x 5, 3, 7.5, 3, 11, 11, 3, 11, 3, 11 + 30, (13) x 6, 3, 7.5, 3, 11, 11, 3, 11, 3, 11 ); my @format = ( {}, {}, {} ); # white row, gray row, yellow (totals) row @@ -134,29 +134,34 @@ foreach (keys(%formatdef)) { italic => 1, %f); } -my $ws = $workbook->add_worksheet('taxreport'); +my $ws = $workbook->add_worksheet('Sales and Tax'); # main title $ws->merge_range(0, 0, 0, 14, $report->title, $format[0]->{title}); +$ws->set_row(0, 30); # excel position my $x = 0; my $y = 2; my $colhead = $format[0]->{colhead}; # print header -$ws->merge_range($y, 1, $y, 5, 'Sales', $colhead); -$ws->merge_range($y, 6, $y+1, 8, 'Rate', $colhead); -$ws->merge_range($y, 9, $y, 15, 'Tax', $colhead); +$ws->merge_range($y, 1, $y, 6, 'Sales', $colhead); +$ws->merge_range($y, 7, $y+1, 9, 'Rate', $colhead); +$ws->merge_range($y, 10, $y, 16, 'Tax', $colhead); $y++; $colhead = $format[0]->{colhead_small}; -$ws->write($y, 1, [ 'Total', 'Exempt customer', 'Exempt package', 'Monthly exemption', +$ws->write($y, 1, [ 'Total', + 'Exempt customer', + 'Exempt package', + 'Monthly exemption', + 'Credited', 'Taxable' ], $colhead); -$ws->write($y, 9, 'Estimated', $colhead); -$ws->write($y, 10, 'Invoiced', $colhead); -$ws->write($y, 12, 'Credited', $colhead); -$ws->write($y, 14, 'Net due', $colhead); -$ws->write($y, 15, 'Collected',$colhead); +$ws->write($y, 10, 'Estimated', $colhead); +$ws->write($y, 11, 'Invoiced', $colhead); +$ws->write($y, 13, 'Credited', $colhead); +$ws->write($y, 15, 'Net due', $colhead); +$ws->write($y, 16, 'Collected',$colhead); $y++; # print data @@ -168,7 +173,7 @@ foreach my $row (@rows) { if ( $row->{pkgclass} ne $prev_row->{pkgclass} ) { $rownum = 1; if ( $params{breakdown}->{pkgclass} ) { - $ws->merge_range($y, 0, $y, 14, + $ws->merge_range($y, 0, $y, 15, $pkgclass_name{$row->{pkgclass}}, $format[0]->{sectionhead} ); @@ -182,7 +187,7 @@ foreach my $row (@rows) { } $ws->write($y, $x, $row->{label}, $f->{rowhead}); $x++; - foreach (qw(sales exempt_cust exempt_pkg exempt_monthly taxable)) { + foreach (qw(sales exempt_cust exempt_pkg exempt_monthly sales_credited taxable)) { $ws->write($y, $x, $row->{$_} || 0, $f->{currency}); $x++; } @@ -229,6 +234,69 @@ for my $x (0..scalar(@widths)-1) { $ws->set_column($x, $x, $widths[$x]); } +# do the same for the credit worksheet +$ws = $workbook->add_worksheet('Credits'); + +my $title = $report->title; +$title =~ s/Tax Report/Credits/; +# main title +$ws->merge_range(0, 0, 0, 14, $title, $format[0]->{title}); +$ws->set_row(0, 30); # height +# excel position +$x = 0; +$y = 2; + +$colhead = $format[0]->{colhead}; +# print header +$ws->merge_range($y, 1, $y+1, 1, 'Total', $colhead); +$ws->merge_range($y, 2, $y, 4, 'Applied to', $colhead); + +$y++; +$colhead = $format[0]->{colhead_small}; +$ws->write($y, 2, [ 'Taxable sales', + 'Tax-exempt sales', + 'Taxes' + ], $colhead); +$y++; + +# print data +$rownum = 1; +$prev_row = { pkgclass => 'DUMMY PKGCLASS' }; + +foreach my $row (@rows) { + $x = 0; + if ( $row->{pkgclass} ne $prev_row->{pkgclass} ) { + $rownum = 1; + if ( $params{breakdown}->{pkgclass} ) { + $ws->merge_range($y, 0, $y, 4, + $pkgclass_name{$row->{pkgclass}}, + $format[0]->{sectionhead} + ); + $y++; + } + } + # pick a format set + my $f = $format[$rownum % 2]; + if ( $row->{total} ) { + $f = $format[2]; + } + $ws->write($y, $x, $row->{label}, $f->{rowhead}); + $x++; + foreach (qw(credits sales_credited exempt_credited tax_credited)) { + $ws->write($y, $x, $row->{$_} || 0, $f->{currency}); + $x++; + } + + $rownum++; + $y++; + $prev_row = $row; +} + +for my $x (0..4) { + $ws->set_column($x, $x, $widths[$x]); +} + + $workbook->close; http_header('Content-Length' => length($data)); diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi index 0ad143f01..2b531ea46 100644 --- a/httemplate/search/report_tax.cgi +++ b/httemplate/search/report_tax.cgi @@ -18,6 +18,7 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } .bigmath { font-size: large; font-weight: bold; font: sans-serif; text-align: center } .total { font-style: italic } + <& /elements/table-grid.html &> @@ -88,7 +89,6 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } % $rowlink .= ';classnum=' . ($row->{pkgclass} || 0); % $rowregion .= ';classnum=' . ($row->{pkgclass} || 0); % } -%warn $rowregion; % % if ( $row->{total} ) { @@ -183,6 +183,90 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } % } +
    +<& /elements/table-grid.html &> + + + + Total credits + Applied to + + + Taxable sales + Tax-exempt sales + Taxes + + + +% $rownum = 0; +% $prev_row = { pkgclass => 'DUMMY PKGCLASS' }; + + +% # mostly duplicates the stuff above... +% # but putting it all in one giant table is no good +% foreach my $row (@rows) { +% if ( $row->{pkgclass} ne $prev_row->{pkgclass} ) { +% if ( $rownum > 0 ) { # start a new section +% $rownum = 0; + +% } +% if ( $params{breakdown}->{pkgclass} ) { # and caption the new section + + + <% $pkgclass_name{$row->{pkgclass}} %> + + +% } +% } # if $row->{pkgclass} ne ... + +% my $rowlink = ';taxnum=' . $row->{taxnums}; +% my $rowregion = ';country=' . $cgi->param('country'); +% foreach my $loc (qw(state county city district)) { +% if ( $row->{$loc} ) { +% $rowregion .= ";$loc=" . uri_escape($row->{$loc}); +% } +% } +% if ( $params{breakdown}->{pkgclass} ) { +% $rowlink .= ';classnum=' . ($row->{pkgclass} || 0); +% $rowregion .= ';classnum=' . ($row->{pkgclass} || 0); +% } +% +% if ( $row->{total} ) { + +% } + + <% $row->{label} |h %> + +% # Total credits + <% $money_sprintf->( $row->{credits} ) %> + +% # Credits to taxable sales + + + <% $money_sprintf->( $row->{sales_credited} ) %> + + +% # ... to exempt sales (link is the same, it shows both exempt and taxable) + + + <% $money_sprintf->( $row->{exempt_credited} ) %> + + +% # ... to taxes + +%# currently broken + <% $money_sprintf->( $row->{tax_credited} ) %> +%# + + +% $rownum++; +% $prev_row = $row; +% } # foreach my $row +% # no "out of taxable region" for credits (yet) + + + + <& /elements/footer.html &> <%init> -- cgit v1.2.1 From 37b0f7bbb5737d02444dca82da5c3234be069f20 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 8 Oct 2015 23:46:58 -0700 Subject: restore credit-date filtering on line item report so we can use it for #37088 --- httemplate/search/cust_bill_pkg.cgi | 25 ++++++++++++++++++---- httemplate/search/cust_credit_bill_pkg.html | 2 +- httemplate/search/report_tax.cgi | 33 +++++++++++------------------ 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index 278382feb..4dc300d2f 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -705,9 +705,16 @@ my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount) "; push @select, "($pay_sub) AS pay_amount"; -#total credits -my $credit_sub = 'SELECT SUM(amount) AS credit_amount, billpkgnum - FROM cust_credit_bill_pkg GROUP BY billpkgnum'; +# showing credited amount, optionally with date filtering +my $credit_where = ''; +if ( $cgi->param('credit_begin') or $cgi->param('credit_end') ) { + my($cr_begin, $cr_end) = FS::UI::Web::parse_beginning_ending($cgi, 'credit'); + $credit_where = "WHERE cust_credit_bill._date >= $cr_begin " . + "AND cust_credit_bill._date <= $cr_end"; +} + +my $credit_sub = "SELECT SUM(amount) AS credit_amount, billpkgnum + FROM cust_credit_bill_pkg $credit_where GROUP BY billpkgnum"; $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit ON (cust_bill_pkg.billpkgnum = item_credit.billpkgnum)"; @@ -737,6 +744,10 @@ if ( $cgi->param('salesnum') =~ /^(\d+)$/ ) { $cgi->param('classnum', 0) unless $cgi->param('classnum'); } +#credit flag (include only those that have credit(s) applied) +if ( $cgi->param('credit') ) { + push @where, 'credit_amount > 0'; +} my $where = join(' AND ', @where); $where &&= "WHERE $where"; @@ -775,7 +786,13 @@ my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ]; my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ]; my $pay_link = ''; #[, 'billpkgnum', ]; -my $credit_link = [ "${p}search/cust_credit_bill_pkg.html?billpkgnum=", 'billpkgnum', ]; +my $credit_param = ''; +foreach ('credit_begin', 'credit_end') { + if ( $cgi->param($_) ) { + $credit_param .= "$_=" . $cgi->param($_) . ';'; + } +} +my $credit_link = [ "${p}search/cust_credit_bill_pkg.html?${credit_param}billpkgnum=", 'billpkgnum', ]; warn "\n\nQUERY:\n".Dumper($query)."\n\nCOUNT_QUERY:\n$count_query\n\n" if $cgi->param('debug'); diff --git a/httemplate/search/cust_credit_bill_pkg.html b/httemplate/search/cust_credit_bill_pkg.html index 5e70c23bd..5facd4ab3 100644 --- a/httemplate/search/cust_credit_bill_pkg.html +++ b/httemplate/search/cust_credit_bill_pkg.html @@ -375,7 +375,7 @@ if ( $cgi->param('report_group') =~ /^(=|!=) (.*)$/ && $cgi->param('istax') ) { } -push @where, 'cust_bill_pkg.pkgnum != 0' if $cgi->param('nottax'); +push @where, '(cust_bill_pkg.pkgnum != 0 OR cust_bill_pkg.feepart is not null)' if $cgi->param('nottax'); push @where, 'cust_bill_pkg.pkgnum = 0' if $cgi->param('istax'); if ( $cgi->param('cust_tax') ) { diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi index 2b531ea46..04bdf12ad 100644 --- a/httemplate/search/report_tax.cgi +++ b/httemplate/search/report_tax.cgi @@ -78,16 +78,17 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } % # cust_bill_pkg.cgi wants a list of specific taxnums (and package class) % # cust_credit_bill_pkg.html wants a geographic scope (and package class) % my $rowlink = ';taxnum=' . $row->{taxnums}; -% my $rowregion = ';country=' . $cgi->param('country'); -% foreach my $loc (qw(state county city district)) { -% if ( $row->{$loc} ) { -% $rowregion .= ";$loc=" . uri_escape($row->{$loc}); -% } -% } +% # DON'T EVER USE THIS +% # my $rowregion = ';country=' . $cgi->param('country'); +% # foreach my $loc (qw(state county city district)) { +% # if ( $row->{$loc} ) { +% # $rowregion .= ";$loc=" . uri_escape($row->{$loc}); +% # } +% # } % # and also the package class, if we're limiting package class % if ( $params{breakdown}->{pkgclass} ) { % $rowlink .= ';classnum=' . ($row->{pkgclass} || 0); -% $rowregion .= ';classnum=' . ($row->{pkgclass} || 0); +% # $rowregion .= ';classnum=' . ($row->{pkgclass} || 0); % } % % if ( $row->{total} ) { @@ -122,7 +123,7 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } % # credited sales - + <% $money_sprintf->( $row->{sales_credited} ) %> @@ -220,16 +221,6 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } % } # if $row->{pkgclass} ne ... % my $rowlink = ';taxnum=' . $row->{taxnums}; -% my $rowregion = ';country=' . $cgi->param('country'); -% foreach my $loc (qw(state county city district)) { -% if ( $row->{$loc} ) { -% $rowregion .= ";$loc=" . uri_escape($row->{$loc}); -% } -% } -% if ( $params{breakdown}->{pkgclass} ) { -% $rowlink .= ';classnum=' . ($row->{pkgclass} || 0); -% $rowregion .= ';classnum=' . ($row->{pkgclass} || 0); -% } % % if ( $row->{total} ) { @@ -242,13 +233,13 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px } % # Credits to taxable sales - + <% $money_sprintf->( $row->{sales_credited} ) %> % # ... to exempt sales (link is the same, it shows both exempt and taxable) - + <% $money_sprintf->( $row->{exempt_credited} ) %> @@ -324,7 +315,7 @@ if ( $params{agentnum} ) { my $saleslink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;nottax=1"; my $taxlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;istax=1"; my $exemptlink = $p. "search/cust_tax_exempt_pkg.cgi?$dateagentlink"; -my $salescreditlink = $p. "search/cust_credit_bill_pkg.html?$dateagentlink;nottax=1"; +my $salescreditlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;nottax=1;credit=1"; if ( $params{'credit_date'} eq 'cust_credit_bill' ) { $salescreditlink =~ s/begin/credit_begin/; $salescreditlink =~ s/end/credit_end/; -- cgit v1.2.1 From 22ee75c6f67806c9b4c366eea4510d4410dfe8df Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 9 Oct 2015 00:05:25 -0700 Subject: make credit date filtering work, #37088 --- httemplate/search/cust_bill_pkg.cgi | 7 +++++-- httemplate/search/report_tax.cgi | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index 4dc300d2f..ab5aad776 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -713,8 +713,11 @@ if ( $cgi->param('credit_begin') or $cgi->param('credit_end') ) { "AND cust_credit_bill._date <= $cr_end"; } -my $credit_sub = "SELECT SUM(amount) AS credit_amount, billpkgnum - FROM cust_credit_bill_pkg $credit_where GROUP BY billpkgnum"; +my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount, billpkgnum + FROM cust_credit_bill_pkg + JOIN cust_credit_bill USING (creditbillnum) + $credit_where + GROUP BY billpkgnum"; $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit ON (cust_bill_pkg.billpkgnum = item_credit.billpkgnum)"; diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi index 04bdf12ad..9e625c80f 100644 --- a/httemplate/search/report_tax.cgi +++ b/httemplate/search/report_tax.cgi @@ -319,6 +319,7 @@ my $salescreditlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;nottax=1;cred if ( $params{'credit_date'} eq 'cust_credit_bill' ) { $salescreditlink =~ s/begin/credit_begin/; $salescreditlink =~ s/end/credit_end/; + $saleslink .= ";credit_begin=$beginning;credit_end=$ending"; } #my $creditlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;credit=1;istax=1"; #if ( $params{'credit_date'} eq 'cust_credit_bill' ) { -- cgit v1.2.1 From eb0db72e7ee45f9495975f194049cf89717cd801 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 9 Oct 2015 23:00:05 -0500 Subject: RT#38481 [noted in FREESIDE_MODIFIED] --- rt/FREESIDE_MODIFIED | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rt/FREESIDE_MODIFIED b/rt/FREESIDE_MODIFIED index ace0d499b..05ffb2a46 100644 --- a/rt/FREESIDE_MODIFIED +++ b/rt/FREESIDE_MODIFIED @@ -165,3 +165,9 @@ share/html/Search/Elements/PickBasics lib/RT/CustomField.pm share/html/Admin/CustomFields/Modify.html share/html/Ticket/Create.html + +#allow RedirectToBasics to be set from schedule-appointments, RT#38481 +share/html/Search/Schedule.html +share/html/Elements/CalendarSlotSchedule +share/html/Ticket/Display.html + -- cgit v1.2.1 From 7032a1f192d519c9531c1fb20f766da6e38f74f1 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Sat, 10 Oct 2015 00:35:42 -0500 Subject: RT#38314: Declined payment shows card as tokenized after first attempt [same fix for approved payments] --- FS/FS/cust_main/Billing_Realtime.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 434815c16..c2ce680a1 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -888,6 +888,7 @@ sub _realtime_bop_result { '_date' => '', 'payby' => $cust_pay_pending->payby, 'payinfo' => $options{'payinfo'}, + 'paymask' => $options{'paymask'}, 'paydate' => $cust_pay_pending->paydate, 'pkgnum' => $cust_pay_pending->pkgnum, 'discount_term' => $options{'discount_term'}, -- cgit v1.2.1 From cd468ecb9a321ca96254b7204f6dc193b11cd903 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 10 Oct 2015 11:56:46 -0700 Subject: minor fixes to deploy zone editing, #30260 --- httemplate/browse/deploy_zone.html | 2 +- httemplate/edit/process/elements/process.html | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/httemplate/browse/deploy_zone.html b/httemplate/browse/deploy_zone.html index 02ebb8b8c..a1bd57f15 100644 --- a/httemplate/browse/deploy_zone.html +++ b/httemplate/browse/deploy_zone.html @@ -57,7 +57,7 @@ '(cir_speed_down, cir_speed_up)', ], links => [ $link_fixed, $link_fixed, ], - align => 'cllllrr', + align => 'cllllrrr', nohtmlheader => 1, disable_maxselect => 1, disable_total => 1, diff --git a/httemplate/edit/process/elements/process.html b/httemplate/edit/process/elements/process.html index a76f4befb..fd12c61d9 100644 --- a/httemplate/edit/process/elements/process.html +++ b/httemplate/edit/process/elements/process.html @@ -164,7 +164,9 @@ process(); % # some false laziness with the above % my ($form_name, $job_fields) = @{ $opt{'progress_init'} };
    + % foreach my $field (@$job_fields) { +% next if $field eq $pkey; % } <& /elements/progress-init.html, -- cgit v1.2.1