summaryrefslogtreecommitdiff
path: root/httemplate
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2015-10-13 10:10:40 -0700
committerIvan Kohler <ivan@freeside.biz>2015-10-13 10:10:40 -0700
commit2b2dd969f3c18751afc583ad1e836ab8e6f73b5d (patch)
tree72ad19092f9d3a5118add9a55067b8a97c168f46 /httemplate
parentd31d59c63c8f4dfd52ca19a02ffcf32fcf49f497 (diff)
parentcd468ecb9a321ca96254b7204f6dc193b11cd903 (diff)
Merge branch 'master' of git.freeside.biz:/home/git/freeside
Diffstat (limited to 'httemplate')
-rw-r--r--httemplate/browse/deploy_zone.html6
-rw-r--r--httemplate/browse/msg_template/email.html (renamed from httemplate/browse/msg_template.html)30
-rw-r--r--httemplate/browse/msg_template/http.html68
-rwxr-xr-xhttemplate/browse/part_pkg.cgi1
-rwxr-xr-xhttemplate/browse/part_svc.cgi2
-rw-r--r--httemplate/browse/template_image.html68
-rwxr-xr-xhttemplate/edit/cust_refund.cgi10
-rw-r--r--httemplate/edit/deploy_zone-fixed.html85
-rw-r--r--httemplate/edit/deploy_zone-mobile.html21
-rw-r--r--httemplate/edit/msg_template.html376
-rw-r--r--httemplate/edit/msg_template/email.html385
-rw-r--r--httemplate/edit/msg_template/http.html82
-rwxr-xr-xhttemplate/edit/part_pkg.cgi23
-rw-r--r--httemplate/edit/process/change-cust_pkg.html39
-rwxr-xr-xhttemplate/edit/process/cust_credit_bill.cgi3
-rwxr-xr-xhttemplate/edit/process/cust_refund.cgi31
-rw-r--r--httemplate/edit/process/deploy_zone-fixed.html35
-rw-r--r--httemplate/edit/process/deploy_zone-mobile.html19
-rw-r--r--httemplate/edit/process/elements/process.html23
-rw-r--r--httemplate/edit/process/msg_template.html18
-rw-r--r--httemplate/edit/process/rate_detail.html1
-rw-r--r--httemplate/edit/rate.cgi4
-rw-r--r--httemplate/elements/contact.html2
-rw-r--r--httemplate/elements/cust_pkg_usageprice.html9
-rw-r--r--httemplate/elements/email-link.html3
-rw-r--r--httemplate/elements/form-create_ticket.html2
-rw-r--r--httemplate/elements/form-file_upload.html1
-rw-r--r--httemplate/elements/htmlarea.html4
-rw-r--r--httemplate/elements/images/ui-icons_ef8c08_256x240.pngbin0 -> 4369 bytes
-rw-r--r--httemplate/elements/menu.html4
-rw-r--r--httemplate/elements/order_pkg.js68
-rw-r--r--httemplate/elements/polygon.html127
-rw-r--r--httemplate/elements/popup_link-send_report_batch.html28
-rw-r--r--httemplate/elements/schedule-appointment.html3
-rw-r--r--httemplate/elements/select-terms.html2
-rw-r--r--httemplate/elements/select-tower_sector.html7
-rw-r--r--httemplate/elements/template_image-dialog.html279
-rw-r--r--httemplate/elements/tr-pkg_svc.html67
-rw-r--r--httemplate/elements/tr-polygon.html5
-rw-r--r--httemplate/elements/tr-select-months.html1
-rw-r--r--httemplate/elements/tr-td-label.html2
-rw-r--r--httemplate/graph/cust_bill_pkg.cgi18
-rw-r--r--httemplate/graph/elements/report.html6
-rw-r--r--httemplate/graph/report_cust_bill_pkg.html7
-rw-r--r--httemplate/misc/batch-cust_pay.html97
-rwxr-xr-xhttemplate/misc/change_pkg.cgi23
-rw-r--r--httemplate/misc/cust_pkg_usageprice.html121
-rwxr-xr-xhttemplate/misc/delete-cust_credit.cgi21
-rwxr-xr-xhttemplate/misc/delete-cust_pay.cgi21
-rw-r--r--httemplate/misc/email-customers.html220
-rw-r--r--httemplate/misc/order_pkg.html21
-rw-r--r--httemplate/misc/process/deploy_zone-block_lookup.cgi13
-rw-r--r--httemplate/misc/process/payment.cgi82
-rw-r--r--httemplate/misc/process/send-report.html7
-rw-r--r--httemplate/misc/process/template_image-delete.cgi28
-rw-r--r--httemplate/misc/process/template_image-upload.cgi26
-rwxr-xr-xhttemplate/misc/process/void-cust_bill.html2
-rw-r--r--httemplate/misc/send-report.html166
-rwxr-xr-xhttemplate/misc/unapply-cust_pay.cgi6
-rw-r--r--httemplate/misc/void-cust_bill.html2
-rw-r--r--httemplate/misc/xmlhttp-part_pkg_usageprice.html18
-rw-r--r--httemplate/misc/xmlhttp-template_image.cgi48
-rw-r--r--httemplate/misc/xmlhttp-ticket-update.html66
-rwxr-xr-xhttemplate/search/cust_bill.html4
-rw-r--r--httemplate/search/cust_bill_pkg.cgi226
-rw-r--r--httemplate/search/cust_credit_bill_pkg.html73
-rw-r--r--httemplate/search/cust_msg.html3
-rwxr-xr-xhttemplate/search/cust_pay.html1
-rwxr-xr-xhttemplate/search/cust_pkg.cgi9
-rwxr-xr-xhttemplate/search/elements/cust_pay_or_refund.html10
-rw-r--r--httemplate/search/elements/search.html4
-rw-r--r--httemplate/search/queue.html6
-rw-r--r--httemplate/search/report_sales_commission_pkg.html10
-rwxr-xr-xhttemplate/search/report_tax-xls.cgi99
-rw-r--r--httemplate/search/report_tax.cgi121
-rw-r--r--httemplate/search/sales_commission_pkg.html7
-rw-r--r--httemplate/search/tax_sales.cgi172
-rwxr-xr-xhttemplate/search/tax_sales.html35
-rw-r--r--httemplate/view/cust_main/appointments.html36
-rw-r--r--httemplate/view/cust_main/billing.html11
-rw-r--r--httemplate/view/cust_main/payment_history.html5
-rw-r--r--httemplate/view/cust_main/payment_history/credit.html11
-rw-r--r--httemplate/view/cust_main/payment_history/payment.html25
-rw-r--r--httemplate/view/cust_svc.cgi8
84 files changed, 2813 insertions, 1026 deletions
diff --git a/httemplate/browse/deploy_zone.html b/httemplate/browse/deploy_zone.html
index 3bd9d07dd..a1bd57f15 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',
@@ -42,6 +43,9 @@
)
},
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 => 'cllllrrr',
nohtmlheader => 1,
disable_maxselect => 1,
disable_total => 1,
diff --git a/httemplate/browse/msg_template.html b/httemplate/browse/msg_template/email.html
index ef0b2dafd..d0ef4e3e9 100644
--- a/httemplate/browse/msg_template.html
+++ b/httemplate/browse/msg_template/email.html
@@ -1,9 +1,9 @@
-<% include( 'elements/browse.html',
+<& /browse/elements/browse.html,
'title' => 'Message templates',
'name_singular' => 'template',
'menubar' => \@menubar,
- 'query' => { 'table' => 'msg_template', },
- 'count_query' => 'SELECT COUNT(*) FROM msg_template',
+ 'query' => $query,
+ 'count_query' => $count_query,
'disableable' => 1,
'disabled_statuspos' => (scalar(@locales) + 3),
'agent_virt' => 1,
@@ -14,8 +14,7 @@
'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;
@@ -26,10 +25,21 @@ die "access denied"
my @menubar = ();
if ( $curuser->access_right(['Edit templates', 'Edit global templates']) ) {
- push @menubar, 'Add a new template' => $p.'edit/msg_template.html';
+ push @menubar, 'Add a new template' => $fsurl.'edit/msg_template/email.html';
}
+push @menubar, 'Template images' => $fsurl.'browse/template_image.html';
-my $link = [ "${p}edit/msg_template.html?msgnum=", 'msgnum' ];
+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';
@@ -42,17 +52,17 @@ foreach my $l ( FS::Locales->locales ) {
};
push @locale_links, sub {
my $content = $_[0]->content_locales->{$l} or return '';
- [ "${p}edit/msg_template.html?locale=$l;msgnum=", 'msgnum' ];
+ [ $fsurl."edit/msg_template/email.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=' .
+ action => $fsurl.'misc/disable-msg_template.cgi?msgnum=' .
$template->msgnum .
($template->disabled ? ';enable=1' : ''),
- actionlabel => 'Disable lemplate',
+ actionlabel => 'Disable template',
);
};
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)' ;
+};
+
+</%init>
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/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!<A HREF="javascript:insertImageDialog(! . $_[0]->imgnum . qq!)">view</A>! };
+
+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;
+<FORM name="delete_template_image_$imgnum">
+<INPUT TYPE="hidden" name="imgnum" value="$imgnum">
+</FORM>
+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 . '<A HREF="javascript:void(0)" ONCLICK="' . $onclick . '">delete</A>';
+} : '';
+
+</%init>
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/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;
+#};
</%init>
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/msg_template.html b/httemplate/edit/msg_template.html
index 7f3824127..889b10731 100644
--- a/httemplate/edit/msg_template.html
+++ b/httemplate/edit/msg_template.html
@@ -1,373 +1,9 @@
-<& elements/edit.html,
- 'html_init' => '<TABLE id="outerTable"><TR><TD>',
- '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' => '</DIV>',
- 'html_table_bottom'=> \&html_table_bottom,
- 'html_foot' => ( $no_submit ? '' : "</TD>$sidebar</TR></TABLE>" ),
- '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 $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'=>'</FORM><FONT SIZE=-1>',
- '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<TR><TD><A href="javascript:insertHtml('{$key}')">$key</A></TD>!;
- $html .= "\n<TD>".shift(@hints).'</TD></TR>';
- }
- $html .= "\n</TABLE>";
- return $html;
- },
-);
-
-my $sidebar = '
-<SCRIPT TYPE="text/javascript">
-function insertHtml(what) {
- var oEditor = CKEDITOR.instances["body"];
- oEditor.insertHtml(what);
-};
-
-function areyousure(url, message) {
- if (confirm(message))
- window.location.href = url;
-}
-</SCRIPT>
-<TD valign="top"><FORM name="dummy">
-Substitutions: '
-. $widget->html .
-'<BR>Click links to insert.
-<BR>Enclose substitutions and other Perl expressions in braces:
-<BR>{ $name } = ExampleCo (Smith, John)
-<BR>{ time2str("%D", time) } = '.time2str("%D", time).'
-</FONT></TD>
-';
-
-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!<A HREF="javascript:areyousure('$url','Really delete this template?')">! .
- 'Delete this template' .
- '</A>';
- $html = qq!<TR><TD></TD>
- <TD STYLE="font-style: italic; font-size: small">$link</TD></TR>!;
- }
- $html;
-}
-
+print $cgi->redirect($fsurl."edit/msg_template/$msgclass.html?".$cgi->query_string);
</%init>
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' => '<TABLE id="outerTable"><TR><TD>',
+ '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' => '</DIV>',
+ 'html_table_bottom'=> \&html_table_bottom,
+ 'html_foot' => ( $no_submit ? '' : "</TD>$sidebar</TR></TABLE>" ),
+ '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'=>'</FORM><FONT SIZE=-1>',
+ '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<TR><TD><A href="javascript:insertHtml('{$key}')">$key</A></TD>!;
+ $html .= "\n<TD>".shift(@hints).'</TD></TR>';
+ }
+ $html .= "\n</TABLE>";
+ return $html;
+ },
+);
+
+my $sidebar = '
+<SCRIPT TYPE="text/javascript">
+function insertHtml(what) {
+ var oEditor = CKEDITOR.instances["body"];
+ oEditor.insertHtml(what);
+};
+
+function areyousure(url, message) {
+ if (confirm(message))
+ window.location.href = url;
+}
+</SCRIPT>
+<TD valign="top"><FORM name="dummy">
+Substitutions: '
+. $widget->html .
+'<P>Click above links to insert substitution code.</P>
+<P>
+Enclose substitutions and other Perl expressions in braces:
+<BR>{ $name } = ExampleCo (Smith, John)
+<BR>{ time2str("%D", time) } = '.time2str("%D", time).'
+</P>';
+$sidebar .= include('/elements/template_image-dialog.html',
+ 'callback' => 'insertHtml'
+ );
+$sidebar .= '<P><A HREF="javascript:insertImageDialog()">Insert Uploaded Image</A></P>
+</FONT></TD>
+';
+
+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!<A HREF="javascript:areyousure('$url','Really delete this template?')">! .
+ 'Delete this template' .
+ '</A>';
+ $html = qq!<TR><TD></TD>
+ <TD STYLE="font-style: italic; font-size: small">$link</TD></TR>!;
+ }
+ $html;
+}
+
+</%init>
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' },
+ );
+
+ }
+};
+
+</%init>
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 .= '</SELECT></TD></TR>';
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 .= '<TR><TD ALIGN="right">'. $href->{$field}{'name'}. '</TD><TD>';
+ $html .= '<TR><TD ALIGN="right">'. $href->{$field}{'name'}. '</TD><TD>
+ ';
my $format = sub { shift };
$format = $href->{$field}{'format'} if exists($href->{$field}{'format'});
@@ -1128,9 +1133,11 @@ my $html_bottom = sub {
$html .= '</TD></TR>';
}
$html .= '</TABLE>';
-
- $html .= qq(<INPUT TYPE="hidden" NAME="${layer}__OPTIONS" VALUE=").
- join(',', keys %{ $href } ). '">';
+
+ $html .= include('/elements/hidden.html',
+ field => $layer.'__OPTIONS',
+ value => join(',', @fields)
+ );
$html;
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/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')
}
</%init>
diff --git a/httemplate/edit/process/cust_refund.cgi b/httemplate/edit/process/cust_refund.cgi
index 52fede8ec..6ad468b6c 100755
--- a/httemplate/edit/process/cust_refund.cgi
+++ b/httemplate/edit/process/cust_refund.cgi
@@ -12,7 +12,7 @@
</BODY></HTML>
% } else {
-<% $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum") %>
+<% $cgi->redirect(popurl(3). "view/cust_main.cgi?custnum=$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;
+ }
}
</%init>
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++;
+ }
+ }
+ '';
+};
+</%init>
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++;
+ }
+ }
+ '';
+};
+</%init>
diff --git a/httemplate/edit/process/elements/process.html b/httemplate/edit/process/elements/process.html
index 69bd605f6..fd12c61d9 100644
--- a/httemplate/edit/process/elements/process.html
+++ b/httemplate/edit/process/elements/process.html
@@ -160,7 +160,28 @@ process();
</script>
<& /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'} };
+<form name="<% $form_name %>">
+ <input type="hidden" name="<% $pkey %>" value="<% $new->get($pkey) %>">
+% foreach my $field (@$job_fields) {
+% next if $field eq $pkey;
+ <input type="hidden" name="<% $field %>" value="<% $cgi->param($field) |h %>">
+% }
+<& /elements/progress-init.html,
+ @{ $opt{'progress_init'} }
+&>
+<input type="submit" style="display:none">
+</form>
+<script>
+<&| /elements/onload.js &>
+process();
+</&>
+</script>
+<& /elements/footer.html &>
+
+% } elsif ( $opt{'popup_reload'} ) {
<% include('/elements/header-popup.html', $opt{'popup_reload'} ) %>
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/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;
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 @@
</TABLE>
<BR>
-<INPUT TYPE="hidden" NAME="preserve_rate_detail" VALUE="1">
-
<INPUT NAME="submit" TYPE="button" VALUE="<%
$rate->ratenum ? "Apply changes" : "Add rate plan"
%>" onClick="document.OneTrueForm.submit.disabled=true; process();">
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;
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);
- <OPTION VALUE="">Additional <% $info->{label} %>
+ <OPTION VALUE="">Additional <% $info->{label} %></OPTION>
% for (1..100) { #100? arbitrary.
- <OPTION VALUE="<% $_ %>"><%
+% my $selected = ($opt{'curr_quantity'} == $_) ? ' SELECTED' : '';
+ <OPTION VALUE="<% $_ %>"<% $selected %>><%
$money_char. sprintf('%.2f', $_ * $part_pkg_usageprice->price ).
' '.
'for'. #( $part_pkg_usageprice->action eq 'increment' ? 'per' : 'for' ).
' '.
( $_ * $amount ). ' '. $info->{label}
- %>
+ %></OPTION>
% }
</SELECT>
</TD>
@@ -42,8 +43,6 @@
% }
<%init>
-#my $targets = FS::part_pkg_usageprice->targets;
-
my( %opt ) = @_;
my $conf = new FS::Conf;
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');
</%init>
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;
}
</SCRIPT>
<A NAME="tickets"><FONT CLASS="fsinnerbox-title">Tickets</FONT></A>
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:
<div style="display:none:" id="uploadError"></div>
<FORM NAME = "<% $opt{name} %>"
+ ID = "<% $opt{id} %>"
ACTION = "<% $fsurl %>misc/file-upload.html"
METHOD = "POST"
ENCTYPE = "multipart/form-data"
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:
</%doc>
% #init
-<SCRIPT TYPE="text/javascript" src="<% $p %>elements/ckeditor/ckeditor.js">
+<SCRIPT TYPE="text/javascript" src="<% $fsurl %>elements/ckeditor/ckeditor.js">
</SCRIPT>
% #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/images/ui-icons_ef8c08_256x240.png b/httemplate/elements/images/ui-icons_ef8c08_256x240.png
new file mode 100644
index 000000000..85e63e9f6
--- /dev/null
+++ b/httemplate/elements/images/ui-icons_ef8c08_256x240.png
Binary files differ
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index a5fb15bc2..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' ];
@@ -767,7 +769,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.' ]
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/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'} || '[]';
+</%init>
+<& hidden.html, %opt &>
+<div id="<% $div_id %>" style="height: 600px; width: 600px"></div>
+
+<script src="https://maps.googleapis.com/maps/api/js?libraries=drawing"></script>
+<script>
+var map;
+var drawingManager;
+
+function updateFormInput(event) {
+ var path = window.polygon.getPath();
+ var vertices = []; // array of arrays, geoJSON style
+ for (var i =0; i < path.getLength(); i++) {
+ var xy = path.getAt(i);
+ vertices[i] = [ xy.lat(), xy.lng() ];
+ }
+ console.log(vertices); //XXX
+ $('#<% $field %>').prop('value', JSON.stringify(vertices));
+}
+
+$(function() {
+ mapOptions = {
+ zoom: 4,
+ center: {lat: 39.40114, lng: -96.57127}, // continental U.S.
+ mapTypeId: google.maps.MapTypeId.ROADMAP,
+ panControl: true,
+ scaleControl: true,
+ streetViewControl: false,
+ };
+ map = new google.maps.Map($('#<% $div_id %>')[0], mapOptions);
+
+ var polygonComplete = function(p) {
+ window.polygon = p;
+ if (drawingManager) {
+ drawingManager.setDrawingMode(null);
+ drawingManager.setOptions({ drawingControl: false });
+ }
+ // double click to delete a vertex (so long as it remains a polygon)
+ p.addListener('dblclick', function (mev) {
+ if (mev.vertex != null && window.polygon.getPath().length > 3) {
+ p.getPath().removeAt(mev.vertex);
+ }
+ });
+ // any time the polygon is modified, update the vertex list
+ p.getPath().addListener('set_at', updateFormInput);
+ p.getPath().addListener('insert_at', updateFormInput);
+ p.getPath().addListener('remove_at', updateFormInput);
+
+ // and also now
+ updateFormInput();
+ };
+
+ var polygonOptions = {
+ fillColor: '#0000a0',
+ fillOpacity: 0.2,
+ strokeColor: '#0000a0',
+ strokeWeight: 2,
+ clickable: false,
+ editable: true,
+ zIndex: 1,
+ map: map,
+ };
+
+ var vertex_array = <% $vertices_json %>;
+ if ( vertex_array.length > 2 ) {
+ // then we already have a polygon. make it acceptable to google maps,
+ // and also create a bounding box for it and fit the map to that.
+
+ var path = [];
+ var bounds = new google.maps.LatLngBounds();
+ for (var i = 0; i < vertex_array.length; i++) {
+ var xy = new google.maps.LatLng(vertex_array[i][0], vertex_array[i][1]);
+ path.push(xy);
+ bounds.extend(xy);
+ }
+
+ polygonOptions.paths = [ path ];
+ polygonComplete(new google.maps.Polygon(polygonOptions));
+ map.fitBounds(bounds);
+
+ } else {
+ // there are no vertices, or not enough to make a polygon, so
+ // enable drawing mode to create a new one
+
+ drawingManager = new google.maps.drawing.DrawingManager({
+ drawingMode: google.maps.drawing.OverlayType.POLYGON,
+ drawingControl: true,
+ drawingControlOptions: {
+ position: google.maps.ControlPosition.TOP_CENTER,
+ drawingModes: [
+ google.maps.drawing.OverlayType.POLYGON,
+ ]
+ },
+ polygonOptions: polygonOptions,
+ });
+
+ // after a single polygon is drawn: remember it, add a listener to let
+ // nodes be deleted, and exit drawing mode
+ drawingManager.addListener('polygoncomplete', polygonComplete);
+ drawingManager.setMap(map);
+
+ // center the map on the user (for lack of a better choice)
+ if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(function(position) {
+ var pos = {
+ lat: position.coords.latitude,
+ lng: position.coords.longitude
+ };
+
+ map.setCenter(pos);
+ map.setZoom(12);
+ });
+ } // on error, or if geolocation isn't available, do nothing
+ }
+
+});
+
+ </script>
+ </body>
+</html>
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',
+&>
+</%doc>
+<& /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'};
+
+</%init>
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/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} } : ();
diff --git a/httemplate/elements/select-tower_sector.html b/httemplate/elements/select-tower_sector.html
index a16d3bfa0..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 => ' ',
@_
@@ -11,8 +13,9 @@
<& /elements/select-table.html,
table => 'tower',
name_col => 'towername',
+ hashref => { 'disabled' => '', },
id => 'towernum',
- field => 'dummy_towernum',
+ field => 'towernum',
onchange => 'change_towernum(this.value);',
element_etc => 'STYLE="vertical-align:top"',
&>
@@ -63,5 +66,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';
</%init>
diff --git a/httemplate/elements/template_image-dialog.html b/httemplate/elements/template_image-dialog.html
new file mode 100644
index 000000000..b471d28da
--- /dev/null
+++ b/httemplate/elements/template_image-dialog.html
@@ -0,0 +1,279 @@
+<%doc>
+
+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
+
+</%doc>
+
+<% include('/elements/xmlhttp.html',
+ 'url' => $fsurl.'misc/xmlhttp-template_image.cgi',
+ 'subs' => [ 'get_template_image' ],
+ ) %>
+
+<DIV ID="insert_image_dialog" title="Template Images">
+
+<TABLE BORDER="0" STYLE="width: 100%"><TR><TD>
+
+<FORM ID="insert_image_form">
+
+<% &ntable("#cccccc", 2) %>
+
+ <TR>
+ <TH>Image</TH>
+ <TD>
+ <SELECT ID="insert_image_imgnum" ONCHANGE="insertImageDialog($('#insert_image_imgnum').val())">
+ <OPTION VALUE="">(select an image)</OPTION>
+ </SELECT>
+ </TD>
+ </TR>
+% if ($opt{'callback'}) {
+ <TR>
+ <TH>Width</TH>
+ <TD><INPUT TYPE="text" SIZE="5" ID="insert_image_width" ONCHANGE="previewInsertImage()"></TD>
+ </TR>
+ <TR>
+ <TH>Height</TH>
+ <TD><INPUT TYPE="text" SIZE="5" ID="insert_image_height" ONCHANGE="previewInsertImage()"></TD>
+ </TR>
+ <TR>
+ <TH>Align</TH>
+ <TD>
+ <SELECT ID="insert_image_float" ONCHANGE="previewInsertImage()">
+ <OPTION VALUE="none">inline</OPTION>
+ <OPTION VALUE="left">left</OPTION>
+ <OPTION VALUE="right">right</OPTION>
+ </SELECT>
+ </TD>
+ </TR>
+ <TR>
+ <TH>Alt Text</TH>
+ <TD><INPUT TYPE="text" SIZE="20" ID="insert_image_alt" ONCHANGE="previewInsertImage()"></TD>
+ </TR>
+ <TR>
+ <TD COLSPAN="2" ALIGN="center" STYLE="padding-top:6px">
+ <INPUT TYPE="button" ID="insert_image_button" VALUE="Insert Image" ONCLICK="insertImage()">
+ </TD>
+ </TR>
+% } # if $opt{'callback'}
+
+</TABLE>
+
+</FORM>
+
+% if ($canedit) {
+
+<P><B><% emt('Upload New Image') %></B></P>
+
+<% include('/elements/form-file_upload.html',
+ 'name' => 'TemplateImageUploadForm',
+ 'id' => 'TemplateImageUploadForm',
+ 'action' => $fsurl.'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' => "<B>Agent</B>",
+ 'empty_label' => '(global)',
+ 'agent_virt' => 1,
+ 'agent_null_right' => 'Edit global templates',
+ )
+ %>
+
+ <% include( '/elements/tr-file-upload.html',
+ 'field' => 'file',
+ 'label' => 'File',
+ )
+ %>
+
+ <TR>
+ <TD COLSPAN="2" ALIGN="center" STYLE="padding-top:6px">
+ <INPUT TYPE = "submit"
+ NAME = "submitButton"
+ ID = "submitButton"
+ VALUE = "Upload image"
+ >
+ </TD>
+ </TR>
+
+</TABLE>
+
+</FORM>
+
+% } #if canedit
+
+</TD><TD width="100%">
+
+<DIV ID="insert_image_preview_box">
+ <P><B><% emt('Image Preview') %></B></P>
+ <SPAN ID="insert_image_loading"><B>(<% emt('Loading image...') %>)</B></SPAN>
+ <IMG SRC="" ID="insert_image_preview">
+</DIV>
+
+</TD></TR></TABLE>
+</DIV>
+
+<SCRIPT>
+
+// initialize & close dialog window, initialize imgobj cache && image list
+$( '#insert_image_dialog' ).dialog({
+ width: 800,
+ height: 550,
+ resizable: true,
+ autoOpen: false,
+});
+var imgobj = new Object;
+refreshImageList(0);
+
+// this is the main func to invoke from links outside this file.
+// opens dialog if needed
+// updates dialog with passed imgnum
+// caches image info through an xmlhttp request if needed
+// pass 'upload' as imgnum for upload-only view
+function insertImageDialog (imgnum) {
+ if (imgnum == 'upload') {
+ $('#insert_image_form').hide();
+ $('#insert_image_preview_box').hide();
+ imgnum = undefined;
+ } else {
+ $('#insert_image_form').show();
+ $('#insert_image_preview_box').show();
+ }
+ if (imgnum && !imgobj[imgnum]) {
+ clearInsertImageDialog();
+ $('#insert_image_loading').show();
+ $('#insert_image_imgnum').val(imgnum);
+ get_template_image('imgnum',imgnum,
+ function (result) {
+ var images = JSON.parse(result) || [];
+ for (i = 0; i < images.length; i++) {
+ imgobj[images[i].imgnum] = images[i];
+ }
+ updateInsertImageDialog();
+ }
+ );
+ } else if (imgnum) {
+ $('#insert_image_imgnum').val(imgnum);
+ updateInsertImageDialog();
+ } else {
+ clearInsertImageDialog();
+ }
+ if (!$( '#insert_image_dialog' ).dialog( 'isOpen' )) {
+ $( '#insert_image_dialog' ).dialog( 'open' );
+ }
+}
+
+// sets dialog values to a default "Loading..." state, including imgnum
+function clearInsertImageDialog () {
+ $('#insert_image_imgnum').val('');
+ $('#insert_image_preview').attr('src','');
+ $('#insert_image_loading').hide();
+}
+
+// updates preview src from cache based on imgnum from form
+// then calls previewInsertImage
+function updateInsertImageDialog () {
+ var imgnum = $('#insert_image_imgnum').val();
+ $('#insert_image_loading').hide();
+ $('#insert_image_preview').attr('src',imgobj[imgnum].src);
+ previewInsertImage();
+}
+
+// updates preview width/height/alt/float based on current form values
+function previewInsertImage () {
+ $('#insert_image_preview').css('width',$('#insert_image_width').val());
+ $('#insert_image_preview').css('height',$('#insert_image_height').val());
+ $('#insert_image_preview').css('float',$('#insert_image_float').val());
+ $('#insert_image_preview').attr('alt',$('#insert_image_alt').val());
+}
+
+// constructs html based on the form contents,
+// passes it to callback & closes dialog
+function insertImage() {
+ var imgnum = $('#insert_image_imgnum').val();
+ if (!(imgnum && imgobj[imgnum])) {
+ return '';
+ }
+ var width = $('#insert_image_width').val() || '';
+ var height = $('#insert_image_height').val() || '';
+ var alt = $('#insert_image_alt').val() || '';
+ var float = $('#insert_image_float').val();
+ var imgtag = '<IMG SRC="' + imgobj[imgnum].src + '"';
+ if (width) {
+ imgtag += ' WIDTH="' + width + '"';
+ }
+ if (height) {
+ imgtag += ' HEIGHT="' + height + '"';
+ }
+ if (alt) {
+ imgtag += ' ALT="' + alt + '"';
+ }
+ if (float) {
+ imgtag += ' STYLE="float: ' + float + '"';
+ }
+ imgtag += '>';
+ <% $opt{'callback'} %>(imgtag);
+ $( '#insert_image_dialog' ).dialog( 'close' );
+}
+
+// uses xmlhttp request to initialize image list & refresh it after uploads
+function refreshImageList (fromupload) {
+ get_template_image('no_src','1',
+ function (result) {
+ if (fromupload) {
+ $("#TemplateImageUploadForm")[0].reset();
+ }
+ var images = JSON.parse(result) || [];
+ var latest;
+ for (i = 0; i < images.length; i++) {
+ if ( $("#insert_image_imgnum option[value='" + images[i].imgnum + "']").length == 0 ) {
+ $("#insert_image_imgnum").append('<OPTION VALUE="'+images[i].imgnum+'">'+images[i].name+'</OPTION>');
+ latest = images[i].imgnum;
+ }
+ }
+ if (fromupload) {
+ location.hash = "insert_image_dialog";
+ if (latest) {
+ // small risk of a race condition with other newly-uploaded images,
+ // but does no real damage (our image still shows up in the list)
+ insertImageDialog(latest);
+ }
+ }
+ }
+ );
+}
+
+</SCRIPT>
+
+<%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' ]);
+</%init>
+
diff --git a/httemplate/elements/tr-pkg_svc.html b/httemplate/elements/tr-pkg_svc.html
index cfef51ce2..de3f95a3a 100644
--- a/httemplate/elements/tr-pkg_svc.html
+++ b/httemplate/elements/tr-pkg_svc.html
@@ -1,8 +1,13 @@
<TR>
<TD CLASS="background" COLSPAN=99>
+<SCRIPT>
+var provision_hold_td = [];
+var provision_hold_input = [];
+</SCRIPT>
+
<% itable('', 4, 1) %><TR><TD VALIGN="top">
-<% $thead %>
+<% pkg_svc_thead() %>
%foreach my $part_svc ( @part_svc ) {
% my $svcpart = $part_svc->svcpart;
@@ -78,24 +83,52 @@
<INPUT TYPE="checkbox" NAME="no_bulk_skip<% $svcpart %>" VALUE="Y"<% $bulk_skip =~ /^Y/i ? '' : ' CHECKED' %>>
</TD>
- <TD ALIGN="center">
- <INPUT TYPE="checkbox" NAME="provision_hold<% $svcpart %>" VALUE="Y"<% $provision_hold =~ /^Y/i ? ' CHECKED' : ''%>>
+ <TD ALIGN="center" ID="td_provision_hold<% $svcpart %>">
+ <INPUT TYPE="checkbox" ID="input_provision_hold<% $svcpart %>" NAME="provision_hold<% $svcpart %>" VALUE="Y"<% $provision_hold =~ /^Y/i ? ' CHECKED' : ''%>>
</TD>
+<SCRIPT>
+provision_hold_td.push(document.getElementById('td_provision_hold<% $svcpart %>'));
+provision_hold_input.push(document.getElementById('input_provision_hold<% $svcpart %>'));
+</SCRIPT>
</TR>
% foreach ( 1 .. $columns-1 ) {
% if ( $count == int( $_ * scalar(@part_svc) / $columns ) ) {
%
- </TABLE></TD><TD VALIGN="top"><% $thead %>
+ </TABLE></TD><TD VALIGN="top"><% pkg_svc_thead() %>
% }
% }
% $count++;
%
-% }
+% } # foreach $part_svc
</TR></TABLE></TD></TR></TABLE>
+<SCRIPT>
+// start_on_hold from edit/part_pkg.cgi
+// toggles display of provision_hold checkboxes when that changes
+function provision_hold_check () {
+ var start_on_hold = document.getElementById('start_on_hold');
+ if (start_on_hold) {
+ 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 () {
+ var start_on_hold = document.getElementById('start_on_hold');
+ if (start_on_hold) {
+ start_on_hold.onchange = provision_hold_check;
+ provision_hold_check();
+ }
+}
+provision_hold_init();
+</SCRIPT>
+
% if ( scalar(@possible_exports) > 0 || scalar(@mapped_exports) > 0 ) {
<TABLE><TR>
<TH BGCOLOR="#dcdcdc">Export</TH>
@@ -130,15 +163,21 @@
my %opt = @_;
my $cgi = $opt{'cgi'};
-my $thead = "\n\n". ntable('#cccccc', 2).
- '<TR>'.
- '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Quan.</FONT></TH>'.
- '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Primary</FONT></TH>'.
- '<TH BGCOLOR="#dcdcdc">Service</TH>'.
- '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Hide<BR>from<BR>Invoices</FONT></TH>'.
- '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Bulk<BR>Charge</FONT></TH>'.
- '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Hold<BR>Until<BR>Provision</FONT></TH>'.
- '</TR>';
+my $thead_count = 0;
+sub pkg_svc_thead {
+ $thead_count += 1;
+ return "\n\n". ntable('#cccccc', 2).
+ '<TR>'.
+ '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Quan.</FONT></TH>'.
+ '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Primary</FONT></TH>'.
+ '<TH BGCOLOR="#dcdcdc">Service</TH>'.
+ '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Hide<BR>from<BR>Invoices</FONT></TH>'.
+ '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Bulk<BR>Charge</FONT></TH>'.
+ '<TH BGCOLOR="#dcdcdc" ID="th_provision_hold' . $thead_count . '"><FONT SIZE=-1>Remove Hold After Provisioning</FONT></TH>'.
+ '</TR>'.
+ qq!<SCRIPT>provision_hold_td.push(document.getElementById('th_provision_hold$thead_count'))</SCRIPT>!;
+;
+}
my $part_pkg = $opt{'object'};
my $pkgpart = $part_pkg->pkgpart;
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 @@
+<tr>
+<td colspan=2>
+<& polygon.html, @_ &>
+</td>
+</tr>
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);
</%init>
<& tr-select.html, %opt &>
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 <TR> <TH> $label </TH>
VALIGN = "<% $opt{'valign'} || 'top' %>"
STYLE = "<% $style %>"
ID = "<% $opt{label_id} || $opt{id}. '_label0' %>"
- ><% $required %><% $opt{label} %></TD>
+ ><% $required %><% $opt{label} %></TH>
<%init>
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/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/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;
</TR>
-
<TR>
<TH CLASS="background" COLSPAN=2>&nbsp;</TH>
</TR>
@@ -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' },
+&>
<TR>
<TD ALIGN="right">Colors</TD>
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: <TR>, <TD>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<select_invnum.options.length; i++) {
- if (select_invnum.options[i].value == this_app.invnum) {
- select_invnum.selectedIndex = i;
+
+ preloading++;
+
+ try {
+ 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<select_invnum.options.length; i++) {
+ if (select_invnum.options[i].value == this_app.invnum) {
+ select_invnum.selectedIndex = i;
+ }
}
- }
- choose_app_invnum.call(select_invnum);
- //set amount
- var input_amount = document.getElementById('amount'+x);
- input_amount.value = this_app.amount;
-
- //set error
- var span_error = document.getElementById('error'+x);
- span_error.innerHTML = this_app.error;
- change_app_amount.call(input_amount); //creates next row
- } //for appnum
+ choose_app_invnum.call(select_invnum);
+ //set amount
+ var input_amount = document.getElementById('amount'+x);
+ input_amount.value = this_app.amount;
+
+ //set error
+ var span_error = document.getElementById('error'+x);
+ span_error.innerHTML = this_app.error;
+
+ // create another row (unconditionally)
+ create_application_row(r, appnum + 1);
+
+ } //for appnum
+
+ } finally {
+ preloading--;
+ }
+
}; //preload_row function
+
+ // enable application rows on the selected customer. this creates
+ // the first row, then kicks off preloading.
toggle_application_row.call(enable, null, preload_row);
+
} // if (row_obj[rownum].length
} //for rownum
}
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 @@
</TABLE>
<BR>
+<% include('/misc/cust_pkg_usageprice.html',
+ 'pkgpart' => (scalar($cgi->param('pkgpart')) || $cust_pkg->pkgpart),
+ 'pkgnum' => ($cust_pkg->change_to_pkgnum || $pkgnum),
+ ) %>
+<BR>
<FONT CLASS="fsinnerbox-title"><% mt('Change') |h %></FONT>
<% 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' %>
>
</FORM>
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.
+</%doc>
+
+<& /elements/xmlhttp.html,
+ 'url' => $p.'misc/xmlhttp-part_pkg_usageprice.html',
+ 'subs' => [ 'get_part_pkg_usageprice' ],
+&>
+
+<FONT CLASS = "fsinnerbox-title"
+ ID = "cust_pkg_usageprice_title"
+ STYLE = "display:none"
+><% mt('Usage add-ons') |h %></FONT>
+<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 ID="cust_pkg_usageprice_table">
+
+</TABLE>
+
+<SCRIPT>
+
+var usagepriceCache = {};
+var usagepriceDisabled = 0;
+
+function usageprice_disable (disabled, pkgpart) {
+ if (disabled) {
+ usageprice_pkg_changed(0);
+ usagepriceDisabled = 1;
+ } else {
+ usagepriceDisabled = 0;
+ usageprice_pkg_changed(pkgpart);
+ }
+}
+
+// main function to invoke when pkgpart changes
+function usageprice_pkg_changed (pkgpart, pkgnum) {
+ if (usagepriceDisabled) return;
+ clear_part_pkg_usageprice();
+
+ if (pkgpart) {
+ if (usagepriceCache[pkgpart]) {
+ update_part_pkg_usageprice(pkgpart);
+ } else {
+ get_part_pkg_usageprice( pkgpart || 0, pkgnum || 0, download_part_pkg_usageprice );
+ }
+ }
+}
+
+// removes table rows & hides table title
+function clear_part_pkg_usageprice () {
+ var table = document.getElementById('cust_pkg_usageprice_table');
+ for ( var r = table.rows.length - 1; r >= 0; r-- ) {
+ table.deleteRow(r);
+ }
+ document.getElementById('cust_pkg_usageprice_title').style.display = 'none';
+}
+
+// catches response from xmlhttp request, updates cache & calls update function
+function download_part_pkg_usageprice (part_pkg_usageprice) {
+ var usagepriceArray = JSON.parse(part_pkg_usageprice);
+ var pkgpart = usagepriceArray[0];
+ usagepriceCache[pkgpart] = usagepriceArray;
+ update_part_pkg_usageprice(pkgpart);
+}
+
+// updates from cache
+function update_part_pkg_usageprice (pkgpart) {
+ if (usagepriceDisabled) return;
+ clear_part_pkg_usageprice();
+
+ var usagepriceArray = usagepriceCache[pkgpart];
+ var table = document.getElementById('cust_pkg_usageprice_table');
+
+ // add the new usage price rows
+ var rownum = 0;
+ for ( var s = 1; s < usagepriceArray.length; s=s+2 ) {
+ var html = usagepriceArray[s];
+ var javascript = usagepriceArray[s+1];
+
+ var row = table.insertRow(rownum++);
+
+ var widget_cell = document.createElement('TD');
+ 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';
+ }
+
+}
+
+% if ($opt{'pkgpart'}) {
+<&| /elements/onload.js &>
+usageprice_pkg_changed(<% $opt{'pkgpart'} %>, <% $opt{'pkgnum'} %>);
+</&>
+% }
+
+</SCRIPT>
+
+<%init>
+my %opt = @_;
+</%init>
+
+
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;
-
-</%init>
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;
-
-</%init>
diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html
index 47e6a5b48..8e2863455 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/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/body cgi params
</%doc>
% if ($popup) {
@@ -35,8 +37,9 @@ should be used to set msgnum or from/subject/html_body cgi params
<& /elements/header.html, $title &>
% }
+<& /elements/error.html &>
-<FORM NAME="OneTrueForm" ACTION="<% $form_action %>" METHOD="GET">
+<FORM NAME="OneTrueForm" ACTION="<% $form_action %>" METHOD="POST">
<INPUT TYPE="hidden" NAME="table" VALUE="<% $table %>">
%# Mixing search params with from address, subject, etc. required special-case
%# handling of those, risked name conflicts, and caused massive problems with
@@ -46,48 +49,40 @@ should be used to set msgnum or from/subject/html_body cgi params
<INPUT TYPE="hidden" NAME="popup" VALUE="<% $popup %>">
<INPUT TYPE="hidden" NAME="url" VALUE="<% $url | h %>">
-% if ( $cgi->param('action') eq 'send' ) {
-
- <FONT SIZE="+2">Sending notice</FONT>
+% 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).
+ <FONT SIZE="+2">Preview notice</FONT>
<& /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' ) {
-
- <FONT SIZE="+2">Preview notice</FONT>
-
-% }
-
-% if ( $cgi->param('action') ) {
-
<TABLE CLASS="fsinnerbox">
- <INPUT TYPE="hidden" NAME="msgnum" VALUE="<% scalar($cgi->param('msgnum')) %>">
-
-% if ( $msg_template ) {
- <& /elements/tr-fixed.html,
- 'label' => 'Template:',
- 'value' => $msg_template->msgname,
- &>
-% }
+ <INPUT TYPE="hidden" NAME="msgnum" VALUE="<% $msg_template->msgnum %>">
+% # kludge these through hidden inputs because they're not really part
+% # of the template, but should be sticky during draft editing
+ <INPUT TYPE="hidden" NAME="from_name" VALUE="<% $cgi->param('from_name') %>">
+ <INPUT TYPE="hidden" NAME="from_addr" VALUE="<% $cgi->param('from_addr') %>">
+
+% if ( !$msg_template->disabled ) {
+ <& /elements/tr-td-label.html, 'label' => 'Template:' &>
+ <td><% $msg_template->msgname |h %></td>
+ </tr>
+% }
- <& /elements/tr-fixed.html,
- 'field' => 'from',
- 'label' => 'From:',
- 'value' => $from,
- &>
+ <& /elements/tr-td-label.html, 'label' => 'From:' &>
+ <td><% $from |h %></td>
+ </tr>
- <& /elements/tr-fixed.html,
- 'field' => 'subject',
- 'label' => 'Subject:',
- 'value' => $subject,
- &>
+ <& /elements/tr-td-label.html, 'label' => 'Subject:' &>
+ <td><% $subject |h %></td>
+ </tr>
- <INPUT TYPE="hidden" NAME="html_body" VALUE="<% $html_body |h %>">
<TR><TD COLSPAN=2>&nbsp;</TD></TR>
<TR>
<TH ALIGN="right" VALIGN="top">Message (HTML display): </TD>
@@ -99,7 +94,6 @@ should be used to set msgnum or from/subject/html_body cgi params
% $html_body
% )
% );
- <INPUT TYPE="hidden" NAME="text_body" VALUE="<% $text_body |h %>">
<TR><TD COLSPAN=2>&nbsp;</TD></TR>
<TR>
<TH ALIGN="right" VALIGN="top">Message (Text display): </TD>
@@ -112,38 +106,37 @@ should be used to set msgnum or from/subject/html_body cgi params
</TABLE>
-% if ( $cgi->param('action') eq 'preview' ) {
+ <SCRIPT>
- <SCRIPT>
-
- function showtext() {
- $('#email-message-text-view').css('display','none');
- $('#email-message-text-hide').css('display','');
- $('#email-message-text').slideDown();
- }
+ function showtext() {
+ $('#email-message-text-view').css('display','none');
+ $('#email-message-text-hide').css('display','');
+ $('#email-message-text').slideDown();
+ }
- function hidetext() {
- $('#email-message-text-view').css('display','');
- $('#email-message-text-hide').css('display','none');
- $('#email-message-text').slideUp();
- }
+ function hidetext() {
+ $('#email-message-text-view').css('display','');
+ $('#email-message-text-hide').css('display','none');
+ $('#email-message-text').slideUp();
+ }
- function areyousure(href) {
- return confirm("Send this notice to <% ($num_cust > 1) ? "$num_cust customers" : '1 customer' %> ?");
+ function areyousure(href) {
+ if (confirm("Send this notice to <% ($num_cust > 1) ? "$num_cust customers" : '1 customer' %> ?")) {
+ process();
}
- </SCRIPT>
+ }
+ </SCRIPT>
- <BR>
- <INPUT TYPE="hidden" NAME="action" VALUE="send">
- <INPUT TYPE="submit" VALUE="Send notice" onClick="return areyousure()">
-
-% }
+ <BR>
+ <INPUT TYPE="submit" NAME="edit" VALUE="Edit">
+ <INPUT TYPE="button" VALUE="Send notice" onClick="areyousure()">
% } elsif ($opt{'alternate_form'}) {
<% &{$opt{'alternate_form'}}() %>
% } else {
+% # Edit mode.
<SCRIPT TYPE="text/javascript">
function toggle(obj) {
@@ -151,11 +144,20 @@ function toggle(obj) {
}
</SCRIPT>
+% 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') || ''),
&>
<BR>
+% }
<TABLE BGCOLOR="#cccccc" CELLSPACING=0 WIDTH="100%" id="table_no_template">
<& /elements/tr-td-label.html, 'label' => 'From:' &>
<TD><& /elements/input-text.html,
@@ -163,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'),
&>&nbsp;&lt;\
<& /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'),
&>&gt;</TD>
<& /elements/tr-input-text.html,
'field' => 'subject',
'label' => 'Subject:',
'size' => 50,
+ 'curr_value' => $subject,
&>
<TR>
<TD ALIGN="right" VALIGN="top" STYLE="padding-top:3px">Message: </TD>
<TD><& /elements/htmlarea.html,
- 'field' => 'html_body',
+ 'field' => 'body',
'width' => 763,
+ 'curr_value' => $body,
&>
</TD>
</TR>
</TABLE>
-%#Substitution vars:
-
- <INPUT TYPE="hidden" NAME="action" VALUE="preview">
- <INPUT TYPE="submit" VALUE="Preview notice">
+ <INPUT TYPE="submit" NAME="preview" VALUE="Preview notice">
% } #end not action or alternate form
</FORM>
-% if ( $cgi->param('action') eq 'send' ) {
- <SCRIPT TYPE="text/javascript">
- process();
- </SCRIPT>
-% }
-
<& /elements/footer.html &>
<%init>
@@ -215,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';
@@ -259,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');
+}
-if ( $cgi->param('action') eq 'preview' ) {
+my $subject = $cgi->param('subject');
+my $body = $cgi->param('body');
+my ($html_body, $text_body);
+
+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'});
@@ -275,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'} = '';
@@ -288,8 +329,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;
+ }
}
}
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 &>
<SCRIPT TYPE="text/javascript" SRC="../elements/order_pkg.js"></SCRIPT>
@@ -121,19 +116,9 @@
</TABLE><BR>
-%#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?
-<FONT CLASS = "fsinnerbox-title"
- ID = "cust_pkg_usageprice_title"
- STYLE = "display:none"
-><% mt('Usage add-ons') |h %></FONT>
-<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 ID="cust_pkg_usageprice_table">
-
-</TABLE>
+<% include('/misc/cust_pkg_usageprice.html',
+ 'pkgpart' => $pkgpart
+ ) %>
<BR>
% my $discount_cust_pkg = $curuser->access_right('Discount customer package');
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
+);
+</%init>
diff --git a/httemplate/misc/process/payment.cgi b/httemplate/misc/process/payment.cgi
index 27b818660..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})$/
@@ -135,6 +138,46 @@ $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); # 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 );
+
+ #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') ) {
@@ -160,6 +203,7 @@ if ( $cgi->param('batch') ) {
'manual' => 1,
'balance' => $balance,
'payinfo' => $payinfo,
+ 'paymask' => $paymask,
'paydate' => "$year-$month-01",
'payname' => $payname,
'payunique' => $payunique,
@@ -190,44 +234,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!
</%init>
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);
+</%init>
+<% $server->process %>
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;
+
+</%init>
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;
+
+</%init>
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')) );
</%init>
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)
+
+</%doc>
+<& /elements/header-popup.html, { title => $report_info->{name} } &>
+<script type="text/javascript">
+
+$().ready(function() {
+ var agent_info = <% encode_json(\%agent) %>;
+
+ $('#agentnum').on('change', function() {
+ var agentnum = this.value;
+ if ( agent_info[agentnum] ) {
+ $('#msgnum').prop('value', agent_info[agentnum]['msgnum']);
+ $('#beginning_text').prop('value', agent_info[agentnum]['beginning']);
+ $('#ending_text').prop('value', agent_info[agentnum]['ending']);
+ } else {
+ $('#msgnum').prop('value', '');
+ $('#beginning_text').prop('value', '');
+ $('#ending_text').prop('value', '');
+ }
+ });
+
+ $('#agentnum').trigger('change');
+
+});
+
+</script>
+<FORM NAME="OneTrueForm" ACTION="process/send-report.html" METHOD="POST">
+
+<table class="inv">
+ <input type="hidden" name="reportname" value="<% $cgi->param('reportname') |h %>">
+
+ <& /elements/tr-select-agent.html &>
+
+ <& /elements/tr-td-label.html, label => emt('Message template') &>
+ <TD>
+ <& /elements/select-msg_template.html, field => 'msgnum' &>
+ </TD>
+ </TR>
+
+ <& /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 }
+ &>
+
+</table>
+
+<INPUT TYPE="button" onclick="process()" VALUE="<% emt('Send reports') %>" />
+</FORM>
+
+<style>
+table.grid {
+ border-collapse: collapse;
+ margin-top: 1ex;
+ margin-left: auto;
+ margin-right: auto;
+}
+.grid caption {
+ font-weight: bold;
+ margin-bottom: 0.5ex;
+}
+.grid th,td {
+ padding-left: 3px;
+ padding-right: 3px;
+ padding-bottom: 2px;
+ border: none;
+ empty-cells: show;
+}
+.grid th {
+ border-bottom: 1px solid #999999;
+ font-size: 90%;
+ vertical-align: bottom;
+}
+</style>
+
+% if ( @report_history ) {
+<hr>
+<table class="grid">
+<caption><% emt('Report history') %></caption>
+<thead>
+ <th>Agent</th>
+ <th>Sent on</th>
+ <th colspan=2>Date range</th>
+ <th>User</th>
+</thead>
+<tbody>
+% my $row = 0;
+% foreach my $report (@report_history) {
+% my $agent = ($report->agentnum ?
+% $report->agent->agent : 'All agents');
+ <tr class="row<% $row % 2 %>">
+ <td><% $agent %></td>
+ <td><% time2str($date_format, $report->send_date) %></td>
+ <td><% time2str($date_format, $report->sdate) %></td>
+ <td><% time2str($date_format, $report->edate) %></td>
+ <td><% $report->access_user->username %></td>
+ </tr>
+% $row++;
+% }
+</tbody>
+</table>
+% }
+
+<& /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,
+ };
+}
+
+</%init>
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;
</%init>
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) %>
<TR>
<TD ALIGN="right">Reason</TD>
- <TD><INPUT TYPE="text" NAME="reason" VALUE="<% $cgi->param('reason') |h %>"></TD>
+ <TD><INPUT TYPE="text" NAME="reason" VALUE="<% scalar($cgi->param('reason')) |h %>"></TD>
</TR>
</TABLE>
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);
</%init>
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
+
+</%doc>
+<% 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;
+
+</%init>
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' };
+}
+
+</%init>
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!<INPUT TYPE="hidden" NAME="$f" VALUE="$_">!, @values;
}
- keys %search
+ sort keys %search
),
qq!</FORM>!
} qw( print_ email_ fax_ ftp_ spool_ ) ).
diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi
index 82e87fba9..ab5aad776 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,12 +461,6 @@ if ( $cgi->param('nottax') ) {
}
- 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,
@@ -579,51 +573,61 @@ 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';
- push @where, 'cust_bill_pkg.pkgnum = 0';
+ # 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.
+
+ 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";
+ # 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
- $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 +635,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,27 +657,46 @@ 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"
+ $tax_subquery = "$tax_select $tax_from";
- # 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;
+ } # end of internal-tax case
+
+ if (@tax_where) {
+ $tax_subquery .= '
+ WHERE ' . join(' AND ', map "($_)", @tax_where);
}
+ $tax_subquery .= ' GROUP BY tax.billpkgnum ';
-} # nottax / istax
+ # 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.)
+
+ $join_pkg .= " JOIN ($tax_subquery) AS _istax USING (billpkgnum) ";
+ push @select, 'tax_total';
+
+ @peritem = ( 'setup' ); # total tax on the invoice, not just the filtered tax
+ @peritem_desc = ( 'Tax charge' );
+
+ @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
+ }
+
+} # nottax / istax
#total payments
my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount)
@@ -681,87 +705,25 @@ my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount)
";
push @select, "($pay_sub) AS pay_amount";
-
-# credit
-if ( $cgi->param('credit') ) {
-
- my $credit_where;
-
+# 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;
-
- 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') ) {
-
- # 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';
-
- } else {
-
- # 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)";
-
- }
-
- } 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)";
- }
-
- 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
-
- 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";
+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)";
+push @select, 'credit_amount';
+# standard customer fields
push @select, 'cust_main.custnum', FS::UI::Web::cust_sql_fields();
#salesnum
@@ -785,6 +747,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";
@@ -823,7 +789,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 63d70c27e..5facd4ab3 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
@@ -61,6 +65,7 @@
'',
'',
'',
+ '',
@post_desc_null,
$ilink,
$ilink,
@@ -68,11 +73,12 @@
FS::UI::Web::cust_header()
),
],
- 'align' => 'rrllll'.
+ 'align' => 'rrrllll'.
$post_desc_align.
'rr'.
FS::UI::Web::cust_aligns(),
- 'color' => [
+ 'color' => [
+ '',
'',
'',
'',
@@ -91,6 +97,7 @@
'',
'',
'',
+ '',
@post_desc_null,
'',
'',
@@ -203,6 +210,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 +250,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 +276,23 @@ 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;
}
my($title, $name);
@@ -357,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') ) {
@@ -381,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 )'.
@@ -393,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
@@ -460,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 = ();
@@ -543,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";
+}
+
</%init>
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',
},
) .
'</TD>
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/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/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';
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
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' => [
@@ -78,6 +79,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;
my $status = $queue->status;
my $changable = $dangerous
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'))
+% {
+<P>
+<& /elements/popup_link-send_report_batch.html,
+ reportname => 'sales_commission_pkg',
+ label => emt('Send these reports by email'),
+&>
+</P>
+% }
+
<FORM ACTION="sales_commission_pkg.html">
<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
diff --git a/httemplate/search/report_tax-xls.cgi b/httemplate/search/report_tax-xls.cgi
index d0ef434f4..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,40 +134,46 @@ 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, 14, '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, 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
-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,
+ $ws->merge_range($y, 0, $y, 15,
$pkgclass_name{$row->{pkgclass}},
$format[0]->{sectionhead}
);
@@ -181,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++;
}
@@ -206,6 +212,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++;
@@ -226,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 491cd42c5..9e625c80f 100644
--- a/httemplate/search/report_tax.cgi
+++ b/httemplate/search/report_tax.cgi
@@ -18,11 +18,12 @@ 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 }
</STYLE>
+
<& /elements/table-grid.html &>
<THEAD>
<TR>
<TH ROWSPAN=3></TH>
- <TH COLSPAN=5>Sales</TH>
+ <TH COLSPAN=6>Sales</TH>
<TH ROWSPAN=3></TH>
<TH ROWSPAN=3>Rate</TH>
<TH ROWSPAN=3></TH>
@@ -32,6 +33,8 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px }
<TH ROWSPAN=3>Tax credited</TH>
<TH ROWSPAN=3></TH>
<TH ROWSPAN=3>Net tax due</TH>
+ <TH ROWSPAN=3></TH>
+ <TH ROWSPAN=3>Tax collected</TH>
</TR>
<TR>
@@ -39,6 +42,7 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px }
<TH ROWSPAN=1>Non-taxable</TH>
<TH ROWSPAN=1>Non-taxable</TH>
<TH ROWSPAN=1>Non-taxable</TH>
+ <TH ROWSPAN=2>Credited</TH>
<TH ROWSPAN=2>Taxable</TH>
</TR>
@@ -71,10 +75,20 @@ 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};
+% # 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);
% }
%
% if ( $row->{total} ) {
@@ -107,6 +121,12 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px }
<% $money_sprintf->( $row->{exempt_monthly} ) %>
</A>
</TD>
+% # credited sales
+ <TD>
+ <A HREF="<% $salescreditlink . $rowlink %>">
+ <% $money_sprintf->( $row->{sales_credited} ) %>
+ </A>
+ </TD>
% # taxable sales
<TD>
<A HREF="<% $saleslink . $rowlink . ";taxable=1" %>">
@@ -131,13 +151,16 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px }
% # credited tax
<TD CLASS="bigmath"> &minus; </TD>
<TD>
- <A HREF="<% $creditlink . $rowlink %>">
- <% $money_sprintf->( $row->{credit} ) %>
- </A>
+%# <A HREF="<% $creditlink . $rowlink %>"> currently broken
+ <% $money_sprintf->( $row->{tax_credited} ) %>
+%# </A>
</TD>
% # net tax due
<TD CLASS="bigmath"> = </TD>
- <TD><% $money_sprintf->( $row->{tax} - $row->{credit} ) %></TD>
+ <TD><% $money_sprintf->( $row->{tax} - $row->{tax_credited} ) %></TD>
+% # tax collected
+ <TD>&nbsp;</TD>
+ <TD><% $money_sprintf->( $row->{tax_paid} ) %></TD>
</TR>
% $rownum++;
% $prev_row = $row;
@@ -161,6 +184,80 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px }
% }
</TABLE>
+<BR>
+<& /elements/table-grid.html &>
+ <THEAD>
+ <TR>
+ <TH ROwSPAN=2></TH>
+ <TH ROWSPAN=2>Total credits</TH>
+ <TH COLSPAN=3>Applied to</TH>
+ </TR>
+ <TR STYLE="font-size: small">
+ <TH>Taxable sales</TH>
+ <TH>Tax-exempt sales</TH>
+ <TH>Taxes</TH>
+ </TR>
+ </THEAD>
+
+% $rownum = 0;
+% $prev_row = { pkgclass => 'DUMMY PKGCLASS' };
+
+ <TBODY>
+% # 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;
+ </TBODY><TBODY>
+% }
+% if ( $params{breakdown}->{pkgclass} ) { # and caption the new section
+ <TR>
+ <TD COLSPAN=5 CLASS="sectionhead">
+ <% $pkgclass_name{$row->{pkgclass}} %>
+ </TD>
+ </TR>
+% }
+% } # if $row->{pkgclass} ne ...
+
+% my $rowlink = ';taxnum=' . $row->{taxnums};
+%
+% if ( $row->{total} ) {
+ </TBODY><TBODY CLASS="total">
+% }
+ <TR CLASS="row<% $rownum % 2 %>">
+ <TD CLASS="rowhead"><% $row->{label} |h %></TD>
+ <TD>
+% # Total credits
+ <% $money_sprintf->( $row->{credits} ) %>
+ </TD>
+% # Credits to taxable sales
+ <TD>
+ <A HREF="<% $salescreditlink . $rowlink %>">
+ <% $money_sprintf->( $row->{sales_credited} ) %>
+ </A>
+ </TD>
+% # ... to exempt sales (link is the same, it shows both exempt and taxable)
+ <TD>
+ <A HREF="<% $salescreditlink . $rowlink %>">
+ <% $money_sprintf->( $row->{exempt_credited} ) %>
+ </A>
+ </TD>
+% # ... to taxes
+ <TD>
+%# <A HREF="<% $creditlink . $rowlink %>"> currently broken
+ <% $money_sprintf->( $row->{tax_credited} ) %>
+%# </A>
+ </TD>
+ </TR>
+% $rownum++;
+% $prev_row = $row;
+% } # foreach my $row
+% # no "out of taxable region" for credits (yet)
+ </TBODY>
+</TABLE>
+
+
<& /elements/footer.html &>
<%init>
@@ -218,12 +315,18 @@ 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";
-
+my $salescreditlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;nottax=1;credit=1";
if ( $params{'credit_date'} eq 'cust_credit_bill' ) {
- $creditlink =~ s/begin/credit_begin/;
- $creditlink =~ s/end/credit_end/;
+ $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' ) {
+# $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';
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 &>
<P ALIGN="right" CLASS="noprint">
Download full results<BR>
as <A HREF="<% $cgi->self_url %>;_type=xls">Excel spreadsheet</A></P>
+% }
<BR>
<STYLE TYPE="text/css">
td.cust_head {
@@ -22,12 +27,14 @@ td.money:before { content: '<% $money_char %>'; }
.row1 { background-color: #ffffff; }
</STYLE>
<& /elements/table-grid.html &>
+<THEAD>
<TR STYLE="background-color: #cccccc">
<TH CLASS="grid">Package</TH>
<TH CLASS="grid">Sales</TH>
<TH CLASS="grid">Percentage</TH>
<TH CLASS="grid">Commission</TH>
</TR>
+</THEAD>
% my ($custnum, $sales, $commission, $row, $bgcolor) = (0, 0, 0, 0);
% foreach my $cust_pkg ( @cust_pkg ) {
% if ( $custnum ne $cust_pkg->custnum ) {
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);
+}
+
+</%init>
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' ) %>
+
+<FORM ACTION="tax_sales.cgi" METHOD="GET">
+
+<TABLE>
+
+ <% include('/elements/tr-select-from_to.html') %>
+
+ <% include('/elements/tr-select.html',
+ 'label' => 'Country',
+ 'field' => 'country',
+ 'options' => \@countries,
+ 'curr_value' => ($conf->config('countrydefault') || 'US'),
+ ) %>
+
+</TABLE>
+
+<BR><INPUT TYPE="submit" VALUE="Get Report">
+
+</FORM>
+
+<% 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 };
+
+</%init>
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 @@
<THEAD>
<TR>
- <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Type') |h %></TH>
+% if ( $custom_field ) {
+ <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Type') |h %></TH>
+% }
<TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Date') |h %></TH>
<TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Status') |h %></TH>
<TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Owner') |h %></TH>
@@ -41,10 +43,12 @@
% if $starts > 86400;
<TR>
-
- <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
- <A HREF=<%$href%>><% 'custom field magic type' %></A>
- </TD>
+
+% if ( $custom_field ) {
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <A HREF=<%$href%>><% $ticket->{"CF.{$custom_field}"} |h %></A>
+ </TD>
+% }
<TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
<A HREF=<%$href%>><% $starts_pretty %></A>
@@ -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;
diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html
index c031ce929..0c9f74a7c 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 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;
%
% 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
diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html
index e3599bc06..d81fe9935 100644
--- a/httemplate/view/cust_main/payment_history.html
+++ b/httemplate/view/cust_main/payment_history.html
@@ -228,9 +228,10 @@ 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',
+ '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/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?'),
diff --git a/httemplate/view/cust_main/payment_history/payment.html b/httemplate/view/cust_main/payment_history/payment.html
index bf88a6607..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>
@@ -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! (<A HREF="${p}edit/cust_refund.cgi?payby=$1;!.
qq!paynum=!. $cust_pay->paynum. '"'.
- qq! TITLE="! .emt('Send a refund for this payment to the payment gateway')
+ qq! TITLE="! . $refundtitle
. '">' . emt('refund') . '</A>)';
}
@@ -184,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?'),
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,
+ );
</%init>