summaryrefslogtreecommitdiff
path: root/httemplate
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2013-10-08 23:00:26 -0700
committerIvan Kohler <ivan@freeside.biz>2013-10-08 23:00:26 -0700
commitfe4515eb37d76849dd08c62782d86bc7ba311dcd (patch)
tree6952cc3598de0c72b6a3eab1d53bde07a16c27f2 /httemplate
parentf2766e203e1aa144d046a26cf13e01e1f5b00f64 (diff)
parent81ae0992cf8506c6a77485548ebde25eb946a9a9 (diff)
Merge branch 'master' of git.freeside.biz:/home/git/freeside
Conflicts: FS/FS/cust_main.pm
Diffstat (limited to 'httemplate')
-rw-r--r--httemplate/browse/cable_provider.html32
-rw-r--r--httemplate/browse/invoice_conf.html70
-rw-r--r--httemplate/docs/about.html2
-rw-r--r--httemplate/edit/cable_provider.html20
-rw-r--r--httemplate/edit/cust_main/bottomfixup.js21
-rw-r--r--httemplate/edit/cust_main/top_misc.html7
-rw-r--r--httemplate/edit/elements/edit.html1
-rw-r--r--httemplate/edit/invoice_conf.html296
-rw-r--r--httemplate/edit/process/cable_provider.html10
-rw-r--r--httemplate/edit/process/invoice_conf.html21
-rw-r--r--httemplate/elements/columnstart.html77
-rw-r--r--httemplate/elements/menu.html6
-rw-r--r--httemplate/elements/select-cable_provider.html7
-rw-r--r--httemplate/elements/tr-select-cable_provider.html12
-rw-r--r--httemplate/elements/tr-select-invoice_mode.html10
-rw-r--r--httemplate/misc/delete-invoice_conf.html19
-rwxr-xr-xhttemplate/misc/email-invoice.cgi2
-rwxr-xr-xhttemplate/misc/fax-invoice.cgi2
-rwxr-xr-xhttemplate/misc/print-invoice.cgi2
-rw-r--r--httemplate/misc/send-invoice.cgi5
-rw-r--r--httemplate/misc/xmlhttp-cust_main-duplicates.html4
-rw-r--r--httemplate/search/phone_state.html167
-rwxr-xr-xhttemplate/view/cust_bill.cgi35
-rwxr-xr-xhttemplate/view/cust_main.cgi1
-rwxr-xr-xhttemplate/view/cust_statement.html4
-rw-r--r--httemplate/view/elements/cust_bill-typeset7
26 files changed, 815 insertions, 25 deletions
diff --git a/httemplate/browse/cable_provider.html b/httemplate/browse/cable_provider.html
new file mode 100644
index 000000000..0d344984b
--- /dev/null
+++ b/httemplate/browse/cable_provider.html
@@ -0,0 +1,32 @@
+<& elements/browse.html,
+ 'title' => 'Cable providers',
+ 'html_init' => $html_init,
+ 'name' => 'providers',
+ 'disableable' => 1,
+ 'disabled_statuspos' => 1,
+ 'query' => { 'table' => 'cable_provider',
+ 'hashref' => {},
+ 'order_by' => 'ORDER BY provider',
+ },
+ 'count_query' => $count_query,
+ 'header' => $header,
+ 'fields' => $fields,
+ 'links' => $links,
+&>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $html_init =
+ qq!<A HREF="${p}edit/cable_provider.html"><I>Add a provider</I></A><BR><BR>!;
+
+my $count_query = 'SELECT COUNT(*) FROM cable_provider';
+
+my $link = [ $p.'edit/cable_provider.html?', 'providernum' ];
+
+my $header = [ 'Provider' ];
+my $fields = [ 'provider' ];
+my $links = [ $link ];
+
+</%init>
diff --git a/httemplate/browse/invoice_conf.html b/httemplate/browse/invoice_conf.html
new file mode 100644
index 000000000..c8fd1bffb
--- /dev/null
+++ b/httemplate/browse/invoice_conf.html
@@ -0,0 +1,70 @@
+<& elements/browse.html,
+ 'title' => 'Invoice modes',
+ 'name_singular' => 'configuration',
+ 'menubar' => \@menubar,
+ 'query' => {
+ 'select' => $select,
+ 'table' => 'invoice_conf',
+ 'addl_from' => ' JOIN invoice_mode USING (modenum)',
+ 'extra_sql' => ' WHERE '.$curuser->agentnums_sql(
+ 'table' => 'invoice_mode',
+ 'null_right' => ['Edit global templates'],
+ ),
+ 'order_by' => q( ORDER BY modename asc, COALESCE(locale,'') asc),
+ },
+ 'count_query' => 'SELECT COUNT(*) FROM invoice_conf JOIN invoice_mode USING (modenum)',
+ 'header' => [ 'Name', 'Agent', 'Locale', 'Overrides', ],
+ 'fields' => [ $modename,
+ $agent,
+ $locale_label,
+ $overrides,
+ ],
+ 'align' => 'llcl',
+ 'links' => [ '', '', $link ],
+ 'disable_maxselect' => 1,
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right([ 'View templates', 'View global templates',
+ 'Edit templates', 'Edit global templates', ]);
+
+my @overrides = grep {$_ ne 'modenum' and $_ ne 'confnum'} FS::invoice_conf->fields;
+my $select = join(',', 'modename', 'agentnum', 'confnum', 'invoice_conf.*');
+
+my @menubar = ();
+if ( $curuser->access_right(['Edit templates', 'Edit global templates']) ) {
+ push @menubar, 'Add a new invoice mode' => $p.'edit/invoice_conf.html';
+}
+
+my $locale_style = 'font-size:0.8em; padding:3px; background-color:';
+
+my $last_modenum = 0;
+my $modename = sub {
+ return '' if $_[0]->modenum == $last_modenum;
+ $_[0]->modename;
+};
+
+my $agent = sub {
+ return '' if $_[0]->modenum == $last_modenum;
+ $last_modenum = $_[0]->modenum;
+ $_[0]->agentnum ? FS::agent->by_key($_[0]->agentnum)->agent : '(global)';
+};
+
+my $locale_label = sub {
+ my $l = $_[0]->locale;
+ $l ? +{ FS::Locales->locale_info($l) }->{'label'} : '(default)';
+};
+
+my $overrides = sub {
+ my $invoice_conf = shift;
+ [ map { [ { data => $_ } ] }
+ grep { length $invoice_conf->get($_) }
+ @overrides
+ ],
+};
+
+my $link = [ $p.'edit/invoice_conf.html?', 'confnum' ];
+</%init>
diff --git a/httemplate/docs/about.html b/httemplate/docs/about.html
index c2ba4e4a0..80d9488b6 100644
--- a/httemplate/docs/about.html
+++ b/httemplate/docs/about.html
@@ -56,7 +56,7 @@ GNU <b>Affero</b> General Public License.<BR>
% unless ( $agentnum ) {
<CENTER>
- <FONT SIZE="-3">"I can't figure out ... if it's an end or the beginning" - R. Hunter</FONT>
+ <FONT SIZE="-3">"" - R. Hunter</FONT>
</CENTER>
% }
diff --git a/httemplate/edit/cable_provider.html b/httemplate/edit/cable_provider.html
new file mode 100644
index 000000000..9a911ccfa
--- /dev/null
+++ b/httemplate/edit/cable_provider.html
@@ -0,0 +1,20 @@
+<& elements/edit.html,
+ 'name_singular' => 'Provider',
+ 'table' => 'cable_provider',
+ 'fields' => [
+ 'provider',
+ { field=>'disabled', type=>'checkbox', value=>'Y', },
+ ],
+ 'labels' => {
+ 'providernum' => 'Provider',
+ 'provider' => 'Provider',
+ 'disabled' => 'Disabled',
+ },
+ 'viewall_dir' => 'browse',
+&>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/edit/cust_main/bottomfixup.js b/httemplate/edit/cust_main/bottomfixup.js
index 9e18fa0df..ecfcb3cbc 100644
--- a/httemplate/edit/cust_main/bottomfixup.js
+++ b/httemplate/edit/cust_main/bottomfixup.js
@@ -10,17 +10,20 @@ my @fixups = ('copy_payby_fields', 'standardize_locations');
push @fixups, 'confirm_censustract'
if $conf->exists('cust_main-require_censustract');
-# currently doesn't work; disable to avoid problems
-#push @fixups, 'check_unique'
-# if $conf->exists('cust_main-check_unique') and !$opt{'custnum'};
+my $uniqueness = $conf->config('cust_main-check_unique');
+push @fixups, 'check_unique'
+ if $uniqueness and !$opt{'custnum'};
push @fixups, 'do_submit'; # always last
</%init>
-
var fixups = <% encode_json(\@fixups) %>;
var fixup_position;
var running = false;
+<&| /elements/onload.js &>
+submit_abort();
+</&>
+
%# state machine to deal with all the asynchronous stuff we're doing
%# call this after each fixup on success:
function submit_continue() {
@@ -132,10 +135,14 @@ function set_censustract(tract, year) {
}
function check_unique() {
- var search_hash = new Object;
-% foreach ($conf->config('cust_main-check_unique')) {
- search_hash['<% $_ %>'] = document.CustomerForm.elements['<% $_ %>'].value;
+ var search_hash = {};
+% if ($uniqueness eq 'address') {
+ search_hash['address'] = [
+ document.CustomerForm.elements['bill_address1'].value,
+ document.CustomerForm.elements['ship_address1'].value
+ ];
% }
+%# no other options yet
%# supported in IE8+, Firefox 3.5+, WebKit, Opera 10.5+
duplicates_form(JSON.stringify(search_hash), confirm_unique);
diff --git a/httemplate/edit/cust_main/top_misc.html b/httemplate/edit/cust_main/top_misc.html
index ebd9b927c..e25506f52 100644
--- a/httemplate/edit/cust_main/top_misc.html
+++ b/httemplate/edit/cust_main/top_misc.html
@@ -154,9 +154,10 @@
% } else {
- <& /elements/tr-select-part_referral.html,
- 'curr_value' => $refnum
- &>
+ <& /elements/tr-select-part_referral.html,
+ 'curr_value' => $refnum,
+ 'label' => "<B>${r}".emt('Advertising source')."</B>"
+ &>
% }
diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html
index 060281115..6c965326b 100644
--- a/httemplate/edit/elements/edit.html
+++ b/httemplate/edit/elements/edit.html
@@ -338,6 +338,7 @@ Example:
% qw( width height config ), #htmlarea
% qw( alt_format ), #select-cust_location
% qw( classnum ), # select-inventory_item
+% qw( aligned ), # columnstart
% ;
%
% #select-table
diff --git a/httemplate/edit/invoice_conf.html b/httemplate/edit/invoice_conf.html
new file mode 100644
index 000000000..b7b3a4ebc
--- /dev/null
+++ b/httemplate/edit/invoice_conf.html
@@ -0,0 +1,296 @@
+<& elements/edit.html,
+ 'body_etc' => $body_etc,
+ 'name_singular' => 'invoice configuration',
+ 'table' => 'invoice_conf',
+ 'viewall_dir' => 'browse',
+ 'fields' => \@fields,
+ 'labels' => \%labels,
+ 'new_callback' => \&new_callback,
+ 'edit_callback' => \&edit_callback,
+ 'error_callback' => \&error_callback,
+ 'html_init' => \&html_init,
+ 'html_table_bottom' => \&html_table_bottom,
+ 'html_bottom' => '</DIV>', # close tablebreak-tabs
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+# ???
+die "access denied"
+ unless $curuser->access_right([ 'Edit templates', 'Edit global templates' ]);
+
+my $body_etc = '';
+$body_etc = q!onload="document.getElementById('locale').onchange()"!
+ if $cgi->param('locale') eq 'new';
+
+my $modenum = $cgi->param('modenum');
+my $mode = $modenum ? qsearchs('invoice_mode', { modenum => $modenum }) : '';
+
+my %textarea = (type => 'textarea', rows => 10, cols => 40);
+my @fields = (
+ { field => 'modenum', type => 'hidden' },
+ { field => 'agentnum',
+ type => 'select-agent',
+ },
+ { field => 'modename', size=>60, },
+ { type => 'tablebreak-tabs',
+ include_opt_callback => \&menubar_opt_callback,
+ },
+ { field => 'locale', type => 'hidden' },
+ { field => 'notice_name', size=>60, },
+ { field => 'subject', size=>60, },
+ { field => 'lpr', size=>60, },
+
+ { type => 'columnstart', aligned => 1 },
+ { type => 'title', value => '<BR>' },
+ map ( { +{ type => 'justtitle', value => $_ } }
+ 'Notes',
+ 'Footer',
+ 'Summary header',
+ 'Return address',
+ 'Coupon',
+ 'Small footer',
+ 'Top margin',
+ 'Header separation',
+ 'Address separation',
+ 'Text height',
+ 'Coupon height',
+ 'Footer separation',
+ ),
+
+ { type => 'columnnext' },
+ { type => 'title', value => 'LaTeX' },
+ { field => 'latexnotes', %textarea },
+ { field => 'latexfooter', %textarea },
+ { field => 'latexsummary', %textarea },
+ { field => 'latexreturnaddress', %textarea },
+ { field => 'latexcoupon', %textarea },
+ { field => 'latexsmallfooter', %textarea },
+ { field => 'latextopmargin', size => 16 },
+ { field => 'latexheadsep', size => 16 },
+ { field => 'latexaddresssep', size => 16 },
+ { field => 'latextextheight', size => 16 },
+ { field => 'latexextracouponspace', size => 16 },
+ { field => 'latexcouponfootsep', size => 16 },
+ # are these still used?
+ #{ field => 'latexcouponamountenclosedsep', size => 16 },
+ #{ field => 'latexverticalreturnaddress', type => 'checkbox' },
+ #{ field => 'latexcouponaddcompanytoaddress',type => 'checkbox' },
+ # logo -- implement if someone really needs it...
+
+ { type => 'columnnext' },
+ { type => 'title', value => 'HTML' },
+ { field => 'htmlnotes', %textarea }, #htmlarea?
+ { field => 'htmlfooter', %textarea },
+ { field => 'htmlsummary', %textarea },
+ { field => 'htmlreturnaddress', %textarea },
+ # logo
+
+ { type => 'columnend' },
+);
+
+my %labels = (
+ 'confnum' => 'Configuration',
+ 'locale' => 'Locale',
+ 'agentnum' => 'Agent',
+ 'modename' => 'Mode name',
+ 'notice_name' => 'Notice name',
+ 'subject' => 'Email Subject: header',
+ 'lpr' => 'Alternate lpr command',
+
+ map { $_ => '' } (qw(
+ latexnotes
+ latexfooter
+ latexsummary
+ latexreturnaddress
+ latexcoupon
+ latexsmallfooter
+ latextopmargin
+ latexheadsep
+ latexaddresssep
+ latextextheight
+ latexextracouponspace
+ latexcouponfootsep
+ htmlnotes
+ htmlfooter
+ htmlsummary
+ htmlreturnaddress
+ logo_png
+ logo_eps
+ ) ),
+
+);
+
+sub get_invoice_mode { # because we can't quite use agent_virt here
+ my $modenum = shift;
+ qsearchs({
+ 'table' => 'invoice_mode',
+ 'hashref' => { 'modenum' => $modenum },
+ 'extra_sql' => ' AND '.
+ $FS::CurrentUser::CurrentUser->agentnums_sql(
+ 'null_right' => 'Edit global templates',
+ 'viewall_right' => 'Edit global templates' ),
+ });
+};
+
+sub error_callback {
+ my ($cgi, $object) = @_;
+ foreach (qw(modename agentnum)) {
+ $object->set($_, $cgi->param($_));
+ }
+ if ($object->confnum) {
+ return edit_callback(@_);
+ } else {
+ return new_callback(@_);
+ }
+}
+
+sub new_callback {
+ my ($cgi, $object, $fields_arrayref, $opt_hashref) = @_;
+ my $modenum;
+ if ( $cgi->param('locale') =~ /^(\w+)$/ ) {
+ $object->set('locale' => $1);
+ }
+
+ if ( $cgi->param('modenum') =~ /^(\d+)$/ ) {
+ $modenum = $1; # we're adding a locale to an existing mode
+ $object->set('modenum' => $modenum);
+ my $invoice_mode = get_invoice_mode($modenum)
+ or die "invoice mode $modenum not found";
+
+ $object->set('modename', $invoice_mode->modename);
+ $object->set('agentnum', $invoice_mode->agentnum);
+
+ # also, need to select a locale
+ # make a list of available locales
+ my %existing_locales = map { $_->locale }
+ qsearch('invoice_conf', { modenum => $modenum });
+
+ my @locales = grep { !exists($existing_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_arrayref->[$i]->{'field'} eq 'locale' );
+ my $locale_field = $fields_arrayref->[$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,
+ );
+
+ } # otherwise it's a completely new mode, so the locale is default
+
+}
+
+sub edit_callback {
+ # massive false laziness with msg_template UI
+ my ($cgi, $object, $fields_arrayref, $opt_hashref) = @_;
+
+ # a little different here in that we treat the content object
+ # as "primary" (this is edit/invoice_conf.html, etc.)
+ # so all we need from the invoice_mode is its name
+ # (and agent identity)
+ my $modenum = $object->modenum;
+ my $invoice_mode = get_invoice_mode($modenum)
+ or die "invoice mode $modenum not found";
+ $object->set('modename', $invoice_mode->modename);
+ $object->set('agentnum', $invoice_mode->agentnum);
+
+}
+
+sub menubar_opt_callback {
+ my $object = shift;
+ my $modenum = $object->modenum or return;
+ my (@tabs, @options, %labels);
+ my $display_new = 0;
+ my $selected = '';
+ foreach my $l ('', FS::Conf->new->config('available-locales')) {
+ my $invoice_conf =
+ qsearchs('invoice_conf', { modenum => $modenum, locale => $l });
+ if ( $invoice_conf ) {
+ my %info = FS::Locales->locale_info($l) if $l;
+ my $label = $info{'label'} || mt('Default');
+ push @tabs, $label, $invoice_conf->confnum;
+ $selected = $label if $object->locale eq $l;
+ }
+ else {
+ $display_new = 1; # there is at least one unused locale left
+ }
+ }
+ push @tabs, mt('New'), "modenum=$modenum;locale=new" if $display_new;
+ $selected = mt('New') if $object->locale eq 'new';
+ $selected ||= mt('Default');
+ (
+ 'url_base' => $cgi->url() . '?',
+ 'selected' => $selected,
+ 'tabs' => \@tabs
+ );
+}
+
+sub html_init {
+q!
+<STYLE>
+.fstabcontainer th { vertical-align: middle; text-align: center }
+</STYLE>
+!
+}
+
+sub html_table_bottom {
+ my $object = shift;
+ my $locale = '';
+ my $modenum = '';
+
+ if ($object->locale =~ /^(\w+)$/) {
+ $locale = $1;
+ }
+ if ($object->modenum =~ /^(\d+)$/) {
+ $modenum = $1;
+ }
+ my $html;
+ my $show_delete = 1;
+ # don't allow the default locale to be removed unless it's the last one
+ # in the mode
+ $show_delete = 0 if (
+ $locale eq 'new' or
+ $modenum eq '' or
+ ($locale eq '' and
+ FS::invoice_conf->count("modenum = $modenum and locale is not null") > 0
+ )
+ );
+
+ if ( $show_delete ) {
+ # set up a delete link
+ my $confnum = $object->confnum;
+ my $url = $p."misc/delete-invoice_conf.html?$confnum";
+ my $link = qq!<A HREF="javascript:areyousure('$url','Really delete this configuration?')">! .
+ 'Delete this configuration' .
+ '</A>';
+ $html = qq!<TR><TD></TD>
+ <TD STYLE="font-style: italic; font-size: small">$link</TD></TR>
+ <SCRIPT TYPE="text/javascript">
+ function areyousure(url, message) {
+ if (confirm(message)) window.location.href = url;
+ }
+ </SCRIPT>
+ !;
+ }
+ $html;
+}
+
+</%init>
diff --git a/httemplate/edit/process/cable_provider.html b/httemplate/edit/process/cable_provider.html
new file mode 100644
index 000000000..ecffaf692
--- /dev/null
+++ b/httemplate/edit/process/cable_provider.html
@@ -0,0 +1,10 @@
+<& elements/process.html,
+ 'table' => 'cable_provider',
+ 'viewall_dir' => 'browse',
+&>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/edit/process/invoice_conf.html b/httemplate/edit/process/invoice_conf.html
new file mode 100644
index 000000000..1d45e126f
--- /dev/null
+++ b/httemplate/edit/process/invoice_conf.html
@@ -0,0 +1,21 @@
+<& elements/process.html,
+ 'table' => 'invoice_conf',
+ 'viewall_dir' => 'browse',
+ 'fields' => [ FS::invoice_conf->fields, 'modename', 'agentnum' ],
+ 'precheck_callback' => \&precheck_callback,
+&>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right(['Edit templates','Edit global templates']);
+
+sub precheck_callback {
+ my $cgi = shift;
+ $cgi->param('locale') =~ /^(\w*)$/;
+ my $locale = $1;
+ return mt('Language required') if $locale eq 'new'; # the user didn't choose
+ die "unknown locale $locale" if ( $locale and
+ !FS::Locales->locale_info($locale) );
+}
+# invoice_conf itself knows to create/update invoice_mode if necessary,
+# so nothing special here
+</%init>
diff --git a/httemplate/elements/columnstart.html b/httemplate/elements/columnstart.html
index be37d817d..1ffbcb9e8 100644
--- a/httemplate/elements/columnstart.html
+++ b/httemplate/elements/columnstart.html
@@ -1,6 +1,81 @@
+<%doc>
+<table>
+ <& /elements/columnstart.html &>
+ <tr> ... </tr>
+ <tr> ... </tr>
+ <& /elements/columnnext.html &>
+ ...
+ <& /elements/columnend.html &>
+</table>
+
+Pass 'aligned' => 1 to have corresponding rows in the columns line up.
+</%doc>
+% my $id = sprintf('table%08d', rand(100000000));
<TR>
<TD CLASS="background" COLSPAN=99>
- <TABLE BORDER=0 CELLSPACING=0 CELLPADDING=0>
+ <TABLE BORDER=0 CELLSPACING=0 CELLPADDING=0 id="<%$id%>">
<TR>
<TD VALIGN="top">
<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
+% if ( $aligned ) {
+%# Instead of changing all the tr-* elements to sometimes output table
+%# cells without wrapping them in a row, we're just going to completely
+%# rebuild the table on the client side.
+<script type="text/javascript">
+<&| onload.js &>
+ var table = document.getElementById('<%$id%>'); // has one row, always
+ var rows = []; // row contents, each containing
+ var n_rows = []; // rows in each subtable
+ var n_cols = []; // cols in each subtable
+ var total_rows = 0; // max(n_rows)
+ for(var i=0; i < table.rows[0].cells.length; i++) {
+ // these are cells created by columnstart/columnnext
+ // each contains a table, and nothing else
+ var subtable = table.rows[0].cells[i].children[0];
+ n_rows[i] = subtable.rows.length;
+ if ( total_rows < n_rows[i] ) {
+ total_rows = n_rows[i];
+ }
+ n_cols[i] = 0;
+ var subrows = []; // the rows of this table
+ for(var j=0; j < n_rows[i]; j++) {
+ // these are the actual tr-* rows within the table, and
+ // can contain multiple cells
+ subrows[j] = [];
+ var tr = subtable.rows[j];
+ if ( n_cols[i] < tr.cells.length ) {
+ n_cols[i] = tr.cells.length;
+ }
+ for(var k=0; k < tr.cells.length; k++) {
+ subrows[j][k] = tr.cells[k];
+ }
+ } // for(j)
+ rows[i] = subrows;
+ } // for(i)
+ var new_table = document.createElement('TABLE');
+ for (var j = 0; j < total_rows; j++) {
+ var tr = document.createElement('TR');
+ for (var i = 0; i < rows.length; i++) { // subtables
+ var k = 0; // subrow position
+ if ( j < n_rows[i] ) { // then subtable i has this row
+ for (k = 0; k < rows[i][j].length; k++) { // cells
+ tr.appendChild(rows[i][j][k]);
+ }
+ } // else k is just 0
+ if ( k < n_cols[i] ) { // then we need a spacer
+ var spacer = document.createElement('TD');
+ spacer.setAttribute('colspan', n_cols[i] - k);
+ tr.appendChild(spacer);
+ }
+ } // for(i); subtables
+ // tr is complete
+ new_table.appendChild(tr);
+ } // for(j); rows
+ table.parentNode.insertBefore( new_table, table );
+ table.parentNode.removeChild(table);
+</&>
+</script>
+% } # if $aligned
+<%args>
+$aligned => 0
+</%args>
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index 8cbbd1742..8cb967518 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -197,6 +197,10 @@ foreach my $svcdb ( FS::part_svc->svc_tables() ) {
[ $fsurl. 'search/report_svc_phone_usage.html',
'Total usage (minutes, and amount billed) for the specified time period, per phone number.',
];
+ $report_svc{"${name} by state"} =
+ [ $fsurl. 'search/phone_state.html',
+ 'Current or historical phone services broken down by state.',
+ ];
}
@@ -517,6 +521,7 @@ tie my %config_radius, 'Tie::IxHash',
;
tie my %config_cable, 'Tie::IxHash',
+ 'Cable providers' => [ $fsurl.'browse/cable_provider.html', '' ],
'Cable modem models' => [ $fsurl.'browse/cable_model.html', '' ],
;
@@ -600,6 +605,7 @@ $config_billing{'Billing events'} = [ $fsurl.'browse/part_event.html', 'Billing
|| $curuser->access_right('Edit global billing events');
if ( $curuser->access_right('Configuration') ) {
#$config_billing{'Invoice events'} = [ $fsurl.'browse/part_bill_event.cgi', 'Deprecated, old-style actions for overdue invoices' ];
+ $config_billing{'Invoice configurations'} = [ $fsurl.'browse/invoice_conf.html', 'Adjust invoice settings for special-purpose notices' ];
$config_billing{'Invoice templates'} = [ $fsurl.'browse/invoice_template.html', 'Edit templates for HTML, plaintext and typeset invoices' ];
$config_billing{'separator'} = ''; #its a separator!
$config_billing{'Prepaid cards'} = [ $fsurl.'search/prepay_credit.html', 'View outstanding cards, generate new cards' ];
diff --git a/httemplate/elements/select-cable_provider.html b/httemplate/elements/select-cable_provider.html
new file mode 100644
index 000000000..9530b78c0
--- /dev/null
+++ b/httemplate/elements/select-cable_provider.html
@@ -0,0 +1,7 @@
+<% include( '/elements/select-table.html',
+ 'table' => 'cable_provider',
+ 'name_col' => 'provider',
+ 'empty_label' => 'Select provider',
+ @_,
+ )
+%>
diff --git a/httemplate/elements/tr-select-cable_provider.html b/httemplate/elements/tr-select-cable_provider.html
new file mode 100644
index 000000000..abb8564dc
--- /dev/null
+++ b/httemplate/elements/tr-select-cable_provider.html
@@ -0,0 +1,12 @@
+% #if ( scalar(@domains) < 2 ) {
+% #} else {
+ <TR>
+ <TD ALIGN="right"><% $opt{'label'} || 'Provider' %></TD>
+ <TD>
+ <% include( '/elements/select-cable_provider.html', %opt) %>
+ </TD>
+ </TR>
+% #}
+<%init>
+ my %opt = @_;
+</%init>
diff --git a/httemplate/elements/tr-select-invoice_mode.html b/httemplate/elements/tr-select-invoice_mode.html
new file mode 100644
index 000000000..3dccdccc2
--- /dev/null
+++ b/httemplate/elements/tr-select-invoice_mode.html
@@ -0,0 +1,10 @@
+<& tr-select-table.html,
+ 'label' => 'Invoice mode',
+ 'table' => 'invoice_mode',
+ 'field' => 'modenum',
+ 'name_col' => 'modename',
+ 'agent_virt' => 1,
+ 'agent_null' => 1,
+ 'empty_label' => '(none)',
+ @_
+&>
diff --git a/httemplate/misc/delete-invoice_conf.html b/httemplate/misc/delete-invoice_conf.html
new file mode 100644
index 000000000..6cc6ddc95
--- /dev/null
+++ b/httemplate/misc/delete-invoice_conf.html
@@ -0,0 +1,19 @@
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied"
+ unless $curuser->access_right(['Edit templates', 'Edit global templates']);
+
+my ($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ or die "bad confnum";
+my $invoice_conf = FS::invoice_conf->by_key($1)
+ or die "couldn't find invoice_conf #$1";
+if ( !$curuser->access_right('Edit global templates') ) {
+ my $agentnum = FS::invoice_mode->by_key($invoice_conf->modenum)->agentnum;
+ die "access denied"
+ unless $curuser->agentnums_href->{$agentnum};
+}
+
+my $error = $invoice_conf->delete; # may also delete the invoice_mode
+my $url = $p.'browse/invoice_conf.html';
+</%init>
+<% $cgi->redirect($url) %>
diff --git a/httemplate/misc/email-invoice.cgi b/httemplate/misc/email-invoice.cgi
index 269722f67..b24e0420f 100755
--- a/httemplate/misc/email-invoice.cgi
+++ b/httemplate/misc/email-invoice.cgi
@@ -12,7 +12,7 @@ my $invnum = $3;
my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
die "Can't find invoice!\n" unless $cust_bill;
-$cust_bill->email($template);
+$cust_bill->email({ 'template' => $template });
my $custnum = $cust_bill->getfield('custnum');
diff --git a/httemplate/misc/fax-invoice.cgi b/httemplate/misc/fax-invoice.cgi
index 2591fceb8..f72fc7eaf 100755
--- a/httemplate/misc/fax-invoice.cgi
+++ b/httemplate/misc/fax-invoice.cgi
@@ -12,7 +12,7 @@ my $invnum = $3;
my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
die "Can't find invoice!\n" unless $cust_bill;
-$cust_bill->fax_invoice($template);
+$cust_bill->fax_invoice({ 'template' => $template });
my $custnum = $cust_bill->getfield('custnum');
diff --git a/httemplate/misc/print-invoice.cgi b/httemplate/misc/print-invoice.cgi
index aeef68795..5ce6e76df 100755
--- a/httemplate/misc/print-invoice.cgi
+++ b/httemplate/misc/print-invoice.cgi
@@ -12,7 +12,7 @@ my $invnum = $3;
my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
die "Can't find invoice!\n" unless $cust_bill;
-$cust_bill->print($template);
+$cust_bill->print({ 'template' => $template});
my $custnum = $cust_bill->getfield('custnum');
diff --git a/httemplate/misc/send-invoice.cgi b/httemplate/misc/send-invoice.cgi
index 32dfe276d..08dd0e01c 100644
--- a/httemplate/misc/send-invoice.cgi
+++ b/httemplate/misc/send-invoice.cgi
@@ -13,6 +13,10 @@ my $invnum = $cgi->param('invnum');
my $template = $cgi->param('template');
my $notice_name = $cgi->param('notice_name') if $cgi->param('notice_name');
my $method = $cgi->param('method');
+my $mode;
+if ( $cgi->param('mode') =~ /^(\d+)$/ ) {
+ $mode = $1;
+}
$method .= '_invoice' if $method eq 'fax'; #!
@@ -21,6 +25,7 @@ die "unknown method $method" unless $method{$method};
my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
die "Can't find invoice!\n" unless $cust_bill;
+$cust_bill->set('mode' => $mode) if $mode;
$cust_bill->$method({ 'template' => $template,
'notice_name' => $notice_name,
});
diff --git a/httemplate/misc/xmlhttp-cust_main-duplicates.html b/httemplate/misc/xmlhttp-cust_main-duplicates.html
index 7ee00af66..7cd463371 100644
--- a/httemplate/misc/xmlhttp-cust_main-duplicates.html
+++ b/httemplate/misc/xmlhttp-cust_main-duplicates.html
@@ -50,7 +50,9 @@ my $conf = new FS::Conf;
my $sub = $cgi->param('sub');
my $hashref = decode_json($cgi->param('arg'));
-my @cust_main = qsearch('cust_main', $hashref);
+my $search = FS::cust_main->search($hashref);
+#warn Dumper($search);
+my @cust_main = qsearch( $search );
my $set_to_customer = <<EOF;
var custnum_array = document.getElementsByName('dup_custnum');
diff --git a/httemplate/search/phone_state.html b/httemplate/search/phone_state.html
new file mode 100644
index 000000000..67965b702
--- /dev/null
+++ b/httemplate/search/phone_state.html
@@ -0,0 +1,167 @@
+<& elements/search.html,
+ 'title' => $title,
+ 'name' => 'states',
+ 'query' => $query,
+ 'count_query' => $count_query,
+ 'count_addl' => $count_addl,
+ 'header' => [ 'State', # if we add more group fields, change this
+ 'Count',
+ 'Phone numbers'
+ ],
+ 'fields' => [ 'state',
+ 'num_svcnums',
+ $detail_sub
+ ],
+ 'html_init' => include('.head', $time),
+&>
+<%def .head>
+% my $time = shift;
+<FORM STYLE="display:inline" ACTION=<% $cgi->url %> METHOD="GET">
+Active phone services as of <& /elements/input-date-field.html, {
+ 'name' => 'date',
+ 'value' => $time,
+ 'format' => FS::Conf->new->config('date_format') || '%m/%d/%Y'
+} &>
+<INPUT TYPE="hidden" NAME="order_by" VALUE="<% $cgi->param('order_by') %>">
+<INPUT TYPE="submit" VALUE="Refresh">
+</FORM>
+<BR>
+<BR>
+</%def>
+<%init>
+# svc_phone-specific for now; may change later
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied" unless $curuser->access_right('Services: Phone numbers');
+my $title = 'Phone services by state';
+
+my $time = time;
+if ( $cgi->param('date') ) {
+ $time = parse_datetime($cgi->param('date'));
+ $title .= time2str(' (%b %o, %Y)', $time);
+}
+
+my @tables = qw(svc_phone cust_svc cust_pkg cust_location cust_main);
+my @pkeys = qw(svcnum svcnum pkgnum locationnum custnum);
+my @h_tables = map "h_$_", @tables;
+
+my $addl_from = '';
+my @where;
+
+for(my $i = 0; $i < scalar(@tables); $i++) {
+ my $last_table = $h_tables[$i-1];
+ my $pkey = $pkeys[$i];
+ my $table = $tables[$i];
+ my $h_table = $h_tables[$i];
+ # alias the preceding table, and join to a subquery that finds the most
+ # recent change to $table.$pkey before $time
+ my $alias = $h_table;
+ my $inside = '';
+ if ( $i > 0 ) {
+ $alias = "t$i";
+ $inside = " AS $alias";
+ }
+ $inside .= "
+ JOIN
+ (SELECT $pkey AS num, MAX(history_date) AS history_date
+ FROM $h_table
+ WHERE history_date <= $time AND
+ history_action IN ('insert', 'replace_new')
+ GROUP BY $pkey
+ ) AS mostrecent_$table
+ ON ($alias.$pkey = mostrecent_$table.num AND
+ $alias.history_date = mostrecent_$table.history_date AND
+ $alias.history_action IN ('insert', 'replace_new')
+ )
+ LEFT JOIN
+ (SELECT $pkey AS num, MAX(history_date) AS history_date, 1 AS deleted
+ FROM $h_table
+ WHERE history_date <= $time AND
+ history_action = 'delete'
+ GROUP BY $pkey
+ ) AS deleted_$table
+ ON (mostrecent_$table.num = deleted_$table.num AND
+ mostrecent_$table.history_date < deleted_$table.history_date
+ )
+";
+ # join to the preceding table if there is one, and filter out
+ # deleted records
+ if ( $i > 0 ) {
+ # special case to make pre-3.x data work; remove this later
+ if ( $table eq 'cust_main' ) {
+ $last_table = 'h_cust_pkg';
+ }
+ $addl_from .= "
+ LEFT JOIN ( $h_table $inside ) AS $h_table
+ ON ($h_table.$pkey = $last_table.$pkey)";
+ push @where, "$h_table.deleted IS NULL";
+ } else {
+ $addl_from .= $inside;
+ push @where, "deleted_$table.deleted IS NULL";
+ }
+}
+
+# so that we know which services are still active
+$addl_from .= "
+ LEFT JOIN svc_phone ON (h_svc_phone.svcnum = svc_phone.svcnum AND
+ h_svc_phone.phonenum = svc_phone.phonenum)";
+
+#warn "\n\nJOIN EXPRESSION:\n$addl_from\n\n";
+
+push @where, $curuser->agentnums_sql(
+ 'table' => 'h_cust_main',
+ 'null_right' => 'View/link unlinked services'
+);
+my $where = " WHERE ".join(' AND ', map "($_)", @where);
+
+# for pre-3.x data
+my $group_field = 'COALESCE(h_cust_location.state, h_cust_main.ship_state, h_cust_main.state)';
+
+my @select = (
+ "$group_field AS state",
+ 'count(DISTINCT h_svc_phone.svcnum) AS num_svcnums',
+ # don't DISTINCT these (it reorders them)
+ "array_to_string(array_agg(h_svc_phone.phonenum), ',') AS all_phonenums",
+ "array_to_string(array_agg(h_svc_phone.svcnum), ',') AS all_svcnums",
+ "array_to_string(array_agg(svc_phone.svcnum), ',') AS active_svcnums",
+);
+
+my $query = {
+ 'select' => join(',', @select),
+ 'table' => 'h_svc_phone',
+ 'addl_from' => $addl_from,
+ 'extra_sql' => " $where GROUP BY $group_field",
+};
+
+# DISTINCT on these because of cross-producting effects when a cust_pkg
+# record (usually) was replaced more than once within one second.
+my $count_query =
+ "SELECT COUNT(DISTINCT $group_field), COUNT(DISTINCT h_svc_phone.svcnum) ".
+ "FROM h_svc_phone $addl_from $where";
+my $count_addl = [ '%d phone services' ];
+
+my $detail_sub = sub {
+ my $rec = shift;
+ warn Dumper $rec;
+ my @svcnums = split(',', $rec->all_svcnums);
+ my @phonenums = split(',', $rec->all_phonenums);
+ # identifies services that still exist with the same svcnum+phonenum
+ my %active = map { $_ => 1 } split(',', $rec->active_svcnums);
+ # make a single column of phonenums
+ my @return;
+ my %seen;
+ while (my $svcnum = shift @svcnums) {
+ my $phonenum = shift @phonenums;
+ next if $seen{$svcnum};
+ $seen{$svcnum} = 1;
+ my $link = $active{$svcnum} ?
+ $p.'view/svc_phone.cgi?'.$svcnum :
+ '';
+ push @return, [ { data => $phonenum,
+ link => $link,
+ data_style => ($active{$svcnum} ? '' : 'i')
+ } ];
+ }
+ \@return;
+};
+
+</%init>
diff --git a/httemplate/view/cust_bill.cgi b/httemplate/view/cust_bill.cgi
index 95ce60b1d..4822ab718 100755
--- a/httemplate/view/cust_bill.cgi
+++ b/httemplate/view/cust_bill.cgi
@@ -104,12 +104,35 @@
% my $br = 0;
% if ( $cust_bill->num_cust_event ) { $br++;
<A HREF="<%$p%>search/cust_event.html?invnum=<% $cust_bill->invnum %>">( <% mt('View invoice events') |h %> )</A>
-% }
+% }
% if ( $cust_bill->num_cust_bill_event ) { $br++;
<A HREF="<%$p%>search/cust_bill_event.cgi?invnum=<% $cust_bill->invnum %>">( <% mt('View deprecated, old-style invoice events') |h %> )</A>
% }
+% my @modes = grep {! $_->disabled}
+% $cust_bill->cust_main->agent->invoice_modes;
+% if ( @modes ) {
+( <% mt('View as:') %>
+<FORM STYLE="display:inline" ACTION="<% $cgi->url %>" METHOD="GET">
+<INPUT NAME="invnum" VALUE="<% $invnum %>" TYPE="hidden">
+<& /elements/select-table.html,
+ table => 'invoice_mode',
+ field => 'mode',
+ curr_value => scalar($cgi->param('mode')),
+ records => \@modes,
+ name_col => 'modename',
+ onchange => 'change_invoice_mode',
+ empty_label => '(default)',
+&> )
+<SCRIPT TYPE="text/javascript">
+function change_invoice_mode(obj) {
+ obj.form.submit();
+}
+</SCRIPT>
+% $br++;
+% }
+
<% $br ? '<BR><BR>' : '' %>
% if ( $conf->exists('invoice_html') ) {
@@ -126,7 +149,9 @@ my $curuser = $FS::CurrentUser::CurrentUser;
die "access denied"
unless $curuser->access_right('View invoices');
-my( $invnum, $template, $notice_name );
+my $conf = FS::Conf->new;
+
+my( $invnum, $mode, $template, $notice_name );
my($query) = $cgi->keywords;
if ( $query =~ /^((.+)-)?(\d+)$/ ) {
$template = $2;
@@ -136,10 +161,9 @@ if ( $query =~ /^((.+)-)?(\d+)$/ ) {
$invnum = $cgi->param('invnum');
$template = $cgi->param('template');
$notice_name = $cgi->param('notice_name');
+ $mode = $cgi->param('mode');
}
-my $conf = new FS::Conf;
-
my %opt = (
'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
'template' => $template,
@@ -163,10 +187,13 @@ my $cust_bill = qsearchs({
});
die "Invoice #$invnum not found!" unless $cust_bill;
+$cust_bill->set('mode' => $mode);
+
my $custnum = $cust_bill->custnum;
my $display_custnum = $cust_bill->cust_main->display_custnum;
my $link = "invnum=$invnum";
+$link .= ';mode=' . $mode if $mode;
$link .= ';template='. uri_escape($template) if $template;
$link .= ';notice_name='. $notice_name if $notice_name;
diff --git a/httemplate/view/cust_main.cgi b/httemplate/view/cust_main.cgi
index 430c50c5f..391988190 100755
--- a/httemplate/view/cust_main.cgi
+++ b/httemplate/view/cust_main.cgi
@@ -147,7 +147,6 @@ function areyousure(href, message) {
% if ( $br ) {
<BR><BR>
% }
-</%doc>
%my $signupurl = $conf->config('signupurl');
%if ( $signupurl ) {
diff --git a/httemplate/view/cust_statement.html b/httemplate/view/cust_statement.html
index 3e1345ed5..5d37b3167 100755
--- a/httemplate/view/cust_statement.html
+++ b/httemplate/view/cust_statement.html
@@ -35,10 +35,10 @@
% if ( $conf->exists('invoice_html') ) {
- <% join('', $cust_statement->print_html('', $templatename) ) %>
+ <% join('', $cust_statement->print_html('template' => $templatename) ) %>
% } else {
- <PRE><% join('', $cust_statement->print_text('', $templatename) ) %></PRE>
+ <PRE><% join('', $cust_statement->print_text('template' => $templatename) ) %></PRE>
% }
<% include('/elements/footer.html') %>
diff --git a/httemplate/view/elements/cust_bill-typeset b/httemplate/view/elements/cust_bill-typeset
index 00f503fbb..778e538d1 100644
--- a/httemplate/view/elements/cust_bill-typeset
+++ b/httemplate/view/elements/cust_bill-typeset
@@ -6,7 +6,7 @@ die "access denied"
my $type = shift;
-my( $invnum, $template, $notice_name );
+my( $invnum, $mode, $template, $notice_name );
my($query) = $cgi->keywords;
if ( $query =~ /^((.+)-)?(\d+)(.pdf)?$/ ) { #probably not necessary anymore?
$template = $2;
@@ -16,7 +16,8 @@ if ( $query =~ /^((.+)-)?(\d+)(.pdf)?$/ ) { #probably not necessary anymore?
$invnum = $cgi->param('invnum');
$invnum =~ s/\.pdf//i; #probably not necessary anymore
$template = $cgi->param('template');
- $notice_name = ( $cgi->param('notice_name') || 'Invoice' );
+ $notice_name = $cgi->param('notice_name');
+ $mode = $cgi->param('mode');
}
my $conf = new FS::Conf;
@@ -36,6 +37,8 @@ my $cust_bill = qsearchs({
});
die "Invoice #$invnum not found!" unless $cust_bill;
+$cust_bill->set(mode => $mode);
+
my $method = "print_$type";
my $content = $cust_bill->$method(\%opt);