summaryrefslogtreecommitdiff
path: root/httemplate
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2018-02-09 19:10:00 -0800
committerIvan Kohler <ivan@freeside.biz>2018-02-09 19:10:00 -0800
commitd45dd4a826f314fb5459747590d3e11cd80c211f (patch)
treec1dd2edd4bc42b12cc9a995e95dd7fb630da925e /httemplate
parent4b67c9f8cfc9f944b7758e7e69ac1f9f188ffa47 (diff)
parent15d596e3090f3bde642917b56563736cd1ee2e90 (diff)
Merge branch 'master' of git.freeside.biz:/home/git/freeside
Diffstat (limited to 'httemplate')
-rw-r--r--httemplate/browse/realestate_location.html43
-rw-r--r--httemplate/browse/realestate_unit.html70
-rw-r--r--httemplate/docs/part_svc-table.html7
-rw-r--r--httemplate/edit/process/cust_main-contacts.html8
-rw-r--r--httemplate/edit/process/realestate_location.html14
-rw-r--r--httemplate/edit/process/realestate_unit.html13
-rw-r--r--httemplate/edit/realestate_location.html72
-rw-r--r--httemplate/edit/realestate_unit.html48
-rw-r--r--httemplate/elements/contact.html6
-rw-r--r--httemplate/elements/menu.html8
-rw-r--r--httemplate/elements/select-multiple-contact_class.html21
-rw-r--r--httemplate/elements/select-realestate_location.html32
-rw-r--r--httemplate/elements/select-realestate_unit.html59
-rw-r--r--httemplate/elements/tr-checkbox-multiple.html20
-rw-r--r--httemplate/elements/tr-select-multiple-contact_class.html32
-rw-r--r--httemplate/elements/tr-select-realestate_location.html17
-rw-r--r--httemplate/elements/tr-select-realestate_unit.html5
-rw-r--r--httemplate/misc/cust_main-import_charges.cgi8
-rw-r--r--httemplate/misc/email-customers.html113
-rw-r--r--httemplate/pref/pref-process.html12
-rw-r--r--httemplate/pref/pref.html14
-rw-r--r--httemplate/search/contact.html332
-rwxr-xr-xhttemplate/search/cust_main.html8
-rw-r--r--httemplate/search/elements/grid-report.html8
-rw-r--r--httemplate/search/elements/search.html6
-rw-r--r--httemplate/search/future_autobill.html189
-rw-r--r--httemplate/search/report_contact.html23
-rw-r--r--httemplate/search/report_future_autobill.html42
-rw-r--r--httemplate/view/cust_main/contacts_new.html2
29 files changed, 1095 insertions, 137 deletions
diff --git a/httemplate/browse/realestate_location.html b/httemplate/browse/realestate_location.html
new file mode 100644
index 000000000..be2cd11f8
--- /dev/null
+++ b/httemplate/browse/realestate_location.html
@@ -0,0 +1,43 @@
+<% include( 'elements/browse.html',
+ title => emt('Real Estate Locations'),
+ name => 'real estate locations',
+
+ menubar => [
+ 'Edit units' => "${p}browse/realestate_unit.html",
+ 'Add a new location' => "${p}edit/realestate_location.html",
+ 'Add a new unit' => "${p}edit/realestate_unit.html",
+ ],
+
+ query => { table => 'realestate_location' },
+ count_query => 'SELECT COUNT(*) FROM realestate_location',
+
+ header => [ 'Location', 'Address', 'Address 2', 'City', 'State', 'Zip' ],
+ fields => [
+ 'location_title',
+ 'address1',
+ 'address2',
+ 'city',
+ 'state',
+ 'zip'
+ ],
+ links => [
+ ["${p}edit/realestate_location.html?", 'realestatelocnum' ],
+ ],
+
+ agent_virt => 1,
+ agent_pos => 0,
+ disableable => 1,
+)
+%>
+<%init>
+
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+die("access denied")
+ unless $curuser->access_right('Edit inventory')
+ || $curuser->access_right('Edit global inventory')
+ || $curuser->access_right('Configuration');
+
+
+
+</%init>
diff --git a/httemplate/browse/realestate_unit.html b/httemplate/browse/realestate_unit.html
new file mode 100644
index 000000000..399cd2583
--- /dev/null
+++ b/httemplate/browse/realestate_unit.html
@@ -0,0 +1,70 @@
+<% include( 'elements/browse.html',
+ title => emt('Real Estate Inventory'),
+ name => 'real estate inventory',
+
+ menubar => [
+ 'Edit locations' => "${p}browse/realestate_location.html",
+ 'Add a new location' => "${p}edit/realestate_location.html",
+ 'Add a new unit' => "${p}edit/realestate_unit.html",
+ ],
+
+ query => {
+ table => 'realestate_unit',
+ select => join(', ',qw(
+ realestate_unit.*
+ realestate_location.location_title
+ cust_main.first
+ cust_main.last
+ cust_main.company
+ )),
+ addl_from => "
+ LEFT JOIN realestate_location
+ ON realestate_location.realestatelocnum
+ = realestate_unit.realestatelocnum
+ LEFT JOIN svc_realestate
+ ON realestate_unit.realestatenum = svc_realestate.realestatenum
+ LEFT JOIN cust_svc
+ ON svc_realestate.svcnum = cust_svc.svcnum
+ LEFT JOIN cust_pkg
+ ON cust_svc.pkgnum = cust_pkg.pkgnum
+ LEFT JOIN cust_main
+ ON cust_pkg.custnum = cust_main.custnum
+ ",
+ order_by => "ORDER BY location_title, unit_title"
+ },
+
+ count_query => 'SELECT COUNT(*) FROM realestate_unit',
+
+ header => [ 'Location', 'Unit', 'Customer' ],
+ fields => [
+ 'location_title',
+ 'unit_title',
+ sub {
+ return '' unless $_[0]->custnum;
+ return $_[0]->company if $_[0]->company;
+ return $_[0]->first.' '.$_[0]->last;
+ },
+ ],
+ links => [
+ ["${p}edit/realestate_location.html?", 'realestatelocnum' ],
+ ["${p}edit/realestate_unit.html?", 'realestatenum' ],
+ ["${p}view/cust_main.cgi?", 'custnum' ]
+ ],
+
+ agent_virt => 1,
+ agent_pos => 0,
+ disableable => 1,
+)
+%>
+<%init>
+
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+die("access denied")
+ unless $curuser->access_right('Edit inventory')
+ || $curuser->access_right('Edit global inventory')
+ || $curuser->access_right('Configuration');
+
+
+
+</%init>
diff --git a/httemplate/docs/part_svc-table.html b/httemplate/docs/part_svc-table.html
index 820d0b9cc..56a4d0e8c 100644
--- a/httemplate/docs/part_svc-table.html
+++ b/httemplate/docs/part_svc-table.html
@@ -39,6 +39,7 @@
<TR>
<TH ALIGN="left">Hosting</TH>
<TH ALIGN="left">Colocation</TH>
+ <TH ALIGN="left">Real Estate</TH>
</TR>
<TD VALIGN="top">
<UL STYLE="margin:0">
@@ -54,6 +55,11 @@
<LI><B>svc_port</B>: Customer router/switch port
</UL>
</TD>
+ <TD VALIGN="top">
+ <UL STYLE="margin:0">
+ <LI><B>svc_realestate</B>: Real estate properties
+ </UL>
+ </TD>
</TR>
<TABLE>
<!-- <LI>svc_charge - One-time charges (Partially unimplemented)
@@ -62,4 +68,3 @@
</BODY>
</HTML>
-
diff --git a/httemplate/edit/process/cust_main-contacts.html b/httemplate/edit/process/cust_main-contacts.html
index 2a7185b5c..5b8319f5a 100644
--- a/httemplate/edit/process/cust_main-contacts.html
+++ b/httemplate/edit/process/cust_main-contacts.html
@@ -1,3 +1,11 @@
+<%doc>
+
+ This form works indirectly with the tables contact_email and
+ contact_phone. The columns are updated against an FS::contact
+ object. The insert/update methods of FS::contact will make the
+ necessary inserts/updates to contact_email and contact_phone.
+
+</%doc>
<% include('elements/process.html',
'table' => 'cust_main',
'error_redirect' => popurl(3). 'edit/cust_main-contacts.html?',
diff --git a/httemplate/edit/process/realestate_location.html b/httemplate/edit/process/realestate_location.html
new file mode 100644
index 000000000..ab5cf230f
--- /dev/null
+++ b/httemplate/edit/process/realestate_location.html
@@ -0,0 +1,14 @@
+<% include( 'elements/process.html',
+ table => 'realestate_location',
+ viewall_dir => 'browse',
+ )
+%>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied"
+ unless $curuser->access_right('Edit inventory')
+ || $curuser->access_right('Edit global inventory')
+ || $curuser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/edit/process/realestate_unit.html b/httemplate/edit/process/realestate_unit.html
new file mode 100644
index 000000000..ba9b5dc92
--- /dev/null
+++ b/httemplate/edit/process/realestate_unit.html
@@ -0,0 +1,13 @@
+<& elements/process.html,
+ 'table' => 'realestate_unit',
+ 'viewall_dir' => 'browse',
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+die("access denied")
+ unless $curuser->access_right('Edit inventory')
+ || $curuser->access_right('Edit global inventory')
+ || $curuser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/edit/realestate_location.html b/httemplate/edit/realestate_location.html
new file mode 100644
index 000000000..34344e98f
--- /dev/null
+++ b/httemplate/edit/realestate_location.html
@@ -0,0 +1,72 @@
+<% include( 'elements/edit.html',
+ 'name_singular' => 'Real Estate Location',
+ 'table' => 'realestate_location',
+
+ 'labels' => {
+ realestatelocnum => 'Location',
+ location_title => "Location",
+ address1 => "Address",
+ address2 => "Address",
+ city => "City",
+ state => "State",
+ zip => "Zip-Code",
+ disabled => "Disabled",
+ },
+ 'fields' => [
+ { field => 'realestatelocnum', type => 'hidden' },
+
+ { field => 'location_title',
+ type=>'text',
+ size => 40,
+ maxlength => 80,
+ },
+ { field => 'address1',
+ type=>'text',
+ size => 40,
+ maxlength => 80,
+ },
+ { field => 'address2',
+ type=>'text',
+ size => 40,
+ maxlength => 80,
+ },
+ { field => 'city',
+ type=>'text',
+ size => 40,
+ maxlength => 80,
+ },
+ { field => 'state',
+ type=>'text',
+ size => 40,
+ maxlength => 80,
+ },
+ { field => 'zip',
+ type=>'text',
+ size => 5,
+ maxlength => 5,
+ },
+
+ { field => 'agentnum',
+ type => 'select-agent',
+ value => 1,
+ },
+ { field => 'disabled',
+ type=>'checkbox',
+ value=>'Y'
+ },
+ ],
+
+ 'viewall_dir' => 'browse',
+ 'agent_virt' => 1,
+)
+%>
+
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+die("access denied")
+ unless $curuser->access_right('Edit inventory')
+ || $curuser->access_right('Edit global inventory')
+ || $curuser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/edit/realestate_unit.html b/httemplate/edit/realestate_unit.html
new file mode 100644
index 000000000..a7ca72dd9
--- /dev/null
+++ b/httemplate/edit/realestate_unit.html
@@ -0,0 +1,48 @@
+<% include( 'elements/edit.html',
+ 'name_singular' => 'Real Estate Unit',
+ 'table' => 'realestate_unit',
+
+ 'labels' => {
+ realestatenum => 'Ref No',
+ unit_title => 'Unit Title',
+ agentnum => 'Agent',
+ realestatelocnum => 'Location',
+ },
+ 'fields' => [
+ { field => 'realestatenum', type => 'hidden' },
+
+ { field => 'unit_title',
+ type=>'text',
+ size => 40,
+ },
+ { field => 'realestatelocnum',
+ type => 'select-realestate_location',
+
+ # possible todo:
+ # I'd like to have this field disabled for editing on existing records,
+ # and only show the full selectbox for new records.
+
+ },
+ { field => 'agentnum',
+ type => 'select-agent',
+ },
+ { field => 'disabled',
+ type=>'checkbox',
+ value=>'Y'
+ },
+ ],
+
+ 'viewall_dir' => 'browse',
+ 'agent_virt' => 1,
+)
+%>
+
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+die("access denied")
+ unless $curuser->access_right('Edit inventory')
+ || $curuser->access_right('Edit global inventory')
+ || $curuser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/elements/contact.html b/httemplate/elements/contact.html
index faee7ead4..43e520155 100644
--- a/httemplate/elements/contact.html
+++ b/httemplate/elements/contact.html
@@ -42,7 +42,8 @@
% $value = join(', ', map $_->emailaddress, $contact->contact_email);
% } elsif ( $field eq 'selfservice_access'
% or $field eq 'comment'
-% or $field eq 'invoice_dest' ) {
+% or $field eq 'invoice_dest'
+% or $field eq 'message_dest' ) {
% $value = $X_contact->get($field);
% } else {
% $value = $contact->get($field);
@@ -78,7 +79,7 @@
return false
}
</SCRIPT>
-% } elsif ( $field eq 'invoice_dest' ) {
+% } elsif ( $field eq 'invoice_dest' || $field eq 'message_dest' ) {
% my $curr_value = $cgi->param($name . '_' . $field);
% $curr_value = $value if !defined($curr_value);
<& select.html,
@@ -168,6 +169,7 @@ tie my %label, 'Tie::IxHash',
unless ($opt{'for_prospect'}) {
$label{'invoice_dest'} = 'Send&nbsp;invoices';
+ $label{'message_dest'} = 'Send&nbsp;messages';
$label{'selfservice_access'} = 'Self-service';
}
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index 3b3d244db..eb065b668 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -418,6 +418,8 @@ if( $curuser->access_right('Financial reports') ) {
$report_financial{'Customer Accounting Summary'} = [ $fsurl.'search/report_customer_accounting_summary.html', 'Customer accounting summary report' ];
+ $report_financial{'Upcoming Auto-Bill Transactions'} = [ $fsurl.'search/report_future_autobill.html', 'Upcoming auto-bill transactions' ];
+
} elsif($curuser->access_right('Receivables report')) {
$report_financial{'A/R Aging'} = [ $fsurl.'search/report_receivables.html', 'Accounts Receivable Aging report' ];
@@ -835,6 +837,11 @@ $config_misc{'Inventory classes and inventory'} = [ $fsurl.'browse/inventory_cla
|| $curuser->access_right('Edit global inventory')
|| $curuser->access_right('Configuration');
+$config_misc{'Real estate inventory'} = [ $fsurl.'browse/realestate_unit.html', 'Setup real estate inventory' ]
+ if $curuser->access_right('Edit realestate inventory')
+ || $curuser->access_right('Edit global inventory')
+ || $curuser->access_right('Configuration');
+
$config_misc{'Upload targets'} = [ $fsurl.'browse/upload_target.html', 'Billing and payment upload destinations' ]
if $curuser->access_right('Configuration');
@@ -1039,4 +1046,3 @@ sub submenu {
}
</%init>
-
diff --git a/httemplate/elements/select-multiple-contact_class.html b/httemplate/elements/select-multiple-contact_class.html
new file mode 100644
index 000000000..81a71cc25
--- /dev/null
+++ b/httemplate/elements/select-multiple-contact_class.html
@@ -0,0 +1,21 @@
+<%doc>
+
+Display a multi-select box containing all Email Types listed in
+the contact_class table.
+
+NOTE:
+ Don't confuse "Contact Type" (contact_email.classnum) with
+ "Customer Class" (cust_main.classnum)
+
+</%doc>
+<% include( '/elements/select-table.html',
+ table => 'contact_class',
+ hashref => { disabled => '' },
+ name_col => 'classname',
+ field => 'classnum',
+ pre_options => [ 0 => '(No Type)' ],
+ multiple => 1,
+ all_selected => 1,
+ @_,
+ )
+%>
diff --git a/httemplate/elements/select-realestate_location.html b/httemplate/elements/select-realestate_location.html
new file mode 100644
index 000000000..001ed3e89
--- /dev/null
+++ b/httemplate/elements/select-realestate_location.html
@@ -0,0 +1,32 @@
+<%doc>
+
+ Displays a selectbox populated with values from realestate_location.
+ key: realestate_location.realestatenum
+ value: realestate_location.location_title
+
+</%doc>
+
+<% include( '/elements/select-table.html',
+ %opt,
+ table => 'realestate_location',
+ name_col => 'location_title',
+ hashref => { 'disabled' => '' },
+ value => $select_value,
+ disable_empty => 1,
+ )
+%>
+
+<%init>
+
+#
+# possible todo:
+# I'd like to change the behavior of this select based on if
+# a new item is being created, or an existing item being edited
+
+my %opt = @_;
+my $select_value = $opt{'curr_value'} || $opt{'value'};
+
+# use Data::Dumper qw(Dumper);
+# print Dumper(\%opt);
+
+</%init>
diff --git a/httemplate/elements/select-realestate_unit.html b/httemplate/elements/select-realestate_unit.html
new file mode 100644
index 000000000..e189d5d99
--- /dev/null
+++ b/httemplate/elements/select-realestate_unit.html
@@ -0,0 +1,59 @@
+<%doc>
+
+Display a pair of select boxes for provisioning a realestate_unit
+- Real Estate Location
+- Real Estate Unit
+
+NOTE:
+ Records are always suppresed if
+ - realestate_location.disabled is set
+ - realestate_unit is provisioned to a customer [not working]
+
+ If it becomes necessary, an option may be added to the template
+ to show disabled/provisioned records, but is not yet implemented
+
+</%doc>
+<& select-tiered.html,
+ 'tiers' => [
+ {
+
+ field => 'realestate_location',
+ table => 'realestate_location',
+ extra_sql => "WHERE realestate_location.disabled IS NULL "
+ . " OR realestate_location.disabled = '' ",
+ name_col => 'location_title',
+ empty_label => '(all)',
+ },
+ {
+ field => 'realestatenum',
+ table => 'realestate_unit',
+ name_col => 'unit_title',
+ value_col => 'realestatenum',
+ link_col => 'realestatelocnum',
+
+ # TODO: Filter units assigned to customers
+ # SQL below breaks the selectbox... why?
+
+ # Also, can we assume if realestatenum doesn't appear in svc_realestate
+ # that the realestate_unit is unprovisioned to a customer? What indicator
+ # should be used to determine when a realestae_unit is not provisioned?
+
+ # addl_from => "
+ # LEFT JOIN svc_realestate
+ # ON svc_realestate.realestatenum = realestate_unit.realestatenum
+ # ",
+
+ #extra_sql => "WHERE svc_realestate.svcnum IS NULL ",
+
+ disable_empty => 1,
+ debug => 1,
+ },
+ ],
+ %opt,
+ 'prefix' => $opt{'prefix'}. $opt{'field'}. '_', #after %opt so it overrides
+&>
+<%init>
+
+my %opt = @_;
+
+</%init>
diff --git a/httemplate/elements/tr-checkbox-multiple.html b/httemplate/elements/tr-checkbox-multiple.html
index 4d754b007..baf18f916 100644
--- a/httemplate/elements/tr-checkbox-multiple.html
+++ b/httemplate/elements/tr-checkbox-multiple.html
@@ -1,3 +1,23 @@
+<%doc>
+
+Display a <tr> containing multiple checkboxes
+
+USAGE:
+
+<& /elements/tr-checkbox-multipe.html,
+ label => emt('Label'),
+ field => 'field_name',
+ options => ['opt1', 'opt2'],
+ labels => {
+ opt1 => 'Option 1',
+ opt2 => 'Option 2',
+ },
+ value => {
+ opt2 => '1', # opt2 defaults as checked
+ }
+&>
+
+</%doc>
<% include('tr-td-label.html', @_ ) %>
<TD <% $style %>>
diff --git a/httemplate/elements/tr-select-multiple-contact_class.html b/httemplate/elements/tr-select-multiple-contact_class.html
new file mode 100644
index 000000000..5de129324
--- /dev/null
+++ b/httemplate/elements/tr-select-multiple-contact_class.html
@@ -0,0 +1,32 @@
+<%doc>
+
+ Displays Contact Types as a multi-select box.
+
+ If no non-disabled Contact Types have been defined in contact_class table,
+ renders a hidden input field with a blank value.
+
+</%doc>
+
+% if ($has_types) {
+<TR>
+ <TD ALIGN="right"><% $opt{'label'} || emt('Contact Type') %></TD>
+ <TD>
+ <% include( '/elements/select-multiple-contact_class.html', %opt ) %>
+ </TD>
+</TR>
+% } else {
+<INPUT TYPE="hidden" NAME="<% $opt{field} %>" VALUE="">
+% }
+
+<%init>
+
+my %opt = @_;
+$opt{field} ||= $opt{element_name} ||= 'classnum';
+
+my $has_types =()= qsearch({
+ table => 'contact_class',
+ hashref => { disabled => '' },
+ extra_sql => ' LIMIT 1 ',
+});
+
+</%init>
diff --git a/httemplate/elements/tr-select-realestate_location.html b/httemplate/elements/tr-select-realestate_location.html
new file mode 100644
index 000000000..1367886ed
--- /dev/null
+++ b/httemplate/elements/tr-select-realestate_location.html
@@ -0,0 +1,17 @@
+<TR>
+ <TH ALIGN="right"><% $opt{'label'} || 'Real Estate Location' %></TD>
+ <TD>
+ <% include( '/elements/select-realestate_location.html',
+ 'curr_value' => $curr_value,
+ %opt
+ )
+ %>
+ </TD>
+</TR>
+
+<%init>
+
+my %opt = @_;
+my $curr_value = $opt{'curr_value'} || $opt{'value'};
+
+</%init>
diff --git a/httemplate/elements/tr-select-realestate_unit.html b/httemplate/elements/tr-select-realestate_unit.html
new file mode 100644
index 000000000..b1a4296d4
--- /dev/null
+++ b/httemplate/elements/tr-select-realestate_unit.html
@@ -0,0 +1,5 @@
+<& tr-td-label.html, @_ &>
+<td>
+<& select-realestate_unit.html, @_ &>
+</td>
+</tr>
diff --git a/httemplate/misc/cust_main-import_charges.cgi b/httemplate/misc/cust_main-import_charges.cgi
index 4eacce13a..215cc4c9d 100644
--- a/httemplate/misc/cust_main-import_charges.cgi
+++ b/httemplate/misc/cust_main-import_charges.cgi
@@ -28,9 +28,9 @@ Import a CSV file containing customer charges.
<TH ALIGN="right">Format</TH>
<TD>
<SELECT NAME="format">
- <OPTION VALUE="simple">Simple
- <OPTION VALUE="ooma">Ooma
-<!-- <OPTION VALUE="extended" SELECTED>Extended -->
+% foreach my $format ( keys %formats ) {
+ <OPTION VALUE="<% $format %>"><% $formats{$format} %></OPTION>
+% }
</SELECT>
</TD>
</TR>
@@ -94,6 +94,8 @@ Field information:
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Import');
+ tie my %formats, 'Tie::IxHash', FS::cust_main::Import_Charges->import_formats;
+
my $custbatch = time2str('webimport-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time);
</%init>
diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html
index fe637abe1..f52c6b36a 100644
--- a/httemplate/misc/email-customers.html
+++ b/httemplate/misc/email-customers.html
@@ -171,16 +171,18 @@ Template:
<TD>Send to contacts:</TD>
<TD>
<div id="contactclassesdiv">
- <& /elements/checkboxes.html,
- 'style' => 'display: inline; vertical-align: top',
- 'disable_links' => 1,
- 'names_list' => \@contact_checkboxes,
- 'element_name_prefix' => 'contact_class_',
- 'checked_callback' => sub {
- my($cgi, $name) = @_;
- $name eq 'invoice' #others default to unchecked
- },
- &>
+ <& /elements/checkboxes.html,
+ 'style' => 'display: inline; vertical-align: top',
+ 'disable_links' => 1,
+ 'names_list' => \@optin_checkboxes,
+ 'element_name_prefix' => 'contact_class_',
+ 'checked_callback' => sub {
+ # Called for each checkbox
+ # Return true to default as checked, false as unchecked
+ my($cgi, $name) = @_;
+ exists $dest_ischecked{$name};
+ },
+ &>
</div>
% if ($send_to_domain) {
<div>
@@ -197,6 +199,27 @@ Template:
</div>
% }
</TD>
+% if (@active_classes) {
+</tr>
+<tr>
+<TD>Contact Type:</TD>
+<TD>
+ <div id="contactclassesdiv">
+ <& /elements/checkboxes.html,
+ 'style' => 'display: inline; vertical-align: top',
+ 'disable_links' => 1,
+ 'names_list' => \@classnum_checkboxes,
+ 'element_name_prefix' => 'contact_class_',
+ 'checked_callback' => sub {
+ # Called for each checkbox
+ # Return true to default as checked, false as unchecked
+ my($cgi, $name) = @_;
+ exists $classnum_ischecked{$name};
+ },
+ &>
+ </div>
+</TD>
+% }
</TR>
</TABLE>
<BR>
@@ -340,6 +363,21 @@ if ( !$cgi->param('preview') ) {
} else {
+ my @checked_email_dest;
+ my @checked_contact_type;
+ for ($cgi->param) {
+ if (/^contact_class_(.+)$/) {
+ my $f = $1;
+ if ($f eq 'invoice' || $f eq 'message') {
+ push @checked_email_dest, $f;
+ } elsif ( $f =~ /^\d+$/ ) {
+ push @checked_contact_type, $f;
+ }
+ }
+ }
+ $search{with_email_dest} = \@checked_email_dest if @checked_email_dest;
+ $search{with_contact_type} = \@checked_contact_type if @checked_contact_type;
+
my $sql_query = "FS::$table"->search(\%search);
my $count_query = delete($sql_query->{'count_query'});
my $count_sth = dbh->prepare($count_query)
@@ -389,6 +427,8 @@ if ( !$cgi->param('preview') ) {
$sql_query->{'select'} = "$table.*";
$sql_query->{'order_by'} = '';
my $object = qsearchs($sql_query);
+ # Could use better error handling here...
+ die "No customers match the search criteria" unless ref $object;
$cust = $object->cust_main;
my %msgopts = (
'cust_main' => $cust,
@@ -422,24 +462,59 @@ if ( !$cgi->param('preview') ) {
push @contact_classnum, $1;
if ( $1 eq 'invoice' ) {
push @contact_classname, 'Invoice recipients';
+ } elsif ( $1 eq 'message' ) {
+ push @contact_classname, 'Message recipients';
} else {
my $contact_class = FS::contact_class->by_key($1);
- push @contact_classname, encode_entities($contact_class->classname);
+ push @contact_classname, encode_entities(
+ $contact_class ? $contact_class->classname : '(none)'
+ );
}
}
}
}
}
-my @contact_checkboxes = (
- [ 'invoice' => { label => 'Invoice recipients' } ]
-);
+# Build data structures for "Opt In" and "Contact Type" checkboxes
+#
+# By default, message recipients will be selected, this is a message.
+# By default, all Contact Types will be selected, but this may be
+# overridden by passing 'classnums' get/post values. If no contact
+# types have been defined, the option will not be presented.
+
+my @active_classes = qsearch(contact_class => {disabled => ''} );
+
+my %classnum_ischecked;
+my %dest_ischecked;
-foreach my $class (qsearch('contact_class', { disabled => '' })) {
- push @contact_checkboxes, [
- $class->classnum,
- { label => $class->classname }
- ];
+$CGI::LIST_CONTEXT_WARN = 0;
+if ( my @in_classnums = $cgi->param('classnums') ) {
+ # Set checked boxes from form input
+ for my $v (@in_classnums) {
+
+ if ( $v =~ /^\d+$/ ) {
+ $classnum_ischecked{$v} = 1
+ } elsif ( $v =~ /^(invoice|message)$/ ) {
+ $dest_ischecked{$v} = 1;
+ }
+
+ }
+} else {
+ # Checked boxes default values
+ $classnum_ischecked{$_->classnum} = 1 for @active_classes;
+ $classnum_ischecked{0} = 1;
}
+# At least one destination is required
+$dest_ischecked{message} = 1 unless %dest_ischecked;
+
+my @optin_checkboxes = (
+ [ 'message' => { label => 'Message recipients' } ],
+ [ 'invoice' => { label => 'Invoice recipients' } ],
+);
+my @classnum_checkboxes = (
+ [ '0' => { label => '(None)' }],
+ map { [ $_->classnum => {label => $_->classname} ] } @active_classes,
+);
+
</%init>
diff --git a/httemplate/pref/pref-process.html b/httemplate/pref/pref-process.html
index 75e57958f..1b18d2ec8 100644
--- a/httemplate/pref/pref-process.html
+++ b/httemplate/pref/pref-process.html
@@ -1,9 +1,15 @@
% if ( $error ) {
% $cgi->param('error', $error);
-<% $cgi->redirect(popurl(1). "pref.html?". $cgi->query_string ) %>
+ <% $cgi->redirect(popurl(1). "pref.html?". $cgi->query_string ) %>
% } else {
-<% include('/elements/header.html', mt('Preferences updated')) %>
-<% include('/elements/footer.html') %>
+ <% $cgi->redirect( -uri => popurl(1). "pref.html",
+ -cookie => CGI::Cookie->new(
+ -name => 'freeside_status',
+ -value => mt('Preferences updated'),
+ -expires => '+5m',
+ ),
+ )
+ %>
% }
<%init>
diff --git a/httemplate/pref/pref.html b/httemplate/pref/pref.html
index abd1ea57f..56fde6d44 100644
--- a/httemplate/pref/pref.html
+++ b/httemplate/pref/pref.html
@@ -1,5 +1,7 @@
<& /elements/header.html, mt('Preferences for [_1]', $FS::CurrentUser::CurrentUser->username) &>
+% my $js_form_validate = { 'pref_form' => { 'name' => 'pref_form' } };
+
<FORM METHOD="POST" NAME="pref_form" ACTION="pref-process.html">
<& /elements/error.html &>
@@ -143,10 +145,14 @@
</TD>
</TR>
+% my $validate_field_cve = 'customer_view_emails';
+% $js_form_validate->{pref_form}->{validate_fields}{$validate_field_cve} = 'digits: true';
+% $js_form_validate->{pref_form}->{error_message}{$validate_field_cve} = 'Please only enter numbers here.';
+
<TR>
<TH ALIGN="right"><% emt("How many recent outbound emails to show in customer view") %></TH>
<TD ALIGN="left" COLSPAN=2>
- <INPUT TYPE="text" NAME="customer_view_emails" VALUE="<% $curuser->option('customer_view_emails') %>"></TD>
+ <INPUT TYPE="text" ID="<% $validate_field_cve %>" NAME="<% $validate_field_cve %>" VALUE="<% $curuser->option('customer_view_emails') %>"></TD>
</TD>
</TR>
@@ -260,7 +266,11 @@
<INPUT TYPE="submit" VALUE="<% emt("Update preferences") %>">
-<&/elements/footer.html &>
+% my %footerdata = (
+% 'formvalidation' => $js_form_validate,
+% );
+<% include("/elements/footer.html", %footerdata) %>
+
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html
index 5f02fef2f..aaa591cf4 100644
--- a/httemplate/search/contact.html
+++ b/httemplate/search/contact.html
@@ -1,126 +1,258 @@
<& elements/search.html,
- title => 'Contacts',
+ title => emt('Contacts'),
name_singular => 'contact',
- query => { select => join(', ', @select),
- table => 'contact',
- addl_from => $addl_from,
- hashref => \%hash,
- extra_sql => $extra_sql,
- },
- count_query => "SELECT COUNT(*) FROM contact $addl_from $extra_sql", #XXX
- header => \@header,
- fields => \@fields,
- links => \@links,
+ query => {
+ select => join(', ', @select),
+ table => $link,
+ addl_from => $addl_from,
+ hashref => {},
+ extra_sql => "WHERE $extra_sql",
+ order_by => "ORDER BY contact_last,contact_first,contact_email_emailaddress"
+ },
+ count_query => "
+ SELECT COUNT(*)
+ FROM $link
+ $addl_from
+ WHERE $extra_sql
+ ",
+ header => \@header,
+ fields => \@fields,
+ links => \@links,
+ html_init => $send_email_link,
+ agent_virt => 1,
+ agent_pos => 11,
&>
<%init>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('List contacts');
-my @select = 'contact.contactnum AS contact_contactnum'; #if we select it as bare contactnum, the multi-customer listings go away
-push @select, map "contact.$_", qw( first last title );
-my %hash = ();
-my $addl_from = '';
+my $DEBUG = 0;
-my $email_sub = sub {
- my $contact = shift;
- #can't because contactnum is in the wrong field #my @contact_email = $contact->contact_email;
- my @contact_email = qsearch('contact_email', { 'contactnum' => $contact->contact_contactnum } );
- join(', ', map $_->emailaddress, @contact_email);
-};
+# Catch classnum values from multi-select box
+# A classnum of 0 indicates to include rows where classnum IS NULL
+$CGI::LIST_CONTEXT_WARN = 0;
+my @classnum = grep{ /^\d+$/ && $_ > 0 } $cgi->param('classnum');
+my $classnum_null = grep{ $_ eq 0 } $cgi->param('classnum');
-my $work_phone_sub = sub {
- my $contact = shift;
- my $phone_type = qsearchs('phone_type', { 'typename' => 'Work' });
- #can't because contactnum is in the wrong field
- my @contact_workphone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } );
- join(', ', map $_->phonenum, @contact_workphone);
-};
+# Catch destination values from dest multi-checkbox, default to message
+# irrelevant to prospect contacts
+my @dest = grep{ /^(message|invoice)$/ } $cgi->param('dest');
+@dest = ('message') unless @dest;
-my $mobile_phone_sub = sub {
- my $contact = shift;
- my $phone_type = qsearchs('phone_type', { 'typename' => 'Mobile' });
- #can't because contactnum is in the wrong field
- my @contact_mobilephone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } );
- join(', ', map $_->phonenum, @contact_mobilephone);
-};
+# Cache the contact_class table
+my %classname =
+ map {$_->classnum => $_->classname}
+ qsearch(contact_class => {disabled => ''});
-my $home_phone_sub = sub {
- my $contact = shift;
- my $phone_type = qsearchs('phone_type', { 'typename' => 'Home' });
- #can't because contactnum is in the wrong field
- my @contact_homephone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } );
- join(', ', map $_->phonenum, @contact_homephone);
-};
+# This data structure is used to generate the sql query parameters
+my %colmap = (
+ # These are included regardless of which tables we're viewing
+ common => {
+ cols => {
+ contact => [qw/first last title contactnum/],
+ contact_email => [qw/emailaddress/],
+ },
+ joinsql => "",
+ },
-my $link; #for closure in this sub, we'll define it later
-my $contact_classname_sub = sub {
- my $contact = shift;
- my %hash = ( 'contactnum' => $contact->contact_contactnum );
- my $X_contact;
- if ( $link eq 'cust_main' ) {
- $X_contact = qsearchs('cust_contact', { %hash, 'custnum' => $contact->custnum } );
- } elsif ( $link eq 'prospect_main' ) {
- $X_contact = qsearchs('prospect_contact', { %hash, 'prospectnum' => $contact->prospectnum } );
- } else {
- die 'guru meditation #5555';
- }
- $X_contact->contact_classname;
-};
+ # These are included if we're viewing customer records
+ cust_main => {
+ cols => {
+ cust_main => [qw/first last company/],
+ cust_contact => [qw/
+ custnum classnum invoice_dest message_dest selfservice_access comment
+ /],
+ },
+ joinsql => "
+ LEFT JOIN cust_contact
+ ON (cust_main.custnum = cust_contact.custnum)
+ LEFT JOIN contact
+ on (cust_contact.contactnum = contact.contactnum)
+ LEFT JOIN contact_email
+ ON (cust_contact.contactnum = contact_email.contactnum)
+ ",
+ },
-my @header = ( 'First', 'Last', 'Title', 'Email', 'Work Phone', 'Mobile Phone', 'Home Phone', 'Type' );
-my @fields = ( 'first', 'last', 'title', $email_sub, $work_phone_sub, $mobile_phone_sub, $home_phone_sub, $contact_classname_sub );
-my @links = ( '', '', '', '', '', '', '', '', );
+ # These are included if we're viewing prospect records
+ prospect_main => {
+ cols => {
+ prospect_main => [qw/company/],
+ prospect_contact => [qw/prospectnum classnum comment/],
+ },
+ joinsql => "
+ LEFT JOIN prospect_contact
+ ON (prospect_main.prospectnum = prospect_contact.prospectnum)
+ LEFT JOIN contact
+ on (prospect_contact.contactnum = contact.contactnum)
+ LEFT JOIN contact_email
+ ON (prospect_contact.contactnum = contact_email.contactnum)
+ ",
+ },
+);
-my $company_link = '';
+my @select;
+my $addl_from;
+my $extra_sql;
+my $hashref;
+my $link = $cgi->param('link'); # cust_main or prospect_main
-if ( $cgi->param('selfservice_access') eq 'Y' ) {
- $hash{'selfservice_access'} = 'Y';
-}
+push @select,'agentnum';
+
+# this shouldn't happen without funny-busines
+die "Invalid \$link type ($link)"
+ unless $link eq 'cust_main' || $link eq 'prospect_main';
-my $extra_sql = '';
-$link = $cgi->param('link');
-if ( $link ) {
-
- my $as = ') AS prospect_or_customer';
-
- if ( $link eq 'cust_main' ) {
- push @header, 'Customer';
- push @select,
- "COALESCE( cust_main.company, cust_main.first||' '||cust_main.last $as",
- map "cust_contact.$_", qw( custnum classnum comment selfservice_access );
- $addl_from =
- ' LEFT JOIN cust_contact USING ( contactnum ) '.
- ' LEFT JOIN cust_main ON ( cust_contact.custnum = cust_main.custnum )';
- $extra_sql = ' cust_contact.custnum IS NOT NULL ';
- $company_link = [ $p.'view/cust_main.cgi?', 'custnum' ];
- } elsif ( $link eq 'prospect_main' ) {
- push @header, 'Prospect';
- push @select,
- "COALESCE( prospect_main.company, contact.first||' '||contact.last $as",
- map "prospect_contact.$_", qw( prospectnum classnum comment );
- $addl_from =
- ' LEFT JOIN prospect_contact USING ( contactnum ) '.
- ' LEFT JOIN prospect_main ON ( prospect_contact.prospectnum = prospect_main.prospectnum )';
- $extra_sql = ' prospect_contact.prospectnum IS NOT NULL ';
- $company_link = [ $p.'view/prospect_main.html?', 'prospectnum' ];
- } else {
- die "don't know how to report on contacts linked to specified table";
+# Build @select and $addl_from
+for my $key ('common', $link) {
+ $addl_from .= $colmap{$key}->{joinsql};
+ my $cols = $colmap{$key}->{cols};
+ for my $tbl (keys %{$cols}) {
+ push @select, map{ "$tbl.$_ AS ${tbl}_$_" } @{$cols->{$tbl}};
}
+}
- #because right now its harder to show it for both kinds of contacts
- push @fields, 'prospect_or_customer';
- push @links, $company_link;
+# Filter for Contact Type
+if (@classnum || $classnum_null) {
+ my @stm;
+ my $tbl = $link eq 'cust_main' ? 'cust_contact' : 'prospect_contact';
+ push @stm, "${tbl}.classnum IN (".join(',',@classnum).')' if @classnum;
+ push @stm, "${tbl}.classnum IS NULL" if $classnum_null;
+ $extra_sql .= " (" . join(' OR ',@stm) . ') ';
+}
+# Filter for destination
+if (@dest && $link eq 'cust_main') {
+ my @stm;
+ push @stm, "cust_contact.${_}_dest IS NOT NULL" for @dest;
+ $extra_sql .= "\nAND (".join(' OR ',@stm).') ';
}
-push @header, 'Self-service';
-push @fields, 'selfservice_access';
+if ($DEBUG) {
+ print "<pre>\n";
+ print "select \n";
+ print join ",\n",@select;
+ print "\n";
+ print "from $link \n";
+ print "$addl_from\n";
+ print "WHERE \n $extra_sql\n";
+ print "</pre>\n";
+}
+
+# Prepare to display phone numbers
+# adds 3 additional queries per table record :-(
+my %phonetype = (qw/1 Work 2 Home 3 Mobile 4 Fax/);
+my %phoneid = (qw/Work 1 Home 2 Mobile 3 Fax 4/);
+my $get_phone_sub = sub {
+ my $type = shift;
+ return sub {
+ my $rec = shift;
+ my @p = qsearch('contact_phone', {
+ contactnum => $rec->contact_contactnum,
+ phonetypenum => $phoneid{$type}
+ });
+ @p ? (join ', ',map{$_->phonenum} @p) : undef;
+ };
+};
-push @header, 'Comment';
-push @fields, 'comment';
+# Cache contact types
+my %classname =
+ map {$_->classnum => $_->classname}
+ qsearch(contact_class => {disabled => ''});
-$extra_sql = (keys(%hash) ? ' AND ' : ' WHERE '). $extra_sql
- if $extra_sql;
+# And now for something completly different:
+my @report = (
+ { label => 'First', field => sub { shift->contact_first }},
+ { label => 'Last', field => sub { shift->contact_last }},
+ { label => 'Title', field => sub { shift->contact_title }},
+ { label => 'E-Mail', field => sub { shift->contact_email_emailaddress }},
+ { label => 'Work Phone', field => $get_phone_sub->('Work') },
+ { label => 'Mobile Phone', field => $get_phone_sub->('Mobile') },
+ { label => 'Home Phone', field => $get_phone_sub->('Home') },
+ { label => 'Type',
+ field => sub {
+ my $rec = shift;
+ if ($rec->cust_contact_custnum) {
+ return $rec->cust_contact_classnum
+ ? $classname{$rec->cust_contact_classnum}
+ : undef;
+ } else {
+ return $rec->prospect_contact_classnum
+ ? $classname{$rec->prospect_contact_classnum}
+ : undef;
+ }
+ }},
+ { label => 'Send Invoices',
+ field => sub {
+ my $rec = shift;
+ return 'N/A' if $rec->prospect_contact_prospectnum;
+ $rec->cust_contact_invoice_dest ? 'Y' : 'N';
+ }},
+ { label => 'Send Messages',
+ field => sub {
+ my $rec = shift;
+ return 'N/A' if $rec->prospect_contact_prospectnum;
+ $rec->cust_contact_message_dest ? 'Y' : 'N';
+ }},
+ { label => 'Customer',
+ link => sub {
+ my $rec = shift;
+ $rec->cust_main_custnum
+ ? ["${p}view/cust_main.cgi?", 'cust_main_custnum' ]
+ : ["${p}view/prospect_main.html?", 'prospect_main_prospectnum' ];
+ },
+ field => sub {
+ my $rec = shift;
+ if ($rec->prospect_contact_prospectnum) {
+ return $rec->contact_company
+ || $rec->contact_last.' '.$rec->contact_first;
+ }
+ $rec->cust_main_company || $rec->cust_main_last.' '.$rec->cust_main_first;
+ }},
+ { label => 'Self-service',
+ field => sub {
+ my $rec = shift;
+ return 'N/A' if $rec->prospect_contact_prospectnum;
+ $rec->cust_contact_selfservice_access ? 'Y' : 'N';
+ }},
+ { label => 'Comment',
+ field => sub {
+ my $rec = shift;
+ $rec->prospect_contact_prospectnum
+ ? $rec->prospect_contact_comment
+ : $rec->cust_contact_comment;
+ }},
+);
+
+my (@header, @fields, @links);
+for my $col (@report) {
+ push @header, emt($col->{label});
+ push @fields, $col->{field};
+ push @links, ($col->{link} || "");
+}
+
+my $classnum_url_part;
+if (@classnum) {
+ $classnum_url_part = join '', map{ "&classnums=$_" } @classnum, @dest;
+ $classnum_url_part .= '&classnums=0' if $classnum_null;
+}
+
+# E-mail pipeline, from email-customers.html through to email queue job,
+# doesn't support cust_prospect table
+my $send_email_link = undef;
+if ($link eq 'cust_main') {
+ $send_email_link =
+ "<a href=\"${fsurl}misc/email-customers.html?".
+ 'table=cust_main'.
+ '&agentnum='.$cgi->param('agentnum').
+ '&POST=on'.
+ '&all_pkg_classnums=0'.
+ '&all_tags=0'.
+ '&any_pkg_status=0'.
+ '&refnum=1'.
+ '&with_email=on'.
+ $classnum_url_part.
+ "\">Email a notice to these customers</a>";
+}
</%init>
diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html
index 30162506f..0a43a82dd 100755
--- a/httemplate/search/cust_main.html
+++ b/httemplate/search/cust_main.html
@@ -140,8 +140,14 @@ my $menubar = [];
if ( $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices') ) {
+ # URI::query_from does not support hashref
+ # results in: ...&contacts=HASH(0x55e16cb81da8)&...
+ my %query_hash = %search_hash;
+ delete $query_hash{contacts}
+ if exists $query_hash{contacts} && ref $query_hash{contacts};
+
my $uri = new URI;
- $uri->query_form( \%search_hash );
+ $uri->query_form( \%query_hash );
my $query = $uri->query;
push @$menubar, emt('Email a notice to these customers') =>
diff --git a/httemplate/search/elements/grid-report.html b/httemplate/search/elements/grid-report.html
index 98e81785f..b1e543012 100644
--- a/httemplate/search/elements/grid-report.html
+++ b/httemplate/search/elements/grid-report.html
@@ -161,7 +161,7 @@ as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR>
.shaded { background-color: #c8c8c8; }
.totalshaded { background-color: #bfc094; }
</style>
-<table class="report" width="100%" cellspacing=0>
+<table class="<% $table_class %>" width="<% $table_width %>" cellspacing=0>
% foreach my $rowinfo (@rows) {
<tr<% $rowinfo->{class} ? ' class="'.$rowinfo->{class}.'"' : ''%>>
% my $thisrow = shift @cells;
@@ -172,7 +172,11 @@ as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR>
% $style .= " rowspan=".$cell->{rowspan} if $cell->{rowspan} > 1;
% $style .= " colspan=".$cell->{colspan} if $cell->{colspan} > 1;
% $style .= ' class="' . $cell->{class} . '"' if $cell->{class};
+% if ($cell->{bypass_filter}) {
+ <<%$td%><%$style%>><% $cell->{value} %></<%$td%>>
+% } else {
<<%$td%><%$style%>><% $cell->{value} |h %></<%$td%>>
+% }
% }
</tr>
% }
@@ -186,4 +190,6 @@ $title
@cells
$head => ''
$foot => ''
+$table_width => "100%"
+$table_class => "report"
</%args>
diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html
index 5762afbe5..18b4c0ec5 100644
--- a/httemplate/search/elements/search.html
+++ b/httemplate/search/elements/search.html
@@ -119,7 +119,11 @@ Example:
#(query needs to be a qsearch hashref and
# header & fields need to be defined)
- #handling agent virtualization
+ # Agent Virtualization parameters:
+ # In this context, only available if your selected table has agentnum.
+ # You must also include agentnum as a SELECT column in your SQL query,
+ # or experience non-obvious problems
+ #
'agent_virt' => 1, # set true if this search should be
# agent-virtualized
'agent_null' => 1, # set true to view global records always
diff --git a/httemplate/search/future_autobill.html b/httemplate/search/future_autobill.html
new file mode 100644
index 000000000..711a25f82
--- /dev/null
+++ b/httemplate/search/future_autobill.html
@@ -0,0 +1,189 @@
+<%doc>
+
+Report listing upcoming auto-bill transactions
+
+Spec requested the ability to run this report with a longer date range,
+and see which charges will process on which day. Checkbox multiple_billing_dates
+enables this functionality.
+
+Performance:
+This is a dynamically generated report. The time this report takes to run
+will depends on the number of customers. Installations with a high number
+of auto-bill customers may find themselves unable to run this report
+because of browser timeout. Report could be implemented as a queued job if
+necessary, to solve the performance problem.
+
+</%doc>
+<& elements/grid-report.html,
+ title => 'Upcoming auto-bill transactions',
+ rows => \@rows,
+ cells => \@cells,
+ table_width => "",
+ table_class => 'gridreport',
+ head => '
+ <style type="text/css">
+ table.gridreport { margin: .5em; border: solid 1px #aaa; }
+ th.gridreport { background-color: #ccc; }
+ tr.gridreport:nth-child(even) { background-color: #eee; }
+ tr.gridreport:nth-child(odd) { background-color: #fff; }
+ td.gridreport { margin: 0 .2em; padding: 0 .4em; }
+ </style>
+ ',
+&>
+
+<%init>
+
+use FS::UID qw( dbh myconnect );
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+ my $target_dt;
+ my @target_dates;
+
+ # Work with all date/time operations @ 12 noon
+ my %noon = (
+ hour => 12,
+ minute => 0,
+ second => 0
+ );
+
+ my $now_dt = DateTime->now;
+ $now_dt = DateTime->new(
+ month => $now_dt->month,
+ day => $now_dt->day,
+ year => $now_dt->year,
+ %noon,
+ );
+
+ # Get target date from form
+ if ($cgi->param('target_date')) {
+ my ($mm, $dd, $yy) = split /[\-\/]/,$cgi->param('target_date');
+ $target_dt = DateTime->new(
+ month => $mm,
+ day => $dd,
+ year => $yy,
+ %noon,
+ ) if $mm && $dd & $yy;
+
+ # Catch a date from the past: time only travels in one direction
+ $target_dt = undef if $target_dt->epoch < $now_dt->epoch;
+ }
+
+ # without a target date, default to tomorrow
+ unless ($target_dt) {
+ $target_dt = DateTime->from_epoch( epoch => time() + 86400) ;
+ $target_dt = DateTime->new(
+ month => $target_dt->month,
+ day => $target_dt->day,
+ year => $target_dt->year,
+ %noon
+ );
+ }
+
+ # If multiple_billing_dates checkbox selected, create a range of dates
+ # from today until the given report date. Otherwise, use target date only.
+ if ($cgi->param('multiple_billing_dates')) {
+ my $walking_dt = DateTime->from_epoch(epoch => $now_dt->epoch);
+ until ($walking_dt->epoch > $target_dt->epoch) {
+ push @target_dates, $walking_dt->epoch;
+ $walking_dt->add(days => 1);
+ }
+ } else {
+ push @target_dates, $target_dt->epoch;
+ }
+
+ # List all customers with an auto-bill method
+ #
+ # my %cust_payby = map {$_->custnum => $_} qsearch({
+ # table => 'cust_payby',
+ # hashref => {
+ # weight => { op => '>', value => '0' },
+ # paydate => { op => '>', value => $target_dt->ymd },
+ # },
+ # order_by => " ORDER BY weight DESC ",
+ # });
+
+ # List all customers with an auto-bill method that's not expired
+ my %cust_payby = map {$_->custnum => $_} qsearch({
+ table => 'cust_payby',
+ hashref => {
+ weight => { op => '>', value => '0' },
+ },
+ order_by => " ORDER BY weight DESC ",
+ extra_sql => " AND ( payby = 'CHEK' OR ( paydate > '".$target_dt->ymd."')) ",
+ });
+
+ my %abreport;
+ my @rows;
+
+ local $@;
+ local $SIG{__DIE__};
+ my $temp_dbh = myconnect();
+ eval { # Creating sandbox dbh where all connections are to be rolled back
+ local $FS::UID::dbh = $temp_dbh;
+ local $FS::UID::AutoCommit = 0;
+
+ # Generate report data into @rows
+ for my $custnum (keys %cust_payby) {
+ my $cust_main = qsearchs('cust_main', {custnum => $custnum});
+
+ # walk forward through billing dates
+ for my $query_epoch (@target_dates) {
+ my $return_bill = [];
+
+ eval { # Don't let an error on one customer crash the report
+ my $error = $cust_main->bill(
+ time => $query_epoch,
+ return_bill => $return_bill,
+ no_usage_reset => 1,
+ );
+ die "$error (simulating future billing)" if $error;
+ };
+ warn ("$@: (future_autobill custnum:$custnum)");
+
+ if (@{$return_bill}) {
+ my $inv = $return_bill->[0];
+ push @rows,{
+ name => $cust_main->name,
+ _date => $inv->_date,
+ cells => [
+ { class => 'gridreport', value => $custnum },
+ { class => 'gridreport',
+ value => '<a href="/view/cust_main.cgi?"'.$custnum.'">'.$cust_main->name.'</a>',
+ bypass_filter => 1,
+ },
+ { class => 'gridreport', value => $inv->charged, format => 'money' },
+ { class => 'gridreport', value => DateTime->from_epoch(epoch=>$inv->_date)->ymd },
+ { class => 'gridreport', value => ($cust_payby{$custnum}->payby || $cust_payby{$custnum}->paytype) },
+ { class => 'gridreport', value => $cust_payby{$custnum}->paymask },
+ ]
+ };
+ }
+
+ }
+ $temp_dbh->rollback;
+ } # /foreach $custnum
+
+ }; # /eval
+ warn("$@") if $@;
+
+ # Sort output by date, and format for output to grid-report.html
+ my @cells = [
+ # header row
+ { class => 'gridreport', value => '#', header => 1 },
+ { class => 'gridreport', value => 'Name', header => 1 },
+ { class => 'gridreport', value => 'Amount', header => 1 },
+ { class => 'gridreport', value => 'Date', header => 1 },
+ { class => 'gridreport', value => 'Type', header => 1 },
+ { class => 'gridreport', value => 'Account', header => 1 },
+ ];
+ push @cells,
+ map { $_->{cells} }
+ sort { $a->{_date} <=> $b->{_date} || $a->{name} cmp $b->{name} }
+ @rows;
+
+ # grid-report.html requires a parallel @rows parameter to accompany @cells
+ @rows = map { {class => 'gridreport'} } 1..scalar(@cells);
+
+</%init>
diff --git a/httemplate/search/report_contact.html b/httemplate/search/report_contact.html
index 3583bb428..048fefb7a 100644
--- a/httemplate/search/report_contact.html
+++ b/httemplate/search/report_contact.html
@@ -10,17 +10,34 @@
'disable_empty' => 0,
&>
+% # Selecting contacts and prospects at the same time has been sacrificed
+% # for agent virtualization
<& /elements/tr-select.html,
- 'label' => 'Contact source', #??? not "type" - contacts have a type
+ 'label' => 'Contact source:',
'field' => 'link',
- 'options' => [ 'prospect_main', 'cust_main', '' ],
+ 'options' => [ 'prospect_main', 'cust_main' ],
'labels' => { 'prospect_main' => 'Prospect contacts',
'cust_main' => 'Customer contacts',
- '' => 'All contacts',
},
'curr_value' => scalar( $cgi->param('link') ),
&>
+ <& /elements/tr-checkbox-multiple.html,
+ label => emt('Destinations').':',
+ field => 'dest',
+ options => [ 'message', 'invoice' ],
+ labels => {
+ invoice => 'Invoice recipients',
+ message => 'Message recipients',
+ },
+ value => { message => 1 },
+ &>
+
+ <& /elements/tr-select-multiple-contact_class.html,
+ label => emt('Contact Type').':',
+ field => 'classnum',
+ &>
+
</FORM>
</TABLE>
diff --git a/httemplate/search/report_future_autobill.html b/httemplate/search/report_future_autobill.html
new file mode 100644
index 000000000..1a0c9f48a
--- /dev/null
+++ b/httemplate/search/report_future_autobill.html
@@ -0,0 +1,42 @@
+<%doc>
+
+Display date selector for the future_autobill.html report
+
+</%doc>
+<% include('/elements/header.html', 'Future Auto-Bill Transactions' ) %>
+
+
+<FORM ACTION="future_autobill.html" METHOD="GET">
+<TABLE>
+<& /elements/tr-input-date-field.html,
+ {
+ name => 'target_date',
+ value => $target_date,
+ label => emt('Target billing date').': ',
+ required => 1
+ }
+&>
+
+<& /elements/tr-checkbox.html,
+ 'label' => emt('Multiple billing dates (slow)').': ',
+ 'field' => 'multiple_billing_dates',
+ 'value' => '1',
+&>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="<% mt('Get Report') |h %>">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+my $target_date = DateTime->from_epoch(epoch=>(time()+86400))->mdy('/');
+
+</%init>
diff --git a/httemplate/view/cust_main/contacts_new.html b/httemplate/view/cust_main/contacts_new.html
index 94488670d..fe412cc00 100644
--- a/httemplate/view/cust_main/contacts_new.html
+++ b/httemplate/view/cust_main/contacts_new.html
@@ -10,6 +10,7 @@
<%$th%>Contact</TH>
<%$th%>Email</TH>
<%$th%>Send invoices</TH>
+ <%$th%>Send messages</TH>
<%$th%>Self-service</TH>
% foreach my $phone_type (@phone_type) {
<%$th%><% $phone_type->typename |h %></TH>
@@ -32,6 +33,7 @@
% my @contact_email = $contact->contact_email;
<%$td%><% join(', ', map $_->emailaddress, @contact_email) %></TD>
<%$td%><% $cust_contact->invoice_dest eq 'Y' ? 'Yes' : 'No' %></TD>
+ <%$td%><% $cust_contact->message_dest eq 'Y' ? 'Yes' : 'No' %></TD>
<%$td%>
% if ( $cust_contact->selfservice_access ) {
Enabled