summaryrefslogtreecommitdiff
path: root/httemplate
diff options
context:
space:
mode:
Diffstat (limited to 'httemplate')
-rwxr-xr-xhttemplate/.htaccess3
-rw-r--r--httemplate/autohandler21
-rw-r--r--httemplate/browse/addr_block.cgi76
-rwxr-xr-xhttemplate/browse/agent.cgi100
-rwxr-xr-xhttemplate/browse/agent_type.cgi60
-rwxr-xr-xhttemplate/browse/cust_main_county.cgi142
-rwxr-xr-xhttemplate/browse/cust_pay_batch.cgi76
-rw-r--r--httemplate/browse/generic.cgi46
-rwxr-xr-xhttemplate/browse/msgcat.cgi50
-rwxr-xr-xhttemplate/browse/nas.cgi80
-rwxr-xr-xhttemplate/browse/part_bill_event.cgi71
-rwxr-xr-xhttemplate/browse/part_export.cgi39
-rwxr-xr-xhttemplate/browse/part_pkg.cgi172
-rwxr-xr-xhttemplate/browse/part_referral.cgi97
-rwxr-xr-xhttemplate/browse/part_svc.cgi133
-rw-r--r--httemplate/browse/part_virtual_field.cgi39
-rwxr-xr-xhttemplate/browse/queue.cgi7
-rw-r--r--httemplate/browse/router.cgi57
-rwxr-xr-xhttemplate/browse/svc_acct_pop.cgi63
-rw-r--r--httemplate/config/config-process.cgi51
-rw-r--r--httemplate/config/config-view.cgi64
-rw-r--r--httemplate/config/config.cgi176
-rw-r--r--httemplate/docs/ach.html12
-rwxr-xr-xhttemplate/docs/admin.html81
-rw-r--r--httemplate/docs/billing.html54
-rw-r--r--httemplate/docs/config.html36
-rw-r--r--httemplate/docs/cvv2.html25
-rwxr-xr-xhttemplate/docs/export.html55
-rw-r--r--httemplate/docs/ieak.html75
-rw-r--r--httemplate/docs/index.html30
-rw-r--r--httemplate/docs/install.html212
-rwxr-xr-xhttemplate/docs/legacy.html39
-rw-r--r--httemplate/docs/man/FS/part_export/.cvs_is_on_crack0
-rw-r--r--httemplate/docs/overview.diabin0 -> 2800 bytes
-rw-r--r--httemplate/docs/overview.pngbin0 -> 13064 bytes
-rwxr-xr-xhttemplate/docs/passwd.html23
-rw-r--r--httemplate/docs/schema.diabin0 -> 14438 bytes
-rw-r--r--httemplate/docs/schema.html457
-rw-r--r--httemplate/docs/schema.pngbin0 -> 681043 bytes
-rw-r--r--httemplate/docs/session.html59
-rw-r--r--httemplate/docs/signup.html54
-rwxr-xr-xhttemplate/docs/ssh.html16
-rwxr-xr-xhttemplate/docs/trouble.html26
-rw-r--r--httemplate/docs/upgrade-1.4.2.html27
-rw-r--r--httemplate/docs/upgrade10.html255
-rw-r--r--httemplate/docs/upgrade7.html24
-rw-r--r--httemplate/docs/upgrade8.html392
-rw-r--r--httemplate/docs/upgrade9.html28
-rwxr-xr-xhttemplate/edit/REAL_cust_pkg.cgi131
-rwxr-xr-xhttemplate/edit/agent.cgi79
-rwxr-xr-xhttemplate/edit/agent_type.cgi63
-rwxr-xr-xhttemplate/edit/cust_bill_pay.cgi95
-rwxr-xr-xhttemplate/edit/cust_credit.cgi63
-rwxr-xr-xhttemplate/edit/cust_credit_bill.cgi101
-rwxr-xr-xhttemplate/edit/cust_main.cgi572
-rwxr-xr-xhttemplate/edit/cust_main_county-expand.cgi54
-rwxr-xr-xhttemplate/edit/cust_main_county.cgi98
-rwxr-xr-xhttemplate/edit/cust_pay.cgi129
-rwxr-xr-xhttemplate/edit/cust_pkg.cgi117
-rwxr-xr-xhttemplate/edit/cust_refund.cgi94
-rwxr-xr-xhttemplate/edit/msgcat.cgi58
-rwxr-xr-xhttemplate/edit/part_bill_event.cgi288
-rw-r--r--httemplate/edit/part_export.cgi128
-rwxr-xr-xhttemplate/edit/part_pkg.cgi627
-rwxr-xr-xhttemplate/edit/part_referral.cgi48
-rwxr-xr-xhttemplate/edit/part_svc.cgi332
-rw-r--r--httemplate/edit/part_virtual_field.cgi92
-rwxr-xr-xhttemplate/edit/process/REAL_cust_pkg.cgi24
-rwxr-xr-xhttemplate/edit/process/addr_block/add.cgi20
-rwxr-xr-xhttemplate/edit/process/addr_block/allocate.cgi25
-rwxr-xr-xhttemplate/edit/process/addr_block/deallocate.cgi24
-rwxr-xr-xhttemplate/edit/process/addr_block/split.cgi19
-rwxr-xr-xhttemplate/edit/process/agent.cgi28
-rwxr-xr-xhttemplate/edit/process/agent_type.cgi55
-rwxr-xr-xhttemplate/edit/process/cust_bill_pay.cgi43
-rwxr-xr-xhttemplate/edit/process/cust_credit.cgi26
-rwxr-xr-xhttemplate/edit/process/cust_credit_bill.cgi43
-rwxr-xr-xhttemplate/edit/process/cust_main.cgi131
-rwxr-xr-xhttemplate/edit/process/cust_main_county-collapse.cgi35
-rwxr-xr-xhttemplate/edit/process/cust_main_county-expand.cgi58
-rwxr-xr-xhttemplate/edit/process/cust_main_county.cgi30
-rwxr-xr-xhttemplate/edit/process/cust_pay.cgi39
-rwxr-xr-xhttemplate/edit/process/cust_pkg.cgi43
-rwxr-xr-xhttemplate/edit/process/cust_refund.cgi42
-rw-r--r--httemplate/edit/process/cust_svc.cgi30
-rwxr-xr-xhttemplate/edit/process/domain_record.cgi34
-rw-r--r--httemplate/edit/process/generic.cgi70
-rw-r--r--httemplate/edit/process/msgcat.cgi20
-rwxr-xr-xhttemplate/edit/process/part_bill_event.cgi54
-rw-r--r--httemplate/edit/process/part_export.cgi39
-rwxr-xr-xhttemplate/edit/process/part_pkg.cgi117
-rwxr-xr-xhttemplate/edit/process/part_referral.cgi28
-rw-r--r--httemplate/edit/process/quick-charge.cgi32
-rw-r--r--httemplate/edit/process/quick-cust_pkg.cgi25
-rw-r--r--httemplate/edit/process/router.cgi67
-rwxr-xr-xhttemplate/edit/process/svc_acct.cgi49
-rwxr-xr-xhttemplate/edit/process/svc_acct_pop.cgi28
-rw-r--r--httemplate/edit/process/svc_broadband.cgi45
-rwxr-xr-xhttemplate/edit/process/svc_domain.cgi31
-rwxr-xr-xhttemplate/edit/process/svc_external.cgi29
-rwxr-xr-xhttemplate/edit/process/svc_forward.cgi29
-rw-r--r--httemplate/edit/process/svc_www.cgi36
-rwxr-xr-xhttemplate/edit/router.cgi77
-rwxr-xr-xhttemplate/edit/svc_acct.cgi301
-rwxr-xr-xhttemplate/edit/svc_acct_pop.cgi56
-rw-r--r--httemplate/edit/svc_broadband.cgi175
-rwxr-xr-xhttemplate/edit/svc_domain.cgi98
-rw-r--r--httemplate/edit/svc_external.cgi105
-rwxr-xr-xhttemplate/edit/svc_forward.cgi177
-rw-r--r--httemplate/edit/svc_www.cgi221
-rw-r--r--httemplate/elements/calendar-en.js123
-rw-r--r--httemplate/elements/calendar-setup.js181
-rw-r--r--httemplate/elements/calendar-win2k-2.css270
-rw-r--r--httemplate/elements/calendar.js1715
-rw-r--r--httemplate/elements/calendar_stripped.js12
-rw-r--r--httemplate/elements/header.html21
-rw-r--r--httemplate/elements/menubar.html8
-rw-r--r--httemplate/elements/pager.html42
-rw-r--r--httemplate/elements/small_custview.html2
-rw-r--r--httemplate/elements/table.html8
-rwxr-xr-xhttemplate/graph/money_time-graph.cgi68
-rw-r--r--httemplate/graph/money_time.cgi125
-rw-r--r--httemplate/images/ach.pngbin0 -> 29759 bytes
-rw-r--r--httemplate/images/calendar.pngbin0 -> 426 bytes
-rw-r--r--httemplate/images/cvv2.pngbin0 -> 3854 bytes
-rw-r--r--httemplate/images/cvv2_amex.pngbin0 -> 4573 bytes
-rw-r--r--httemplate/images/small-logo.pngbin0 -> 5261 bytes
-rw-r--r--httemplate/index.html212
-rwxr-xr-xhttemplate/misc/bill.cgi38
-rwxr-xr-xhttemplate/misc/cancel-unaudited.cgi34
-rwxr-xr-xhttemplate/misc/cancel_pkg.cgi15
-rwxr-xr-xhttemplate/misc/catchall.cgi133
-rwxr-xr-xhttemplate/misc/change_pkg.cgi66
-rwxr-xr-xhttemplate/misc/cust_main-cancel.cgi16
-rw-r--r--httemplate/misc/cust_main-import.cgi51
-rw-r--r--httemplate/misc/cust_main-import_charges.cgi14
-rwxr-xr-xhttemplate/misc/delete-cust_credit.cgi16
-rwxr-xr-xhttemplate/misc/delete-cust_pay.cgi16
-rwxr-xr-xhttemplate/misc/delete-customer.cgi60
-rwxr-xr-xhttemplate/misc/delete-domain_record.cgi15
-rwxr-xr-xhttemplate/misc/delete-part_export.cgi15
-rw-r--r--httemplate/misc/download-batch.cgi16
-rw-r--r--httemplate/misc/dump.cgi19
-rwxr-xr-xhttemplate/misc/email-invoice.cgi23
-rwxr-xr-xhttemplate/misc/expire_pkg.cgi55
-rwxr-xr-xhttemplate/misc/link.cgi74
-rw-r--r--httemplate/misc/meta-import.cgi64
-rw-r--r--httemplate/misc/payment.cgi209
-rwxr-xr-xhttemplate/misc/print-invoice.cgi29
-rwxr-xr-xhttemplate/misc/process/catchall.cgi33
-rw-r--r--httemplate/misc/process/cust_main-import.cgi30
-rw-r--r--httemplate/misc/process/cust_main-import_charges.cgi26
-rwxr-xr-xhttemplate/misc/process/delete-customer.cgi29
-rwxr-xr-xhttemplate/misc/process/expire_pkg.cgi25
-rwxr-xr-xhttemplate/misc/process/link.cgi57
-rw-r--r--httemplate/misc/process/meta-import.cgi178
-rw-r--r--httemplate/misc/process/payment.cgi148
-rw-r--r--httemplate/misc/queue.cgi47
-rwxr-xr-xhttemplate/misc/susp_pkg.cgi15
-rwxr-xr-xhttemplate/misc/unapply-cust_credit.cgi18
-rwxr-xr-xhttemplate/misc/unapply-cust_pay.cgi18
-rwxr-xr-xhttemplate/misc/unprovision.cgi29
-rwxr-xr-xhttemplate/misc/unsusp_pkg.cgi15
-rw-r--r--httemplate/misc/upload-batch.cgi30
-rwxr-xr-xhttemplate/misc/void-cust_pay.cgi16
-rw-r--r--httemplate/misc/whois.cgi25
-rwxr-xr-xhttemplate/search/cust_bill.cgi165
-rwxr-xr-xhttemplate/search/cust_bill.html101
-rw-r--r--httemplate/search/cust_bill_event.cgi62
-rwxr-xr-xhttemplate/search/cust_bill_event.html54
-rwxr-xr-xhttemplate/search/cust_credit.html80
-rwxr-xr-xhttemplate/search/cust_main-otaker.cgi28
-rwxr-xr-xhttemplate/search/cust_main-payinfo.html20
-rwxr-xr-xhttemplate/search/cust_main-quickpay.html44
-rwxr-xr-xhttemplate/search/cust_main.cgi608
-rwxr-xr-xhttemplate/search/cust_main.html42
-rwxr-xr-xhttemplate/search/cust_pay.cgi137
-rwxr-xr-xhttemplate/search/cust_pay.html18
-rwxr-xr-xhttemplate/search/cust_pkg.cgi363
-rwxr-xr-xhttemplate/search/cust_pkg_report.cgi63
-rw-r--r--httemplate/search/elements/search.html139
-rw-r--r--httemplate/search/report_cust_credit.html58
-rw-r--r--httemplate/search/report_cust_pay.html55
-rw-r--r--httemplate/search/report_prepaid_income.cgi86
-rw-r--r--httemplate/search/report_prepaid_income.html39
-rwxr-xr-xhttemplate/search/report_receivables.cgi158
-rwxr-xr-xhttemplate/search/report_tax.cgi184
-rwxr-xr-xhttemplate/search/report_tax.html44
-rw-r--r--httemplate/search/sql.html7
-rw-r--r--httemplate/search/sqlradius.cgi260
-rw-r--r--httemplate/search/sqlradius.html70
-rwxr-xr-xhttemplate/search/svc_acct.cgi294
-rwxr-xr-xhttemplate/search/svc_acct.html19
-rwxr-xr-xhttemplate/search/svc_domain.cgi161
-rwxr-xr-xhttemplate/search/svc_domain.html19
-rwxr-xr-xhttemplate/search/svc_external.cgi101
-rwxr-xr-xhttemplate/search/svc_forward.cgi79
-rwxr-xr-xhttemplate/search/svc_www.cgi42
-rwxr-xr-xhttemplate/view/cust_bill-pdf.cgi18
-rwxr-xr-xhttemplate/view/cust_bill-ps.cgi14
-rwxr-xr-xhttemplate/view/cust_bill.cgi82
-rwxr-xr-xhttemplate/view/cust_main.cgi1064
-rwxr-xr-xhttemplate/view/cust_pkg.cgi164
-rwxr-xr-xhttemplate/view/svc_acct.cgi272
-rw-r--r--httemplate/view/svc_broadband.cgi142
-rwxr-xr-xhttemplate/view/svc_domain.cgi108
-rw-r--r--httemplate/view/svc_external.cgi54
-rwxr-xr-xhttemplate/view/svc_forward.cgi84
-rw-r--r--httemplate/view/svc_www.cgi61
209 files changed, 19597 insertions, 0 deletions
diff --git a/httemplate/.htaccess b/httemplate/.htaccess
new file mode 100755
index 0000000..f8c6b9c
--- /dev/null
+++ b/httemplate/.htaccess
@@ -0,0 +1,3 @@
+AuthName Freeside
+AuthType Basic
+require valid-user
diff --git a/httemplate/autohandler b/httemplate/autohandler
new file mode 100644
index 0000000..2bd3adf
--- /dev/null
+++ b/httemplate/autohandler
@@ -0,0 +1,21 @@
+% $m->call_next;
+<%init>
+ dbh->{'private_profile'} = {} if UNIVERSAL::can(dbh, 'sprintProfile');
+</%init>
+<%filter>
+
+my $profile = '';
+if ( UNIVERSAL::can(dbh, 'sprintProfile') ) {
+
+ if ( lc($r->content_type) eq 'text/html' ) {
+
+ $profile = '<PRE>'. ("\n"x4096). encode_entities(dbh->sprintProfile()).
+ #"\n\n". &sprintAutoProfile(). '</PRE>';
+ "\n\n". '</PRE>';
+ }
+
+ dbh->{'private_profile'} = {};
+}
+
+s/(<\/BODY>[\s\n]*<\/HTML>[\s\n]*)$/$profile$1/i;
+</%filter>
diff --git a/httemplate/browse/addr_block.cgi b/httemplate/browse/addr_block.cgi
new file mode 100644
index 0000000..06ac556
--- /dev/null
+++ b/httemplate/browse/addr_block.cgi
@@ -0,0 +1,76 @@
+<%= header('Address Blocks', menubar('Main Menu' => $p)) %>
+<%
+
+use NetAddr::IP;
+
+my @addr_block = qsearch('addr_block', {});
+my @router = qsearch('router', {});
+my $block;
+my $p2 = popurl(2);
+my $path = $p2 . "edit/process/addr_block";
+
+%>
+
+<% if ($cgi->param('error')) { %>
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+ <BR><BR>
+<% } %>
+
+<%=table()%>
+
+<% foreach $block (sort {$a->NetAddr cmp $b->NetAddr} @addr_block) { %>
+ <TR>
+ <TD><%=$block->NetAddr%></TD>
+ <% if (my $router = $block->router) { %>
+ <% if (scalar($block->svc_broadband) == 0) { %>
+ <TD>
+ <%=$router->routername%>
+ </TD>
+ <TD>
+ <FORM ACTION="<%=$path%>/deallocate.cgi" METHOD="POST">
+ <INPUT TYPE="hidden" NAME="blocknum" VALUE="<%=$block->blocknum%>">
+ <INPUT TYPE="submit" NAME="submit" VALUE="Deallocate">
+ </FORM>
+ </TD>
+ <% } else { %>
+ <TD COLSPAN="2">
+ <%=$router->routername%>
+ </TD>
+ <% } %>
+ <% } else { %>
+ <TD>
+ <FORM ACTION="<%=$path%>/allocate.cgi" METHOD="POST">
+ <INPUT TYPE="hidden" NAME="blocknum" VALUE="<%=$block->blocknum%>">
+ <SELECT NAME="routernum" SIZE="1">
+ <% foreach (@router) { %>
+ <OPTION VALUE="<%=$_->routernum %>"><%=$_->routername%></OPTION>
+ <% } %>
+ </SELECT>
+ <INPUT TYPE="submit" NAME="submit" VALUE="Allocate">
+ </FORM>
+ </TD>
+ <TD>
+ <FORM ACTION="<%=$path%>/split.cgi" METHOD="POST">
+ <INPUT TYPE="hidden" NAME="blocknum" VALUE="<%=$block->blocknum%>">
+ <INPUT TYPE="submit" NAME="submit" VALUE="Split">
+ </FORM>
+ </TD>
+ </TR>
+<% }
+ } %>
+ <TR><TD COLSPAN="3"><BR></TD></TR>
+ <TR>
+ <FORM ACTION="<%=$path%>/add.cgi" METHOD="POST">
+ <TD>Gateway/Netmask</TD>
+ <TD>
+ <INPUT TYPE="text" NAME="ip_gateway" SIZE="15">/<INPUT TYPE="text" NAME="ip_netmask" SIZE="2">
+ </TD>
+ <TD>
+ <INPUT TYPE="submit" NAME="submit" VALUE="Add">
+ </TD>
+ </FORM>
+ </TR>
+</TABLE>
+</BODY>
+</HTML>
+
diff --git a/httemplate/browse/agent.cgi b/httemplate/browse/agent.cgi
new file mode 100755
index 0000000..f389342
--- /dev/null
+++ b/httemplate/browse/agent.cgi
@@ -0,0 +1,100 @@
+<!-- mason kludge -->
+
+<%
+
+ my %search;
+ if ( $cgi->param('showdisabled')
+ || !dbdef->table('agent')->column('disabled') ) {
+ %search = ();
+ } else {
+ %search = ( 'disabled' => '' );
+ }
+
+%>
+
+<%= header('Agent Listing', menubar(
+ 'Main Menu' => $p,
+ 'Agent Types' => $p. 'browse/agent_type.cgi',
+# 'Add new agent' => '../edit/agent.cgi'
+)) %>
+Agents are resellers of your service. Agents may be limited to a subset of your
+full offerings (via their type).<BR><BR>
+<A HREF="<%= $p %>edit/agent.cgi"><I>Add a new agent</I></A><BR><BR>
+
+<% if ( dbdef->table('agent')->column('disabled') ) { %>
+ <%= $cgi->param('showdisabled')
+ ? do { $cgi->param('showdisabled', 0);
+ '( <a href="'. $cgi->self_url. '">hide disabled agents</a> )'; }
+ : do { $cgi->param('showdisabled', 1);
+ '( <a href="'. $cgi->self_url. '">show disabled agents</a> )'; }
+ %>
+<% } %>
+
+<%= table() %>
+<TR>
+ <TH COLSPAN=<%= ( $cgi->param('showdisabled') || !dbdef->table('agent')->column('disabled') ) ? 2 : 3 %>>Agent</TH>
+ <TH>Type</TH>
+ <TH>Customers</TH>
+ <TH><FONT SIZE=-1>Freq.</FONT></TH>
+ <TH><FONT SIZE=-1>Prog.</FONT></TH>
+</TR>
+<%
+# <TH><FONT SIZE=-1>Agent #</FONT></TH>
+# <TH>Agent</TH>
+
+foreach my $agent ( sort {
+ #$a->getfield('agentnum') <=> $b->getfield('agentnum')
+ $a->getfield('agent') cmp $b->getfield('agent')
+} qsearch('agent', \%search ) ) {
+
+ my $cust_main_link = $p. 'search/cust_main.cgi?agentnum_on=1&'.
+ 'agentnum='. $agent->agentnum;
+
+%>
+
+ <TR>
+ <TD><A HREF="<%=$p%>edit/agent.cgi?<%= $agent->agentnum %>">
+ <%= $agent->agentnum %></A></TD>
+<% if ( dbdef->table('agent')->column('disabled')
+ && !$cgi->param('showdisabled') ) { %>
+ <TD><%= $agent->disabled ? 'DISABLED' : '' %></TD>
+<% } %>
+
+ <TD><A HREF="<%=$p%>edit/agent.cgi?<%= $agent->agentnum %>">
+ <%= $agent->agent %></A></TD>
+ <TD><A HREF="<%=$p%>edit/agent_type.cgi?<%= $agent->typenum %>"><%= $agent->agent_type->atype %></A></TD>
+ <TD>
+
+ <B>
+ <%= my $num_prospect = $agent->num_prospect_cust_main %>
+ </B>
+ <% if ( $num_prospect ) { %>
+ <A HREF="<%= $cust_main_link %>&prospect=1"><% } %>prospects<% if ($num_prospect ) { %></A><% } %>
+
+ <BR><FONT COLOR="#00CC00"><B>
+ <%= my $num_active = $agent->num_active_cust_main %>
+ </B></FONT>
+ <% if ( $num_active ) { %>
+ <A HREF="<%= $cust_main_link %>&active=1"><% } %>active<% if ( $num_active ) { %></A><% } %>
+
+ <BR><FONT COLOR="#FF9900"><B>
+ <%= my $num_susp = $agent->num_susp_cust_main %>
+ </B></FONT>
+ <% if ( $num_susp ) { %>
+ <A HREF="<%= $cust_main_link %>&suspended=1"><% } %>suspended<% if ( $num_susp ) { %></A><% } %>
+
+ <BR><FONT COLOR="#FF0000"><B>
+ <%= my $num_cancel = $agent->num_cancel_cust_main %>
+ </B></FONT>
+ <% if ( $num_cancel ) { %>
+ <A HREF="<%= $cust_main_link %>&showcancelledcustomers=1&cancelled=1"><% } %>cancelled<% if ( $num_cancel ) { %></A><% } %>
+ </TD>
+ <TD><%= $agent->freq %></TD>
+ <TD><%= $agent->prog %></TD>
+ </TR>
+
+<% } %>
+
+ </TABLE>
+ </BODY>
+</HTML>
diff --git a/httemplate/browse/agent_type.cgi b/httemplate/browse/agent_type.cgi
new file mode 100755
index 0000000..5473804
--- /dev/null
+++ b/httemplate/browse/agent_type.cgi
@@ -0,0 +1,60 @@
+<!-- mason kludge -->
+<%= header("Agent Type Listing", menubar(
+ 'Main Menu' => $p,
+ 'Agents' => $p. 'browse/agent.cgi',
+)) %>
+Agent types define groups of packages that you can then assign to particular
+agents.<BR><BR>
+<A HREF="<%= $p %>edit/agent_type.cgi"><I>Add a new agent type</I></A><BR><BR>
+
+<%= table() %>
+<TR>
+ <TH COLSPAN=2>Agent Type</TH>
+ <TH COLSPAN=2>Packages</TH>
+</TR>
+
+<%
+foreach my $agent_type ( sort {
+ $a->getfield('typenum') <=> $b->getfield('typenum')
+} qsearch('agent_type',{}) ) {
+ my $hashref = $agent_type->hashref;
+ #more efficient to do this with SQL...
+ my @type_pkgs = grep { $_->part_pkg and ! $_->part_pkg->disabled }
+ qsearch('type_pkgs',{'typenum'=> $hashref->{typenum} });
+ my $rowspan = scalar(@type_pkgs);
+ $rowspan = int($rowspan/2+0.5) ;
+ print <<END;
+ <TR>
+ <TD ROWSPAN=$rowspan><A HREF="${p}edit/agent_type.cgi?$hashref->{typenum}">
+ $hashref->{typenum}
+ </A></TD>
+ <TD ROWSPAN=$rowspan><A HREF="${p}edit/agent_type.cgi?$hashref->{typenum}">$hashref->{atype}</A></TD>
+END
+
+ my($type_pkgs);
+ my($tdcount) = -1 ;
+ foreach $type_pkgs ( @type_pkgs ) {
+ my($pkgpart)=$type_pkgs->getfield('pkgpart');
+ my($part_pkg) = qsearchs('part_pkg',{'pkgpart'=> $pkgpart });
+ print qq!<TR>! if ($tdcount == 0) ;
+ $tdcount = 0 if ($tdcount == -1) ;
+ print qq!<TD><A HREF="${p}edit/part_pkg.cgi?$pkgpart">!,
+ $part_pkg->getfield('pkg'),"</A></TD>";
+ $tdcount ++ ;
+ if ($tdcount == 2)
+ {
+ print qq!</TR>\n! ;
+ $tdcount = 0 ;
+ }
+ }
+
+ print "</TR>";
+}
+
+print <<END;
+ </TABLE>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/browse/cust_main_county.cgi b/httemplate/browse/cust_main_county.cgi
new file mode 100755
index 0000000..1e0e088
--- /dev/null
+++ b/httemplate/browse/cust_main_county.cgi
@@ -0,0 +1,142 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+my $enable_taxclasses = $conf->exists('enable_taxclasses');
+
+print header("Tax Rate Listing", menubar(
+ 'Main Menu' => $p,
+ 'Edit tax rates' => $p. "edit/cust_main_county.cgi",
+)),<<END;
+ Click on <u>expand country</u> to specify a country's tax rates by state.
+ <BR>Click on <u>expand state</u> to specify a state's tax rates by county.
+END
+
+if ( $enable_taxclasses ) {
+ print '<BR>Click on <u>expand taxclasses</u> to specify tax classes';
+}
+
+print '<BR><BR>'. &table(). <<END;
+ <TR>
+ <TH><FONT SIZE=-1>Country</FONT></TH>
+ <TH><FONT SIZE=-1>State</FONT></TH>
+ <TH>County</TH>
+ <TH>Taxclass<BR><FONT SIZE=-1>(per-package classification)</FONT></TH>
+ <TH>Tax name<BR><FONT SIZE=-1>(printed on invoices)</FONT></TH>
+ <TH><FONT SIZE=-1>Tax</FONT></TH>
+ <TH><FONT SIZE=-1>Exemption</TH>
+ </TR>
+END
+
+my @regions = sort { $a->country cmp $b->country
+ or $a->state cmp $b->state
+ or $a->county cmp $b->county
+ or $a->taxclass cmp $b->taxclass
+ } qsearch('cust_main_county',{});
+
+my $sup=0;
+#foreach $cust_main_county ( @regions ) {
+for ( my $i=0; $i<@regions; $i++ ) {
+ my $cust_main_county = $regions[$i];
+ my $hashref = $cust_main_county->hashref;
+ print <<END;
+ <TR>
+ <TD BGCOLOR="#ffffff">$hashref->{country}</TD>
+END
+
+ my $j;
+ if ( $sup ) {
+ $sup--;
+ } else {
+
+ #lookahead
+ for ( $j=1; $i+$j<@regions; $j++ ) {
+ last if $hashref->{country} ne $regions[$i+$j]->country
+ || $hashref->{state} ne $regions[$i+$j]->state
+ || $hashref->{tax} != $regions[$i+$j]->tax
+ || $hashref->{exempt_amount} != $regions[$i+$j]->exempt_amount
+ || $hashref->{setuptax} ne $regions[$i+$j]->setuptax
+ || $hashref->{recurtax} ne $regions[$i+$j]->recurtax;
+ }
+
+ my $newsup=0;
+ if ( $j>1 && $i+$j+1 < @regions
+ && ( $hashref->{state} ne $regions[$i+$j+1]->state
+ || $hashref->{country} ne $regions[$i+$j+1]->country
+ )
+ && ( ! $i
+ || $hashref->{state} ne $regions[$i-1]->state
+ || $hashref->{country} ne $regions[$i-1]->country
+ )
+ ) {
+ $sup = $j-1;
+ } else {
+ $j = 1;
+ }
+
+ print "<TD ROWSPAN=$j", $hashref->{state}
+ ? ' BGCOLOR="#ffffff">'. $hashref->{state}
+ : qq! BGCOLOR="#cccccc">(ALL) <FONT SIZE=-1>!.
+ qq!<A HREF="${p}edit/cust_main_county-expand.cgi?!. $hashref->{taxnum}.
+ qq!">expand country</A></FONT>!;
+
+ print qq! <FONT SIZE=-1><A HREF="${p}edit/process/cust_main_county-collapse.cgi?!. $hashref->{taxnum}. qq!">collapse state</A></FONT>! if $j>1;
+
+ print "</TD>";
+ }
+
+# $sup=$newsup;
+
+ print "<TD";
+ if ( $hashref->{county} ) {
+ print ' BGCOLOR="#ffffff">'. $hashref->{county};
+ } else {
+ print ' BGCOLOR="#cccccc">(ALL)';
+ if ( $hashref->{state} ) {
+ print qq!<FONT SIZE=-1>!.
+ qq!<A HREF="${p}edit/cust_main_county-expand.cgi?!. $hashref->{taxnum}.
+ qq!">expand state</A></FONT>!;
+ }
+ }
+ print "</TD>";
+
+ print "<TD";
+ if ( $hashref->{taxclass} ) {
+ print ' BGCOLOR="#ffffff">'. $hashref->{taxclass};
+ } else {
+ print ' BGCOLOR="#cccccc">(ALL)';
+ if ( $enable_taxclasses ) {
+ print qq!<FONT SIZE=-1>!.
+ qq!<A HREF="${p}edit/cust_main_county-expand.cgi?taxclass!.
+ $hashref->{taxnum}. qq!">expand taxclasses</A></FONT>!;
+ }
+
+ }
+ print "</TD>";
+
+ print "<TD";
+ if ( $hashref->{taxname} ) {
+ print ' BGCOLOR="#ffffff">'. $hashref->{taxname};
+ } else {
+ print ' BGCOLOR="#cccccc">Tax';
+ }
+ print "</TD>";
+
+ print "<TD BGCOLOR=\"#ffffff\">$hashref->{tax}%</TD>".
+ '<TD BGCOLOR="#ffffff">';
+ print '$'. sprintf("%.2f", $hashref->{exempt_amount} ).
+ '&nbsp;per&nbsp;month<BR>'
+ if $hashref->{exempt_amount} > 0;
+ print 'Setup&nbsp;fee<BR>' if $hashref->{setuptax} =~ /^Y$/i;
+ print 'Recurring&nbsp;fee<BR>' if $hashref->{recurtax} =~ /^Y$/i;
+ print '</TD></TR>';
+
+}
+
+print <<END;
+ </TABLE>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/browse/cust_pay_batch.cgi b/httemplate/browse/cust_pay_batch.cgi
new file mode 100755
index 0000000..3420e97
--- /dev/null
+++ b/httemplate/browse/cust_pay_batch.cgi
@@ -0,0 +1,76 @@
+<!-- mason kludge -->
+<%= header("Pending credit card batch", menubar( 'Main Menu' => $p,)) %>
+
+<FORM ACTION="<%=$p%>misc/download-batch.cgi" METHOD="POST">
+Download batch in format <SELECT NAME="format">
+<OPTION VALUE="csv-td_canada_trust-merchant_pc_batch">CSV file for TD Canada Trust Merchant PC Batch</OPTION>
+</SELECT><INPUT TYPE="submit" VALUE="Download"></FORM>
+<BR><BR>
+
+<FORM ACTION="<%=$p%>misc/upload-batch.cgi" METHOD="POST" ENCTYPE="multipart/form-data">
+Upload results<BR>
+Filename <INPUT TYPE="file" NAME="batch_results"><BR>
+Format <SELECT NAME="format">
+<OPTION VALUE="csv-td_canada_trust-merchant_pc_batch">CSV results from TD Canada Trust Merchant PC Batch</OPTION>
+</SELECT><BR>
+<INPUT TYPE="submit" VALUE="Upload"></FORM>
+<BR>
+
+<%
+ my $statement = "SELECT SUM(amount) from cust_pay_batch";
+ my $sth = dbh->prepare($statement) or die dbh->errstr. "doing $statement";
+ $sth->execute or die "Error executing \"$statement\": ". $sth->errstr;
+ my $total = $sth->fetchrow_arrayref->[0];
+
+ my $c_statement = "SELECT COUNT(*) from cust_pay_batch";
+ my $c_sth = dbh->prepare($c_statement)
+ or die dbh->errstr. "doing $c_statement";
+ $c_sth->execute or die "Error executing \"$c_statement\": ". $c_sth->errstr;
+ my $cards = $c_sth->fetchrow_arrayref->[0];
+%>
+<%= $cards %> credit card payments batched<BR>
+$<%= sprintf("%.2f", $total) %> total in pending batch<BR>
+
+<BR>
+<%= &table() %>
+ <TR>
+ <TH>#</TH>
+ <TH><font size=-1>inv#</font></TH>
+ <TH COLSPAN=2>Customer</TH>
+ <TH>Card name</TH>
+ <TH>Card</TH>
+ <TH>Exp</TH>
+ <TH>Amount</TH>
+ </TR>
+
+<%
+foreach my $cust_pay_batch ( sort { $a->paybatchnum <=> $b->paybatchnum }
+ qsearch('cust_pay_batch', {} )
+) {
+ my $cardnum = $cust_pay_batch->cardnum;
+ #$cardnum =~ s/.{4}$/xxxx/;
+ $cardnum = 'x'x(length($cardnum)-4). substr($cardnum,(length($cardnum)-4));
+
+ $cust_pay_batch->exp =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+ my( $mon, $year ) = ( $2, $1 );
+ $mon = "0$mon" if $mon < 10;
+ my $exp = "$mon/$year";
+
+%>
+
+ <TR>
+ <TD><%= $cust_pay_batch->paybatchnum %></TD>
+ <TD><A HREF="../view/cust_bill.cgi?<%= $cust_pay_batch->invnum %>"><%= $cust_pay_batch->invnum %></TD>
+ <TD><A HREF="../view/cust_main.cgi?<%= $cust_pay_batch->custnum %>"><%= $cust_pay_batch->custnum %></TD>
+ <TD><%= $cust_pay_batch->get('last'). ', '. $cust_pay_batch->first %></TD>
+ <TD><%= $cust_pay_batch->payname %></TD>
+ <TD><%= $cardnum %></TD>
+ <TD><%= $exp %></TD>
+ <TD align="right">$<%= $cust_pay_batch->amount %></TD>
+ </TR>
+
+<% } %>
+
+ </TABLE>
+ </BODY>
+</HTML>
diff --git a/httemplate/browse/generic.cgi b/httemplate/browse/generic.cgi
new file mode 100644
index 0000000..9ac0f23
--- /dev/null
+++ b/httemplate/browse/generic.cgi
@@ -0,0 +1,46 @@
+<%
+
+use FS::Record qw(qsearch dbdef);
+use DBIx::DBSchema;
+use DBIx::DBSchema::Table;
+
+my $error;
+my $p2 = popurl(2);
+my ($table) = $cgi->keywords;
+my $dbdef = dbdef or die "Cannot fetch dbdef!";
+my $dbdef_table = $dbdef->table($table) or die "Cannot fetch schema for $table";
+
+my $pkey = $dbdef_table->primary_key or die "Cannot fetch pkey for $table";
+print header("Browse $table", menubar('Main Menu' => $p));
+
+my @rec = qsearch($table, {});
+my @col = $dbdef_table->columns;
+
+if ($cgi->param('error')) { %>
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+ <BR><BR>
+<% }
+%>
+<A HREF="<%=$p2%>edit/<%=$table%>.cgi"><I>Add a new <%=$table%></I></A><BR><BR>
+
+<%=table()%>
+<TH>
+<% foreach (grep { $_ ne $pkey } @col) {
+ %><TD><%=$_%></TD>
+ <% } %>
+</TH>
+<% foreach $rec (sort {$a->getfield($pkey) cmp $b->getfield($pkey) } @rec) {
+ %>
+ <TR>
+ <TD>
+ <A HREF="<%=$p2%>edit/<%=$table%>.cgi?<%=$rec->getfield($pkey)%>">
+ <%=$rec->getfield($pkey)%></A> </TD> <%
+ foreach $col (grep { $_ ne $pkey } @col) { %>
+ <TD><%=$rec->getfield($col)%></TD> <% } %>
+ </A>
+ </TR>
+<% } %>
+</TABLE>
+</BODY>
+</HTML>
+
diff --git a/httemplate/browse/msgcat.cgi b/httemplate/browse/msgcat.cgi
new file mode 100755
index 0000000..d4adf9f
--- /dev/null
+++ b/httemplate/browse/msgcat.cgi
@@ -0,0 +1,50 @@
+<!-- mason kludge -->
+<%
+
+print header("View Message catalog", menubar(
+ 'Main Menu' => $p,
+ 'Edit message catalog' => $p. "edit/msgcat.cgi",
+)), '<BR>';
+
+my $widget = new HTML::Widgets::SelectLayers(
+ 'selected_layer' => 'en_US',
+ 'options' => { 'en_US'=>'en_US' },
+ 'layer_callback' => sub {
+ my $layer = shift;
+ my $html = "<BR>Messages for locale $layer<BR>". table().
+ "<TR><TH COLSPAN=2>Code</TH>".
+ "<TH>Message</TH>";
+ $html .= "<TH>en_US Message</TH>" unless $layer eq 'en_US';
+ $html .= '</TR>';
+
+ #foreach my $msgcat ( sort { $a->msgcode cmp $b->msgcode }
+ # qsearch('msgcat', { 'locale' => $layer } ) ) {
+ foreach my $msgcat ( qsearch('msgcat', { 'locale' => $layer } ) ) {
+ $html .= '<TR><TD>'. $msgcat->msgnum. '</TD>'.
+ '<TD>'. $msgcat->msgcode. '</TD>'.
+ '<TD>'. $msgcat->msg. '</TD>';
+ unless ( $layer eq 'en_US' ) {
+ my $en_msgcat = qsearchs('msgcat', {
+ 'locale' => 'en_US',
+ 'msgcode' => $msgcat->msgcode,
+ } );
+ $html .= '<TD>'. $en_msgcat->msg. '</TD>';
+ }
+ $html .= '</TR>';
+ }
+
+ $html .= '</TABLE>';
+ $html;
+ },
+
+);
+
+print $widget->html;
+
+print <<END;
+ </TABLE>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/browse/nas.cgi b/httemplate/browse/nas.cgi
new file mode 100755
index 0000000..9ccbfe6
--- /dev/null
+++ b/httemplate/browse/nas.cgi
@@ -0,0 +1,80 @@
+<!-- mason kludge -->
+<%
+
+print header('NAS ports', menubar(
+ 'Main Menu' => $p,
+));
+
+my $now = time;
+
+foreach my $nas ( sort { $a->nasnum <=> $b->nasnum } qsearch( 'nas', {} ) ) {
+ print $nas->nasnum. ": ". $nas->nas. " ".
+ $nas->nasfqdn. " (". $nas->nasip. ") ".
+ "as of ". time2str("%c",$nas->last).
+ " (". &pretty_interval($now - $nas->last). " ago)<br>".
+ &table(). "<TR><TH>Nas<BR>Port #</TH><TH>Global<BR>Port #</BR></TH>".
+ "<TH>IP address</TH><TH>User</TH><TH>Since</TH><TH>Duration</TH><TR>",
+ ;
+ foreach my $port ( sort {
+ $a->nasport <=> $b->nasport || $a->portnum <=> $b->portnum
+ } qsearch( 'port', { 'nasnum' => $nas->nasnum } ) ) {
+ my $session = $port->session;
+ my($user, $since, $pretty_since, $duration);
+ if ( ! $session ) {
+ $user = "(empty)";
+ $since = 0;
+ $pretty_since = "(never)";
+ $duration = '';
+ } elsif ( $session->logout ) {
+ $user = "(empty)";
+ $since = $session->logout;
+ } else {
+ my $svc_acct = $session->svc_acct;
+ $user = "<A HREF=\"$p/view/svc_acct.cgi?". $svc_acct->svcnum. "\">".
+ $svc_acct->username. "</A>";
+ $since = $session->login;
+ }
+ $pretty_since = time2str("%c", $since) if $since;
+ $duration = pretty_interval( $now - $since ). " ago"
+ unless defined($duration);
+ print "<TR><TD>". $port->nasport. "</TD><TD>". $port->portnum. "</TD><TD>".
+ $port->ip. "</TD><TD>$user</TD><TD>$pretty_since".
+ "</TD><TD>$duration</TD></TR>"
+ ;
+ }
+ print "</TABLE><BR>";
+}
+
+#Time::Duration??
+sub pretty_interval {
+ my $interval = shift;
+ my %howlong = (
+ '604800' => 'week',
+ '86400' => 'day',
+ '3600' => 'hour',
+ '60' => 'minute',
+ '1' => 'second',
+ );
+
+ my $pretty = "";
+ foreach my $key ( sort { $b <=> $a } keys %howlong ) {
+ my $value = int( $interval / $key );
+ if ( $value ) {
+ if ( $value == 1 ) {
+ $pretty .=
+ ( $howlong{$key} eq 'hour' ? 'an ' : 'a ' ). $howlong{$key}. " "
+ } else {
+ $pretty .= $value. ' '. $howlong{$key}. 's ';
+ }
+ }
+ $interval -= $value * $key;
+ }
+ $pretty =~ /^\s*(\S.*\S)\s*$/;
+ $1;
+}
+
+#print &table(), <<END;
+#<TR>
+# <TH>#</TH>
+# <TH>NAS</
+%>
diff --git a/httemplate/browse/part_bill_event.cgi b/httemplate/browse/part_bill_event.cgi
new file mode 100755
index 0000000..670474d
--- /dev/null
+++ b/httemplate/browse/part_bill_event.cgi
@@ -0,0 +1,71 @@
+<!-- mason kludge -->
+<%
+
+my %search;
+if ( $cgi->param('showdisabled') ) {
+ %search = ();
+} else {
+ %search = ( 'disabled' => '' );
+}
+
+my @part_bill_event = qsearch('part_bill_event', \%search );
+my $total = scalar(@part_bill_event);
+
+%>
+<%= header('Invoice Event Listing', menubar( 'Main Menu' => $p) ) %>
+
+ Invoice events are actions taken on overdue invoices.<BR><BR>
+<A HREF="<%= $p %>edit/part_bill_event.cgi"><I>Add a new invoice event</I></A>
+<BR><BR>
+<%= $total %> events
+<%= $cgi->param('showdisabled')
+ ? do { $cgi->param('showdisabled', 0);
+ '( <a href="'. $cgi->self_url. '">hide disabled events</a> )'; }
+ : do { $cgi->param('showdisabled', 1);
+ '( <a href="'. $cgi->self_url. '">show disabled events</a> )'; }
+%>
+<%= table() %>
+ <TR>
+ <TH COLSPAN=<%= $cgi->param('showdisabled') ? 2 : 3 %>>Event</TH>
+ <TH>Payby</TH>
+ <TH>After</TH>
+ <TH>Action</TH>
+ <TH>Options</TH>
+ <TH>Code</TH>
+ </TR>
+
+<% foreach my $part_bill_event ( sort { $a->payby cmp $b->payby
+ || $a->seconds <=> $b->seconds
+ || $a->weight <=> $b->weight
+ || $a->eventpart <=> $b->eventpart
+ } @part_bill_event ) {
+ my $url = "${p}edit/part_bill_event.cgi?". $part_bill_event->eventpart;
+ use Time::Duration;
+ my $delay = duration_exact($part_bill_event->seconds);
+ my $plandata = $part_bill_event->plandata;
+ $plandata =~ s/\n/<BR>/go;
+%>
+ <TR>
+ <TD><A HREF="<%= $url %>">
+ <%= $part_bill_event->eventpart %></A></TD>
+<% unless ( $cgi->param('showdisabled') ) { %>
+ <TD>
+ <%= $part_bill_event->disabled ? 'DISABLED' : '' %></TD>
+<% } %>
+ <TD><A HREF="<%= $url %>">
+ <%= $part_bill_event->event %></A></TD>
+ <TD>
+ <%= $part_bill_event->payby %></TD>
+ <TD>
+ <%= $delay %></TD>
+ <TD>
+ <%= $part_bill_event->plan %></TD>
+ <TD>
+ <%= $plandata %></TD>
+ <TD><FONT SIZE="-1">
+ <%= $part_bill_event->eventcode %></FONT></TD>
+ </TR>
+<% } %>
+</TABLE>
+</BODY>
+</HTML>
diff --git a/httemplate/browse/part_export.cgi b/httemplate/browse/part_export.cgi
new file mode 100755
index 0000000..79c57ae
--- /dev/null
+++ b/httemplate/browse/part_export.cgi
@@ -0,0 +1,39 @@
+<!-- mason kludge -->
+<%= header("Export Listing", menubar( 'Main Menu' => "$p#sysadmin" )) %>
+Provisioning services to external machines, databases and APIs.<BR><BR>
+<A HREF="<%= $p %>edit/part_export.cgi"><I>Add a new export</I></A><BR><BR>
+<SCRIPT>
+function part_export_areyousure(href) {
+ if (confirm("Are you sure you want to delete this export?") == true)
+ window.location.href = href;
+}
+</SCRIPT>
+
+<%= table() %>
+ <TR>
+ <TH COLSPAN=2>Export</TH>
+ <TH>Options</TH>
+ </TR>
+
+<% foreach my $part_export ( sort {
+ $a->getfield('exportnum') <=> $b->getfield('exportnum')
+ } qsearch('part_export',{}) ) {
+%>
+ <TR>
+ <TD><A HREF="<%= $p %>edit/part_export.cgi?<%= $part_export->exportnum %>"><%= $part_export->exportnum %></A></TD>
+ <TD><%= $part_export->exporttype %> to <%= $part_export->machine %> (<A HREF="<%= $p %>edit/part_export.cgi?<%= $part_export->exportnum %>">edit</A>&nbsp;|&nbsp;<A HREF="javascript:part_export_areyousure('<%= $p %>misc/delete-part_export.cgi?<%= $part_export->exportnum %>')">delete</A>)</TD>
+ <TD>
+ <%= itable() %>
+ <% my %opt = $part_export->options;
+ foreach my $opt ( keys %opt ) { %>
+ <TR><TD><%= $opt %></TD><TD><%= encode_entities($opt{$opt}) %></TD></TR>
+ <% } %>
+ </TABLE>
+ </TD>
+ </TR>
+
+<% } %>
+
+</TABLE>
+</BODY>
+</HTML>
diff --git a/httemplate/browse/part_pkg.cgi b/httemplate/browse/part_pkg.cgi
new file mode 100755
index 0000000..be67338
--- /dev/null
+++ b/httemplate/browse/part_pkg.cgi
@@ -0,0 +1,172 @@
+<!-- mason kludge -->
+<%
+
+my %search;
+if ( $cgi->param('showdisabled') ) {
+ %search = ();
+} else {
+ %search = ( 'disabled' => '' );
+}
+
+my @part_pkg = qsearch('part_pkg', \%search );
+my $total = scalar(@part_pkg);
+
+my $sortby;
+my %num_active_cust_pkg = ();
+my( $suspended_sth, $canceled_sth ) = ( '', '' );
+if ( $cgi->param('active') ) {
+ my $active_sth = dbh->prepare(
+ 'SELECT COUNT(*) FROM cust_pkg WHERE pkgpart = ?'.
+ ' AND ( cancel IS NULL OR cancel = 0 )'.
+ ' AND ( susp IS NULL OR susp = 0 )'
+ ) or die dbh->errstr;
+ foreach my $part_pkg ( @part_pkg ) {
+ $active_sth->execute($part_pkg->pkgpart) or die $active_sth->errstr;
+ $num_active_cust_pkg{$part_pkg->pkgpart} =
+ $active_sth->fetchrow_arrayref->[0];
+ }
+ $sortby = sub {
+ $num_active_cust_pkg{$b->pkgpart} <=> $num_active_cust_pkg{$a->pkgpart};
+ };
+
+ $suspended_sth = dbh->prepare(
+ 'SELECT COUNT(*) FROM cust_pkg WHERE pkgpart = ?'.
+ ' AND ( cancel IS NULL OR cancel = 0 )'.
+ ' AND susp IS NOT NULL AND susp != 0'
+ ) or die dbh->errstr;
+
+ $canceled_sth = dbh->prepare(
+ 'SELECT COUNT(*) FROM cust_pkg WHERE pkgpart = ?'.
+ ' AND cancel IS NOT NULL AND cancel != 0'
+ ) or die dbh->errstr;
+
+} else {
+ $sortby = sub { $a->pkgpart <=> $b->pkgpart; };
+}
+
+my $conf = new FS::Conf;
+my $taxclasses = $conf->exists('enable_taxclasses');
+
+%>
+<%= header("Package Definition Listing",menubar( 'Main Menu' => $p )) %>
+<% unless ( $cgi->param('active') ) { %>
+ One or more service definitions are grouped together into a package
+ definition and given pricing information. Customers purchase packages
+ rather than purchase services directly.<BR><BR>
+ <A HREF="<%= $p %>edit/part_pkg.cgi"><I>Add a new package definition</I></A>
+ <BR><BR>
+<% } %>
+
+<%= $total %> package definitions
+<% if ( $cgi->param('showdisabled') ) { $cgi->param('showdisabled', 0); %>
+ ( <a href="<%= $cgi->self_url %>">hide disabled packages</a> )
+<% } else { $cgi->param('showdisabled', 1); %>
+ ( <a href="<%= $cgi->self_url %>">show disabled packages</a> )
+<% } %>
+
+<% my $colspan = $cgi->param('showdisabled') ? 2 : 3; %>
+
+<%= &table() %>
+ <TR>
+ <TH COLSPAN=<%= $colspan %>>Package</TH>
+ <TH>Comment</TH>
+<% if ( $cgi->param('active') ) { %>
+ <TH><FONT SIZE=-1>Customer<BR>packages</FONT></TH>
+<% } %>
+ <TH><FONT SIZE=-1>Freq.</FONT></TH>
+<% if ( $taxclasses ) { %>
+ <TH><FONT SIZE=-1>Taxclass</FONT></TH>
+<% } %>
+ <TH><FONT SIZE=-1>Plan</FONT></TH>
+ <TH><FONT SIZE=-1>Data</FONT></TH>
+ <TH>Service</TH>
+ <TH><FONT SIZE=-1>Quan.</FONT></TH>
+<% if ( dbdef->table('pkg_svc')->column('primary_svc') ) { %>
+ <TH><FONT SIZE=-1>Primary</FONT></TH>
+<% } %>
+
+ </TR>
+
+<%
+foreach my $part_pkg ( sort $sortby @part_pkg ) {
+ my($hashref)=$part_pkg->hashref;
+ my(@pkg_svc)=grep $_->getfield('quantity'),
+ qsearch('pkg_svc',{'pkgpart'=> $hashref->{pkgpart} });
+ my($rowspan)=scalar(@pkg_svc);
+ my $plandata;
+ if ( $hashref->{plan} ) {
+ $plandata = $hashref->{plandata};
+ $plandata =~ s/^(\w+)=/$1&nbsp;/mg;
+ $plandata =~ s/\n/<BR>/g;
+ } else {
+ $hashref->{plan} = "(legacy)";
+ $plandata = "Setup&nbsp;". $hashref->{setup}.
+ "<BR>Recur&nbsp;". $hashref->{recur};
+ }
+%>
+ <TR>
+ <TD ROWSPAN=<%= $rowspan %>><A HREF="<%=$p%>edit/part_pkg.cgi?<%= $hashref->{pkgpart} %>"><%= $hashref->{pkgpart} %></A></TD>
+
+<% unless ( $cgi->param('showdisabled') ) { %>
+ <TD ROWSPAN=<%= $rowspan %>>
+ <% if ( $hashref->{disabled} ) { %>
+ DISABLED
+ <% } %>
+ </TD>
+<% } %>
+
+ <TD ROWSPAN=<%= $rowspan %>><A HREF="<%=$p%>edit/part_pkg.cgi?<%= $hashref->{pkgpart} %>"><%= $hashref->{pkg} %></A></TD>
+ <TD ROWSPAN=<%= $rowspan %>><%= $hashref->{comment} %></TD>
+
+<% if ( $cgi->param('active') ) { %>
+ <TD ROWSPAN=<%= $rowspan %>>
+ <FONT COLOR="#00CC00"><B><%= $num_active_cust_pkg{$hashref->{'pkgpart'}} %></B></FONT>&nbsp;<A HREF="<%=$p%>search/cust_pkg.cgi?magic=active;pkgpart=<%= $hashref->{pkgpart} %>">active</A><BR>
+
+ <% $suspended_sth->execute( $part_pkg->pkgpart )
+ or die $suspended_sth->errstr;
+ my $num_suspended = $suspended_sth->fetchrow_arrayref->[0];
+ %>
+ <FONT COLOR="#FF9900"><B><%= $num_suspended %></B></FONT>&nbsp;<A HREF="<%=$p%>search/cust_pkg.cgi?magic=suspended;pkgpart=<%= $hashref->{pkgpart} %>">suspended</A><BR>
+
+ <% $canceled_sth->execute( $part_pkg->pkgpart )
+ or die $canceled_sth->errstr;
+ my $num_canceled = $canceled_sth->fetchrow_arrayref->[0];
+ %>
+ <FONT COLOR="#FF0000"><B><%= $num_canceled %></B></FONT>&nbsp;<A HREF="<%=$p%>search/cust_pkg.cgi?magic=canceled;pkgpart=<%= $hashref->{pkgpart} %>">canceled</A>
+ </TD>
+<% } %>
+
+ <TD ROWSPAN=<%= $rowspan %>><%= $hashref->{freq} %></TD>
+
+<% if ( $taxclasses ) { %>
+ <TD ROWSPAN=<%= $rowspan %>><%= $hashref->{taxclass} || '&nbsp;' %></TD>
+<% } %>
+
+ <TD ROWSPAN=<%= $rowspan %>><%= $hashref->{plan} %></TD>
+ <TD ROWSPAN=<%= $rowspan %>><%= $plandata %></TD>
+
+<%
+ my($pkg_svc);
+ my($n)="";
+ foreach $pkg_svc ( @pkg_svc ) {
+ my($svcpart)=$pkg_svc->getfield('svcpart');
+ my($part_svc) = qsearchs('part_svc',{'svcpart'=> $svcpart });
+ print $n,qq!<TD><A HREF="${p}edit/part_svc.cgi?$svcpart">!,
+ $part_svc->getfield('svc'),"</A></TD><TD>",
+ $pkg_svc->getfield('quantity'),"</TD>";
+ if ( dbdef->table('pkg_svc')->column('primary_svc') ) {
+ print '<TD>';
+ print 'PRIMARY' if $pkg_svc->primary_svc =~ /^Y/i;
+ print '</TD>';
+ }
+ print "</TR>\n";
+ $n="<TR>";
+ }
+%>
+
+ </TR>
+<% } %>
+
+ </TABLE>
+ </BODY>
+</HTML>
diff --git a/httemplate/browse/part_referral.cgi b/httemplate/browse/part_referral.cgi
new file mode 100755
index 0000000..581e01b
--- /dev/null
+++ b/httemplate/browse/part_referral.cgi
@@ -0,0 +1,97 @@
+<!-- mason kludge -->
+<%= header("Advertising source Listing", menubar(
+ 'Main Menu' => $p,
+# 'Add new referral' => "../edit/part_referral.cgi",
+)) %>
+Where a customer heard about your service. Tracked for informational purposes.
+<BR><BR>
+<A HREF="<%= $p %>edit/part_referral.cgi"><I>Add a new advertising source</I></A>
+<BR><BR>
+
+<%
+ my $today = timelocal(0, 0, 0, (localtime(time))[3..5] );
+ my %after;
+ tie %after, 'Tie::IxHash',
+ 'Today' => 0,
+ 'Yesterday' => 86400, # 60sec * 60min * 24hrs
+ 'Past week' => 518400, # 60sec * 60min * 24hrs * 6days
+ 'Past 30 days' => 2505600, # 60sec * 60min * 24hrs * 29days
+ 'Past 60 days' => 5097600, # 60sec * 60min * 24hrs * 59days
+ 'Past 90 days' => 7689600, # 60sec * 60min * 24hrs * 89days
+ 'Past 6 months' => 15724800, # 60sec * 60min * 24hrs * 182days
+ 'Past year' => 31486000, # 60sec * 60min * 24hrs * 364days
+ 'Total' => $today,
+ ;
+ my %before = (
+ 'Today' => 86400, # 60sec * 60min * 24hrs
+ 'Yesterday' => 0,
+ 'Past week' => 86400, # 60sec * 60min * 24hrs
+ 'Past 30 days' => 86400, # 60sec * 60min * 24hrs
+ 'Past 60 days' => 86400, # 60sec * 60min * 24hrs
+ 'Past 90 days' => 86400, # 60sec * 60min * 24hrs
+ 'Past 6 months' => 86400, # 60sec * 60min * 24hrs
+ 'Past year' => 86400, # 60sec * 60min * 24hrs
+ 'Total' => 86400, # 60sec * 60min * 24hrs
+ );
+
+ my $statement = "SELECT COUNT(*) FROM h_cust_main
+ WHERE history_action = 'insert'
+ AND refnum = ?
+ AND history_date >= ?
+ AND history_date < ?
+ ";
+ my $sth = dbh->prepare($statement)
+ or die dbh->errstr;
+%>
+
+<%= table() %>
+<TR>
+ <TH COLSPAN=2 ROWSPAN=2>Advertising source</TH>
+ <TH COLSPAN=<%= scalar(keys %after) %>>Customers</TH>
+</TR>
+<% for my $period ( keys %after ) { %>
+ <TH><FONT SIZE=-1><%= $period %></FONT></TH>
+<% } %>
+</TR>
+
+<%
+foreach my $part_referral ( sort {
+ $a->getfield('refnum') <=> $b->getfield('refnum')
+} qsearch('part_referral',{}) ) {
+%>
+ <TR>
+ <TD><A HREF="<%= $p %>edit/part_referral.cgi?<%= $part_referral->refnum %>">
+ <%= $part_referral->refnum %></A></TD>
+ <TD><A HREF="<%= $p %>edit/part_referral.cgi?<%= $part_referral->refnum %>">
+ <%= $part_referral->referral %></A></TD>
+ <% for my $period ( keys %after ) {
+ $sth->execute( $part_referral->refnum,
+ $today-$after{$period},
+ $today+$before{$period},
+ ) or die $sth->errstr;
+ my $number = $sth->fetchrow_arrayref->[0];
+ %>
+ <TD ALIGN="right"><%= $number %></TD>
+ <% } %>
+ </TR>
+<% } %>
+
+<%
+ $statement =~ s/AND refnum = \?//;
+ $sth = dbh->prepare($statement)
+ or die dbh->errstr;
+%>
+ <TR>
+ <TH COLSPAN=2>Total</TH>
+ <% for my $period ( keys %after ) {
+ $sth->execute( $today-$after{$period},
+ $today+$before{$period},
+ ) or die $sth->errstr;
+ my $number = $sth->fetchrow_arrayref->[0];
+ %>
+ <TD ALIGN="right"><%= $number %></TD>
+ <% } %>
+ </TR>
+ </TABLE>
+ </BODY>
+</HTML>
diff --git a/httemplate/browse/part_svc.cgi b/httemplate/browse/part_svc.cgi
new file mode 100755
index 0000000..ef0de13
--- /dev/null
+++ b/httemplate/browse/part_svc.cgi
@@ -0,0 +1,133 @@
+<!-- mason kludge -->
+<%
+
+my %search;
+if ( $cgi->param('showdisabled') ) {
+ %search = ();
+} else {
+ %search = ( 'disabled' => '' );
+}
+
+my @part_svc =
+ sort { $a->getfield('svcpart') <=> $b->getfield('svcpart') }
+ qsearch('part_svc', \%search );
+my $total = scalar(@part_svc);
+
+my %num_active_cust_svc = ();
+if ( $cgi->param('active') ) {
+ my $active_sth = dbh->prepare(
+ 'SELECT COUNT(*) FROM cust_svc WHERE svcpart = ?'
+ ) or die dbh->errstr;
+ foreach my $part_svc ( @part_svc ) {
+ $active_sth->execute($part_svc->svcpart) or die $active_sth->errstr;
+ $num_active_cust_svc{$part_svc->svcpart} =
+ $active_sth->fetchrow_arrayref->[0];
+ }
+ @part_svc = sort { $num_active_cust_svc{$b->svcpart} <=>
+ $num_active_cust_svc{$a->svcpart} } @part_svc;
+}
+
+%>
+<%= header('Service Definition Listing', menubar( 'Main Menu' => $p) ) %>
+
+<SCRIPT>
+function part_export_areyousure(href) {
+ if (confirm("Are you sure you want to delete this export?") == true)
+ window.location.href = href;
+}
+</SCRIPT>
+
+ Service definitions are the templates for items you offer to your customers.<BR><BR>
+
+<FORM METHOD="POST" ACTION="<%= $p %>edit/part_svc.cgi">
+<A HREF="<%= $p %>edit/part_svc.cgi"><I>Add a new service definition</I></A><% if ( @part_svc ) { %>&nbsp;or&nbsp;<SELECT NAME="clone"><OPTION></OPTION>
+<% foreach my $part_svc ( @part_svc ) { %>
+ <OPTION VALUE="<%= $part_svc->svcpart %>"><%= $part_svc->svc %></OPTION>
+<% } %>
+</SELECT><INPUT TYPE="submit" VALUE="Clone existing service">
+<% } %>
+</FORM><BR>
+
+<%= $total %> service definitions
+<%= $cgi->param('showdisabled')
+ ? do { $cgi->param('showdisabled', 0);
+ '( <a href="'. $cgi->self_url. '">hide disabled services</a> )'; }
+ : do { $cgi->param('showdisabled', 1);
+ '( <a href="'. $cgi->self_url. '">show disabled services</a> )'; }
+%>
+<%= table() %>
+ <TR>
+ <TH COLSPAN=<%= $cgi->param('showdisabled') ? 2 : 3 %>>Service</TH>
+ <TH>Table</TH>
+<% if ( $cgi->param('active') ) { %>
+ <TH><FONT SIZE=-1>Customer<BR>Services</FONT></TH>
+<% } %>
+ <TH>Export</TH>
+ <TH>Field</TH>
+ <TH COLSPAN=2>Modifier</TH>
+ </TR>
+
+<% foreach my $part_svc ( @part_svc ) {
+ my $hashref = $part_svc->hashref;
+ my $svcdb = $hashref->{svcdb};
+ my $svc_x = "FS::$svcdb"->new( { svcpart => $part_svc->svcpart } );
+ my @dfields = $svc_x->fields;
+ push @dfields, 'usergroup' if $svcdb eq 'svc_acct'; #kludge
+ my @fields =
+ grep { $svc_x->pvf($_)
+ or $_ ne 'svcnum' && $part_svc->part_svc_column($_)->columnflag }
+ @dfields ;
+ my $rowspan = scalar(@fields) || 1;
+ my $url = "${p}edit/part_svc.cgi?$hashref->{svcpart}";
+%>
+
+ <TR>
+ <TD ROWSPAN=<%= $rowspan %>><A HREF="<%= $url %>">
+ <%= $hashref->{svcpart} %></A></TD>
+<% unless ( $cgi->param('showdisabled') ) { %>
+ <TD ROWSPAN=<%= $rowspan %>>
+ <%= $hashref->{disabled} ? 'DISABLED' : '' %></TD>
+<% } %>
+ <TD ROWSPAN=<%= $rowspan %>><A HREF="<%= $url %>">
+ <%= $hashref->{svc} %></A></TD>
+ <TD ROWSPAN=<%= $rowspan %>>
+ <%= $hashref->{svcdb} %></TD>
+<% if ( $cgi->param('active') ) { %>
+ <TD ROWSPAN=<%= $rowspan %>>
+ <FONT COLOR="#00CC00"><B><%= $num_active_cust_svc{$hashref->{svcpart}} %></B></FONT>&nbsp;<A HREF="<%=$p%>search/<%= $hashref->{svcdb} %>.cgi?svcpart=<%= $hashref->{svcpart} %>">active</A>
+ </TD>
+<% } %>
+ <TD ROWSPAN=<%= $rowspan %>><%= itable() %>
+<%
+# my @part_export =
+map { qsearchs('part_export', { exportnum => $_->exportnum } ) } qsearch('export_svc', { svcpart => $part_svc->svcpart } ) ;
+ foreach my $part_export (
+ map { qsearchs('part_export', { exportnum => $_->exportnum } ) }
+ qsearch('export_svc', { svcpart => $part_svc->svcpart } )
+ ) {
+%>
+ <TR>
+ <TD><A HREF="<%= $p %>edit/part_export.cgi?<%= $part_export->exportnum %>"><%= $part_export->exportnum %>:&nbsp;<%= $part_export->exporttype %>&nbsp;to&nbsp;<%= $part_export->machine %></A></TD></TR>
+<% } %>
+ </TABLE></TD>
+
+<% my($n1)='';
+ foreach my $field ( @fields ) {
+ my $flag = $part_svc->part_svc_column($field)->columnflag;
+%>
+ <%= $n1 %><TD><%= $field %></TD><TD>
+
+<% if ( $flag eq "D" ) { print "Default"; }
+ elsif ( $flag eq "F" ) { print "Fixed"; }
+ elsif ( not $flag ) { }
+ else { print "(Unknown!)"; }
+%>
+ </TD><TD><%= $part_svc->part_svc_column($field)->columnvalue%></TD>
+<% $n1="</TR><TR>";
+ }
+%>
+ </TR>
+<% } %>
+</TABLE>
+</BODY>
+</HTML>
diff --git a/httemplate/browse/part_virtual_field.cgi b/httemplate/browse/part_virtual_field.cgi
new file mode 100644
index 0000000..a0009da
--- /dev/null
+++ b/httemplate/browse/part_virtual_field.cgi
@@ -0,0 +1,39 @@
+<%= header('Virtual field definitions', menubar('Main Menu' => $p)) %>
+<%
+
+my %pvfs;
+my $block;
+my $p2 = popurl(2);
+my $dbtable;
+
+foreach (qsearch('part_virtual_field', {})) {
+ push @{ $pvfs{$_->dbtable} }, $_;
+}
+%>
+
+<% if ($cgi->param('error')) { %>
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+ <BR><BR>
+<% } %>
+
+<A HREF="<%=$p2%>edit/part_virtual_field.cgi"><I>Add a new field</I></A><BR><BR>
+
+<% foreach $dbtable (sort { $a cmp $b } keys (%pvfs)) { %>
+<H3><%=$dbtable%></H3>
+
+<%=table()%>
+<TH><TD>Field name</TD><TD>Description</TD></TH>
+<% foreach my $pvf (sort {$a->name cmp $b->name} @{ $pvfs{$dbtable} }) { %>
+ <TR>
+ <TD></TD>
+ <TD>
+ <A HREF="<%=$p2%>edit/part_virtual_field.cgi?<%=$pvf->vfieldpart%>">
+ <%=$pvf->name%></A></TD>
+ <TD><%=$pvf->label%></TD>
+ </TR>
+<% } %>
+</TABLE>
+<% } %>
+</BODY>
+</HTML>
+
diff --git a/httemplate/browse/queue.cgi b/httemplate/browse/queue.cgi
new file mode 100755
index 0000000..b53c140
--- /dev/null
+++ b/httemplate/browse/queue.cgi
@@ -0,0 +1,7 @@
+<!-- mason kludge -->
+<%
+
+print header("Job Queue", menubar( 'Main Menu' => $p, )).
+ joblisting({}). '</BODY></HTML>';
+
+%>
diff --git a/httemplate/browse/router.cgi b/httemplate/browse/router.cgi
new file mode 100644
index 0000000..149db49
--- /dev/null
+++ b/httemplate/browse/router.cgi
@@ -0,0 +1,57 @@
+<%= header('Routers', menubar('Main Menu' => $p)) %>
+<%
+
+my @router = qsearch('router', {});
+my $p2 = popurl(2);
+
+%>
+
+<% if ($cgi->param('error')) { %>
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+ <BR><BR>
+<% } %>
+
+<%
+my $hidecustomerrouters = 0;
+my $hideurl = '';
+if ($cgi->param('hidecustomerrouters') eq '1') {
+ $hidecustomerrouters = 1;
+ $cgi->param('hidecustomerrouters', 0);
+ $hideurl = '<A HREF="' . $cgi->self_url() . '">Show customer routers</A>';
+} else {
+ $hidecustomerrouters = 0;
+ $cgi->param('hidecustomerrouters', 1);
+ $hideurl = '<A HREF="' . $cgi->self_url() . '">Hide customer routers</A>';
+}
+%>
+
+<A HREF="<%=$p2%>edit/router.cgi">Add a new router</A>&nbsp;|&nbsp;<%=$hideurl%>
+
+<%=table()%>
+ <TR>
+ <TD><B>Router name</B></TD>
+ <TD><B>Address block(s)</B></TD>
+ </TR>
+<% foreach my $router (sort {$a->routernum <=> $b->routernum} @router) {
+ next if $hidecustomerrouters && $router->svcnum;
+ my @addr_block = $router->addr_block;
+ if (scalar(@addr_block) == 0) {
+ push @addr_block, '&nbsp;';
+ }
+%>
+ <TR>
+ <TD ROWSPAN="<%=scalar(@addr_block)+1%>">
+ <A HREF="<%=$p2%>edit/router.cgi?<%=$router->routernum%>"><%=$router->routername%></A>
+ </TD>
+ </TR>
+ <% foreach my $block ( @addr_block ) { %>
+ <TR>
+ <TD><%=UNIVERSAL::isa($block, 'FS::addr_block') ? $block->NetAddr : '&nbsp;'%></TD>
+ </TR>
+ <% } %>
+ </TR>
+<% } %>
+</TABLE>
+</BODY>
+</HTML>
+
diff --git a/httemplate/browse/svc_acct_pop.cgi b/httemplate/browse/svc_acct_pop.cgi
new file mode 100755
index 0000000..44cda81
--- /dev/null
+++ b/httemplate/browse/svc_acct_pop.cgi
@@ -0,0 +1,63 @@
+<!-- mason kludge -->
+<%
+ my $accounts_sth = dbh->prepare("SELECT COUNT(*) FROM svc_acct
+ WHERE popnum = ? ")
+ or die dbh->errstr;
+%>
+<%= header('Access Number Listing', menubar( 'Main Menu' => $p )) %>
+Points of Presence<BR><BR>
+<A HREF="<%= $p %>edit/svc_acct_pop.cgi"><I>Add new Access Number</I></A><BR><BR>
+<%= table() %>
+ <TR>
+ <TH></TH>
+ <TH>City</TH>
+ <TH>State</TH>
+ <TH>Area code</TH>
+ <TH>Exchange</TH>
+ <TH>Local</TH>
+ <TH>Accounts</TH>
+ </TR>
+
+<%
+foreach my $svc_acct_pop ( sort {
+ #$a->getfield('popnum') <=> $b->getfield('popnum')
+ $a->state cmp $b->state || $a->city cmp $b->city
+ || $a->ac <=> $b->ac || $a->exch <=> $b->exch || $a->loc <=> $b->loc
+} qsearch('svc_acct_pop',{}) ) {
+
+ my $svc_acct_pop_link = $p . 'edit/svc_acct_pop.cgi?'. $svc_acct_pop->popnum;
+
+ $accounts_sth->execute($svc_acct_pop->popnum) or die $accounts_sth->errstr;
+ my $num_accounts = $accounts_sth->fetchrow_arrayref->[0];
+
+ my $svc_acct_link = $p. 'search/svc_acct.cgi?popnum='. $svc_acct_pop->popnum;
+
+%>
+ <TR>
+ <TD><A HREF="<%= $svc_acct_pop_link %>">
+ <%= $svc_acct_pop->popnum %></A></TD>
+ <TD><A HREF="<%= $svc_acct_pop_link %>">
+ <%= $svc_acct_pop->city %></A></TD>
+ <TD><A HREF="<%= $svc_acct_pop_link %>">
+ <%= $svc_acct_pop->state %></A></TD>
+ <TD><A HREF="<%= $svc_acct_pop_link %>">
+ <%= $svc_acct_pop->ac %></A></TD>
+ <TD><A HREF="<%= $svc_acct_pop_link %>">
+ <%= $svc_acct_pop->exch %></A></TD>
+ <TD><A HREF="<%= $svc_acct_pop_link %>">
+ <%= $svc_acct_pop->loc %></A></TD>
+ <TD>
+ <FONT COLOR="#00CC00"><B><%= $num_accounts %></B></FONT>
+ <% if ( $num_accounts ) { %><A HREF="<%= $svc_acct_link %>"><% } %>
+ active
+ <% if ( $num_accounts ) { %></A><% } %>
+ </TD>
+ </TR>
+<% } %>
+
+ <TR>
+ </TR>
+ </TABLE>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/config/config-process.cgi b/httemplate/config/config-process.cgi
new file mode 100644
index 0000000..2597132
--- /dev/null
+++ b/httemplate/config/config-process.cgi
@@ -0,0 +1,51 @@
+<%
+ my $conf = new FS::Conf;
+ $FS::Conf::DEBUG = 1;
+ my @config_items = $conf->config_items;
+
+ foreach my $i ( @config_items ) {
+ my @touch = ();
+ my @delete = ();
+ my $n = 0;
+ foreach my $type ( ref($i->type) ? @{$i->type} : $i->type ) {
+ if ( $type eq '' ) {
+ } elsif ( $type eq 'textarea' ) {
+ if ( $cgi->param($i->key. $n) ne '' ) {
+ my $value = $cgi->param($i->key. $n);
+ $value =~ s/\r\n/\n/g; #browsers?
+ $conf->set($i->key, $value);
+ } else {
+ $conf->delete($i->key);
+ }
+ } elsif ( $type eq 'checkbox' ) {
+# if ( defined($cgi->param($i->key. $n)) && $cgi->param($i->key. $n) ) {
+ if ( defined $cgi->param($i->key. $n) ) {
+ #$conf->touch($i->key);
+ push @touch, $i->key;
+ } else {
+ #$conf->delete($i->key);
+ push @delete, $i->key;
+ }
+ } elsif ( $type eq 'text' || $type eq 'select' ) {
+ if ( $cgi->param($i->key. $n) ne '' ) {
+ $conf->set($i->key, $cgi->param($i->key. $n));
+ } else {
+ $conf->delete($i->key);
+ }
+ } elsif ( $type eq 'editlist' || $type eq 'selectmultiple' ) {
+ if ( scalar(@{[ $cgi->param($i->key. $n) ]}) ) {
+ $conf->set($i->key, join("\n", @{[ $cgi->param($i->key. $n) ]} ));
+ } else {
+ $conf->delete($i->key);
+ }
+ } else {
+ }
+ $n++;
+ }
+ # warn @touch;
+ $conf->touch($_) foreach @touch;
+ $conf->delete($_) foreach @delete;
+ }
+
+%>
+<%= $cgi->redirect("config-view.cgi") %>
diff --git a/httemplate/config/config-view.cgi b/httemplate/config/config-view.cgi
new file mode 100644
index 0000000..9a00067
--- /dev/null
+++ b/httemplate/config/config-view.cgi
@@ -0,0 +1,64 @@
+<!-- mason kludge -->
+<%= header('View Configuration', menubar( 'Main Menu' => $p,
+ 'Edit Configuration' => 'config.cgi' ) ) %>
+
+<% my $conf = new FS::Conf; my @config_items = $conf->config_items; %>
+
+<% foreach my $section ( qw(required billing username password UI session
+ shell BIND
+ ),
+ '', 'deprecated') { %>
+ <A NAME="<%= $section || 'unclassified' %>"></A>
+ <FONT SIZE="-2">
+ <% foreach my $nav_section ( qw(required billing username password UI session
+ shell BIND
+ ),
+ '', 'deprecated') { %>
+ <% if ( $section eq $nav_section ) { %>
+ [<A NAME="not<%= $nav_section || 'unclassified' %>" style="background-color: #cccccc"><%= ucfirst($nav_section || 'unclassified') %></A>]
+ <% } else { %>
+ [<A HREF="#<%= $nav_section || 'unclassified' %>"><%= ucfirst($nav_section || 'unclassified') %></A>]
+ <% } %>
+ <% } %>
+ </FONT><BR>
+ <%= table("#cccccc", 2) %>
+ <tr>
+ <th colspan="2" bgcolor="#dcdcdc">
+ <%= ucfirst($section || 'unclassified') %> configuration options
+ </th>
+ </tr>
+ <% foreach my $i (grep $_->section eq $section, @config_items) { %>
+ <tr>
+ <td><a name="<%= $i->key %>">
+ <b><%= $i->key %></b>&nbsp;-&nbsp;<%= $i->description %>
+ </a></td>
+ <td><table border=0>
+ <% foreach my $type ( ref($i->type) ? @{$i->type} : $i->type ) {
+ my $n = 0; %>
+ <% if ( $type eq '' ) { %>
+ <tr><td><font color="#ff0000">no type</font></td></tr>
+ <% } elsif ( $type eq 'textarea'
+ || $type eq 'editlist'
+ || $type eq 'selectmultiple' ) { %>
+ <tr><td bgcolor="#ffffff">
+<pre>
+<%= encode_entities(join("\n", $conf->config($i->key) ) ) %>
+</pre>
+ </td></tr>
+ <% } elsif ( $type eq 'checkbox' ) { %>
+ <tr><td bgcolor="#<%= $conf->exists($i->key) ? '00ff00">YES' : 'ff0000">NO' %></td></tr>
+ <% } elsif ( $type eq 'text' || $type eq 'select' ) { %>
+ <tr><td bgcolor="#ffffff"><%= $conf->exists($i->key) ? $conf->config($i->key) : '' %></td></tr>
+ <% } else { %>
+ <tr><td>
+ <font color="#ff0000">unknown type <%= $type %></font>
+ </td></tr>
+ <% } %>
+ <% $n++; } %>
+ </table></td>
+ </tr>
+ <% } %>
+ </table><br><br>
+<% } %>
+
+</body></html>
diff --git a/httemplate/config/config.cgi b/httemplate/config/config.cgi
new file mode 100644
index 0000000..409869e
--- /dev/null
+++ b/httemplate/config/config.cgi
@@ -0,0 +1,176 @@
+<!-- mason kludge -->
+<%= header('Edit Configuration', menubar( 'Main Menu' => $p ) ) %>
+<SCRIPT>
+var gSafeOnload = new Array();
+var gSafeOnsubmit = new Array();
+window.onload = SafeOnload;
+function SafeAddOnLoad(f) {
+ gSafeOnload[gSafeOnload.length] = f;
+}
+function SafeOnload() {
+ for (var i=0;i<gSafeOnload.length;i++)
+ gSafeOnload[i]();
+}
+function SafeAddOnSubmit(f) {
+ gSafeOnsubmit[gSafeOnsubmit.length] = f;
+}
+function SafeOnsubmit() {
+ for (var i=0;i<gSafeOnsubmit.length;i++)
+ gSafeOnsubmit[i]();
+}
+</SCRIPT>
+
+<% my $conf = new FS::Conf; my @config_items = $conf->config_items; %>
+
+<form name="OneTrueForm" action="config-process.cgi" METHOD="POST" onSubmit="SafeOnsubmit()">
+
+<% foreach my $section ( qw(required billing username password UI session
+ shell BIND
+ ),
+ '', 'deprecated') { %>
+ <A NAME="<%= $section || 'unclassified' %>"></A>
+ <FONT SIZE="-2">
+ <% foreach my $nav_section ( qw(required billing username password UI session
+ shell BIND
+ ),
+ '', 'deprecated') { %>
+ <% if ( $section eq $nav_section ) { %>
+ [<A NAME="not<%= $nav_section || 'unclassified' %>" style="background-color: #cccccc"><%= ucfirst($nav_section || 'unclassified') %></A>]
+ <% } else { %>
+ [<A HREF="#<%= $nav_section || 'unclassified' %>"><%= ucfirst($nav_section || 'unclassified') %></A>]
+ <% } %>
+ <% } %>
+ </FONT><BR>
+ <%= table("#cccccc", 2) %>
+ <tr>
+ <th colspan="2" bgcolor="#dcdcdc">
+ <%= ucfirst($section || 'unclassified') %> configuration options
+ </th>
+ </tr>
+ <% foreach my $i (grep $_->section eq $section, @config_items) { %>
+ <tr>
+ <td>
+ <% my $n = 0;
+ foreach my $type ( ref($i->type) ? @{$i->type} : $i->type ) {
+ #warn $i->key unless defined($type);
+ %>
+ <% if ( $type eq '' ) { %>
+ <font color="#ff0000">no type</font>
+ <% } elsif ( $type eq 'textarea' ) { %>
+ <textarea name="<%= $i->key. $n %>" rows=5><%= "\n". join("\n", $conf->config($i->key) ) %></textarea>
+ <% } elsif ( $type eq 'checkbox' ) { %>
+ <input name="<%= $i->key. $n %>" type="checkbox" value="1"<%= $conf->exists($i->key) ? ' CHECKED' : '' %>>
+ <% } elsif ( $type eq 'text' ) { %>
+ <input name="<%= $i->key. $n %>" type="<%= $type %>" value="<%= $conf->exists($i->key) ? $conf->config($i->key) : '' %>">
+ <% } elsif ( $type eq 'select' || $type eq 'selectmultiple' ) { %>
+ <select name="<%= $i->key. $n %>" <%= $type eq 'selectmultiple' ? 'MULTIPLE' : '' %>>
+ <% my %saw;
+ foreach my $value ( "", @{$i->select_enum} ) {
+ local($^W)=0; next if $saw{$value}++; %>
+ <option value="<%= $value %>"<%= $value eq $conf->config($i->key) || ( $type eq 'selectmultiple' && grep { $_ eq $value } $conf->config($i->key) ) ? ' SELECTED' : '' %>><%= $value %>
+ <% } %>
+ <% if ( $conf->exists($i->key) && $conf->config($i->key) && ! grep { $conf->config($i->key) eq $_ } @{$i->select_enum}) { %>
+ <option value=<%= $conf->config($i->key) %> SELECTED><%= $conf->config($i->key) %>
+ <% } %>
+ </select>
+ <% } elsif ( $type eq 'editlist' ) { %>
+ <script>
+ function doremove<%= $i->key. $n %>() {
+ fromObject = document.OneTrueForm.<%= $i->key. $n %>;
+ for (var i=fromObject.options.length-1;i>-1;i--) {
+ if (fromObject.options[i].selected)
+ deleteOption<%= $i->key. $n %>(fromObject,i);
+ }
+ }
+ function deleteOption<%= $i->key. $n %>(object,index) {
+ object.options[index] = null;
+ }
+ function selectall<%= $i->key. $n %>() {
+ fromObject = document.OneTrueForm.<%= $i->key. $n %>;
+ for (var i=fromObject.options.length-1;i>-1;i--) {
+ fromObject.options[i].selected = true;
+ }
+ }
+ function doadd<%= $i->key. $n %>(object) {
+ var myvalue = "";
+ <% if ( defined($i->editlist_parts) ) { %>
+
+ <% foreach my $pnum ( 0 .. scalar(@{$i->editlist_parts})-1 ) { %>
+
+ if ( myvalue != "" ) { myvalue = myvalue + " "; }
+ <% if ( $i->editlist_parts->[$pnum]{type} eq 'select' ) { %>
+ myvalue = myvalue + object.add<%= $i->key. $n . "_$pnum" %>.options[object.add<%= $i->key. $n . "_$pnum" %>.selectedIndex].value;
+ <!-- #RESET SELECT?? maybe not... -->
+ <% } elsif ( $i->editlist_parts->[$pnum]{type} eq 'immutable' ) { %>
+ myvalue = myvalue + object.add<%= $i->key. $n . "_$pnum" %>.value;
+ <% } else { %>
+ myvalue = myvalue + object.add<%= $i->key. $n . "_$pnum" %>.value;
+ object.add<%= $i->key. $n. "_$pnum" %>.value = "";
+ <% } %>
+
+
+ <% } %>
+ <% } else { %>
+ myvalue = object.add<%= $i->key. $n. "_1" %>.value;
+ <% } %>
+ var optionName = new Option(myvalue, myvalue);
+ var length = object.<%= $i->key. $n %>.length;
+ object.<%= $i->key. $n %>.options[length] = optionName;
+ }
+ </script>
+ <select multiple size=5 name="<%= $i->key. $n %>">
+ <option selected>----------------------------------------------------------------</option>
+ <% foreach my $line ( $conf->config($i->key) ) { %>
+ <option value="<%= $line %>"><%= $line %></option>
+ <% } %>
+ </select><br>
+ <input type="button" value="remove selected" onClick="doremove<%= $i->key. $n %>()">
+ <script>SafeAddOnLoad(doremove<%= $i->key. $n %>);
+ SafeAddOnSubmit(selectall<%= $i->key. $n %>);</script>
+ <br>
+ <%= itable() %><tr>
+ <% if ( defined $i->editlist_parts ) { %>
+ <% my $pnum=0; foreach my $part ( @{$i->editlist_parts} ) { %>
+ <td>
+ <% if ( $part->{type} eq 'text' ) { %>
+ <input type="text" name="add<%= $i->key. $n."_$pnum" %>">
+ <% } elsif ( $part->{type} eq 'immutable' ) { %>
+ <%= $part->{value} %><input type="hidden" name="add<%= $i->key. $n. "_$pnum" %>" value="<%= $part->{value} %>">
+ <% } elsif ( $part->{type} eq 'select' ) { %>
+ <select name="add<%= $i->key. $n. "_$pnum" %>">
+ <% foreach my $key ( keys %{$part->{select_enum}} ) { %>
+ <option value="<%= $key %>"><%= $part->{select_enum}{$key} %></option>
+ <% } %>
+ </select>
+ <% } else { %>
+ <font color="#ff0000">unknown type <%= $part->type %></font>
+ <% } %>
+ </td>
+ <% $pnum++; } %>
+ <% } else { %>
+ <td><input type="text" name="add<%= $i->key. $n %>_0"></td>
+ <% } %>
+ <td><input type="button" value="add" onClick="doadd<%= $i->key. $n %>(this.form)"></td>
+ </tr></table>
+ <% } else { %>
+ <font color="#ff0000">unknown type <%= $type %></font>
+ <% } %>
+ <% $n++; } %>
+ </td>
+ <td><a name="<%= $i->key %>">
+ <b><%= $i->key %></b> - <%= $i->description %>
+ </a></td>
+ </tr>
+ <% } %>
+ </table><br>
+
+ You may need to restart Apache and/or freeside-queued for configuration
+ changes to take effect.<br>
+
+ <input type="submit" value="Apply changes"><br><br>
+
+<% } %>
+
+</form>
+
+</body></html>
diff --git a/httemplate/docs/ach.html b/httemplate/docs/ach.html
new file mode 100644
index 0000000..b79df78
--- /dev/null
+++ b/httemplate/docs/ach.html
@@ -0,0 +1,12 @@
+<HTML>
+ <HEAD>
+ <TITLE>
+ Electronic check (ACH) information
+ </TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <IMG BORDER=0 SRC="../images/ach.png">
+ <BR>
+ <CENTER><A HREF="javascript:close()">(close window)</A></CENTER>
+ </BODY>
+</HTML>
diff --git a/httemplate/docs/admin.html b/httemplate/docs/admin.html
new file mode 100755
index 0000000..50beafe
--- /dev/null
+++ b/httemplate/docs/admin.html
@@ -0,0 +1,81 @@
+<head>
+ <title>Administration</title>
+</head>
+<body>
+ <h1>Administration</h1>
+</body>
+<ul>
+ <li>Open up the root of the Freeside document tree in your web
+ browser. For example, if you created the Freeside document tree in
+ /home/httpd/html/freeside, and your web browser's DocumentRoot is
+ /home/httpd/html, open https://your_host/freeside/. Replace
+ "your_host" with the name or network address of your web server.
+ <li>Select <u>Configuration</u> from the main menu and update your configuration values.
+ <li>Next you must create a service definition. An example of a service
+ definition would be a dial-up account or a domain. First, it is
+ necessary to create a domain definition. Click on <u>View/Edit service
+ definitions</u> and <u>Add a new service definition</u> with <i>Table</i>
+ <b>svc_domain</b> (and no modifiers).
+
+ <li>Now that you have created your first service, you must create a package
+ including this service which you can sell to customers. Zero, one, or many
+ services are bundled into a package. Click on <u>View/Edit package
+ definitions</u> and <u>Add a new package definition</u> which includes
+ quantity <b>1</b> of the svc_domain service you created above.
+
+ <li>After you create your first package, then you must define who is
+ able to sell that package by creating an agent type. An example of
+ an agent type would be an internal sales representitive which sells
+ regular and promotional packages, as opposed to an external sales
+ representitive which would only sell regular packages of services. Click on
+ <u>View/Edit agent types</u> and <u>Add a new agent type</u>. Allow this
+ agent type to sell the package you created above.
+
+ <li>After creating a new agent type, you must create an agent. Click on
+ <u>View/Edit agents</u> and <u>Add a new agent</u>.
+
+ <li>Set up at least one Advertising source. Advertising sources will help
+ you keep track of how effective your advertising is, tracking where customers
+ heard of your service offerings. You must create at least one advertising
+ source. If you do not wish to use the referral functionality, simply create
+ a single advertising source only. Click on <u>View/Edit advertising
+ sources</u> and <u>Add a new advertising source</u>.
+
+ <li>Click on <u>New Customer</u> and create a new customer for your system
+ accounts with billing type <b>Complimentary</b>.
+
+ <li>From the Customer View screen of the newly created customer, order the
+ package you defined above.
+
+ <li>From the Package View screen of the newly created package, choose
+ <u>(Provision)</u> to add the customer's service for this new package.
+
+ <li>Add your own domain.
+
+ <li>Go back to <u>View/Edit service definitions</u> on the main menu, and
+ <u>Add a new service definition</u> with <i>Table</i> <b>svc_acct</b>.
+ Select your domain in the <b>domsvc</b> Modifier. Set <b>Fixed</b> to define
+ a service locked-in to this domain, or <b>Default</b> to define a service
+ which may select from among this domain and the customer's domains.
+
+ <li><table><tr>
+ <td> Create at least POP (Point of Presence) by selecting
+ <u>View/Edit POPs</u> from the main menu.</td>
+ <th align="left"> OR </th>
+ <td>If you are not doing dialup, set slipip to fixed and blank for all your
+ Service Definitions which have Table <b>svc_acct</b>.</td>
+ </tr></table>
+
+ <li>If you are using Freeside to keep track of sales taxes, define tax
+ information for your locales by clicking on the <u>View/Edit locales and tax
+ rates</b> on the main menu.
+
+ <li>If you would like Freeside to notify your customers when their credit
+ cards and other billing arrangements are about to expire, arrange for
+ <b>freeside-expiration-alerter</b> to be run daily by cron or similar
+ facility. The message it sends can be configured from the
+ <u>Configuration</u> choice of the main menu as <u>alerter_template</u>.
+
+</ul>
+</body>
+</html>
diff --git a/httemplate/docs/billing.html b/httemplate/docs/billing.html
new file mode 100644
index 0000000..1d6f8c4
--- /dev/null
+++ b/httemplate/docs/billing.html
@@ -0,0 +1,54 @@
+<head>
+ <title>Billing</title>
+</head>
+<body>
+ <h1>Billing</h1>
+ <ul>
+ <li>You can bill individual customers by clicking on the <i>Bill now</i> link on the main customer view.
+ <li>The <a href="man/bin/freeside-daily.html"><b>freeside-daily</b></a> script should be run daily to bill customers and run invoice collection events.
+ <li>Real-time credit card processing: Install the <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a> module for your processor. Configure the <a href="../config/config-view.cgi#business-onlinepayment">business-onlinepayment</a> configuration option. Disable the default <b>Batch card</b> <a href="../browse/part_bill_event.cgi">invoice event</a> and add one for Business::OnlinePayment.
+ <li>Optional: Credit card expiration alerts: Customize <a href="../config/config.cgi#alerter_template">alerter_template</a> configuration option and run <a href="man/bin/freeside-expiration-alerter.html">freeside-expiration-alerter</a> daily.
+ <li>Credit card decline alerts: Customize the <a href="../config/config.cgi#declinetemplate">declinetemplate</a> configuration option and set the <a href="../config/config.cgi#emaildecline">emaildecline</a> configuration option.
+ <li>Optional: Invoice template customization
+ <ul>
+ <li>See the <a href="http://search.cpan.org/~mjd/Text-Template/lib/Text/Template.pm">Text::Template</a> documentation for details on the substitution language.
+ <li>You <b>must</b> call the invoice_lines() function at least once - pass it a number of lines, and it returns a list of array references, each of two elements: a service description column, and a price column. Alternatively, call invoice_lines() with no arguments, and pagination will be disabled - all invoice line items will print on one page, with no padding (recommended for email invoices).
+ <li>In addition, the following variables are available:
+ <ul>
+ <li>$invnum - invoice number
+ <li>$date - as a UNIX timestamp (see <a href="http://search.cpan.org/doc/GBARR/TimeDate-1.09/lib/Date/Format.pm">Date::Format</a> for conversion functions).
+ <li>$page - current page
+ <li>$total_pages - total pages
+ <li>@address - A six-element array containing the customer name, company, and address.
+<!-- <li>$overdue - true if this invoice is overdue -->
+ </ul>
+ </ul>
+ <li>Batch credit card processing
+ <ul>
+ <li>After <a href="man/bin/freeside-daily.html"><b>freeside-daily</b></a> is run, a credit card batch will be in the <a href="schema.html#cust_pay_batch">cust_pay_batch</a> table. Export this table to your credit card batching.
+ <li>When your batch completes, erase the cust_pay_batch records in that batch and add any necessary paymants to the <a href="schema.html#cust_pay">cust_pay</a> table. Example code to add payments is:
+ <pre>use FS::cust_pay;
+
+# loop over all records in batch
+
+my $payment=create FS::cust_pay (
+ 'invnum' => $invnum,
+ 'paid' => $paid,
+ '_date' => $_date,
+ 'payby' => $payby,
+ 'payinfo' => $payinfo,
+ 'paybatch' => $paybatch,
+);
+
+my $error=$payment->insert;
+if ( $error ) {
+ #process error
+}
+
+# end loop
+</pre>
+All fields except paybatch are contained in the cust_pay_batch table. You can use paybatch field to track particular batches and/or particular transactions within a batch.
+ </ul>
+<!-- <li>The <a href="man/bin/freeside-print-batch.html"><b>freeside-print-batch</b></a> script can print or email pending credit card batches for manual entry. -->
+ </ul>
+</body>
diff --git a/httemplate/docs/config.html b/httemplate/docs/config.html
new file mode 100644
index 0000000..9caf3bb
--- /dev/null
+++ b/httemplate/docs/config.html
@@ -0,0 +1,36 @@
+<head>
+ <title>Configuration files</title>
+</head>
+<body>
+ <h1>Configuration files</h1>
+<font size="+1" color="#ff0000">Configuration is now done by the top-level Makefile and web interface. The instructions below are no longer necessary.</font>
+<ul>
+ <li>Create the <b>/usr/local/etc/freeside</b> directory to hold your configuration.
+ <li>Setting up <a href="http://www.apache.org/docs/misc/FAQ.html#user-authentication">Apache user authetication</a> is mandatory.
+ <li>Create the <b>/usr/local/etc/freeside/mapsecrets</b> file, which maps Apache users to a secrets file which contains a DBI data source, username and password. Every
+line in <b>/usr/local/etc/freeside/mapsecrets</b> should contain a username and
+filename, separated by whitespace. Note that these are not local usernames -
+they are passed from Apache. <a href="http://www.apache.org/docs/misc/FAQ.html#user-authentication">
+Apache user authetication</a> is mandatory. For example, if you had the Apache users admin,
+john, and sam,
+you mapsecrets file might look like:
+<pre>
+admin secretfile
+john secretfile
+sam secretfile
+</pre>
+ <li>Next, the filename(s) referenced in <b>/usr/local/etc/freeside/mapsecrets</b> file should be created in the <b>/usr/local/etc/freeside/</b> directory. Each file contains three lines: <a href="http://search.cpan.org/doc/TIMB/DBI-1.20/DBI.pm">DBI data source</a> (for example,
+ <tt>DBI:mysql:freeside</tt> or <tt>DBI:Pg:host=localhost;dbname=freeside</tt>), database username, and database password.
+ These files should not be world readable. See the <a href="http://search.cpan.org/doc/TIMB/DBI-1.20/DBI.pm">DBI manpage</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD">manpage for your DBD</a> for the exact syntax of a DBI data source. In a normal installation such as the example above, a single file <b>/usr/local/etc/freeside/secretfile</b> would be created - for example:
+<pre>
+DBI:Pg:host=localhost;dbname=freeside
+dbusername
+dbpassword
+</pre>
+<li>Create the <b>/usr/local/etc/freeside/conf.<i>datasource</i></b> directory, for example, <b>/usr/local/etc/freeside/conf.DBI:Pg:host=localhost;dbname=freeside</b> (remember to backslash-escape the ; character when creating directories in the shell:
+<pre>mkdir&nbsp;/usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=freeside
+</pre>
+<li>The rest of the configuration can be done with the web interface. Select <u>Configuration</u> from the main menu and update your configuration values.
+</ul>
+</body>
+</html>
diff --git a/httemplate/docs/cvv2.html b/httemplate/docs/cvv2.html
new file mode 100644
index 0000000..fe8a17f
--- /dev/null
+++ b/httemplate/docs/cvv2.html
@@ -0,0 +1,25 @@
+<HTML>
+ <HEAD>
+ <TITLE>
+ CVV2 information
+ </TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ The CVV2 number (also called CVC2 or CID) is a three- or four-digit
+ security code used to reduce credit card fraud.<BR><BR>
+ <TABLE BORDER=0 CELLSPACING=4>
+ <TR>
+ <TH>Visa / MasterCard / Discover</TH>
+ <TH>American Express</TH>
+ </TR>
+ <TR>
+ <TD>
+ <IMG BORDER=0 ALT="Visa/MasterCard/Discover" SRC="../images/cvv2.png">
+ </TD>
+ <TD>
+ <IMG BORDER=0 ALT="American Express" SRC="../images/cvv2_amex.png">
+ </TD>
+ </TABLE>
+ <CENTER><A HREF="javascript:close()">(close window)</A></CENTER>
+ </BODY>
+</HTML>
diff --git a/httemplate/docs/export.html b/httemplate/docs/export.html
new file mode 100755
index 0000000..71e3acf
--- /dev/null
+++ b/httemplate/docs/export.html
@@ -0,0 +1,55 @@
+<head>
+ <title>File exporting</title>
+</head>
+<body>
+ <h1>File exporting</h1>
+ <font size="+2">NOTE: This file is OUT OF DATE with the landing of the new export code and is only here for reference. DO NOT follow these instructions. Instead use the new exports in the web interface.</font>
+ <ul>
+ <li>bin/svc_acct.export will create UNIX <b>passwd</b>, <b>shadow</b> and <b>master.passwd</b> files, ERPCD <b>acp_passwd</b> and <b>acp_dialup</b> files and a RADIUS <b>users</b> file in the <b>/usr/local/etc/freeside/export.<i>datasrc</i></b> directory. Some RADIUS servers (such as <a href="http://www.open.com.au/radiator/">Radiator</a>, <a href="ftp://ftp.cheapnet.net/pub/icradius/">ICRADIUS</a> and <a href="http://www.freeradius.org/">FreeRADIUS</a>) will authenticate directly out of an SQL database. In these cases,
+it is reccommended that you replicate (<a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Replication">Replication in MySQL</a>) the data to an external RADIUS machine or point icradius_secrets to the external machine rather than running the RADIUS server on your Freeside machine. Using the appropriate <a href="../config/config-view.cgi">configuration settings</a>, you can export these files to your remote machines unattended:
+ <ul>
+ <li>shellmachines - <b>passwd</b> and <b>shadow</b> are copied to the remote machine as <b>/etc/passwd.new</b> and <b>/etc/shadow.new</b> and then moved to <b>/etc/passwd</b> and <b>/etc/shadow</b> if no errors occur.
+ <li>bsdshellmachines - <b>passwd</b> and <b>master.passwd</b> are copied to the remote machine as <b>/etc/passwd.new</b> and <b>/etc/master.passwd.new</b> and moved to <b>/etc/passwd</b> and <b>/etc/master.passwd</b> if no errors occur.
+ <li>nismachines - <b>passwd</b> and <b>shadow</b> are copied to the <b>/etc/global</b> directory on the remote machine. If no errors occur, the command <b>( cd /var/yp; make; )</b> is executed on the remote machine.
+ <li>erpcdmachines - <b>acp_passwd</b> and <b>acp_dialup</b> are copied to the <b>/usr/annex</b> directory on the remote machine. If no errors occur, the command <b>( kill -USR1 `cat /usr/annex/erpcd.pid` )</b> is executed on the remote machine.
+ <li>radiusmachines - <b>users</b> is copied to the <b>/etc/raddb</b> directory on the remote machine. If no errors occur, the command <b>( builddbm )</b> is executed on the remote machine.
+ <li>icradiusmachines - Turn this option on to enable radcheck table population - by default in the Freeside database, or in the database specified by the <a href="http://rootwood.haze.st/aspside/config/config-view.cgi#icradius_secrets">icradius_secrets</a> config option (the radcheck table needs to be created manually). You do not need to use MySQL for your Freeside database to export to an ICRADIUS/FreeRADIUS MySQL database with this option. <blockquote><b>ADDITIONAL DEPRECATED FUNCTIONALITY</b> (instead use <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Replication">MySQL replication</a> or point icradius_secrets to the external database) - your <a href="ftp://ftp.cheapnet.net/pub/icradius">ICRADIUS</a> machines or <a href="http://www.freeradius.org/">FreeRADIUS</a> (with MySQL authentication) machines, one per line. Machines listed in this file will have the radcheck table exported to them. Each line should contain four items, separted by whitespace: machine name, MySQL database name, MySQL username, and MySQL password. For example: <CODE>"radius.isp.tld&nbsp;radius_db&nbsp;radius_user&nbsp;passw0rd"</CODE></blockquote>
+ </ul>
+ <li>svc_acct.pm - If a shellmachine is defined, users can be created, modified and deleted remotely; see below.
+ <ul>
+ <li>Account creation - If the <b>username</b>, <b>uid</b> and <b>dir</b> fields are defined for a new user, the command(s) specified in the <a href="../config/config-view.cgi#shellmachine-useradd">shellmachine-useradd</a> configuration file are executed on shellmachine via ssh. If this file does not exist, <code>useradd -d $dir -m -s $shell -u $uid $username</code> is the default. If the file exists but is empty, <code>cp -pr /etc/skel $dir; chown -R $uid.$gid $dir</code> is the default instead. Otherwise the contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$username</code>, <code>$uid</code>, <code>$gid</code>, <code>$dir</code>, and <code>$shell</code>.
+ <li>Account deletion - The command(s) specified in the <a href="../config/config-view.cgi#shellmachine-userdel">shellmachine-userdel</a> configuration file are executed on shellmachine via ssh. If this file does not exist, <code>userdel $username</code> is the default. If the file exists but is empty, <code>rm -rf $dir</code> is the default instead. Otherwise the contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$username</code> and <code>$dir</code>.
+ <li>Account modification - If a user's home directory changes, the command(s) specified in the <a href="../config/config-view.cgi#shellmachine-usermod">shellmachine-usermod</a> configuration file are execute on shellmachine via ssh. If this file does not exist or is empty, <code>[ -d $old_dir ] &amp;&amp; mv $old_dir $new_dir || ( chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; find . -depth -print | cpio -pdm $new_dir; chmod u-t $new_dir; chown -R $uid.$gid $new_dir; rm -rf $old_dir )</code> is the default. Otherwise the contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$old_dir</code>, <code>$new_dir</code>, <code>$uid</code> and <code>$gid</code>.
+ </ul>
+ <li>svc_acct.pm - <a href="http://asg.web.cmu.edu/cyrus/imapd/">Cyrus IMAP Server</a> integration, enabled by the <a href="../config/config-view.cgi#cyrus">cyrus configuration file</a>
+ <ul>
+ <li>Account creation - (Cyrus::IMAP::Admin should be installed locally)
+ <li>Account deletion - (Cyrus::IMAP::Admin should be installed locally)
+ <li>Account modification - (not yet implemented)
+ </ul>
+ <li>bin/svc_acct_sm.export will create <a href="http://www.qmail.org">Qmail</a> <b>rcpthosts</b>, <b>recipientmap</b> and <b>virtualdomains</b> files and <a href="http://www.sendmail.org">Sendmail</a> <b>virtusertable</b> and <b>sendmail.cw</b> files in the <b>/usr/local/etc/freeside/export.<i>datasrc</i></b> directory. Using the appropriate <a href="../config/config-view.cgi">configuration files</a>, you can export these files to your remote machines unattemded:
+ <ul>
+ <li>qmailmachines - <b>recipientmap</b>, <b>virtualdomains</b> and <b>rcpthosts</b> are copied to the <b>/var/qmail/control</b> directory on the remote machine. Note: If you <a href="legacy.html#svc_acct_sm">imported</a> qmail configuration files, run the generated <b>/usr/local/etc/freeside/export.<i>datasrc</i>/virtualdomains.FIX</b> on a machine with your user home directories before exporting qmail configuration files.
+ <li>shellmachine - The command <b>[ -e <i>homedir</i>/.qmail-default ] || { touch <i>homedir</i>/.qmail-default; chown <i>uid</i>.<i>gid</i> <i>homedir</i>/.qmail-default; }</b> will be run on this machine for users in the virtualdomains file.
+ <li>sendmailmachines - <b>sendmail.cw</b> and <b>virtusertable</b> are copied to the remote machine as <b>/etc/sendmail.cw.new</b> and <b>/etc/virtusertable.new</b>. If no errors occur, they are moved to <b>/etc/sendmail.cw</b> and <b>/etc/virtusertable</b> and the command specified in the <a href="../config/config-view.cgi#sendmailrestart">sendmailrestart</a> configuration file is executed. (The path can be changed from the default <b>/etc</b> with the <a href="../config/config-view.cgi#sendmailconfigpath">sendmailconfigpath</a> configuration file.)
+ </ul>
+ <li>svc_domain.pm - If the qmailmachines configuration file exists and a shellmachine is defined, user <b>.qmail-</b> files can be updated for catchall mailboxes.
+ <ul>
+ <li>The command <pre>[ -e <i>homedir</i>/.qmail-<i>domain</i>-default ] || {
+ touch <i>homedir</i>/.qmail-<i>domain</i>-default;
+ chown <i>uid</i>.<i>gid</i> <i>homedir</i>/.qmail-<i>domain</i>-default;
+}</pre> is run.
+ </ul>
+ <li>svc_forward.pm - Not yet documented; see manpage.
+ <li>svc_www.pm - Not yet documented; see manpage.
+ </ul>
+ <br><a name=ssh>Unattended remote login</a> - Freeside can login to remote machines unattended using SSH. This can pose a security risk if not configured correctly, and will allow an intruder who breaks into your freeside machine full access to your remote machines. <b>Do not use this feature unless you understand what you are doing!</b>
+ <ul>
+ <li>As the freeside user (on your freeside machine), generate an authentication key using <a href="http://www.tac.eu.org/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>. Since this is for unattended operation, use a blank passphrase.
+ <li>Append the newly-created <code>identity.pub</code> file to <code>~root/.ssh/authorized_keys</code> on the remote machine(s).
+ <li>Some new SSH v2 implementation accept v2 style keys only. Use the <code>-t</code> option to <a href="http://www.tac.eu.org/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>, and append the created <code>id_dsa.pub</code> or <code>id_rsa.pub</code> to <code>~root/.ssh/authorized_keys2</code> on the remote machine(s).
+ <li>You may need to set <code>PermitRootLogin without-password</code> (meaning with keys only) in your <code>sshd_config</code> file on the remote machine(s).
+ </ul>
+
+</body>
+
diff --git a/httemplate/docs/ieak.html b/httemplate/docs/ieak.html
new file mode 100644
index 0000000..00c5342
--- /dev/null
+++ b/httemplate/docs/ieak.html
@@ -0,0 +1,75 @@
+<pre>
+this is incomplete
+mostly it should be merged into signup.html and fs_signup/ieak.template
+
+- download and install the IEAK from
+ http://www.microsoft.com/windows/ieak/default.asp
+
+- Good examples may be found in
+ C:\Program Files\IEAK\toolkit\isp\server\ICW\signup\perl\signup08.pl
+ C:\Program Files\IEAK\toolkit\isp\server\ICW\reconfig\perl\reconfig04.pl
+ C:\Program Files\IEAK6\toolkit\isp\servless\basic\sample.ins
+ C:\Program Files\IEAK6\toolkit\isp\servless\advanced\4567.ins
+ C:\Program Files\IEAK6\toolkit\isp\servless\advanced\4568.ins
+ C:\Program Files\IEAK6\toolkit\isp\servless\advanced\7890.ins
+ C:\Program Files\IEAK6\toolkit\isp\servless\advanced\7891.ins
+
+- Full documentation on all the settings available in .INS files is
+ avaialble under Program Files | Microsoft IEAK 6 | IEAK Help
+ | Reference | Internet Settings (.ins) Files
+
+- Freeside will make the following substitutions before sending the file
+ to the user:
+
+ { $ac } - area code of selected POP
+ { $exch } - exchange of selected POP
+ { $loc } - local part of selected POP
+ { $username }
+ { $password }
+ { $email_name } - first and last name
+ { $pkg } - package name
+
+- Simple example follows:
+
+[Entry]
+Entry Name = IEAK Sample
+[Phone]
+Dial_As_Is = No
+Phone_Number = { $exch }{ $loc }
+Area_Code = { $ac }
+Country_Code = 1
+Country_Id = 1
+[Server]
+Type = PPP
+SW_Compress = Yes
+PW_Encrypt = Yes
+Negotiate_TCP/IP = Yes
+Disable_LCP = No
+[TCP/IP]
+Specity_IP_Address = No
+Specity_Server_Address = No
+IP_Header_Compress = Yes
+Gateway_On_Remote = Yes
+[User]
+Name = { $username }
+Passowrd = { $password }
+Display_Password = Yes
+[Internet_Mail]
+Email_Name = { $email_name }
+Email_Address = { $username }@example.com
+POP_Server = mail.example.com
+POP_Server_Port_Number = 110
+POP_Logon_Password = { $password }
+SMTP_Server = mail.example.com
+SMTP_Server_Port_Number = 25
+Install_Mail = 1
+[URL]
+Help_Page = http://www.ieaksample.net/helpdesk
+Home_Page = http://www.ieaksample.net
+Search_Page = http://www.ieaksample,net/search
+[Favorites]
+IEAK Sample \\ IEAK Sample Home Page.url = http://acme.ieaksample.net/
+[Branding]
+Window_Title = Internet Explorer from Acme Internet Services
+
+</pre>
diff --git a/httemplate/docs/index.html b/httemplate/docs/index.html
new file mode 100644
index 0000000..648cb98
--- /dev/null
+++ b/httemplate/docs/index.html
@@ -0,0 +1,30 @@
+<head>
+ <title>Documentation</title>
+</head>
+<body bgcolor="#ffffff">
+ <h1>Documentation</h1>
+<img src="overview.png">
+<ul>
+ <li><a href="install.html">New Installation</a>
+ <li><a href="upgrade7.html">Upgrading from 1.3.0 to 1.3.1</a>
+ <li><a href="upgrade8.html">Upgrading from 1.3.1 to 1.4.0</a>
+ <li><a href="upgrade9.html">Upgrading from 1.4.0 to 1.4.1</a>
+ <li><a href="upgrade-1.4.2.html">Upgrading from 1.4.1 to 1.4.2</a>
+ <li><a href="upgrade10.html">Upgrading from 1.4.1 (or 1.4.2?) to 1.5.0</a>
+<!--
+ <li><a href="config.html">Configuration files</a>
+!-->
+ <li><a href="admin.html">Administration</a>
+<!--
+ <li><a href="../index.html#admin">Administration</a>
+!-->
+ <li><a href="legacy.html">Importing legacy data</a>
+ <li><a href="export.html">File exporting and remote setup</a>
+ <li><a href="passwd.html">fs_passwd</a>
+ <li><a href="signup.html">Signup server</a>
+ <li><a href="session.html">Session monitor</a>
+ <li><a href="billing.html">Billing</a>
+ <li><a href="schema.html">Schema reference</a>
+ <li><a href="man/FS.html">Perl API</a>
+</ul>
+</body>
diff --git a/httemplate/docs/install.html b/httemplate/docs/install.html
new file mode 100644
index 0000000..d4507a2
--- /dev/null
+++ b/httemplate/docs/install.html
@@ -0,0 +1,212 @@
+<head>
+ <title>Installation</title>
+</head>
+<body>
+<h1>Installation</h1>
+<i>Note: Install Freeside on a firewalled, private server, not a public (web, RADIUS, etc.) server.</i><br><br>
+Before installing, you need:
+<ul>
+ <li><a href="http://www.perl.com/">Perl</a>
+ <li><a href="http://www.apache.org">Apache</a> (<a href="http://www.modssl.org/">mod_ssl</a> or <a href="http://www.apache-ssl.org">Apache-SSL</a> highly recommended)
+ <li><a href="http://perl.apache.org/">mod_perl</a> (if compiling your own mod_perl, make sure you set the <a href="http://perl.apache.org/guide/install.html#EVERYTHING">EVERYTHING</a>=1 compile-time option)
+ <li><a href="http://www.openssh.com/">SSH</a> (<a href="http://www.openssh.com//">OpenSSH</a> is recommended. SSH Communications Security <a href="http://www.ssh.com/products/ssh/download.cfm">commercial SSH version 3</a> has been reported incompatible with Freeside.)
+ <li><a href="http://rsync.samba.org/">rsync</a>
+ <li>A <b>transactional</b> database engine <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">supported</a> by Perl's <a href="http://dbi.perl.org">DBI</a>.
+ <ul>
+ <li><a href="http://www.postgresql.org/">PostgreSQL</a> is recommended (v7or later).
+ <li><a href="http://www.mysql.com/">MySQL</a> <b>MINIMUM VERSION 4.1</b> is untested but may work. Versions before 4.1 do not support standard SQL subqueries and are <b>NOT SUPPORTED</b>. If you are a developer who wishes to contribute MySQL 3.x/4.0 support, see <a href="http://pouncequick.420.am/rt/Ticket/Display.html?id=438">ticket #438</a> in the bug-tracking system and ask on the -devel mailing list.
+<!-- <li>MySQL has been reported to work. -->
+ <b>MySQL's default <a href="http://www.mysql.com/doc/M/y/MyISAM.html">MyISAM</a> and <a href="http://www.mysql.com/doc/I/S/ISAM.html">ISAM</a> table types are not supported</b>. If you want to use MySQL, you <b>must</b> use one of the new <a href="http://www.mysql.com/doc/T/a/Table_types.html">transaction-safe table types</a> such as <a href="http://www.mysql.com/doc/B/D/BDB.html">BDB</a> or <a href="http://www.mysql.com/doc/I/n/InnoDB.html">InnoDB</a>, and set it as the default table type using the <code>--default-table-type=BDB</code> or <code>--default-table-type=InnoDB</code> <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Command-line_options">mysqld command-line option</a> or by setting <code>default-table-type=BDB</code> or <code>default-table-type=InnoDB</code> in the <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Option_files">my.cnf option file</a>.
+ </ul>
+ <li>Perl modules (<a href="http://search.cpan.org/~andk/CPAN/lib/CPAN.pm">CPAN</a> will query, download and build perl modules automatically)
+ <ul>
+<!-- <li><a href="http://search.cpan.org/search?dist=Array-PrintCols">Array-PrintCols</a>
+ <li><a href="http://search.cpan.org/search?dist=Term-Query">Term-Query</a> (make test broken; install manually) -->
+ <li><a href="http://search.cpan.org/search?dist=MIME-Base64">MIME-Base64</a>
+ <li><a href="http://search.cpan.org/search?dist=Digest-MD5">Digest-MD5</a>
+<!-- <li><a href="http://search.cpan.org/search?dist=MD5">MD5</a> -->
+ <li><a href="http://search.cpan.org/search?dist=URI">URI</a>
+ <li><a href="http://search.cpan.org/search?dist=HTML-Tagset">HTML-Tagset</a>
+ <li><a href="http://search.cpan.org/search?dist=HTML-Parser">HTML-Parser</a>
+ <li><a href="http://search.cpan.org/search?dist=libnet">libnet</a>
+ <li><a href="http://search.cpan.org/search?dist=Locale-Codes">Locale-Codes</a>
+ <li><a href="http://search.cpan.org/search?dist=Net-Whois-Raw">Net-Whois-Raw</a>
+ <li><a href="http://search.cpan.org/search?dist=libwww-perl">libwww-perl</a>
+ <li><a href="http://search.cpan.org/search?dist=Business-CreditCard">Business-CreditCard</a>
+<!-- <li><a href="http://search.cpan.org/search?dist=Data-ShowTable">Data-ShowTable</a> -->
+ <li><a href="http://search.cpan.org/search?dist=MailTools">MailTools</a>
+ <li><a href="http://search.cpan.org/search?dist=TimeDate">TimeDate</a>
+ <li><a href="http://search.cpan.org/search?dist=DateManip">DateManip</a>
+ <li><a href="http://search.cpan.org/search?dist=File-CounterFile">File-CounterFile</a>
+ <li><a href="http://search.cpan.org/search?dist=FreezeThaw">FreezeThaw</a>
+ <li><a href="http://search.cpan.org/search?dist=String-Approx">String-Approx</a>
+ <li><a href="http://search.cpan.org/search?dist=Text-Template">Text-Template</a>
+ <li><a href="http://search.cpan.org/search?dist=DBI">DBI</a>
+ <li><a href="http://search.cpan.org/search?mode=module&query=DBD">DBD for your database engine</a> (<a href="http://search.cpan.org/search?dist=DBD-Pg">DBD::Pg</a> for PostgreSQL, <a href="http://search.cpan.org/search?dist=DBD-mysql">DBD::mysql</a> for MySQL)
+ <li><a href="http://search.cpan.org/search?dist=DBIx-DataSource">DBIx-DataSource</a>
+ <li><a href="http://search.cpan.org/search?dist=DBIx-DBSchema">DBIx-DBSchema</a>
+ <li><a href="http://search.cpan.org/search?dist=Net-SSH">Net-SSH</a>
+ <li><a href="http://search.cpan.org/search?dist=String-ShellQuote">String-ShellQuote</a>
+ <li><a href="http://search.cpan.org/search?dist=Net-SCP">Net-SCP</a>
+ <li><a href="http://www.masonhq.com/">HTML::Mason</a> (recommended, enables full functionality) or <a href="http://www.apache-asp.org/">Apache::ASP</a> (deprecated, integrated RT ticketing will not be available)
+ <li><a href="http://search.cpan.org/search?dist=Tie-IxHash">Tie-IxHash</a>
+ <li><a href="http://search.cpan.org/search?dist=Time-Duration">Time-Duration</a>
+ <li><a href="http://search.cpan.org/search?dist=HTML-Widgets-SelectLayers">HTML-Widgets-SelectLayers</a>
+ <li><a href="http://search.cpan.org/search?dist=Storable">Storable</a>
+<!-- MyAccounts, maybe only for dev <li><a href="http://search.cpan.org/search?dist=Cache-Cache">Cache::Cache</a> -->
+ <li><a href="http://search.cpan.org/search?dist=NetAddr-IP">NetAddr-IP</a>
+ <li><a href="http://search.cpan.org/search?dist=Chart">Chart</a>
+ <li><a href="http://search.cpan.org/search?dist=Crypt-PasswdMD5">Crypt::PasswdMD5</a>
+ <li><a href="http://search.cpan.org/search?dist=ApacheDBI">Apache::DBI</a> <i>(optional but recommended for better webinterface performance)</i>
+ </ul>
+</ul>
+Install the Freeside distribution:
+<ul>
+ <li>Add the user and group `freeside' to your system.
+ <li>Allow the freeside user full access to the freeside database.
+ <ul>
+ <li> with <a href="http://www.postgresql.org/users-lounge/docs/7.1/postgres/user-manag.html#DATABASE-USERS">PostgreSQL</a>:
+ <pre>
+$ su postgres (pgsql on some distributions)
+$ createuser -P freeside
+Enter password for user "freeside":
+Enter it again:
+Shall the new user be allowed to create databases? (y/n) y
+Shall the new user be allowed to create more new users? (y/n) n
+CREATE USER</pre>
+ <li> with <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#User_Account_Management">MySQL</a>:
+ <pre>
+$ mysqladmin -u root password '<i>set_a_root_database_password</i>'
+$ mysql -u root -p
+mysql> GRANT SELECT,INSERT,UPDATE,DELETE,INDEX,ALTER,CREATE,DROP on freeside.* TO freeside@localhost IDENTIFIED BY '<i>set_a_freeside_database_password</i>';</pre>
+ </ul>
+<!-- <li>Unpack the tarball: <pre>gunzip -c fs-x.y.z.tar.gz | tar xvf -</pre>-->
+ <li>Edit the top-level Makefile:
+ <ul>
+ <li>Set <tt>DATASOURCE</tt> to your <a href="http://search.cpan.org/doc/TIMB/DBI-1.28/DBI.pm">DBI data source</a>, for example, <tt>DBI:Pg:dbname=freeside</tt> for PostgresSQL or <tt>DBI:mysql:freeside</tt> for MySQL. See the <a href="http://search.cpan.org/doc/TIMB/DBI-1.28/DBI.pm">DBI manpage</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">manpage for your DBD</a> for the exact syntax of your DBI data source.
+ <li>Set <tt>DB_PASSWORD</tt> to the freeside database user's password.
+ </ul>
+ <li>Add the freeside database to your database engine:
+ <pre>
+$ su
+# make create-database</pre>
+ (or manually, with Postgres:)
+ <pre>
+$ su freeside
+$ createdb freeside</pre>
+ (with MySQL:)
+ <pre>
+$ mysqladmin -u freeside -p create freeside </pre>
+ <li>Build and install the Perl modules:
+ <pre>
+$ make perl-modules
+$ su
+# make install-perl-modules</pre>
+ <li>Create the necessary configuration files:<pre>
+$ su
+# make create-config
+</pre>
+ <li>Run a <b>separate</b> iteration of Apache[-SSL] with mod_perl enabled <b>as the freeside user</b>.
+</ul>
+<table>
+ <tr>
+ <th>Apache::ASP</th><th>Mason</th>
+ </tr>
+ <tr>
+ <td><ul>
+ <li>Run <tt>make aspdocs</tt>
+ <li>Copy <tt>aspdocs/</tt> to your web server's document space:
+<font size="-1"><pre>
+cp&nbsp;aspdocs&nbsp;/usr/local/apache/htdocs/freeside-asp
+</pre></font>
+ <li>Create a <a href="http://www.apache-asp.org/config.html#Global">Global</a> directory, such as <tt>/usr/local/etc/freeside/asp-global/</tt>:
+<font size="-1"><pre>
+mkdir&nbsp;/usr/local/etc/freeside/asp-global/
+chown&nbsp;freeside&nbsp;/usr/local/etc/freeside/asp-global/
+</pre></font>
+ <li>Copy <tt>htetc/global.asa</tt> to the Global directory:
+<font size="-1"><pre>
+cp&nbsp;htetc/global.asa&nbsp;/usr/local/etc/freeside/asp-global/global.asa
+</pre></font>
+ <li>Configure Apache for the Global directory and to execute .cgi files using Apache::ASP. For example, add something like the following to your Apache httpd.conf file, adjusting for your actual paths:
+<font size="-1"><pre>
+PerlModule Apache::ASP
+# your freeside document root
+&lt;Directory&nbsp;/usr/local/apache/htdocs/freeside-asp&gt;
+&lt;Files ~ (\.cgi|\.html)&gt;
+AddHandler perl-script .cgi .html
+PerlHandler Apache::ASP
+&lt;/Files&gt;
+&lt;Perl&gt;
+$MLDBM::RemoveTaint = 1;
+&lt;/Perl&gt;
+PerlSetVar&nbsp;Global&nbsp;/usr/local/etc/freeside/asp-global/
+PerlSetVar&nbsp;Debug&nbsp;2
+PerlSetVar&nbsp;RequestBinaryRead&nbsp;Off
+# your freeside document root
+PerlSetVar&nbsp;IncludesDir&nbsp;/usr/local/apache/htdocs/freeside-asp
+&lt;/Directory&gt;
+</pre></font>
+ </ul></td>
+ <td><ul>
+ <li>Run <tt>make masondocs</tt>
+ <li>Copy <tt>masondocs/</tt> to your web server's document space. (For example: <tt>/usr/local/apache/htdocs/freeside-mason</tt>)
+ <li>Copy <tt>htetc/handler.pl</tt> to <tt>/usr/local/etc/freeside</tt>
+ <li>Edit <tt>handler.pl</tt> and:
+ <ul>
+ <li> set an appropriate <tt>comp_root</tt>, such as <tt>/usr/local/apache/htdocs/freeside-mason</tt>
+ <li> set an appropriate <tt>data_dir</tt>, such as <tt>/usr/local/etc/freeside/masondata</tt>
+ </ul>
+
+ <li>Configure Apache to use the <tt>handler.pl</tt> file and to execute .cgi files using HTML::Mason. For example, add something like the following to your Apache httpd.conf file, adjusting for your actual paths:
+<font size="-1"><pre>
+PerlModule HTML::Mason
+&lt;Directory&nbsp;/usr/local/apache/htdocs/freeside-mason&gt;
+&lt;Files ~ (\.cgi|\.html)&gt;
+AddHandler perl-script .cgi .html
+PerlHandler HTML::Mason
+&lt;/Files&gt;
+&lt;Perl&gt;
+require&nbsp;"/usr/local/etc/freeside/handler.pl";
+&lt;/Perl&gt;
+&lt;/Directory&gt;
+</pre></font>
+ </ul></td>
+ </tr>
+</table>
+<ul>
+<li>Restrict access to this web interface - see the <a href="http://httpd.apache.org/docs/misc/FAQ.html#user-authentication">Apache documentation on user authentication</a>. For example, to configure user authentication with <a href="http://httpd.apache.org/docs/mod/mod_auth.html">mod_auth</a> (flat files), add something like the following to your Apache httpd.conf file, adjusting for your actual paths:
+<pre>
+&lt;Directory /usr/local/apache/htdocs/freeside-asp&gt;
+AuthName Freeside
+AuthType Basic
+AuthUserFile /usr/local/etc/freeside/htpasswd
+require valid-user
+&lt;/Directory&gt;
+</pre>
+ <li>Create one or more Freeside users (your internal sales/tech folks, not customer accounts). These users are setup using using Apache authentication, not UNIX user accounts. For example, using <a href="http://httpd.apache.org/docs/mod/mod_auth.html">mod_auth</a> (flat files):
+ <ul>
+ <li>First user:<font size="-1">
+<pre>$ su
+$ <a href="man/bin/freeside-adduser.html">freeside-adduser</a> -c -h /usr/local/etc/freeside/htpasswd <i>username</i></pre></font>
+ <li>Additional users:<font size="-1">
+<pre>$ su
+$ <a href="man/bin/freeside-adduser.html">freeside-adduser</a> -h /usr/local/etc/freeside/htpasswd <i>username</i></pre></font>
+ </ul>
+ <i>(using other auth types, add each user to your <a href="http://httpd.apache.org/docs/misc/FAQ.html#user-authentication">Apache authentication</a> and then run: <tt>freeside-adduser <b>username</b></tt></i>
+ <li>As the freeside UNIX user, run <tt>freeside-setup <b>username</b></tt> to create the database tables, passing the username of a Freeside user you created above:
+<pre>
+$ su freeside
+$ freeside-setup <b>username</b>
+</pre>
+ Alternately, use the -s option to enable shipping addresses: <tt>freeside-setup -s <b>username</b></tt>
+ <li>As the freeside UNIX user, run <tt>bin/populate-msgcat <b>username</b></tt> (in the untar'ed freeside directory) to populate the message catalog, passing the username of a Freeside user you created above:
+<pre>
+$ su freeside
+$ cd <b>/path/to/freeside/</b>
+$ bin/populate-msgcat <b>username</b>
+</pre>
+ <li><tt>freeside-queued</tt> was installed with the Perl modules. Start it now and ensure that is run upon system startup (Do this manually, or edit the top-level Makefile, replacing INIT_FILE with the appropriate location on your systemand QUEUED_USER with the username of a Freeside user you created above, and run <tt>make install-init</tt>)
+ <li>Now proceed to the initial <a href="admin.html">administration</a> of your installation.
+</ul>
+</body>
diff --git a/httemplate/docs/legacy.html b/httemplate/docs/legacy.html
new file mode 100755
index 0000000..94efe53
--- /dev/null
+++ b/httemplate/docs/legacy.html
@@ -0,0 +1,39 @@
+<head>
+ <title>Importing legacy data</title>
+</head>
+<body>
+ <h1>Importing legacy data</h1>
+<font size="+2">In almost all cases, legacy data import will require writing custom code to deal with your particular legacy data. The example scripts here will probably <b>not</b> work "out-of-the-box", and are provided <b>as a starting point only</b>.</font>
+<br><br><i>Some import scripts may require installation of the <a href="http://search.cpan.org/search?dist=Array-PrintCols">Array-PrintCols</a> and <a href="http://search.cpan.org/search?dist=Term-Query">Term-Query</a> (make test broken; install manually) modules.</i><br>
+<ul>
+ <li><a name="bind">bin/bind.import</a> - Import domain information from BIND named
+ <li><a name="passwd">bin/passwd.import</a> - Just import `passwd' and `shadow' or `master.passwd', no RADIUS import.
+ <li><a name="svc_acct">bin/svc_acct.import</a> - Import `passwd', ( `shadow' or `master.passwd' ) and RADIUS `users'. Before running bin/svc_acct.import, you need <a href="../browse/part_svc.cgi">services</a> (with table svc_acct) as follows:
+ <ul>
+ <li>Most accounts probably have entries in passwd and users (with Port-Limit nonexistant or 1)
+ <li>Some accounts have entries in passwd and users, but with Port-Limit 2 (or more)
+ <li>Some accounts might have entries in users only (Port-Limit 1)
+ <li>Some accounts might have entries in users only (Port-Limit >= 2)
+ <li>POP mail accounts have entries in passwd only, and have a particular shell.
+ <li>Everything else in passwd is a shell account.
+ </ul>
+<!-- <li><a name="svc_acct_sm">bin/svc_acct_sm.import</a> - Import qmail ( `virtualdomains' and `rcpthosts' ), or sendmail ( `virtusertable' and `sendmail.cw' ) files. Before running bin/svc_acct_sm.import, you need <a href="../browse/part_svc.cgi">services</a> as follows:
+ <ul>
+ <li>Domain (table svc_acct)
+ <li>Mail alias (table svc_acct_sm)
+ </ul>
+-->
+ <li><a name="cust_main">Importing customer data</a>
+ <ul>
+ <li>Manually
+ <ul>
+ <li>Add a <a href="../edit/cust_main.cgi">new customer</a>
+ <li>Add one or more packages for this customer
+ <li>Enter a package by clicking on the package number
+ <li>Pick the `Link to existing' option
+ </ul>
+ <li>Batch - You will need to write a script to import your particular legacy data. You can use eg/TEMPLATE_cust_main.import as a starting point.
+ </ul>
+</ul>
+</body>
+
diff --git a/httemplate/docs/man/FS/part_export/.cvs_is_on_crack b/httemplate/docs/man/FS/part_export/.cvs_is_on_crack
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/httemplate/docs/man/FS/part_export/.cvs_is_on_crack
diff --git a/httemplate/docs/overview.dia b/httemplate/docs/overview.dia
new file mode 100644
index 0000000..a0e34c3
--- /dev/null
+++ b/httemplate/docs/overview.dia
Binary files differ
diff --git a/httemplate/docs/overview.png b/httemplate/docs/overview.png
new file mode 100644
index 0000000..bf2dbc2
--- /dev/null
+++ b/httemplate/docs/overview.png
Binary files differ
diff --git a/httemplate/docs/passwd.html b/httemplate/docs/passwd.html
new file mode 100755
index 0000000..fc1dde9
--- /dev/null
+++ b/httemplate/docs/passwd.html
@@ -0,0 +1,23 @@
+<head>
+ <title>fs_passwd</title>
+</head>
+<body>
+ <h1>fs_passwd</h1>
+You may use fs_passwd/fs_passwd as a "passwd", "chfn" and "chsh" replacement on your shell machine(s) to cause password, gecos and shell changes to update your freeside machine. You can also use the fs_passwd/fs_passwd.html and fs_passwd/fs_passwd.cgi to run a public password change CGI on a public web server. This can pose a security risk if not configured correctly. <b>Do not use this feature unless you understand what you are doing!</b>
+<br><br>Currently it is assumed that the the crypt(3) function in the C library is the same on the Freeside machine as on the target machine.
+<ul>
+ <li>Create a freeside account on the shell or web machine(s).
+ <li>Setup SSH keys:
+ <ul>
+ <li>As the freeside user (on your freeside machine), generate an authentication key using <a href="http://www.tac.eu.org/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>. Since this is for unattended operation, use a blank passphrase.
+ <li>Append the newly-created <code>identity.pub</code> file to <code>~freeside
+/.ssh/authorized_keys</code> on the shell or web machine(s).
+ <li>Some new SSH v2 implementation accept v2 style keys only. Use the <code>-t</code> option to <a href="http://www.tac.eu.org/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>, and append the created <code>id_dsa.pub</code> or <code>id_rsa.pub</code> to <code>~freeside/.ssh/authorized_keys2</code> on the remote machine(s).
+ </ul>
+ <li>Copy fs_passwd/fs_passwdd to /usr/local/sbin on the shell or web machine(s). (chown freeside, chmod 500)
+ <li>Create /usr/local/freeside on the shell or web machine(s). (chown freeside, chmod 700)
+ <li>Run an iteration of "fs_passwd/fs_passwd_server <i>user</i> shell.machine" as the freeside user for each shell or web machine (this is a daemon process). <i>user</i> refers to a freeside user added by <a href="man/bin/freeside-adduser.html">freeside-adduser</a>.
+ <li>Copy fs_passwd/fs_passwd to /usr/local/bin on the shell machine(s). (chown freeside, chmod 4755). You may link it to passwd, chfn and chsh as well.
+ <li>Copy fs_passwd/fs_passwd.cgi to the cgi-bin directory on your web machine(s). Use <a href="http://www.apache.org/docs/suexec.html">suEXEC</a> or <a href="http://www.perldoc.com/perl5.6.1/pod/perlsec.html">suidperl</a> to run fs_passwd.cgi as the freeside user.
+</ul>
+</body>
diff --git a/httemplate/docs/schema.dia b/httemplate/docs/schema.dia
new file mode 100644
index 0000000..7465615
--- /dev/null
+++ b/httemplate/docs/schema.dia
Binary files differ
diff --git a/httemplate/docs/schema.html b/httemplate/docs/schema.html
new file mode 100644
index 0000000..8dee8ad
--- /dev/null
+++ b/httemplate/docs/schema.html
@@ -0,0 +1,457 @@
+<head>
+ <title>Schema reference</title>
+</head>
+<body>
+ <h1>Schema reference</h1>
+ Schema diagram: <a href="schema.png">as a giant .png</a> or <a href="schema.dia">dia source</a> (<a href="http://www.lysator.liu.se/~alla/dia/">dia homepage</a>).
+ <ul>
+ <li><a name="agent" href="man/FS/agent.html">agent</a> - Agents are resellers of your service. Agents may be limited to a subset of your full offerings (via their agent type).
+ <ul>
+ <li>agentnum - primary key
+ <li>agent - name of this agent
+ <li>typenum - <a href="#agent_type">agent type</a>
+ <li>prog - (unimplemented)
+ <li>freq - (unimplemented)
+ <li>disabled - Disabled flag, empty or 'Y'
+ <li>username - Username for the Agent interface
+ <li>_password - Password for the Agent interface
+ </ul>
+ <li><a name="agent_type" href="man/FS/agent_type.html">agent_type</a> - Agent types define groups of packages that you can then assign to particular agents.
+ <ul>
+ <li>typenum - primary key
+ <li>atype - name of this agent type
+ </ul>
+ <li><a name="cust_bill" href="man/FS/cust_bill.html">cust_bill</a> - Invoices. Declarations that a customer owes you money. The specific charges are itemized in <a href="#cust_bill_pkg">cust_bill_pkg</a>.
+ <ul>
+ <li>invnum - primary key
+ <li>custnum - <a href="#cust_main">customer</a>
+ <li>_date
+ <li>charged - amount of this invoice
+ <li>printed - how many times this invoice has been printed automatically
+ <li>closed - books closed flag, empty or `Y'
+ </ul>
+ <li><a name="cust_bill_event" href="man/FS/cust_bill_event.html">cust_bill_event</a> - Invoice event history
+ <ul>
+ <li>eventnum - primary key
+ <li>invnum - <a href="#cust_bill">invoice</a>
+ <li>eventpart - <a href="#part_bill_event">event definition</a>
+ <li>_date
+ <li>status
+ <li>statustext
+ </ul>
+ <li><a name="part_bill_event" href="man/FS/part_bill_event.html">part_bill_event</a> - Invoice event definitions
+ <ul>
+ <li>eventpart - primary key
+ <li>payby - CARD, DCRD, CHEK, DCHK, LECB, BILL, or COMP
+ <li>event - event name
+ <li>eventcode - event action
+ <li>seconds - how long after the invoice date (<a href="#cust_bill">cust_bill</a>._date) events of this type are triggered
+ <li>weight - ordering for events with identical seconds
+ <li>plan - eventcode plan
+ <li>plandata - additional plan data
+ <li>disabled - Disabled flag, empty or `Y'
+ <li>taxclass - Texas tax class flag, empty or "none", "access", or "hosting"
+ </ul>
+ <li><a name="cust_bill_pkg" href="man/FS/cust_bill_pkg.html">cust_bill_pkg</a> - Invoice line items
+ <ul>
+ <li>invnum - (multiple) key
+ <li>pkgnum - <a href="#cust_pkg">package</a> or 0 for the special virtual sales tax package
+ <li>setup - setup fee
+ <li>recur - recurring fee
+ <li>sdate - starting date
+ <li>edate - ending date
+ <li>itemdesc - Line item description (currently used only when pkgnum is 0)
+ </ul>
+ <li><a name="cust_bill_pkg_detail" href="man/FS/cust_bill_pkg_detail.html">cust_bill_pkg_detail</a> - Invoice line items detail
+ <ul>
+ <li>detailnum - primary key
+ <li>pkgnum -
+ <li>invnum -
+ <li>detail - Detail description
+ </ul>
+ <li><a name="cust_credit" href="man/FS/cust_credit.html">cust_credit</a> - Credits. The equivalent of a negative <a href="#cust_bill">cust_bill</a> record.
+ <ul>
+ <li>crednum - primary key
+ <li>custnum - <a href="#cust_main">customer</a>
+ <li>amount - amount credited
+ <li>_date
+ <li>otaker - order taker
+ <li>reason
+ <li>closed - books closed flag, empty or `Y'
+ </ul>
+ <li><a name="cust_credit_bill" href="man/FS/cust_credit_bill.html">cust_credit_bill</a> - Credit invoice application. Links a credit to an invoice.
+ <ul>
+ <li>creditbillnum - primary key
+ <li>crednum - <a href="#cust_credit">credit</a> being applied
+ <li>invnum - <a href="#cust_bill">invoice</a> to which credit is applied
+ <li>amount - amount applied
+ <li>_date
+ </ul>
+ <li><a name="cust_pay_refund" href="man/FS/cust_pay_refund.html">cust_credit_bill</a> - Refund payment application. Links a refund to a payment.
+ <ul>
+ <li>payrefundnum - primary key
+ <li>paynum - <a href="#cust_pay">payment</a>
+ <li>refundnum - <a href="#cust_refund">refund</a>
+ <li>amount - amount applied
+ <li>_date
+ </ul>
+ <li><a name="cust_main" href="man/FS/cust_main.html">cust_main</a> - Customers
+ <ul>
+ <li>custnum - primary key
+ <li>agentnum - <a href="#agent">agent</a>
+ <li>refnum - <a href="#part_referral">referral</a>
+ <li>first - name
+ <li>last - name
+ <li>ss - social security number
+ <li>company
+ <li>address1
+ <li>address2
+ <li>city
+ <li>county
+ <li>state
+ <li>zip
+ <li>country
+ <li>daytime - phone
+ <li>night - phone
+ <li>fax - phone
+ <li><i>ship_first</i>
+ <li><i>ship_last</i>
+ <li><i>ship_company</i>
+ <li><i>ship_address1</i>
+ <li><i>ship_address2</i>
+ <li><i>ship_city</i>
+ <li><i>ship_county</i>
+ <li><i>ship_state</i>
+ <li><i>ship_zip</i>
+ <li><i>ship_country</i>
+ <li><i>ship_daytime</i>
+ <li><i>ship_night</i>
+ <li><i>ship_fax</i>
+ <li>payby - CARD, DCHK, CHEK, DCHK, LECB, BILL, or COMP
+ <li>payinfo - card number, P.O.#, or comp issuer
+ <li>paycvv - Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
+ <li>paydate - expiration date
+ <li>payname - billing name (name on card)
+ <li>tax - tax exempt, Y or null
+ <li>otaker - order taker
+ <li>referral_custnum
+ <li>comments
+ </ul>
+ (columns in <i>italics</i> are optional)
+ <li><a name="cust_main_invoice" href="man/FS/cust_main_invoice.html">cust_main_invoice</a> - Invoice destinations for email invoices. Note that a customer can have many email destinations for their invoice (either literal or via svcnum), but only one postal destination.
+ <ul>
+ <li>destnum - primary key
+ <li>custnum - <a href="#cust_main">customer</a>
+ <li>dest - Invoice destination. Freeside supports three types of invoice delivery: send directly to a service defined in Freeside, send to an arbitrary email address, or print the invoice to a printer and have someone send it out via snail mail. Freeside determines which method to use based on the contents of the dest field. If the contents are numeric, a <a href="#svc_acct">svcnum</a> pointing to a valid service is expected in the field. If the contents are a string, a literal email address is expected to be in the field. If the special keyword `POST' is present, the snail mail method is used (which is the default if no cust_main_invoice records exist). Snail mail invoices get their address information from <A name="#cust_main">cust_main</A> and are printed with the printer defined in the configuration files.
+ </ul>
+ <li><a name="cust_main_county" href="man/FS/cust_main_county.html">cust_main_county</a> - Tax rates
+ <ul>
+ <li>taxnum - primary key
+ <li>state
+ <li>county
+ <li>country
+ <li>tax - % rate
+ <li>taxclass
+ <li>exempt_amount
+ <li>taxname - if defined, printed on invoices instead of "Tax"
+ <li>setuptax - if 'Y', this tax does not apply to setup fees
+ <li>recurtax - if 'Y', this tax does not apply to recurring fees
+ </ul>
+ <li><a name="cust_tax_exempt" href="man/FS/cust_tax_exempt.html">cust_tax_exempt</a> - Tax exemption record
+ <ul>
+ <li>exemptnum - primary key
+ <li>taxnum - <a href="#cust_main_county">tax rate</a>
+ <li>year
+ <li>month
+ <li>amount
+ </ul>
+ <li><a name="cust_pay" href="man/FS/cust_pay.html">cust_pay</a> - Payments. Money being transferred from a customer.
+ <ul>
+ <li>paynum - primary key
+ <li>custnum - <a href="#cust_main">customer</a>
+ <li>paid - amount
+ <li>_date
+ <li>payby - CARD, CHEK, LECB, BILL, or COMP
+ <li>payinfo - card number, P.O.#, or comp issuer
+ <li>paybatch - text field for tracking card processor batches
+ <li>closed - books closed flag, empty or `Y'
+ </ul>
+ <li><a name="cust_pay-void" href="man/FS/cust_pay_void.html">cust_pay_void</a> - Voided payments.
+ <ul>
+ <li>paynum - primary key
+ <li>custnum - <a href="#cust_main">customer</a>
+ <li>paid - amount
+ <li>_date
+ <li>payby - CARD, CHEK, LECB, BILL, or COMP
+ <li>payinfo - card number, P.O.#, or comp issuer
+ <li>paybatch - text field for tracking card processor batches
+ <li>closed - books closed flag, empty or `Y'
+ <li>void_date
+ <li>reason
+ <li>otaker - order taker
+ </ul>
+ <li><a name="cust_bill_pay" href="man/FS/cust_bill_pay.html">cust_bill_pay</a> - Applicaton of a payment to a specific invoice.
+ <ul>
+ <li>billpaynum
+ <li>invnum - <a href="#cust_bill">invoice</a>
+ <li>paynum - <a href="#cust_pay">payment</a>
+ <li>amount
+ <li>_date
+ </ul>
+ <li><a name="cust_pay_batch" href="man/FS/cust_pay_batch.html">cust_pay_batch</a> - Pending batch
+ <ul>
+ <li>paybatchnum
+ <li>cardnum
+ <li>exp - card expiration
+ <li>amount
+ <li>invnum - <a href="#cust_bill">invoice</a>
+ <li>custnum - <a href="#cust_main">customer</a>
+ <li>payname - name on card
+ <li>first - name
+ <li>last - name
+ <li>address1
+ <li>address2
+ <li>city
+ <li>state
+ <li>zip
+ <li>country
+ </ul>
+ <li><a name="cust_pkg" href="man/FS/cust_pkg.html">cust_pkg</a> - Customer billing items
+ <ul>
+ <li>pkgnum - primary key
+ <li>custnum - <a href="#cust_main">customer</a>
+ <li>pkgpart - <a href="#part_pkg">Package definition</a>
+ <li>setup - date
+ <li>bill - next bill date
+ <li>last_bill - last bill date
+ <li>susp - (past) suspension date
+ <li>expire - (future) cancellation date
+ <li>cancel - (past) cancellation date
+ <li>otaker - order taker
+ <li>manual_flag - If this field is set to 1, disables the automatic unsuspensiond of this package when using the <a href="config.html#unsuspendauto">unsuspendauto</a> config file.
+ </ul>
+ <li><a name="cust_refund" href="man/FS/cust_refund.html">cust_refund</a> - Refunds. The transfer of money to a customer; equivalent to a negative <a href="#cust_pay">cust_pay</a> record.
+ <ul>
+ <li>refundnum - primary key
+ <li>custnum - <a href="#cust_main">customer</a>
+ <li>refund - amount
+ <li>_date
+ <li>payby - CARD, CHEK, LECB, BILL or COMP
+ <li>payinfo - card number, P.O.#, or comp issuer
+ <li>otaker - order taker
+ <li>closed - books closed flag, empty or `Y'
+ </ul>
+ <li><a name="cust_credit_refund" href="man/FS/cust_credit_refund.html">cust_credit_refund</a> - Applicaton of a refund to a specific credit.
+ <ul>
+ <li>creditrefundnum - primary key
+ <li>crednum - <a href="#cust_credit">credit</a>
+ <li>refundnum - <a href="#cust_refund">refund</a>
+ <li>amount
+ <li>_date
+ </ul>
+ <li><a name="cust_svc" href="man/FS/cust_svc.html">cust_svc</a> - Customer services
+ <ul>
+ <li>svcnum - primary key
+ <li>pkgnum - <a href="#cust_pkg">package</a>
+ <li>svcpart - <a href="#part_svc">Service definition</a>
+ </ul>
+ <li><a name="nas" href="man/FS/nas.html">nas</a> - Network Access Server (terminal server)
+ <ul>
+ <li>nasnum - primary key
+ <li>nas - NAS name
+ <li>nasip - NAS ip address
+ <li>nasfqdn - NAS fully-qualified domain name
+ <li>last - timestamp indicating the last instant the NAS was in a known state (used by the session monitoring).
+ </ul>
+ <li><a name="part_pkg" href="man/FS/part_pkg.html">part_pkg</a> - Package definitions
+ <ul>
+ <li>pkgpart - primary key
+ <li>pkg - package name
+ <li>comment - non-customer visable package comment
+ <li>setup - setup fee expression
+ <li>freq - recurring frequency (months)
+ <li>recur - recurring fee expression
+ <li>setuptax - Setup fee tax exempt flag, empty or `Y'
+ <li>recurtax - Recurring fee tax exempt flag, empty or `Y'
+ <li>plan - price plan
+ <li>plandata - additional price plan data
+ <li>disabled - Disabled flag, empty or `Y'
+ </ul>
+ <li><a name="part_referral" href="man/FS/part_referral.html">part_referral</a> - Referral listing
+ <ul>
+ <li>refnum - primary key
+ <li>referral - referral
+ </ul>
+ <li><a name="part_svc" href="man/FS/part_svc.html">part_svc</a> - Service definitions
+ <ul>
+ <li>svcpart - primary key
+ <li>svc - name of this service
+ <li>svcdb - table used for this service: svc_acct, svc_forward, svc_domain, svc_charge or svc_wo
+ <li>disabled - Disabled flag, empty or `Y'
+<!-- <li><i>table</i>__<i>field</i> - Default or fixed value for <i>field</i> in <i>table</i>
+ <li><i>table</i>__<i>field</i>_flag - null, D or F
+-->
+ </ul>
+ <li><a name="part_svc_column" href="man/FS/part_svc_column.html">part_svc_column</a>
+ <ul>
+ <li>columnnum - primary key
+ <li>svcpart - <a href="#part_svc">Service definition</a>
+ <li>columnname - column name in part_svc.svcdb table
+ <li>columnvalue - default or fixed value for the column
+ <li>columnflag - null, D or F
+ </ul>
+ <li><a name="pkg_svc" href="man/FS/pkg_svc.html">pkg_svc</a>
+ <ul>
+ <li>pkgpart - <a href="#part_pkg">Package definition</a>
+ <li>svcpart - <a href="#part_svc">Service definition</a>
+ <li>quantity - quantity of this service that this package includes
+ <li>primary_svc - blank or Y: primary service
+ </ul>
+ <li><a name="export_svc" href="man/FS/export_svc.html">export_svc</a>
+ <ul>
+ <li>exportsvcnum - primary key
+ <li>svcpart - <a href="#part_svc">Service definition</a>
+ <li>exportnum - <a href="#exportnum">Export</a>
+ </ul>
+ <li><a name="part_export" href="man/FS/part_export.html">part_export</a> - Export to external provisioning
+ <ul>
+ <li>exportnum - primary key
+ <li>machine - Machine name
+ <li>exporttype - Export type
+ <li>nodomain - blank or Y: usernames are exported to this service with no domain
+ </ul>
+ <li><a name="part_export_option" href="man/FS/part_export_option.html">part_export_option</a> - provisioning options
+ <ul>
+ <li>optionnum - primary key
+ <li>exportnum - <a href="#part_export">Export</a>
+ <li>optionname - option name
+ <li>optionvalue - option value
+ </ul>
+ <li><a name="port" href="man/FS/port.html">port</a> - individual port on a <a href="#nas">nas</a>
+ <ul>
+ <li>portnum - primary key
+ <li>ip - IP address of this port
+ <li>nasport - port number on the NAS
+ <li>nasnum - <a href="#nas">NAS</a>
+ </ul>
+ <li><a name="prepay_credit" href="man/FS/prepay_credit.html">prepay_credit</a>
+ <ul>
+ <li>prepaynum - primary key
+ <li>identifier - text or numeric string used to receive this credit
+ <li>amount - amount of credit
+ </ul>
+ <li><a name="session" href="man/FS/session.html">session</a>
+ <ul>
+ <li>sessionnum - primary key
+ <li>portnum - <a href="#port">Port</a>
+ <li>svcnum - <a href="#svc_acct">Account</a>
+ <li>login - timestamp indicating the beginning of this user session.
+ <li>logout - timestamp indicating the end of this user session. May be null, which indicates a currently open session.
+ </ul>
+
+ <li><a name="svc_acct" href="man/FS/svc_acct.html">svc_acct</a> - Accounts
+ <ul>
+ <li>svcnum - <a href="#cust_svc">primary key</a>
+ <li>username
+ <li>_password
+ <li>sec_phrase - security phrase
+ <li>popnum - <a href="#svc_acct_pop">Point of Presence</a>
+ <li>uid
+ <li>gid
+ <li>finger - GECOS
+ <li>dir
+ <li>shell
+ <li>quota - (unimplementd)
+ <li>slipip - IP address
+ <li>seconds
+ <li>domsvc
+ <li>radius_<i>Radius_Reply_Attribute</i> - Radius-Reply-Attribute
+ <li>rc_<i>Radius_Check_Attribute</i> - Radius-Check-Attribute
+ </ul>
+ <li><a name="svc_acct_pop" href="man/FS/svc_acct_pop.html">svc_acct_pop</a> - Points of Presence
+ <ul>
+ <li>popnum - primary key
+ <li>city
+ <li>state
+ <li>ac - area code
+ <li>exch - exchange
+ <li>loc - rest of number
+ </ul>
+ <li><a name="part_pop_local" href="man/FS/part_pop_local.html">part_pop_local</a> - Local calling areas
+ <ul>
+ <li>localnum - primary key
+ <li>popnum - primary key
+ <li>city
+ <li>state
+ <li>npa - area code
+ <li>nxx - exchange
+ </ul>
+ <li><a name="svc_domain" href="man/FS/svc_domain.html">svc_domain</a> - Domains
+ <ul>
+ <li>svcnum - <a href="#cust_svc">primary key</a>
+ <li>domain
+ </ul>
+ <li><a name="svc_forward" href="man/FS/svc_forward.html">svc_forward</a> - Mail forwarding aliases
+ <ul>
+ <li>svcnum - <a href="#cust_svc">primary key</a>
+ <li>srcsvc - <a href="#svc_acct">svcnum of the source of this forward</a>
+ <li>src - literal source (username or full email address)
+ <li>dstsvc - <a href="#svc_acct">svcnum of the destination of this forward</a>
+ <li>dst - literal destination (username or full email address)
+ </ul>
+ <li><a name="domain_record" href="man/FS/domain_record.html">domain_record</a> - Domain zone detail
+ <ul>
+ <li>recnum - primary key
+ <li>svcnum - <a href="#svc_domain">Domain</a> (by svcnum)
+ <li>reczone - zone for this line
+ <li>recaf - address family, usually <b>IN</b>
+ <li>rectype - type for this record (<b>A</b>, <b>MX</b>, etc.)
+ <li>recdata - data for this record
+ </ul>
+ <li><a name="svc_www" href="man/FS/svc_www.html">svc_www</a>
+ <ul>
+ <li>svcnum - <a href="#cust-svc">primary key</a>
+ <li>recnum - <a href="#domain_record">host</a>
+ <li>usersvc - <a href="#svc_acct">account</a>
+ </ul>
+ <li><a name="type_pkgs" href="man/FS/type_pkgs.html">type_pkgs</a>
+ <ul>
+ <li>typenum - <a href="#agent_type">agent type</a>
+ <li>pkgpart - <a href="#part_pkg">Package definition</a>
+ </ul>
+ <li><a name="queue" href="man/FS/queue.html">queue</a> - job queue
+ <ul>
+ <li>jobnum - primary key
+ <li>job
+ <li>_date
+ <li>status
+ <li>statustext
+ <li>svcnum
+ </ul>
+ <li><a name="queue_arg" href="man/FS/queue_arg.html">queue_arg</a> - job arguments
+ <ul>
+ <li>argnum - primary key
+ <li>jobnum - <a href="#queue">job</a>
+ <li>arg - argument
+ </ul>
+ <li><a name="queue_depend" href="man/FS/queue_depend.html">queue_depend</a> - job dependancies
+ <ul>
+ <li>dependnum - primary key
+ <li>jobnum - source jobnum
+ <li>depend_jobnum - dependancy jobnum
+ </ul>
+ <li><a name="radius_usergroup" href="man/FS/radius_usergroup.html">radius_usergroup</a> - Link users to RADIUS groups.
+ <ul>
+ <li>usergroupnum - primary key
+ <li>svcnum - <a href="#svc_acct">account</a>
+ <li>groupname
+ </ul>
+ <li><a name="msgcat" href="man/FS/msgcat.html">msgcat</a> - i18n message catalog
+ <ul>
+ <li>msgnum - primary key
+ <li>msgcode - message code
+ <li>locale - locale
+ <li>msg - Message text
+ </ul>
+ </ul>
+</body>
diff --git a/httemplate/docs/schema.png b/httemplate/docs/schema.png
new file mode 100644
index 0000000..d0392e7
--- /dev/null
+++ b/httemplate/docs/schema.png
Binary files differ
diff --git a/httemplate/docs/session.html b/httemplate/docs/session.html
new file mode 100644
index 0000000..72e1642
--- /dev/null
+++ b/httemplate/docs/session.html
@@ -0,0 +1,59 @@
+<head>
+ <title>Session monitor</title>
+</head>
+<body>
+<h1>Session monitor</h1>
+<h2>Installation</h2>
+For security reasons, the client portion of the session montior may run on one
+or more external public machine(s). On these machines, install:
+<ul>
+ <li><a href="http://www.perl.com/CPAN/doc/relinfo/INSTALL.html">Perl</a> (at l
+east 5.004_05 for the 5.004 series or 5.005_03 for the 5.005 series. Don't enable experimental features like threads or the PerlIO abstraction layer.)
+ <li><a href="man/FS/SessionClient.html">FS::SessionClient</a> (copy the fs_session/FS-SessionClient directory to the external machine, then: perl Makefile.PL; make; make install)
+</ul>
+Then:
+<ul>
+ <li>Add the user `freeside' to the the external machine.
+ <li>Create the /usr/local/freeside directory on the external machine (owned by the freeside user).
+ <li>touch /usr/local/freeside/fs_sessiond_socket; chown freeside /usr/local/freeside/fs_sessiond_socket; chmod 600 /usr/local/freeside/fs_sessiond_socket
+ <li>Append the identity.pub from the freeside user on your freeside machine to the authorized_keys file of the newly created freeside user on the external machine(s).
+ <li>Run <pre>fs_session_server <i>user</i> <i>machine</i></pre> on the Freeside machine.
+ <ul>
+ <li><i>user</i> is a user from the mapsecrets file.
+ <li><i>machine</i> is the name of the external machine.
+ </ul>
+</ul>
+<h2>Usage</h2>
+<ul>
+ <li>Web
+ <ul>
+ <li>Copy FS-SessionClient/cgi/login.cgi and logout.cgi to your web
+ server's document space.
+ <li>Use <a href="http://www.apache.org/docs/suexec.html">suEXEC</a> or <a href="http://www.perl.com/CPAN-local/doc/manual/html/pod/perlsec.html#Security_Bugs">setuid</a> (see <a href="install.html">install.html</a> for details) to run login.cgi and logout.cgi as the freeside user.
+ </ul>
+ <li>Command-line
+ <br><pre>freeside-login username ( portnum | ip | nasnum nasport )
+freeside-logout username ( portnum | ip | nasnum nasport )</pre>
+ <ul>
+ <li><i>username</i> is a customer username from the svc_acct table
+ <li><i>portnum</i>, <i>ip</i> or <i>nasport</i> and <i>nasnum</i> uniquely identify a port in the <a href="schema.html#port">port</a> database table.
+ </ul>
+ <li>RADIUS - One of:
+ <ul>
+ <li>Run the <b>freeside-sqlradius-radacctd</b> daemon to import radacct
+ records from all configured sqlradius exports:
+ <tt>freeside-sqlradius-radacctd username</tt>
+ <li>Configure your RADIUS server's login and logout callbacks to use the command-line <tt>freeside-login</tt> and <tt>freeside-logout</tt> utilites.
+ <li> <i>(incomplete)</i>Use the <b>fs_radlog/fs_radlogd</b> tool to
+ import records from a text radacct file.
+ </ul>
+</ul>
+<h2>Callbacks</h2>
+<ul>
+ <li>Sesstion start - The command(s) specified in the <a href="config.html#session-start">session-start</a> configuration file are executed on the Freeside machine. The contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$ip</code>, <code>$nasip</code> and <code>$nasfqdn</code>, which are the IP address of the starting session, and the IP address and fully-qualified domain name of the NAS this session is on.
+ <li>Session end - The command(s) specified in the <a href="config.html#session-stop">session-stop</a> configuration file are executed on the Freeside machine. The contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$ip</code>, <code>$nasip</code> and <code>$nasfqdn</code>, which are the IP address of the starting session, and the IP address and fully-qualified domain name of the NAS this session is on.
+</ul>
+<h2>Dropping expired users</h2>
+Run <pre>bin/freeside-session-kill username</pre> periodically from cron.
+</body>
+</html>
diff --git a/httemplate/docs/signup.html b/httemplate/docs/signup.html
new file mode 100644
index 0000000..97d7aa7
--- /dev/null
+++ b/httemplate/docs/signup.html
@@ -0,0 +1,54 @@
+<head>
+ <title>Signup server</title>
+</head>
+<body>
+ <h1>Signup server</h1>
+For security reasons, the signup server should run on an external public
+webserver. On this machine, install:
+<ul>
+ <li>A web server, such as <a href="http://www.apache-ssl.org">Apache-SSL</a> or <a href="http://www.apache.org">Apache</a>
+ <li><a href="ftp://ftp.cs.hut.fi/pub/ssh/">SSH</a>
+ <li><a href="http://www.perl.com/CPAN/doc/relinfo/INSTALL.html">Perl</a> (at least 5.004_05 for the 5.004 series or 5.005_03 for the 5.005 series. Don't enable experimental features like threads or the PerlIO abstraction layer.)
+ <li><a href="http://search.cpan.org/search?dist=Text-Template">Text::Template</a>
+ <li><a href="http://search.cpan.org/search?dist=Storable">Storable</a>
+ <li><a href="http://search.cpan.org/search?dist=Business-CreditCard">Business-CreditCard</a>
+ <li><a href="http://search.cpan.org/search?dist=HTTP-BrowserDetect">HTTP::BrowserDetect</a>
+
+ <li><a href="man/FS/SignupClient.html">FS::SignupClient</a> (copy the fs_signup/FS-SignupClient directory to the external machine, then: perl Makefile.PL; make; make install)
+</ul>
+Then:
+<ul>
+ <li>Add the user `freeside' to the the external machine.
+ <li>Copy or symlink fs_signup/FS-SignupClient/cgi/signup.cgi into the web server's document space.
+ <li>When linking to signup.cgi, you can include a referring custnum in the URL as follows: <code>http://public.web.server/path/signup.cgi?ref=1542</code>
+ <li>Enable CGI execution for files with the `.cgi' extension. (with <a href="http://www.apache.org/docs/mod/mod_mime.html#addhandler">Apache</a>)
+ <li>Create the /usr/local/freeside directory on the external machine (owned by the freeside user).
+ <li>touch /usr/local/freeside/fs_signupd_socket; chown freeside /usr/local/freeside/fs_signupd_socket; chmod 600 /usr/local/freeside/fs_signupd_socket
+ <li>Use <a href="http://www.apache.org/docs/suexec.html">suEXEC</a> or <a href="http://www.perl.com/CPAN-local/doc/manual/html/pod/perlsec.html#Security_Bugs">setuid</a> (see <a href="install.html">install.html</a> for details) to run signup.cgi as the freeside user.
+ <li>Append the identity.pub from the freeside user on your freeside machine to the authorized_keys file of the newly created freeside user on the external machine(s).
+ <li>Run <pre>fs_signup_server <i>user</i> <i>machine</i> <i>agentnum</i> <i>refnum</i></pre> on the Freeside machine.
+ <ul>
+ <li><i>user</i> is a user from the mapsecrets file.
+ <li><i>machine</i> is the name of the external machine.
+ <li><i>agentnum</i> and <i>refnum</i> are the <a href="schema.html#agent">agent</a> and <a href="schema.html#part_referral">referral</a>, respectively, to use for customers who sign up via this signup server.
+ </ul>
+</ul>
+Optional:
+<ul>
+ <li>If you create a <b>/usr/local/freeside/ieak.template</b> file on the external machine, it will be sent to IE users with MIME type <i>application/x-Internet-signup</i>. This file will be processed with <a href="http://search.cpan.org/doc/MJD/Text-Template-1.23/Template.pm">Text::Template</a> with the variables listed below available.
+ (an example file is included as <b>fs_signup/ieak.template</b>) See the section on <a href="http://www.microsoft.com/windows/ieak/techinfo/deploy/60/en/INS.HTM">internet settings files</a> in the <a href="http://www.microsoft.com/windows/ieak/techinfo/deploy/60/en/toc.asp">IEAK documentation</a> for more information.
+ <li>If you create a <b>/usr/local/freeside/success.html</b> file on the external machine, it will be used as the success HTML page. Although template substiutions are available, a regular HTML file will work fine here, unlike signup.html. An example file is included as <b>fs_signup/FS-SignupClient/cgi/success.html</b>
+ <li>Variable substitutions available in <b>ieak.template</b>, <b>cck.template</b> and <b>success.html</b>:
+ <ul>
+ <li>$ac - area code of selected POP
+ <li>$exch - exchange of selected POP
+ <li>$loc - local part of selected POP
+ <li>$username
+ <li>$password
+ <li>$email_name - first and last name
+ <li>$pkg - package name
+ </ul>
+ <li>If you create a <b>/usr/local/freeside/signup.html</b> file on the external machine, it will be used as a template for the form HTML. This requires the template to be constructed appropriately; probably best to start with the example file included as <b>fs_signup/FS-SignupClient/cgi/signup.html</b>.
+ <li>If there are any entries in the <i>prepay_credit</i> table, a user can enter a string matching the <b>identifier</i> column to receive the credit specified in the <b>amount</b> column, and/or the time specified in the <b>seconds</b> column (for use with the <a href="session.html">session monitor</a>), after which that <b>identifier</b> is no longer valid. This can be used to implement pre-paid "calling card" type signups. The <i>bin/generate-prepay</i> script can be used to populate the <i>prepay_credit</i> table.
+</ul>
+</body>
diff --git a/httemplate/docs/ssh.html b/httemplate/docs/ssh.html
new file mode 100755
index 0000000..d2c501e
--- /dev/null
+++ b/httemplate/docs/ssh.html
@@ -0,0 +1,16 @@
+<head>
+ <title>Unattended SSH</title>
+</head>
+<body>
+ <h1>Unattended SSH</h1>
+ <br><a name=ssh>Unattended remote login</a> - Freeside can login to remote machines unattended using SSH. This can pose a security risk if not configured correctly, and will allow an intruder who breaks into your freeside machine full access to your remote machines. <b>Do not use this feature unless you understand what you are doing!</b>
+ <ul>
+ <li>As the freeside user (on your freeside machine), generate an authentication key using <a href="http://www.tac.eu.org/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>. Since this is for unattended operation, use a blank passphrase.
+ <li>Append the newly-created <code>identity.pub</code> file to <code>~root/.ssh/authorized_keys</code> (or the appopriate <code>~username/.ssh/authorized_keys</code>) on the remote machine(s).
+ <li>Some new SSH v2 implementation accept v2 style keys only. Use the <code>-t</code> option to <a href="http://www.tac.eu.org/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>, and append the created <code>id_dsa.pub</code> or <code>id_rsa.pub</code> to <code>~root/.ssh/authorized_keys2</code> (or the appopriate <code>~username/.ssh/authorized_keys</code>) on the remote machine(s).
+ <li>You may need to set <code>PermitRootLogin without-password</code> (meaning with keys only) in your <code>sshd_config</code> file on the remote machine(s).
+ <li>You may want to set <code>ForwardX11 = no</code> in <code>~root/.ssh/config</code> to prevent spurious errors if your distribution turns on X11 forwarding by default.
+ </ul>
+
+</body>
+
diff --git a/httemplate/docs/trouble.html b/httemplate/docs/trouble.html
new file mode 100755
index 0000000..fce7439
--- /dev/null
+++ b/httemplate/docs/trouble.html
@@ -0,0 +1,26 @@
+<head>
+ <title>Troubleshooting</title>
+</head>
+<body>
+ <h1>Troubleshooting</h1>
+ <ul>
+ <li>When troubleshooting the web interface, helpful information is often in your web server's error log.
+ <li>If bin/svc_acct.import fails with an "Out of memory!" error using MySQL, upgrede MySQL and recompile the Perl DBD. There was a memory leak in some older versions of MySQL.
+ <li>If you get tons of errors in your web server's error log like this:
+<pre>
+Ambiguous use of value => resolved to "value" =>
+at /usr/lib/perl5/site_perl/File/CounterFile.pm line 132.
+</pre>
+ This clutters up your log files but is otherwise harmless. Upgrade to the latest File::CounterFile.
+ <li>If you get errors like this:
+<pre>
+UID.pm: Can't open /var/spool/freeside/conf/secrets: Permission denied
+at <i>/your/path</i>/site_perl/FS/UID.pm line 26.
+BEGIN failed--compilation aborted at
+<i>/your/path</i>/edit/process/part_svc.cgi line 15.
+</pre>
+ Then the scripts are not running as the freeside freeside user. See
+the <a href="install.html">New Installation</a> section of the documentation.
+ <li>If you receive `can not connect to server' errors using MySQL on a system that doesn't support native threading, you may need to specify the full hostname in your DBI datasource. See the <a href="http://www.mysql.com/Manual_chapter/manual_Problems.html#Can_not_connect_to_server">MySQL documentation</a>, DBI manpage and the DBD::mysql manpage for details.
+ </ul>
+</body>
diff --git a/httemplate/docs/upgrade-1.4.2.html b/httemplate/docs/upgrade-1.4.2.html
new file mode 100644
index 0000000..a246611
--- /dev/null
+++ b/httemplate/docs/upgrade-1.4.2.html
@@ -0,0 +1,27 @@
+<head>
+ <title>Upgrading to 1.4.2</title>
+</head>
+<body>
+<h1>Upgrading to 1.4.2 from 1.4.1</h1>
+<ul>
+ <li>If migrating from less than 1.4.1, see these <a href="upgrade9.html">instructions</a> first.
+ <li>Back up your data and current Freeside installation.
+ <li>Install <a href="http://search.cpan.org/search?dist=Locale-SubCountry">Locale::SubCountry</a>
+ <li>Install <a href="http://search.cpan.org/search?dist=IPC-ShareLite">IPC::ShareLite</a>
+ <li>Install <a href="http://search.cpan.org/search?dist=HTML-Widgets-SelectLayers">HTML::Widgets::SelectLayers</a> 0.04.
+ <li>Install <a href="http://search.cpan.org/search?dist=DBIx-DBSchema">DBIx::DBSchema</a> 0.23.
+ <li>Install <a href="http://search.cpan.org/search?dist=DBD-Pg">DBD::Pg</a> 1.32.
+ <li>Install <a href="http://search.cpan.org/search?dist=Cache-Cache">Cache::Cache</a>.
+ <li>Install <a href="http://search.cpan.org/search?dist=Net-SSH">Net::SSH</a> 0.08.
+ <li>Install <a href="http://search.cpan.org/search?dist=Crypt-PasswdMD5">Crypt::PasswdMD5</a>
+ <li>Install <a href="http://search.cpan.org/search?dist=Net-Whois-Raw">Net::Whois::Raw</a>
+ <li>CGI.pm minimum version 2.47 is required. You will probably need to install a current CGI.pm from CPAN if you are using Perl 5.005 or earlier.
+ <li>File::Temp minimum version 0.14 is required. You will probably need to install a currrent File::Temp from CPAN if you are using Perl 5.6 or earlier.
+ <li>If using Apache::ASP, add <code>PerlSetVar RequestBinaryRead Off</code> to your Apache configuration and make sure you are using Apache::ASP minimum version 2.55.
+ <li>Run <code>make aspdocs</code> or <code>make masondocs</code>.
+ <li>Copy <code>aspdocs/</code> or <code>masondocs/</code> to your web server's document space.
+ <li>Run <code>make install-perl-modules</code>.
+ <li>The signup server and password server are deprecated in 1.4.2. Their functionality has been incorperated into the self-service server. Edit or reinstall your init script, and set the "signup_server-default_agentnum" and "signup_server-default_refnum" configuration options. The FS::SignupClient interface is still available as a compatibility wrapper, so you should be able to continue to use your current signup.cgi.
+ <li>Optional: To use typeset invoices, install tetex and ghostscript, and copy conf/invoice_latex, conf/invoice_latexnotes, and conf/invoice_latexfooter to /usr/local/etc/freeside/conf.<datasrc>/
+ <li>Restart Apache and freeside-queued.
+</body>
diff --git a/httemplate/docs/upgrade10.html b/httemplate/docs/upgrade10.html
new file mode 100644
index 0000000..dc60865
--- /dev/null
+++ b/httemplate/docs/upgrade10.html
@@ -0,0 +1,255 @@
+<pre>
+this is incomplete
+
+install DBD::Pg 1.32 (or, if you're using a Perl version before 5.6, you could try installing DBD::Pg 1.22 with <a href="http://420.am/~ivan/DBD-Pg-1.22-fixvercmp.patch">this patch</a> and commenting out the "use DBD::Pg 1.32" at the top of DBIx/DBSchema/DBD/Pg.pm)
+install DBIx::DBSchema 0.23
+install Net::SSH 0.08
+- If using Apache::ASP, add PerlSetVar RequestBinaryRead Off and PerlSetVar IncludesDir /your/freeside/document/root/ to your Apache configuration and make sure you are using Apache::ASP minimum version 2.55.
+- In httpd.conf, change &lt;Files ~ \.cgi&gt; to &lt;Files ~ (\.cgi|\.html)&gt;
+- In httpd.conf, change <b>AddHandler perl-script .cgi</b> or <b>SetHandler perl-script</b> to <b>AddHandler perl-script .cgi .html</b>
+
+install NetAddr::IP, Chart::Base, IPC::ShareLite and Locale::SubCountry
+
+INSERT INTO msgcat ( msgnum, msgcode, locale, msg ) VALUES ( 20, 'svc_external-id', 'en_US', 'External ID' );
+INSERT INTO msgcat ( msgnum, msgcode, locale, msg ) VALUES ( 21, 'svc_external-title', 'en_US', 'Title' );
+
+CREATE TABLE cust_bill_pkg_detail (
+ detailnum serial,
+ pkgnum int NOT NULL,
+ invnum int NOT NULL,
+ detail varchar(80),
+ PRIMARY KEY (detailnum)
+);
+CREATE INDEX cust_bill_pkg_detail1 ON cust_bill_pkg_detail ( pkgnum, invnum );
+
+CREATE TABLE part_virtual_field (
+ vfieldpart int NOT NULL,
+ dbtable varchar(32) NOT NULL,
+ name varchar(32) NOT NULL,
+ check_block text,
+ list_source text,
+ length integer,
+ label varchar(80),
+ PRIMARY KEY (vfieldpart)
+);
+
+CREATE TABLE virtual_field (
+ recnum integer NOT NULL,
+ vfieldpart integer NOT NULL,
+ value varchar(128) NOT NULL,
+ PRIMARY KEY (vfieldpart, recnum)
+);
+
+CREATE TABLE router (
+ routernum serial,
+ routername varchar(80),
+ svcnum int,
+ PRIMARY KEY (routernum)
+);
+
+CREATE TABLE part_svc_router (
+ svcpart int NOT NULL,
+ routernum int NOT NULL
+);
+
+CREATE TABLE addr_block (
+ blocknum serial,
+ routernum int NOT NULL,
+ ip_gateway varchar(15) NOT NULL,
+ ip_netmask int NOT NULL,
+ PRIMARY KEY (blocknum)
+);
+CREATE UNIQUE INDEX addr_block1 ON addr_block ( blocknum, routernum );
+
+CREATE TABLE svc_broadband (
+ svcnum int NOT NULL,
+ blocknum int NOT NULL,
+ speed_up int NOT NULL,
+ speed_down int NOT NULL,
+ ip_addr varchar(15),
+ PRIMARY KEY (svcnum)
+);
+
+CREATE TABLE acct_snarf (
+ snarfnum serial,
+ svcnum int NOT NULL,
+ machine varchar(255) NULL,
+ protocol varchar(80) NULL,
+ username varchar(80) NULL,
+ _password varchar(80) NULL,
+ PRIMARY KEY (snarfnum)
+);
+CREATE INDEX acct_snarf1 ON acct_snarf ( svcnum );
+
+CREATE TABLE svc_external (
+ svcnum int NOT NULL,
+ id int,
+ title varchar(80),
+ PRIMARY KEY (svcnum)
+);
+
+CREATE TABLE part_pkg_temp (
+ pkgpart serial NOT NULL,
+ pkg varchar(80) NOT NULL,
+ "comment" varchar(80) NOT NULL,
+ setup text NULL,
+ freq varchar(80) NOT NULL,
+ recur text NULL,
+ setuptax char(1) NULL,
+ recurtax char(1) NULL,
+ plan varchar(80) NULL,
+ plandata text NULL,
+ disabled char(1) NULL,
+ taxclass varchar(80) NULL,
+ PRIMARY KEY (pkgpart)
+);
+INSERT INTO part_pkg_temp SELECT * from part_pkg;
+DROP TABLE part_pkg;
+ALTER TABLE part_pkg_temp RENAME TO part_pkg;
+CREATE INDEX part_pkg1 ON part_pkg(disabled);
+
+On modern Pg:
+ALTER TABLE part_pkg DROP CONSTRAINT part_pkg_temp_pkey;
+ALTER TABLE part_pkg ADD PRIMARY KEY (pkgpart);
+select setval('public.part_pkg_temp_pkgpart_seq', ( select max(pkgpart) from part_pkg) );
+
+Or on Pg versions that don't support DROP CONSTRAINT and ADD PRIMARY KEY (tested on 7.1 and 7.2 so far):
+DROP INDEX part_pkg_temp_pkey;
+CREATE UNIQUE INDEX part_pkg_pkey ON part_pkg (pkgpart);
+probably this one?: select setval('part_pkg_temp_pkgpart_seq', ( select max(pkgpart) from part_pkg) );
+probably not this one?: select setval('part_pkg_pkgpart_seq', ( select max(pkgpart) from part_pkg) );
+
+CREATE TABLE h_part_pkg_temp (
+ historynum serial NOT NULL,
+ history_date int,
+ history_user varchar(80) NOT NULL,
+ history_action varchar(80) NOT NULL,
+ pkgpart int NOT NULL,
+ pkg varchar(80) NOT NULL,
+ "comment" varchar(80) NOT NULL,
+ setup text NULL,
+ freq varchar(80) NOT NULL,
+ recur text NULL,
+ setuptax char(1) NULL,
+ recurtax char(1) NULL,
+ plan varchar(80) NULL,
+ plandata text NULL,
+ disabled char(1) NULL,
+ taxclass varchar(80) NULL,
+ PRIMARY KEY (historynum)
+);
+INSERT INTO h_part_pkg_temp SELECT * from h_part_pkg;
+DROP TABLE h_part_pkg;
+ALTER TABLE h_part_pkg_temp RENAME TO h_part_pkg;
+CREATE INDEX h_part_pkg1 ON h_part_pkg(disabled);
+
+On modern Pg:
+ALTER TABLE h_part_pkg DROP CONSTRAINT h_part_pkg_temp_pkey;
+ALTER TABLE h_part_pkg ADD PRIMARY KEY (historynum);
+select setval('public.h_part_pkg_temp_historynum_seq', ( select max(historynum) from h_part_pkg) );
+
+Or on Pg versions that don't support DROP CONSTRAINT and ADD PRIMARY KEY (tested on 7.1 and 7.2 so far):
+DROP INDEX h_part_pkg_temp_pkey;
+CREATE UNIQUE INDEX h_part_pkg_pkey ON h_part_pkg (historynum);
+probably this one?: select setval('h_part_pkg_temp_historynum_seq', ( select max(historynum) from h_part_pkg) );
+probably not this one?: select setval('h_part_pkg_historynum_seq', ( select max(historynum) from h_part_pkg) );
+
+CREATE TABLE cust_pay_refund (
+ payrefundnum serial NOT NULL,
+ paynum int NOT NULL,
+ refundnum int NOT NULL,
+ _date int NOT NULL,
+ amount decimal(10,2) NOT NULL,
+ PRIMARY KEY (payrefundnum)
+);
+CREATE INDEX cust_pay_refund1 ON cust_pay_refund(paynum);
+CREATE INDEX cust_pay_refund2 ON cust_pay_refund(refundnum);
+
+CREATE TABLE cust_pay_void (
+ paynum int NOT NULL,
+ custnum int NOT NULL,
+ paid decimal(10,2) NOT NULL,
+ _date int,
+ payby char(4) NOT NULL,
+ payinfo varchar(80),
+ paybatch varchar(80),
+ closed char(1),
+ void_date int,
+ reason varchar(80),
+ otaker varchar(32) NOT NULL,
+ PRIMARY KEY (paynum)
+);
+CREATE INDEX cust_pay_void1 ON cust_pay_void(custnum);
+
+DROP INDEX cust_bill_pkg1;
+
+ALTER TABLE cust_bill_pkg ADD itemdesc varchar(80) NULL;
+ALTER TABLE h_cust_bill_pkg ADD itemdesc varchar(80) NULL;
+ALTER TABLE cust_main_county ADD taxname varchar(80) NULL;
+ALTER TABLE h_cust_main_county ADD taxname varchar(80) NULL;
+ALTER TABLE cust_main_county ADD setuptax char(1) NULL;
+ALTER TABLE h_cust_main_county ADD setuptax char(1) NULL;
+ALTER TABLE cust_main_county ADD recurtax char(1) NULL;
+ALTER TABLE h_cust_main_county ADD recurtax char(1) NULL;
+ALTER TABLE cust_pkg ADD last_bill int NULL;
+ALTER TABLE h_cust_pkg ADD last_bill int NULL;
+ALTER TABLE agent ADD disabled char(1) NULL;
+ALTER TABLE h_agent ADD disabled char(1) NULL;
+ALTER TABLE agent ADD username varchar(80) NULL;
+ALTER TABLE h_agent ADD username varchar(80) NULL;
+ALTER TABLE agent ADD _password varchar(80) NULL;
+ALTER TABLE h_agent ADD _password varchar(80) NULL;
+ALTER TABLE cust_main ADD paycvv varchar(4) NULL;
+ALTER TABLE h_cust_main ADD paycvv varchar(4) NULL;
+ALTER TABLE part_referral ADD disabled char(1) NULL;
+ALTER TABLE h_part_referral ADD disabled char(1) NULL;
+CREATE INDEX part_referral1 ON part_referral ( disabled );
+ALTER TABLE pkg_svc ADD primary_svc char(1) NULL;
+ALTER TABLE h_pkg_svc ADD primary_svc char(1) NULL;
+ALTER TABLE svc_forward ADD src varchar(255) NULL;
+ALTER TABLE h_svc_forward ADD src varchar(255) NULL;
+
+On recent Pg versions:
+
+ALTER TABLE svc_forward ALTER COLUMN srcsvc DROP NOT NULL;
+ALTER TABLE h_svc_forward ALTER COLUMN srcsvc DROP NOT NULL;
+ALTER TABLE svc_forward ALTER COLUMN dstsvc DROP NOT NULL;
+ALTER TABLE h_svc_forward ALTER COLUMN dstsvc DROP NOT NULL;
+
+Or on Pg versions that don't support DROP NOT NULL (tested on 7.1 and 7.2 so far):
+UPDATE pg_attribute SET attnotnull = FALSE WHERE ( attname = 'srcsvc' OR attname = 'dstsvc' ) AND ( attrelid = ( SELECT oid FROM pg_class WHERE relname = 'svc_forward' ) OR attrelid = ( SELECT oid FROM pg_class WHERE relname = 'h_svc_forward' ) );
+
+If you created your database with a version before 1.4.2, dump database, edit:
+- cust_main and h_cust_main: increase otaker from 8 to 32
+- cust_main and h_cust_main: change ss from char(11) to varchar(11) ( "character(11)" to "character varying(11)" )
+- cust_credit and h_cust_credit: increase otaker from 8 to 32
+- cust_pkg and h_cust_pkg: increase otaker from 8 to 32
+- cust_refund and h_cust_refund: increase otaker from 8 to 32
+- domain_record and h_domain_record: increase reczone from 80 to 255
+- domain_record and h_domain_record: change rectype from char to varchar ( "character(5)" to "character varying(5)" )
+- domain_record and h_domain_record: increase recdata from 80 to 255
+then reload
+
+optionally:
+
+ CREATE INDEX cust_main6 ON cust_main ( daytime );
+ CREATE INDEX cust_main7 ON cust_main ( night );
+ CREATE INDEX cust_main8 ON cust_main ( fax );
+ CREATE INDEX cust_main9 ON cust_main ( ship_daytime );
+ CREATE INDEX cust_main10 ON cust_main ( ship_night );
+ CREATE INDEX cust_main11 ON cust_main ( ship_fax );
+ CREATE INDEX agent2 ON agent ( disabled );
+ CREATE INDEX part_bill_event2 ON part_bill_event ( disabled );
+ CREATE INDEX cust_pay4 ON cust_pay (_date);
+
+ serial columns
+
+mandatory again:
+
+dbdef-create username
+create-history-tables username cust_bill_pkg_detail router part_svc_router addr_block svc_broadband acct_snarf svc_external cust_pay_refund cust_pay_void
+dbdef-create username
+
+apache - fix <Files> sections to include .html also
+
+</pre>
diff --git a/httemplate/docs/upgrade7.html b/httemplate/docs/upgrade7.html
new file mode 100644
index 0000000..d9dcfe2
--- /dev/null
+++ b/httemplate/docs/upgrade7.html
@@ -0,0 +1,24 @@
+<head>
+ <title>Upgrading to 1.3.1</title>
+</head>
+<body>
+<h1>Upgrading to 1.3.1 from 1.3.0</h1>
+<ul>
+ <li>If migrating from 1.0.0, see these <a href="upgrade.html">instructions</a> first.
+ <li>If migrating from less than 1.1.4, see these <a href="upgrade2.html">instructions</a> first.
+ <li>If migrating from less than 1.2.0, see these <a href="upgrade3.html">instructions</a> first.
+ <li>If migrating from less than 1.2.2, see these <a href="upgrade4.html">instructions</a> first.
+ <li>If migrating from less than 1.2.3, see these <a href="upgrade5.html">instructions</a> first.
+ <li>If migrating from less than 1.3.0, see these <a href="upgrade6.html">instructions</a> first.
+ <li>Back up your data and current Freeside installation.
+ <li>Copy or symlink htdocs to the new copy.
+ <li>Change to the FS directory in the new tarball, and build and install the
+ Perl modules:
+ <pre>
+$ cd FS/
+$ perl Makefile.PL
+$ make
+$ su
+# make install UNINST=1</pre>
+ <li>Run bin/dbdef-create.
+</body>
diff --git a/httemplate/docs/upgrade8.html b/httemplate/docs/upgrade8.html
new file mode 100644
index 0000000..cf60a85
--- /dev/null
+++ b/httemplate/docs/upgrade8.html
@@ -0,0 +1,392 @@
+<head>
+ <title>Upgrading to 1.4.0</title>
+</head>
+<body>
+<h1>Upgrading to 1.4.0 from 1.3.1</h1>
+<ul>
+ <li>If migrating from less than 1.3.1, see these <a href="upgrade7.html">instructions</a> first.
+ <li><font size="+2" color="#ff0000">Backup your database and current Freeside installation.</font> (with&nbsp;<a href="http://www.ca.postgresql.org/devel-corner/docs/postgres/backup.html">PostgreSQL</a>) (with&nbsp;<a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Backup">MySQL</a>)
+ <li><a href="http://perl.apache.org/">mod_perl</a> is now required.
+ <li>Install <a href="http://search.cpan.org/search?dist=Time-Duration">Time-Duration</a>, <a href="http://search.cpan.org/search?dist=Tie-IxHash">Tie-IxHash</a> and <a href="http://search.cpan.org/search?dist=HTML-Widgets-SelectLayers">HTML-Widgets-SelectLayers</a> (minimum version 0.02).
+ <li>Install <a href="http://www.apache-asp.org/">Apache::ASP</a> or <a href="http://www.masonhq.com/">HTML::Mason</a> (use version 1.0x - Freeside is not yet compatible with version 1.1x).
+ <li>Install <a href="http://rsync.samba.org/">rsync</a>
+</ul>
+<table>
+ <tr>
+ <th>Apache::ASP</th><th>Mason</th>
+ </tr>
+ <tr>
+ <td><ul>
+ <li>Run <tt>make aspdocs</tt>
+ <li>Copy <tt>aspdocs/</tt> to your web server's document space.
+ <li>Create a <a href="http://www.apache-asp.org/config.html#Global">Global</a> directory, such as <tt>/usr/local/etc/freeside/asp-global/</tt>
+ <li>Copy <tt>htetc/global.asa</tt> to the Global directory.
+ <li>Configure Apache for the Global directory and to execute .cgi files using Apache::ASP. For example:
+<font size="-1"><pre>
+&lt;Directory /usr/local/apache/htdocs/freeside-asp&gt;
+&lt;Files ~ (\.cgi)&gt;
+AddHandler perl-script .cgi
+PerlHandler Apache::ASP
+&lt;/Files&gt;
+&lt;Perl&gt;
+$MLDBM::RemoveTaint = 1;
+&lt;/Perl&gt;
+PerlSetVar Global /usr/local/etc/freeside/asp-global/
+&lt;/Directory&gt;
+</pre></font>
+ </ul></td>
+ <td><ul>
+ <li>(use version 1.0x - Freeside is not yet compatible with version 1.1x)
+ <li>Run <tt>make masondocs</tt>
+ <li>Copy <tt>masondocs/</tt> to your web server's document space.
+ <li>Copy <tt>htetc/handler.pl</tt> to your web server's configuration directory.
+ <li>Edit <tt>handler.pl</tt> and set an appropriate <tt>data_dir</tt>, such as <tt>/usr/local/etc/freeside/mason-data</tt>
+ <li>Configure Apache to use the <tt>handler.pl</tt> file and to execute .cgi files using HTML::Mason. For example:
+<font size="-1"><pre>
+&lt;Directory /usr/local/apache/htdocs/freeside-mason&gt;
+&lt;Files ~ (\.cgi)&gt;
+AddHandler perl-script .cgi
+PerlHandler HTML::Mason
+&lt;/Files&gt;
+&lt;Perl&gt;
+require "/usr/local/apache/conf/handler.pl";
+&lt;/Perl&gt;
+&lt;/Directory&gt;
+</pre></font>
+ </ul></td>
+ </tr>
+</table>
+<ul>
+ <li>Build and install the Perl modules:
+ <pre>
+$ su
+# make install-perl-modules</pre>
+ <li>Apply the following changes to your database:
+<pre>
+CREATE TABLE svc_forward (
+ svcnum int NOT NULL,
+ srcsvc int NOT NULL,
+ dstsvc int NOT NULL,
+ dst varchar(80),
+ PRIMARY KEY (svcnum)
+);
+ALTER TABLE part_svc ADD svc_forward__srcsvc varchar(80) NULL;
+ALTER TABLE part_svc ADD svc_forward__srcsvc_flag char(1) NULL;
+ALTER TABLE part_svc ADD svc_forward__dstsvc varchar(80) NULL;
+ALTER TABLE part_svc ADD svc_forward__dstsvc_flag char(1) NULL;
+ALTER TABLE part_svc ADD svc_forward__dst varchar(80) NULL;
+ALTER TABLE part_svc ADD svc_forward__dst_flag char(1) NULL;
+
+CREATE TABLE cust_credit_bill (
+ creditbillnum int primary key,
+ crednum int not null,
+ invnum int not null,
+ _date int not null,
+ amount decimal(10,2) not null
+);
+
+CREATE TABLE cust_bill_pay (
+ billpaynum int primary key,
+ invnum int not null,
+ paynum int not null,
+ _date int not null,
+ amount decimal(10,2) not null
+);
+
+CREATE TABLE cust_credit_refund (
+ creditrefundnum int primary key,
+ crednum int not null,
+ refundnum int not null,
+ _date int not null,
+ amount decimal(10,2) not null
+);
+
+CREATE TABLE part_svc_column (
+ columnnum int primary key,
+ svcpart int not null,
+ columnname varchar(64) not null,
+ columnvalue varchar(80) null,
+ columnflag char(1) null
+);
+
+CREATE TABLE queue (
+ jobnum int primary key,
+ job text not null,
+ _date int not null,
+ status varchar(80) not null,
+ statustext text null,
+ svcnum int null
+);
+CREATE INDEX queue1 ON queue ( svcnum );
+CREATE INDEX queue2 ON queue ( status );
+
+CREATE TABLE queue_arg (
+ argnum int primary key,
+ jobnum int not null,
+ arg text null
+);
+CREATE INDEX queue_arg1 ON queue_arg ( jobnum );
+
+CREATE TABLE queue_depend (
+ dependnum int primary key,
+ jobnum int not null,
+ depend_jobnum int not null
+);
+CREATE INDEX queue_depend1 ON queue_depend ( jobnum );
+CREATE INDEX queue_depend2 ON queue_depend ( depend_jobnum );
+
+CREATE TABLE part_pop_local (
+ localnum int primary key,
+ popnum int not null,
+ city varchar(80) null,
+ state char(2) null,
+ npa char(3) not null,
+ nxx char(3) not null
+);
+CREATE UNIQUE INDEX part_pop_local1 ON part_pop_local ( npa, nxx );
+
+CREATE TABLE cust_bill_event (
+ eventnum int primary key,
+ invnum int not null,
+ eventpart int not null,
+ _date int not null
+);
+CREATE UNIQUE INDEX cust_bill_event1 ON cust_bill_event ( eventpart, invnum );
+CREATE INDEX cust_bill_event2 ON cust_bill_event ( invnum );
+
+CREATE TABLE part_bill_event (
+ eventpart int primary key,
+ payby char(4) not null,
+ event varchar(80) not null,
+ eventcode text null,
+ seconds int null,
+ weight int not null,
+ plan varchar(80) null,
+ plandata text null,
+ disabled char(1) null
+);
+CREATE INDEX part_bill_event1 ON part_bill_event ( payby );
+
+CREATE TABLE export_svc (
+ exportsvcnum int primary key,
+ exportnum int not null,
+ svcpart int not null
+);
+CREATE UNIQUE INDEX export_svc1 ON export_svc ( exportnum, svcpart );
+CREATE INDEX export_svc2 ON export_svc ( exportnum );
+CREATE INDEX export_svc3 ON export_svc ( svcpart );
+
+CREATE TABLE part_export (
+ exportnum int primary key,
+ machine varchar(80) not null,
+ exporttype varchar(80) not null,
+ nodomain char(1) NULL
+);
+CREATE INDEX part_export1 ON part_export ( machine );
+CREATE INDEX part_export2 ON part_export ( exporttype );
+
+CREATE TABLE part_export_option (
+ optionnum int primary key,
+ exportnum int not null,
+ optionname varchar(80) not null,
+ optionvalue text NULL
+);
+CREATE INDEX part_export_option1 ON part_export_option ( exportnum );
+CREATE INDEX part_export_option2 ON part_export_option ( optionname );
+
+CREATE TABLE radius_usergroup (
+ usergroupnum int primary key,
+ svcnum int not null,
+ groupname varchar(80) not null
+);
+CREATE INDEX radius_usergroup1 ON radius_usergroup ( svcnum );
+CREATE INDEX radius_usergroup2 ON radius_usergroup ( groupname );
+
+CREATE TABLE msgcat (
+ msgnum int primary key,
+ msgcode varchar(80) not null,
+ locale varchar(16) not null,
+ msg text not null
+);
+CREATE INDEX msgcat1 ON msgcat ( msgcode, locale );
+
+CREATE TABLE cust_tax_exempt (
+ exemptnum int primary key,
+ custnum int not null,
+ taxnum int not null,
+ year int not null,
+ month int not null,
+ amount decimal(10,2)
+);
+CREATE UNIQUE INDEX cust_tax_exempt1 ON cust_tax_exempt ( taxnum, year, month );
+
+ALTER TABLE svc_acct ADD domsvc integer NULL;
+ALTER TABLE part_svc ADD svc_acct__domsvc varchar(80) NULL;
+ALTER TABLE part_svc ADD svc_acct__domsvc_flag char(1) NULL;
+ALTER TABLE svc_domain ADD catchall integer NULL;
+ALTER TABLE cust_main ADD referral_custnum integer NULL;
+ALTER TABLE cust_main ADD comments text NULL;
+ALTER TABLE cust_pay ADD custnum integer;
+ALTER TABLE cust_pay_batch ADD paybatchnum integer;
+ALTER TABLE cust_refund ADD custnum integer;
+ALTER TABLE cust_pkg ADD manual_flag char(1) NULL;
+ALTER TABLE part_pkg ADD plan varchar(80) NULL;
+ALTER TABLE part_pkg ADD plandata text NULL;
+ALTER TABLE part_pkg ADD setuptax char(1) NULL;
+ALTER TABLE part_pkg ADD recurtax char(1) NULL;
+ALTER TABLE part_pkg ADD disabled char(1) NULL;
+ALTER TABLE part_svc ADD disabled char(1) NULL;
+ALTER TABLE cust_bill ADD closed char(1) NULL;
+ALTER TABLE cust_pay ADD closed char(1) NULL;
+ALTER TABLE cust_credit ADD closed char(1) NULL;
+ALTER TABLE cust_refund ADD closed char(1) NULL;
+ALTER TABLE cust_bill_event ADD status varchar(80);
+ALTER TABLE cust_bill_event ADD statustext text NULL;
+ALTER TABLE svc_acct ADD sec_phrase varchar(80) NULL;
+ALTER TABLE part_svc ADD svc_acct__sec_phrase varchar(80) NULL;
+ALTER TABLE part_svc ADD svc_acct__sec_phrase_flag char(1) NULL;
+ALTER TABLE part_pkg ADD taxclass varchar(80) NULL;
+ALTER TABLE cust_main_county ADD taxclass varchar(80) NULL;
+ALTER TABLE cust_main_county ADD exempt_amount decimal(10,2);
+CREATE INDEX cust_main3 ON cust_main ( referral_custnum );
+CREATE INDEX cust_credit_bill1 ON cust_credit_bill ( crednum );
+CREATE INDEX cust_credit_bill2 ON cust_credit_bill ( invnum );
+CREATE INDEX cust_bill_pay1 ON cust_bill_pay ( invnum );
+CREATE INDEX cust_bill_pay2 ON cust_bill_pay ( paynum );
+CREATE INDEX cust_credit_refund1 ON cust_credit_refund ( crednum );
+CREATE INDEX cust_credit_refund2 ON cust_credit_refund ( refundnum );
+CREATE UNIQUE INDEX cust_pay_batch_pkey ON cust_pay_batch ( paybatchnum );
+CREATE UNIQUE INDEX part_svc_column1 ON part_svc_column ( svcpart, columnname );
+CREATE INDEX cust_pay2 ON cust_pay ( paynum );
+CREATE INDEX cust_pay3 ON cust_pay ( custnum );
+CREATE INDEX cust_pay4 ON cust_pay ( paybatch );
+</pre>
+
+ <li>If you are using PostgreSQL, apply the following changes to your database:
+<pre>
+CREATE UNIQUE INDEX agent_pkey ON agent ( agentnum );
+CREATE UNIQUE INDEX agent_type_pkey ON agent_type ( typenum );
+CREATE UNIQUE INDEX cust_bill_pkey ON cust_bill ( invnum );
+CREATE UNIQUE INDEX cust_credit_pkey ON cust_credit ( crednum );
+CREATE UNIQUE INDEX cust_main_pkey ON cust_main ( custnum );
+CREATE UNIQUE INDEX cust_main_county_pkey ON cust_main_county ( taxnum );
+CREATE UNIQUE INDEX cust_main_invoice_pkey ON cust_main_invoice ( destnum );
+CREATE UNIQUE INDEX cust_pay_pkey ON cust_pay ( paynum );
+CREATE UNIQUE INDEX cust_pkg_pkey ON cust_pkg ( pkgnum );
+CREATE UNIQUE INDEX cust_refund_pkey ON cust_refund ( refundnum );
+CREATE UNIQUE INDEX cust_svc_pkey ON cust_svc ( svcnum );
+CREATE UNIQUE INDEX domain_record_pkey ON domain_record ( recnum );
+CREATE UNIQUE INDEX nas_pkey ON nas ( nasnum );
+CREATE UNIQUE INDEX part_pkg_pkey ON part_pkg ( pkgpart );
+CREATE UNIQUE INDEX part_referral_pkey ON part_referral ( refnum );
+CREATE UNIQUE INDEX part_svc_pkey ON part_svc ( svcpart );
+CREATE UNIQUE INDEX port_pkey ON port ( portnum );
+CREATE UNIQUE INDEX prepay_credit_pkey ON prepay_credit ( prepaynum );
+CREATE UNIQUE INDEX session_pkey ON session ( sessionnum );
+CREATE UNIQUE INDEX svc_acct_pkey ON svc_acct ( svcnum );
+CREATE UNIQUE INDEX svc_acct_pop_pkey ON svc_acct_pop ( popnum );
+CREATE UNIQUE INDEX svc_acct_sm_pkey ON svc_acct_sm ( svcnum );
+CREATE UNIQUE INDEX svc_domain_pkey ON svc_domain ( svcnum );
+CREATE UNIQUE INDEX svc_www_pkey ON svc_www ( svcnum );
+</pre>
+ <li>If you wish to enable service/shipping addresses, apply the following
+ changes to your database:
+<pre>
+ALTER TABLE cust_main ADD COLUMN ship_last varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_first varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_company varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_address1 varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_address2 varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_city varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_county varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_state varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_zip varchar(10) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_country char(2) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_daytime varchar(20) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_night varchar(20) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_fax varchar(12) NULL;
+CREATE INDEX cust_main4 ON cust_main ( ship_last );
+CREATE INDEX cust_main5 ON cust_main ( ship_company );
+</pre>
+ <li>If you are using the signup server, reinstall it according to the <a href="signup.html">instructions</a>. The 1.3.x signup server is not compatible with 1.4.x.
+ <li>Run <tt>bin/dbdef-create <i>username</i></tt>
+ <li>If you have svc_acct_sm records or service definitions:
+ <ul>
+ <li>Create a service definition with table svc_forward
+ <li>Run <tt>bin/fs-migrate-svc_acct_sm <i>username</i></tt>
+ </ul>
+ <li>Or if you just have svc_acct records:
+ <ul>
+ <li>Order and provision a package for your default domain and note down the <b>Service #</b> or <i>svcnum</i>.
+ <li><tt>UPDATE svc_acct SET domsvc = </tt><i>svcnum</i>
+ <li>Update your service definitions to have default (or fixed) <b>domsvc</b>.
+ </ul>
+ <li>Run <tt>bin/fs-migrate-payref<i>username</i></tt>
+ <li>Run <tt>bin/fs-migrate-part_svc<i>username</i></tt>
+ <li><b>After running bin/fs-migrate-payref</b>, apply the following changes to your database:
+ <table border><tr><th>PostgreSQL</th><th>MySQL, others</th></tr>
+<tr><td>
+<font size=-1><pre>
+CREATE TABLE cust_pay_temp (
+ paynum int primary key,
+ custnum int not null,
+ paid decimal(10,2) not null,
+ _date int null,
+ payby char(4) not null,
+ payinfo varchar(16) null,
+ paybatch varchar(80) null,
+ closed char(1) null
+);
+INSERT INTO cust_pay_temp SELECT paynum, custnum, paid, _date, payby, payinfo, paybatch, closed FROM cust_pay;
+DROP TABLE cust_pay;
+ALTER TABLE cust_pay_temp RENAME TO cust_pay;
+CREATE UNIQUE INDEX cust_pay1 ON cust_pay (paynum);
+CREATE TABLE cust_refund_temp (
+ refundnum int primary key,
+ custnum int not null,
+ _date int null,
+ refund decimal(10,2) not null,
+ otaker varchar(8) not null,
+ reason varchar(80) not null,
+ payby char(4) not null,
+ payinfo varchar(16) null,
+ paybatch varchar(80) null,
+ closed char(1) null
+);
+INSERT INTO cust_refund_temp SELECT refundnum, custnum, _date, refund, otaker, reason, payby, payinfo, '', closed from cust_refund;
+DROP TABLE cust_refund;
+ALTER TABLE cust_refund_temp RENAME TO cust_refund;
+CREATE UNIQUE INDEX cust_refund1 ON cust_refund (refundnum);
+</pre></font>
+</td><td>
+<font size=-1><pre>
+ALTER TABLE cust_pay DROP COLUMN invnum;
+ALTER TABLE cust_refund DROP COLUMN crednum;
+</pre></font>
+</td></tr></table>
+ <li><b>IMPORTANT: After applying the second set of database changes</b>, run <tt>bin/dbdef-create <i>username</i></tt> again.
+ <li><b>IMPORTANT</b>: run <tt>bin/create-history-tables <i>username</i></tt>
+ <li><b>IMPORTANT: After running bin/create-history-tables</b>, run <tt>bin/dbdef-create <i>username</i></tt> again.
+ <li>As the freeside UNIX user, run <tt>bin/populate-msgcat <i>username</i></tt
+> to populate the message catalog
+<!-- <li>set the <a href="../config/config.cgi#username_policy">user_policy configuration value</a> as appropriate for your site. -->
+ <li>set the <a href="../config/config.cgi#locale">locale configuration value</a> to en_US.
+ <li>the mxmachines, nsmachines, arecords and cnamerecords configuration values have been deprecated. Set the <a href="../config/config.cgi#defaultrecords">defaultrecords configuration value</a> instead.
+ <li>Create the `/usr/local/etc/freeside/cache.<i>datasrc</i>' directory
+ (owned by the freeside user).
+ <li>freeside-queued was installed with the Perl modules. Start it now and ensure that is run upon system startup.
+ <li>Set appropriate <a href="../browse/part_bill_event.cgi">invoice events</a> for your site. At the very least, you'll want to set some invoice events "<i>After 0 days</i>": a <i>BILL</i> invoice event to print invoices, a <i>CARD</i> invoice event to batch or run cards real-time, and a <i>COMP</i> invoice event to "pay" complimentary customers. If you were using the <i>-i</i> option to <a href="man/bin/freeside-bill.html">freeside-bill</a> it should be removed.
+ <li>Use <a href="man/bin/freeside-daily.html">freeside-daily</a> instead of <a href="man/bin/freeside-bill.html">freeside-bill</a>.
+ <li>If you would like Freeside to notify your customers when their credit
+ cards and other billing arrangements are about to expire, arrange for
+ <b>freeside-expiration-alerter</b> to be run daily by cron or similar
+ facility. The message it sends can be configured from the
+ <u>Configuration</u> choice of the main menu as <u>alerter_template</u>.
+ <li>Export has been rewritten. If you were using the icradiusmachines,
+ icradius_mysqldest, icradius_mysqlsource, or icradius_secrets files, add
+ an appropriate "sqlradius" export to all relevant Service Definitions
+ instead. Use <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Replication">MySQL replication</a> or
+ point the "sqlradius" export directly at your external ICRADIUS or FreeRADIUS
+ database (or through an SSL-necrypting proxy...)
+</ul>
+</body>
diff --git a/httemplate/docs/upgrade9.html b/httemplate/docs/upgrade9.html
new file mode 100644
index 0000000..6a8fd96
--- /dev/null
+++ b/httemplate/docs/upgrade9.html
@@ -0,0 +1,28 @@
+<head>
+ <title>Upgrading to 1.4.1</title>
+</head>
+<body>
+<h1>Upgrading to 1.4.1 from 1.4.0</h1>
+<ul>
+ <li>If migrating from less than 1.4.0, see these <a href="upgrade8.html">instructions</a> first.
+ <li>Back up your data and current Freeside installation.
+ <li>Run <code>make aspdocs</code> or <code>make masondocs</code>.
+ <li>Copy <code>aspdocs/</code> or <code>masondocs/</code> to your web server's document space.
+ <li>Run <code>make install-perl-modules</code>.
+ <li>Install <a href="http://search.cpan.org/search?dist=Net-SSH">Net::SSH</a> minimum version 0.07
+ <li>Apply the following changes to your database:
+<pre>
+INSERT INTO msgcat ( msgnum, msgcode, locale, msg ) VALUES ( 18, 'daytime', 'en_US', 'Day Phone' );
+INSERT INTO msgcat ( msgnum, msgcode, locale, msg ) VALUES ( 19, 'night', 'en_US', 'Night Phone' );
+</pre>
+ <li>Optionally, apply the following changes to your database (performance improvements):
+<pre>
+CREATE INDEX part_pkg1 ON part_pkg ( disabled );
+CREATE INDEX part_svc1 ON part_svc ( disabled );
+CREATE INDEX cust_bill2 ON cust_bill ( _date );
+</pre>
+ <li>If you want to use ACH (electronic checks), you will need to make changes to your database. The easiest way to make these changes is to dump your database (with pg_dump), change the payinfo field in the cust_pay, cust_refund, h_cust_pay and h_cust_refund tables from varchar(16) to varchar(80), reload the database from the dump.
+ <li>If you will be doing bind exports you should make additional changes to your database. Follow the directions above to dump the database and change the reczone and recdata fields in the domain_record and h_domain_record tables from varchar(80) to varchar(255).
+ <li>If you made changes to your db schema from a dump as listed above run dbdef-create.
+ <li>Restart Apache and freeside-queued.
+</body>
diff --git a/httemplate/edit/REAL_cust_pkg.cgi b/httemplate/edit/REAL_cust_pkg.cgi
new file mode 100755
index 0000000..d9b7579
--- /dev/null
+++ b/httemplate/edit/REAL_cust_pkg.cgi
@@ -0,0 +1,131 @@
+<!-- mason kludge -->
+<%
+# <!-- $Id: REAL_cust_pkg.cgi,v 1.7 2003-11-19 12:21:09 ivan Exp $ -->
+
+my $error ='';
+my $pkgnum = '';
+if ( $cgi->param('error') ) {
+ $error = $cgi->param('error');
+ $pkgnum = $cgi->param('pkgnum');
+} else {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/ or die "no pkgnum";
+ $pkgnum = $1;
+}
+
+#get package record
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+die "No package!" unless $cust_pkg;
+my $part_pkg = qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->getfield('pkgpart')});
+
+if ( $error ) {
+ #$cust_pkg->$_(str2time($cgi->param($_)) foreach qw(setup bill);
+ $cust_pkg->setup(str2time($cgi->param('setup')));
+ $cust_pkg->bill(str2time($cgi->param('bill')));
+}
+
+#my $custnum = $cust_pkg->getfield('custnum');
+print header('Package Edit'); #, menubar(
+# "View this customer (#$custnum)" => popurl(2). "view/cust_main.cgi?$custnum",
+# 'Main Menu' => popurl(2)
+#));
+
+%>
+
+ <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
+
+<%
+
+#print info
+my($susp,$cancel,$expire)=(
+ $cust_pkg->getfield('susp'),
+ $cust_pkg->getfield('cancel'),
+ $cust_pkg->getfield('expire'),
+);
+my($pkg,$comment)=($part_pkg->getfield('pkg'),$part_pkg->getfield('comment'));
+my($setup,$bill)=($cust_pkg->getfield('setup'),$cust_pkg->getfield('bill'));
+my $otaker = $cust_pkg->getfield('otaker');
+
+print '<FORM NAME="formname" ACTION="process/REAL_cust_pkg.cgi" METHOD="POST">', qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!;
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: $error</FONT>!
+ if $error;
+
+#my $format = "%c %z (%Z)";
+my $format = "%m/%d/%Y %T %z (%Z)";
+
+print ntable("#cccccc",2),
+ '<TR><TD ALIGN="right">Package number</TD><TD BGCOLOR="#ffffff">',
+ $pkgnum, '</TD></TR>',
+ '<TR><TD ALIGN="right">Package</TD><TD BGCOLOR="#ffffff">',
+ $pkg, '</TD></TR>',
+ '<TR><TD ALIGN="right">Comment</TD><TD BGCOLOR="#ffffff">',
+ $comment, '</TD></TR>',
+ '<TR><TD ALIGN="right">Order taker</TD><TD BGCOLOR="#ffffff">',
+ $otaker, '</TD></TR>',
+ '<TR><TD ALIGN="right">Setup date</TD><TD>'.
+ '<INPUT TYPE="text" NAME="setup" SIZE=32 ID="setup_text" VALUE="',
+ ( $setup ? time2str($format, $setup) : "" ), '">'.
+ ' <IMG SRC="../images/calendar.png" ID="setup_button" STYLE="cursor: pointer" TITLE="Select date">'.
+ '</TD></TR>';
+
+print '<TR><TD ALIGN="right">Last bill date</TD><TD>',
+ '<INPUT TYPE="text" NAME="last_bill" SIZE=32 ID="last_bill_text" VALUE="',
+ ( $cust_pkg->last_bill
+ ? time2str($format, $cust_pkg->last_bill)
+ : "" ),
+ '">'.
+ ' <IMG SRC="../images/calendar.png" ID="last_bill_button" STYLE="cursor: pointer" TITLE="Select date">'.
+ '</TD></TR>'
+ if $cust_pkg->dbdef_table->column('last_bill');
+
+print '<TR><TD ALIGN="right">Next bill date</TD><TD>',
+ '<INPUT TYPE="text" NAME="bill" SIZE=32 ID="bill_text" VALUE="',
+ ( $bill ? time2str($format, $bill) : "" ), '">'.
+ ' <IMG SRC="../images/calendar.png" ID="bill_button" STYLE="cursor: pointer" TITLE="Select date">'.
+ '</TD></TR>';
+
+print '<TR><TD ALIGN="right">Suspension date</TD><TD BGCOLOR="#ffffff">',
+ time2str($format, $susp), '</TD></TR>'
+ if $susp;
+
+#print '<TR><TD ALIGN="right">Expiration date</TD><TD BGCOLOR="#ffffff">',
+# time2str("%D",$expire), '</TD></TR>'
+# if $expire;
+print '<TR><TD ALIGN="right">Expiration date'.
+ '</TD><TD>',
+ '<INPUT TYPE="text" NAME="expire" SIZE=32 ID="expire_text" VALUE="',
+ ( $expire ? time2str($format, $expire) : "" ), '">'.
+ ' <IMG SRC="../images/calendar.png" ID="expire_button" STYLE="cursor: pointer" TITLE="Select date">'.
+ '<BR><FONT SIZE=-1>(will <b>cancel</b> this package'.
+ ' when the date is reached)</FONT>'.
+ '</TD></TR>';
+
+print '<TR><TD ALIGN="right">Cancellation date</TD><TD BGCOLOR="#ffffff">',
+ time2str($format, $cancel), '</TD></TR>'
+ if $cancel;
+
+%>
+</TABLE>
+<SCRIPT TYPE="text/javascript">
+<%
+ my @cal = qw( setup bill expire );
+ push @cal, 'last_bill'
+ if $cust_pkg->dbdef_table->column('last_bill');
+ foreach my $cal (@cal) {
+%>
+ Calendar.setup({
+ inputField: "<%= $cal %>_text",
+ ifFormat: "%m/%d/%Y",
+ button: "<%= $cal %>_button",
+ align: "BR"
+ });
+<% } %>
+</SCRIPT>
+<BR><INPUT TYPE="submit" VALUE="Apply Changes">
+</FORM>
+</BODY>
+</HTML>
diff --git a/httemplate/edit/agent.cgi b/httemplate/edit/agent.cgi
new file mode 100755
index 0000000..8a1cb2a
--- /dev/null
+++ b/httemplate/edit/agent.cgi
@@ -0,0 +1,79 @@
+<!-- mason kludge -->
+<%
+
+my $agent;
+if ( $cgi->param('error') ) {
+ $agent = new FS::agent ( {
+ map { $_, scalar($cgi->param($_)) } fields('agent')
+ } );
+} elsif ( $cgi->keywords ) {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ $agent = qsearchs( 'agent', { 'agentnum' => $1 } );
+} else { #adding
+ $agent = new FS::agent {};
+}
+my $action = $agent->agentnum ? 'Edit' : 'Add';
+my $hashref = $agent->hashref;
+
+%>
+
+<%= header("$action Agent", menubar(
+ 'Main Menu' => $p,
+ 'View all agents' => $p. 'browse/agent.cgi',
+)) %>
+
+<% if ( $cgi->param('error') ) { %>
+<FONT SIZE="+1" COLOR="#ff0000">Error: <%= $cgi->param('error') %></FONT>
+<% } %>
+
+<FORM ACTION="<%=popurl(1)%>process/agent.cgi" METHOD=POST>
+<INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $hashref->{agentnum} %>">
+Agent #<%= $hashref->{agentnum} ? $hashref->{agentnum} : "(NEW)" %>
+
+<%= &ntable("#cccccc", 2, '') %>
+<TR>
+ <TH ALIGN="right">Agent</TH>
+ <TD><INPUT TYPE="text" NAME="agent" SIZE=32 VALUE="<%= $hashref->{agent} %>"></TD>
+</TR>
+<TR>
+ <TH ALIGN="right">Agent type</TH>
+ <TD><SELECT NAME="typenum" SIZE=1>
+
+<% foreach my $agent_type (qsearch('agent_type',{})) { %>
+ <OPTION VALUE="<%= $agent_type->typenum %>"<%= ( $hashref->{typenum} && ( $hashref->{typenum} == $agent_type->typenum ) ) ? ' SELECTED' : '' %>>
+ <%= $agent_type->getfield('typenum') %>: <%= $agent_type->getfield('atype') %>
+<% } %>
+
+</SELECT></TD>
+</TR>
+<% if ( dbdef->table('agent')->column('disabled') ) { %>
+ <TR>
+ <TD ALIGN="right">Disable</TD>
+ <TD><INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<%= $hashref->{disabled} eq 'Y' ? ' CHECKED' : '' %>></TD>
+ </TR>
+<% } %>
+<TR>
+ <TD ALIGN="right"><!--Frequency--></TD>
+ <TD><INPUT TYPE="hidden" NAME="freq" VALUE="<%= $hashref->{freq} %>"></TD>
+</TR>
+<TR>
+ <TD ALIGN="right"><!--Program--></TD>
+ <TD><INPUT TYPE="hidden" NAME="prog" VALUE="<%= $hashref->{prog} %>"></TD>
+</TR>
+<% if ( dbdef->table('agent')->column('username') ) { %>
+ <TR>
+ <TD ALIGN="right">Agent interface username</TD>
+ <TD><INPUT TYPE="text" NAME="username" VALUE="<%= $hashref->{username} %>"></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Agent interface password</TD>
+ <TD><INPUT TYPE="text" NAME="_password" VALUE="<%= $hashref->{_password} %>"></TD>
+ </TR>
+<% } %>
+</TABLE>
+
+<BR><INPUT TYPE="submit" VALUE="<%= $hashref->{agentnum} ? "Apply changes" : "Add agent" %>">
+ </FORM>
+ </BODY>
+</HTML>
diff --git a/httemplate/edit/agent_type.cgi b/httemplate/edit/agent_type.cgi
new file mode 100755
index 0000000..637c710
--- /dev/null
+++ b/httemplate/edit/agent_type.cgi
@@ -0,0 +1,63 @@
+<!-- mason kludge -->
+<%
+
+my($agent_type);
+if ( $cgi->param('error') ) {
+ $agent_type = new FS::agent_type ( {
+ map { $_, scalar($cgi->param($_)) } fields('agent')
+ } );
+} elsif ( $cgi->keywords ) { #editing
+ my( $query ) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ $agent_type=qsearchs('agent_type',{'typenum'=>$1});
+} else { #adding
+ $agent_type = new FS::agent_type {};
+}
+my $action = $agent_type->typenum ? 'Edit' : 'Add';
+my $hashref = $agent_type->hashref;
+
+print header("$action Agent Type", menubar(
+ 'Main Menu' => "$p",
+ 'View all agent types' => "${p}browse/agent_type.cgi",
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+print '<FORM ACTION="', popurl(1), 'process/agent_type.cgi" METHOD=POST>',
+ qq!<INPUT TYPE="hidden" NAME="typenum" VALUE="$hashref->{typenum}">!,
+ "Agent Type #", $hashref->{typenum} ? $hashref->{typenum} : "(NEW)";
+
+print <<END;
+<BR><BR>Agent Type <INPUT TYPE="text" NAME="atype" SIZE=32 VALUE="$hashref->{atype}">
+<BR><BR>Select which packages agents of this type may sell to customers<BR>
+END
+
+foreach my $part_pkg ( qsearch('part_pkg',{ 'disabled' => '' }) ) {
+ print qq!<BR><INPUT TYPE="checkbox" NAME="pkgpart!,
+ $part_pkg->getfield('pkgpart'), qq!" !,
+ # ( 'CHECKED 'x scalar(
+ qsearchs('type_pkgs',{
+ 'typenum' => $agent_type->getfield('typenum'),
+ 'pkgpart' => $part_pkg->getfield('pkgpart'),
+ })
+ ? 'CHECKED '
+ : '',
+ qq!VALUE="ON"> !,
+ qq!<A HREF="${p}edit/part_pkg.cgi?!, $part_pkg->pkgpart,
+ '">', $part_pkg->pkgpart. ": ". $part_pkg->getfield('pkg'), '</A>',
+ ;
+}
+
+print qq!<BR><BR><INPUT TYPE="submit" VALUE="!,
+ $hashref->{typenum} ? "Apply changes" : "Add agent type",
+ qq!">!;
+
+print <<END;
+ </FORM>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_bill_pay.cgi b/httemplate/edit/cust_bill_pay.cgi
new file mode 100755
index 0000000..24bce30
--- /dev/null
+++ b/httemplate/edit/cust_bill_pay.cgi
@@ -0,0 +1,95 @@
+<!-- mason kludge -->
+<%
+
+my($paynum, $amount, $invnum);
+if ( $cgi->param('error') ) {
+ $paynum = $cgi->param('paynum');
+ $amount = $cgi->param('amount');
+ $invnum = $cgi->param('invnum');
+} else {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ $paynum = $1;
+ $amount = '';
+ $invnum = '';
+}
+
+my $otaker = getotaker;
+
+my $p1 = popurl(1);
+
+print header("Apply Payment", '');
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT><BR><BR>"
+ if $cgi->param('error');
+print <<END;
+ <FORM ACTION="${p1}process/cust_bill_pay.cgi" METHOD=POST>
+END
+
+my $cust_pay = qsearchs('cust_pay', { 'paynum' => $paynum } );
+die "payment $paynum not found!" unless $cust_pay;
+
+my $unapplied = $cust_pay->unapplied;
+
+print "Payment # <B>$paynum</B>".
+ qq!<INPUT TYPE="hidden" NAME="paynum" VALUE="$paynum">!.
+ '<BR>Date: <B>'. time2str("%D", $cust_pay->_date). '</B>'.
+ '<BR>Amount: $<B>'. $cust_pay->paid. '</B>'.
+ "<BR>Unapplied amount: \$<B>$unapplied</B>"
+ ;
+
+my @cust_bill = grep $_->owed != 0,
+ qsearch('cust_bill', { 'custnum' => $cust_pay->custnum } );
+
+print <<END;
+<SCRIPT>
+function changed(what) {
+ cust_bill = what.options[what.selectedIndex].value;
+END
+
+foreach my $cust_bill ( @cust_bill ) {
+ my $invnum = $cust_bill->invnum;
+ my $changeto = $cust_bill->owed < $unapplied
+ ? $cust_bill->owed
+ : $unapplied;
+ print <<END;
+ if ( cust_bill == $invnum ) {
+ what.form.amount.value = "$changeto";
+ }
+END
+}
+
+print <<END;
+ if ( cust_bill == "Refund" ) {
+ what.form.amount.value = "$unapplied";
+ }
+}
+</SCRIPT>
+END
+
+print qq!<BR>Invoice #<SELECT NAME="invnum" SIZE=1 onChange="changed(this)">!,
+ '<OPTION VALUE="">';
+foreach my $cust_bill ( @cust_bill ) {
+ print '<OPTION'. ( $cust_bill->invnum eq $invnum ? ' SELECTED' : '' ).
+ ' VALUE="'. $cust_bill->invnum. '">'. $cust_bill->invnum.
+ ' - '. time2str("%D",$cust_bill->_date).
+ ' - $'. $cust_bill->owed;
+}
+print qq!<OPTION VALUE="Refund">Refund!;
+print "</SELECT>";
+
+print qq!<BR>Amount \$<INPUT TYPE="text" NAME="amount" VALUE="$amount" SIZE=8 MAXLENGTH=8>!;
+
+print <<END;
+<BR>
+<INPUT TYPE="submit" VALUE="Apply">
+END
+
+print <<END;
+
+ </FORM>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_credit.cgi b/httemplate/edit/cust_credit.cgi
new file mode 100755
index 0000000..aae0df2
--- /dev/null
+++ b/httemplate/edit/cust_credit.cgi
@@ -0,0 +1,63 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+my($custnum, $amount, $reason);
+if ( $cgi->param('error') ) {
+ #$cust_credit = new FS::cust_credit ( {
+ # map { $_, scalar($cgi->param($_)) } fields('cust_credit')
+ #} );
+ $custnum = $cgi->param('custnum');
+ $amount = $cgi->param('amount');
+ #$refund = $cgi->param('refund');
+ $reason = $cgi->param('reason');
+} else {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ $custnum = $1;
+ $amount = '';
+ #$refund = 'yes';
+ $reason = '';
+}
+my $_date = time;
+
+my $otaker = getotaker;
+
+my $p1 = popurl(1);
+
+print header("Post Credit", '');
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+print <<END, small_custview($custnum, $conf->config('countrydefault'));
+ <FORM ACTION="${p1}process/cust_credit.cgi" METHOD=POST>
+ <INPUT TYPE="hidden" NAME="crednum" VALUE="">
+ <INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">
+ <INPUT TYPE="hidden" NAME="paybatch" VALUE="">
+ <INPUT TYPE="hidden" NAME="_date" VALUE="$_date">
+ <INPUT TYPE="hidden" NAME="credited" VALUE="">
+ <INPUT TYPE="hidden" NAME="otaker" VALUE="$otaker">
+END
+
+print '<BR><BR>Credit'. ntable("#cccccc", 2).
+ '<TR><TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff">'.
+ time2str("%D",$_date). '</TD></TR>';
+
+print qq!<TR><TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">\$<INPUT TYPE="text" NAME="amount" VALUE="$amount" SIZE=8 MAXLENGTH=8></TD></TR>!;
+
+#print qq! <INPUT TYPE="checkbox" NAME="refund" VALUE="$refund">Also post refund!;
+
+print qq!<TR><TD ALIGN="right">Reason</TD><TD BGCOLOR="#ffffff"><INPUT TYPE="text" NAME="reason" VALUE="$reason"></TD></TR>!;
+
+print qq!<TR><TD ALIGN="right">Auto-apply<BR>to invoices</TD><TD><SELECT NAME="apply"><OPTION VALUE="yes" SELECTED>yes<OPTION>no</SELECT></TD>!;
+
+print <<END;
+</TABLE>
+<BR>
+<INPUT TYPE="submit" VALUE="Post credit">
+ </FORM>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_credit_bill.cgi b/httemplate/edit/cust_credit_bill.cgi
new file mode 100755
index 0000000..1a97e13
--- /dev/null
+++ b/httemplate/edit/cust_credit_bill.cgi
@@ -0,0 +1,101 @@
+<!-- mason kludge -->
+<%
+
+my($crednum, $amount, $invnum);
+if ( $cgi->param('error') ) {
+ #$cust_credit_bill = new FS::cust_credit_bill ( {
+ # map { $_, scalar($cgi->param($_)) } fields('cust_credit_bill')
+ #} );
+ $crednum = $cgi->param('crednum');
+ $amount = $cgi->param('amount');
+ #$refund = $cgi->param('refund');
+ $invnum = $cgi->param('invnum');
+} else {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ $crednum = $1;
+ $amount = '';
+ #$refund = 'yes';
+ $invnum = '';
+}
+
+my $otaker = getotaker;
+
+my $p1 = popurl(1);
+
+print header("Apply Credit", '');
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT><BR><BR>"
+ if $cgi->param('error');
+print <<END;
+ <FORM ACTION="${p1}process/cust_credit_bill.cgi" METHOD=POST>
+END
+
+my $cust_credit = qsearchs('cust_credit', { 'crednum' => $crednum } );
+die "credit $crednum not found!" unless $cust_credit;
+
+my $credited = $cust_credit->credited;
+
+print "Credit # <B>$crednum</B>".
+ qq!<INPUT TYPE="hidden" NAME="crednum" VALUE="$crednum">!.
+ '<BR>Date: <B>'. time2str("%D", $cust_credit->_date). '</B>'.
+ '<BR>Amount: $<B>'. $cust_credit->amount. '</B>'.
+ "<BR>Unapplied amount: \$<B>$credited</B>".
+ '<BR>Reason: <B>'. $cust_credit->reason. '</B>'
+ ;
+
+my @cust_bill = grep $_->owed != 0,
+ qsearch('cust_bill', { 'custnum' => $cust_credit->custnum } );
+
+print <<END;
+<SCRIPT>
+function changed(what) {
+ cust_bill = what.options[what.selectedIndex].value;
+END
+
+foreach my $cust_bill ( @cust_bill ) {
+ my $invnum = $cust_bill->invnum;
+ my $changeto = $cust_bill->owed < $cust_credit->credited
+ ? $cust_bill->owed
+ : $cust_credit->credited;
+ print <<END;
+ if ( cust_bill == $invnum ) {
+ what.form.amount.value = "$changeto";
+ }
+END
+}
+
+print <<END;
+ if ( cust_bill == "Refund" ) {
+ what.form.amount.value = "$credited";
+ }
+}
+</SCRIPT>
+END
+
+print qq!<BR>Invoice #<SELECT NAME="invnum" SIZE=1 onChange="changed(this)">!,
+ '<OPTION VALUE="">';
+foreach my $cust_bill ( @cust_bill ) {
+ print '<OPTION'. ( $cust_bill->invnum eq $invnum ? ' SELECTED' : '' ).
+ ' VALUE="'. $cust_bill->invnum. '">'. $cust_bill->invnum.
+ ' - '. time2str("%D",$cust_bill->_date).
+ ' - $'. $cust_bill->owed;
+}
+print qq!<OPTION VALUE="Refund">Refund!;
+print "</SELECT>";
+
+print qq!<BR>Amount \$<INPUT TYPE="text" NAME="amount" VALUE="$amount" SIZE=8 MAXLENGTH=8>!;
+
+print <<END;
+<BR>
+<INPUT TYPE="submit" VALUE="Apply">
+END
+
+print <<END;
+
+ </FORM>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi
new file mode 100755
index 0000000..4a8f705
--- /dev/null
+++ b/httemplate/edit/cust_main.cgi
@@ -0,0 +1,572 @@
+<!-- mason kludge -->
+<%
+
+ #for misplaced logic below
+ #use FS::part_pkg;
+
+ #for false laziness below (now more properly lazy)
+ #use FS::svc_acct_pop;
+
+ #for (other) false laziness below
+ #use FS::agent;
+ #use FS::type_pkgs;
+
+my $conf = new FS::Conf;
+
+#get record
+
+my $error = '';
+my($custnum, $username, $password, $popnum, $cust_main, $saved_pkgpart);
+my(@invoicing_list);
+if ( $cgi->param('error') ) {
+ $error = $cgi->param('error');
+ $cust_main = new FS::cust_main ( {
+ map { $_, scalar($cgi->param($_)) } fields('cust_main')
+ } );
+ $custnum = $cust_main->custnum;
+ $saved_pkgpart = $cgi->param('pkgpart_svcpart') || '';
+ if ( $saved_pkgpart =~ /^(\d+)_/ ) {
+ $saved_pkgpart = $1;
+ } else {
+ $saved_pkgpart = '';
+ }
+ $username = $cgi->param('username');
+ $password = $cgi->param('_password');
+ $popnum = $cgi->param('popnum');
+ @invoicing_list = split( /\s*,\s*/, $cgi->param('invoicing_list') );
+} elsif ( $cgi->keywords ) { #editing
+ my( $query ) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ $custnum=$1;
+ $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+ if ( $cust_main->dbdef_table->column('paycvv')
+ && length($cust_main->paycvv) ) {
+ my $paycvv = $cust_main->paycvv;
+ $paycvv =~ s/./*/g;
+ $cust_main->paycvv($paycvv);
+ }
+ $saved_pkgpart = 0;
+ $username = '';
+ $password = '';
+ $popnum = 0;
+ @invoicing_list = $cust_main->invoicing_list;
+} else {
+ $custnum='';
+ $cust_main = new FS::cust_main ( {} );
+ $cust_main->otaker( &getotaker );
+ $cust_main->referral_custnum( $cgi->param('referral_custnum') );
+ $saved_pkgpart = 0;
+ $username = '';
+ $password = '';
+ $popnum = 0;
+ @invoicing_list = ();
+}
+$cgi->delete_all();
+my $action = $custnum ? 'Edit' : 'Add';
+
+# top
+
+my $p1 = popurl(1);
+print header("Customer $action", '', ' onUnload="myclose()"');
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $error, "</FONT>"
+ if $error;
+
+print qq!<FORM ACTION="${p1}process/cust_main.cgi" METHOD=POST NAME="form1" onSubmit="document.form1.submit.disabled=true">!,
+ qq!<INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">!,
+ qq!Customer # !, ( $custnum ? "<B>$custnum</B>" : " (NEW)" ),
+
+;
+
+# agent
+
+my $r = qq!<font color="#ff0000">*</font>&nbsp;!;
+
+my %agent_search = dbdef->table('agent')->column('disabled')
+ ? ( 'disabled' => '' ) : ();
+my @agents = qsearch( 'agent', \%agent_search );
+#die "No agents created!" unless @agents;
+eidiot "You have not created any agents (or all agents are disabled). You must create at least one agent before adding a customer. Go to ". popurl(2). "browse/agent.cgi and create one or more agents." unless @agents;
+my $agentnum = $cust_main->agentnum || $agents[0]->agentnum; #default to first
+if ( scalar(@agents) == 1 ) {
+ print qq!<INPUT TYPE="hidden" NAME="agentnum" VALUE="$agentnum">!;
+} else {
+ print qq!<BR><BR>${r}Agent <SELECT NAME="agentnum" SIZE="1">!;
+ my $agent;
+ foreach $agent (sort {
+ $a->agent cmp $b->agent;
+ } @agents) {
+ print '<OPTION VALUE="', $agent->agentnum, '"',
+ " SELECTED"x($agent->agentnum==$agentnum),
+ ">". $agent->agent;
+ #">", $agent->agentnum,": ", $agent->agent;
+ }
+ print "</SELECT>";
+}
+
+#referral
+
+my $refnum = $cust_main->refnum || $conf->config('referraldefault') || 0;
+if ( $custnum && ! $conf->exists('editreferrals') ) {
+ print qq!<INPUT TYPE="hidden" NAME="refnum" VALUE="$refnum">!;
+} else {
+ my(@referrals) = qsearch('part_referral',{});
+ if ( scalar(@referrals) == 0 ) {
+ eidiot "You have not created any advertising sources. You must create at least one advertising source before adding a customer. Go to ". popurl(2). "browse/part_referral.cgi and create one or more advertising sources.";
+ } elsif ( scalar(@referrals) == 1 ) {
+ $refnum ||= $referrals[0]->refnum;
+ print qq!<INPUT TYPE="hidden" NAME="refnum" VALUE="$refnum">!;
+ } else {
+ print qq!<BR><BR>${r}Advertising source <SELECT NAME="refnum" SIZE="1">!;
+ print "<OPTION> " unless $refnum;
+ my($referral);
+ foreach $referral (sort {
+ $a->refnum <=> $b->refnum;
+ } @referrals) {
+ print "<OPTION" . " SELECTED"x($referral->refnum==$refnum),
+ ">", $referral->refnum, ": ", $referral->referral;
+ }
+ print "</SELECT>";
+ }
+}
+
+#referring customer
+
+#print qq!<BR><BR>Referring Customer: !;
+my $referring_cust_main = '';
+if ( $cust_main->referral_custnum
+ and $referring_cust_main =
+ qsearchs('cust_main', { custnum => $cust_main->referral_custnum } )
+) {
+ print '<BR><BR>Referring Customer: <A HREF="'. popurl(1). '/cust_main.cgi?'.
+ $cust_main->referral_custnum. '">'.
+ $cust_main->referral_custnum. ': '.
+ ( $referring_cust_main->company
+ || $referring_cust_main->last. ', '. $referring_cust_main->first ).
+ '</A><INPUT TYPE="hidden" NAME="referral_custnum" VALUE="'.
+ $cust_main->referral_custnum. '">';
+} elsif ( ! $conf->exists('disable_customer_referrals') ) {
+ print '<BR><BR>Referring customer number: <INPUT TYPE="text" NAME="referral_custnum" VALUE="">';
+} else {
+ print '<INPUT TYPE="hidden" NAME="referral_custnum" VALUE="">';
+}
+
+# contact info
+
+my($last,$first,$ss,$company,$address1,$address2,$city,$zip)=(
+ $cust_main->last,
+ $cust_main->first,
+ $cust_main->ss,
+ $cust_main->company,
+ $cust_main->address1,
+ $cust_main->address2,
+ $cust_main->city,
+ $cust_main->zip,
+);
+
+print "<BR><BR>Billing address", &itable("#cccccc"), <<END;
+<TR><TH ALIGN="right">${r}Contact&nbsp;name<BR>(last,&nbsp;first)</TH><TD COLSPAN=3>
+END
+
+print <<END;
+<INPUT TYPE="text" NAME="last" VALUE="$last"> ,
+<INPUT TYPE="text" NAME="first" VALUE="$first">
+</TD>
+END
+
+if ( $conf->exists('show_ss') ) {
+ print qq!<TD ALIGN="right">SS#</TD><TD><INPUT TYPE="text" NAME="ss" VALUE="$ss" SIZE=11></TD>!;
+} else {
+ print qq!<TD><INPUT TYPE="hidden" NAME="ss" VALUE="$ss"></TD>!;
+}
+
+print <<END;
+</TR>
+<TR><TD ALIGN="right">Company</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="company" VALUE="$company" SIZE=70></TD></TR>
+<TR><TH ALIGN="right">${r}Address</TH><TD COLSPAN=5><INPUT TYPE="text" NAME="address1" VALUE="$address1" SIZE=70></TD></TR>
+<TR><TD ALIGN="right">&nbsp;</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="address2" VALUE="$address2" SIZE=70></TD></TR>
+<TR><TH ALIGN="right">${r}City</TH><TD><INPUT TYPE="text" NAME="city" VALUE="$city"></TD><TH ALIGN="right">${r}State</TH><TD>
+END
+
+#false laziness with ship state
+my $countrydefault = $conf->config('countrydefault') || 'US';
+$cust_main->country( $countrydefault ) unless $cust_main->country;
+
+my $statedefault = $conf->config('statedefault')
+ || ($countrydefault eq 'US' ? 'CA' : '');
+$cust_main->state( $statedefault )
+ unless $cust_main->state || $cust_main->country ne $countrydefault;
+
+my($county_html, $state_html, $country_html) =
+ FS::cust_main_county::regionselector( $cust_main->county,
+ $cust_main->state,
+ $cust_main->country );
+
+print "$county_html $state_html";
+
+print qq!</TD><TH>${r}Zip</TH><TD><INPUT TYPE="text" NAME="zip" VALUE="$zip" SIZE=10></TD></TR>!;
+
+my($daytime,$night,$fax)=(
+ $cust_main->daytime,
+ $cust_main->night,
+ $cust_main->fax,
+);
+
+my $daytime_label = FS::Msgcat::_gettext('daytime') || 'Day Phone';
+my $night_label = FS::Msgcat::_gettext('night') || 'Night Phone';
+
+print <<END;
+<TR><TH ALIGN="right">${r}Country</TH><TD>$country_html</TD></TR>
+<TR><TD ALIGN="right">$daytime_label</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="daytime" VALUE="$daytime" SIZE=18></TD></TR>
+<TR><TD ALIGN="right">$night_label</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="night" VALUE="$night" SIZE=18></TD></TR>
+<TR><TD ALIGN="right">Fax</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="fax" VALUE="$fax" SIZE=12></TD></TR>
+END
+
+print "</TABLE>${r}required fields<BR>";
+
+# service address
+
+if ( defined $cust_main->dbdef_table->column('ship_last') ) {
+
+ print "\n", <<END;
+ <SCRIPT>
+ function changed(what) {
+ what.form.same.checked = false;
+ }
+ function samechanged(what) {
+ if ( what.checked ) {
+END
+print " what.form.ship_$_.value = what.form.$_.value;\n"
+ for (qw( last first company address1 address2 city zip daytime night fax ));
+print <<END;
+ what.form.ship_country.selectedIndex = what.form.country.selectedIndex;
+ ship_country_changed(what.form.ship_country);
+ what.form.ship_state.selectedIndex = what.form.state.selectedIndex;
+ ship_state_changed(what.form.ship_state);
+ what.form.ship_county.selectedIndex = what.form.county.selectedIndex;
+ }
+ }
+ </SCRIPT>
+END
+
+ print '<BR>Service address ',
+ '(<INPUT TYPE="checkbox" NAME="same" VALUE="Y" onClick="samechanged(this)"';
+ unless ( $cust_main->ship_last && $cgi->param('same') ne 'Y' ) {
+ print ' CHECKED';
+ foreach (
+ qw( last first company address1 address2 city county state zip country
+ daytime night fax )
+ ) {
+ $cust_main->set("ship_$_", $cust_main->get($_) );
+ }
+ }
+ print '>same as billing address)<BR>';
+
+ my($ship_last,$ship_first,$ship_company,$ship_address1,$ship_address2,$ship_city,$ship_zip)=(
+ $cust_main->ship_last,
+ $cust_main->ship_first,
+ $cust_main->ship_company,
+ $cust_main->ship_address1,
+ $cust_main->ship_address2,
+ $cust_main->ship_city,
+ $cust_main->ship_zip,
+ );
+
+ print &itable("#cccccc"), <<END;
+ <TR><TH ALIGN="right">${r}Contact&nbsp;name<BR>(last,&nbsp;first)</TH><TD COLSPAN=5>
+END
+
+ print <<END;
+ <INPUT TYPE="text" NAME="ship_last" VALUE="$ship_last" onChange="changed(this)"> ,
+ <INPUT TYPE="text" NAME="ship_first" VALUE="$ship_first" onChange="changed(this)">
+END
+
+ print <<END;
+ </TD></TR>
+ <TR><TD ALIGN="right">Company</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="ship_company" VALUE="$ship_company" SIZE=70 onChange="changed(this)"></TD></TR>
+ <TR><TH ALIGN="right">${r}Address</TH><TD COLSPAN=5><INPUT TYPE="text" NAME="ship_address1" VALUE="$ship_address1" SIZE=70 onChange="changed(this)"></TD></TR>
+ <TR><TD ALIGN="right">&nbsp;</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="ship_address2" VALUE="$ship_address2" SIZE=70 onChange="changed(this)"></TD></TR>
+ <TR><TH ALIGN="right">${r}City</TH><TD><INPUT TYPE="text" NAME="ship_city" VALUE="$ship_city" onChange="changed(this)"></TD><TH ALIGN="right">${r}State</TH><TD>
+END
+
+ #false laziness with regular state
+ $cust_main->ship_country( $countrydefault ) unless $cust_main->ship_country;
+
+ $cust_main->ship_state( $statedefault )
+ unless $cust_main->ship_state
+ || $cust_main->ship_country ne $countrydefault;
+
+ my($ship_county_html, $ship_state_html, $ship_country_html) =
+ FS::cust_main_county::regionselector( $cust_main->ship_county,
+ $cust_main->ship_state,
+ $cust_main->ship_country,
+ 'ship_',
+ 'changed(this)', );
+
+ print "$ship_county_html $ship_state_html";
+
+ print qq!</TD><TH>${r}Zip</TH><TD><INPUT TYPE="text" NAME="ship_zip" VALUE="$ship_zip" SIZE=10 onChange="changed(this)"></TD></TR>!;
+
+ my($ship_daytime,$ship_night,$ship_fax)=(
+ $cust_main->ship_daytime,
+ $cust_main->ship_night,
+ $cust_main->ship_fax,
+ );
+
+ print <<END;
+ <TR><TH ALIGN="right">${r}Country</TH><TD>$ship_country_html</TD></TR>
+ <TR><TD ALIGN="right">$daytime_label</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="ship_daytime" VALUE="$ship_daytime" SIZE=18 onChange="changed(this)"></TD></TR>
+ <TR><TD ALIGN="right">$night_label</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="ship_night" VALUE="$ship_night" SIZE=18 onChange="changed(this)"></TD></TR>
+ <TR><TD ALIGN="right">Fax</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="ship_fax" VALUE="$ship_fax" SIZE=12 onChange="changed(this)"></TD></TR>
+END
+
+ print "</TABLE>${r}required fields<BR>";
+
+}
+
+# billing info
+
+sub expselect {
+ my $prefix = shift;
+ my( $m, $y ) = (0, 0);
+ if ( scalar(@_) ) {
+ my $date = shift || '01-2000';
+ if ( $date =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #PostgreSQL date format
+ ( $m, $y ) = ( $2, $1 );
+ } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
+ ( $m, $y ) = ( $1, $3 );
+ } else {
+ die "unrecognized expiration date format: $date";
+ }
+ }
+
+ my $return = qq!<SELECT NAME="$prefix!. qq!_month" SIZE="1">!;
+ for ( 1 .. 12 ) {
+ $return .= "<OPTION";
+ $return .= " SELECTED" if $_ == $m;
+ $return .= ">$_";
+ }
+ $return .= qq!</SELECT>/<SELECT NAME="$prefix!. qq!_year" SIZE="1">!;
+ my @t = localtime;
+ my $thisYear = $t[5] + 1900;
+ for ( ($thisYear > $y && $y > 0 ? $y : $thisYear) .. 2037 ) {
+ $return .= "<OPTION";
+ $return .= " SELECTED" if $_ == $y;
+ $return .= ">$_";
+ }
+ $return .= "</SELECT>";
+
+ $return;
+}
+
+my $payby_default = $conf->config('payby-default');
+
+if ( $payby_default eq 'HIDE' ) {
+
+ $cust_main->payby('BILL') unless $cust_main->payby;
+
+ foreach my $field (qw( tax payby )) {
+ print qq!<INPUT TYPE="hidden" NAME="$field" VALUE="!.
+ $cust_main->getfield($field). '">';
+ }
+
+ print qq!<INPUT TYPE="hidden" NAME="invoicing_list" VALUE="!.
+ join(', ', $cust_main->invoicing_list). '">';
+
+ foreach my $payby (qw( CARD DCRD CHEK DCHK LECB BILL COMP )) {
+ foreach my $field (qw( payinfo payname )) {
+ print qq!<INPUT TYPE="hidden" NAME="${payby}_$field" VALUE="!.
+ $cust_main->getfield($field). '">';
+ }
+
+ #false laziness w/expselect
+ my( $m, $y );
+ my $date = $cust_main->paydate || '12-2037';
+ if ( $date =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #PostgreSQL date format
+ ( $m, $y ) = ( $2, $1 );
+ } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
+ ( $m, $y ) = ( $1, $3 );
+ } else {
+ die "unrecognized expiration date format: $date";
+ }
+
+ print qq!<INPUT TYPE="hidden" NAME="${payby}_month" VALUE="$m">!.
+ qq!<INPUT TYPE="hidden" NAME="${payby}_year" VALUE="$y">!;
+
+ }
+
+} else {
+
+ print "<BR>Billing information", &itable("#cccccc"),
+ qq!<TR><TD><INPUT TYPE="checkbox" NAME="tax" VALUE="Y"!;
+ print qq! CHECKED! if $cust_main->tax eq "Y";
+ print qq!>Tax Exempt</TD></TR><TR><TD>!.
+ qq!<INPUT TYPE="checkbox" NAME="invoicing_list_POST" VALUE="POST"!;
+
+ #my @invoicing_list = $cust_main->invoicing_list;
+ print qq! CHECKED!
+ if ( ! @invoicing_list && ! $conf->exists('disablepostalinvoicedefault') )
+ || grep { $_ eq 'POST' } @invoicing_list;
+ print qq!>Postal mail invoice</TD></TR>!;
+ my $invoicing_list = join(', ', grep { $_ ne 'POST' } @invoicing_list );
+ print qq!<TR><TD>Email invoice <INPUT TYPE="text" NAME="invoicing_list" VALUE="$invoicing_list"></TD></TR>!;
+
+ print "<TR><TD>Billing type</TD></TR>",
+ "</TABLE>", '<SCRIPT>
+ var mywindow = -1;
+ function myopen(filename,windowname,properties) {
+ myclose();
+ mywindow = window.open(filename,windowname,properties);
+ }
+ function myclose() {
+ if ( mywindow != -1 )
+ mywindow.close();
+ mywindow = -1;
+ }
+ var achwindow = -1;
+ function achopen(filename,windowname,properties) {
+ achclose();
+ achwindow = window.open(filename,windowname,properties);
+ }
+ function achclose() {
+ if ( achwindow != -1 )
+ achwindow.close();
+ achwindow = -1;
+ }
+ </SCRIPT>',
+ &table("#cccccc"), "<TR>";
+
+ my($payinfo, $payname)=(
+ $cust_main->payinfo,
+ $cust_main->payname,
+ );
+
+ my %payby = (
+ 'CARD' => qq!Credit card (automatic)<BR>${r}<INPUT TYPE="text" NAME="CARD_payinfo" VALUE="" MAXLENGTH=19><BR>${r}Exp !. expselect("CARD"). qq!<BR>${r}Name on card<BR><INPUT TYPE="text" NAME="CARD_payname" VALUE="">!,
+ 'DCRD' => qq!Credit card (on-demand)<BR>${r}<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="" MAXLENGTH=19><BR>${r}Exp !. expselect("DCRD"). qq!<BR>${r}Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="">!,
+ 'CHEK' => qq!Electronic check (automatic)<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE=""><BR>${r}ABA/Routing number <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="" SIZE=10 MAXLENGTH=9> (<A HREF="javascript:achopen('../docs/ach.html','ach','toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=384,height=256')">help</A>)<INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="">!,
+ 'DCHK' => qq!Electronic check (on-demand)<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE=""><BR>${r}ABA/Routing number <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="" SIZE=10 MAXLENGTH=9> (<A HREF="javascript:achopen('../docs/ach.html','ach','toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=384,height=256')">help</A>)<INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="">!,
+ 'LECB' => qq!Phone bill billing<BR>${r}Phone number <INPUT TYPE="text" BANE="LECB_payinfo" VALUE="" MAXLENGTH=15 SIZE=16><INPUT TYPE="hidden" NAME="LECB_month" VALUE="12"><INPUT TYPE="hidden" NAME="LECB_year" VALUE="2037"><INPUT TYPE="hidden" NAME="LECB_payname" VALUE="">!,
+ 'BILL' => qq!Billing<BR>P.O. <INPUT TYPE="text" NAME="BILL_payinfo" VALUE=""><BR><INPUT TYPE="hidden" NAME="BILL_month" VALUE="12"><INPUT TYPE="hidden" NAME="BILL_year" VALUE="2037">Attention<BR><INPUT TYPE="text" NAME="BILL_payname" VALUE="">!,
+ 'COMP' => qq!Complimentary<BR>${r}Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE=""><BR>${r}Exp !. expselect("COMP"),
+);
+
+ if ( $cust_main->dbdef_table->column('paycvv') ) {
+ foreach my $payby ( grep { exists $payby{$_} } qw(CARD DCRD) ) { #1.4/1.5 bs
+ $payby{$payby} .= qq!<BR>CVV2&nbsp;(<A HREF="javascript:myopen('../docs/cvv2.html','cvv2','toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=480,height=288')">help</A>)&nbsp;<INPUT TYPE="text" NAME=${payby}_paycvv VALUE="" SIZE=4 MAXLENGTH=4>!;
+ }
+ }
+
+ my( $account, $aba ) = split('@', $payinfo);
+
+ my %paybychecked = (
+ 'CARD' => qq!Credit card (automatic)<BR>${r}<INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR>${r}Exp !. expselect("CARD", $cust_main->paydate). qq!<BR>${r}Name on card<BR><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname">!,
+ 'DCRD' => qq!Credit card (on-demand)<BR>${r}<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR>${r}Exp !. expselect("DCRD", $cust_main->paydate). qq!<BR>${r}Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="$payname">!,
+ 'CHEK' => qq!Electronic check (automatic)<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE="$account"><BR>${r}ABA/Routing number <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9> (<A HREF="javascript:achopen('../docs/ach.html','ach','toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=384,height=256')">help</A>)<INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="$payname">!,
+ 'DCHK' => qq!Electronic check (on-demand)<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE="$account"><BR>${r}ABA/Routing number <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9> (<A HREF="javascript:achopen('../docs/ach.html','ach','toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=384,height=256')">help</A>)<INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="$payname">!,
+ 'LECB' => qq!Phone bill billing<BR>${r}Phone number <INPUT TYPE="text" BANE="LECB_payinfo" VALUE="$payinfo" MAXLENGTH=15 SIZE=16><INPUT TYPE="hidden" NAME="LECB_month" VALUE="12"><INPUT TYPE="hidden" NAME="LECB_year" VALUE="2037"><INPUT TYPE="hidden" NAME="LECB_payname" VALUE="">!,
+ 'BILL' => qq!Billing<BR>P.O. <INPUT TYPE="text" NAME="BILL_payinfo" VALUE="$payinfo"><BR><INPUT TYPE="hidden" NAME="BILL_month" VALUE="12"><INPUT TYPE="hidden" NAME="BILL_year" VALUE="2037">Attention<BR><INPUT TYPE="text" NAME="BILL_payname" VALUE="$payname">!,
+ 'COMP' => qq!Complimentary<BR>${r}Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE="$payinfo"><BR>${r}Exp !. expselect("COMP", $cust_main->paydate),
+);
+
+ if ( $cust_main->dbdef_table->column('paycvv') ) {
+ my $paycvv = $cust_main->paycvv;
+
+ foreach my $payby ( grep { exists $payby{$_} } qw(CARD DCRD) ) { #1.4/1.5 bs
+ $paybychecked{$payby} .= qq!<BR>CVV2&nbsp;(<A HREF="javascript:myopen('../docs/cvv2.html','cvv2','toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=480,height=288')">help</A>)&nbsp;<INPUT TYPE="text" NAME=${payby}_paycvv VALUE="$paycvv" SIZE=4 MAXLENGTH=4>!;
+ }
+ }
+
+
+ $cust_main->payby($payby_default) unless $cust_main->payby;
+ for (qw(CARD DCRD CHEK DCHK LECB BILL COMP)) {
+ print qq!<TD VALIGN=TOP><INPUT TYPE="radio" NAME="payby" VALUE="$_"!;
+ if ($cust_main->payby eq "$_") {
+ print qq! CHECKED> $paybychecked{$_}</TD>!;
+ } else {
+ print qq!> $payby{$_}</TD>!;
+ }
+ }
+
+ print "</TR></TABLE>$r required fields for each billing type";
+
+}
+
+if ( defined $cust_main->dbdef_table->column('comments') ) {
+ print "<BR><BR>Comments", &itable("#cccccc"),
+ qq!<TR><TD><TEXTAREA COLS=80 ROWS=5 WRAP="HARD" NAME="comments">!,
+ $cust_main->comments, "</TEXTAREA>",
+ "</TD></TR></TABLE>";
+}
+
+unless ( $custnum ) {
+ # pry the wrong place for this logic. also pretty expensive
+ #use FS::part_pkg;
+
+ #false laziness, copied from FS::cust_pkg::order
+ my $pkgpart;
+ if ( scalar(@agents) == 1 ) {
+ # $pkgpart->{PKGPART} is true iff $custnum may purchase PKGPART
+ my($agent)=qsearchs('agent',{'agentnum'=> $agentnum });
+ $pkgpart = $agent->pkgpart_hashref;
+ } else {
+ #can't know (agent not chosen), so, allow all
+ my %typenum;
+ foreach my $agent ( @agents ) {
+ next if $typenum{$agent->typenum}++;
+ #fixed in 5.004_05 #$pkgpart->{$_}++ foreach keys %{ $agent->pkgpart_hashref }
+ foreach ( keys %{ $agent->pkgpart_hashref } ) { $pkgpart->{$_}++; } #5.004_04 workaround
+ }
+ }
+ #eslaf
+
+ my @part_pkg = grep { $_->svcpart('svc_acct') && $pkgpart->{ $_->pkgpart } }
+ qsearch( 'part_pkg', { 'disabled' => '' } );
+
+ if ( @part_pkg ) {
+
+# print "<BR><BR>First package", &itable("#cccccc", "0 ALIGN=LEFT"),
+#apiabuse & undesirable wrapping
+ print "<BR><BR>First package", &itable("#cccccc"),
+ qq!<TR><TD COLSPAN=2><SELECT NAME="pkgpart_svcpart">!;
+
+ print qq!<OPTION VALUE="">(none)!;
+
+ foreach my $part_pkg ( @part_pkg ) {
+ print qq!<OPTION VALUE="!,
+# $part_pkg->pkgpart. "_". $pkgpart{ $part_pkg->pkgpart }, '"';
+ $part_pkg->pkgpart. "_". $part_pkg->svcpart('svc_acct'), '"';
+ print " SELECTED" if $saved_pkgpart && ( $part_pkg->pkgpart == $saved_pkgpart );
+ print ">", $part_pkg->pkg, " - ", $part_pkg->comment;
+ }
+ print "</SELECT></TD></TR>";
+
+ #false laziness: (mostly) copied from edit/svc_acct.cgi
+ #$ulen = $svc_acct->dbdef_table->column('username')->length;
+ my $ulen = dbdef->table('svc_acct')->column('username')->length;
+ my $ulen2 = $ulen+2;
+ my $passwordmax = $conf->config('passwordmax') || 8;
+ my $pmax2 = $passwordmax + 2;
+ print <<END;
+<TR><TD ALIGN="right">Username</TD>
+<TD><INPUT TYPE="text" NAME="username" VALUE="$username" SIZE=$ulen2 MAXLENGTH=$ulen></TD></TR>
+<TR><TD ALIGN="right">Password</TD>
+<TD><INPUT TYPE="text" NAME="_password" VALUE="$password" SIZE=$pmax2 MAXLENGTH=$passwordmax>
+(blank to generate)</TD></TR>
+END
+
+ print '<TR><TD ALIGN="right">Access number</TD><TD WIDTH="100%">'
+ .
+ &FS::svc_acct_pop::popselector($popnum).
+ '</TD></TR></TABLE>'
+ ;
+ }
+}
+
+my $otaker = $cust_main->otaker;
+print qq!<INPUT TYPE="hidden" NAME="otaker" VALUE="$otaker">!,
+ qq!<BR><INPUT NAME="submit" TYPE="submit" VALUE="!,
+ $custnum ? "Apply Changes" : "Add Customer", qq!">!,
+ "</FORM></BODY></HTML>",
+;
+
+%>
diff --git a/httemplate/edit/cust_main_county-expand.cgi b/httemplate/edit/cust_main_county-expand.cgi
new file mode 100755
index 0000000..9f314a4
--- /dev/null
+++ b/httemplate/edit/cust_main_county-expand.cgi
@@ -0,0 +1,54 @@
+<!-- mason kludge -->
+<%
+
+my($taxnum, $delim, $expansion, $taxclass );
+my($query) = $cgi->keywords;
+if ( $cgi->param('error') ) {
+ $taxnum = $cgi->param('taxnum');
+ $delim = $cgi->param('delim');
+ $expansion = $cgi->param('expansion');
+ $taxclass = $cgi->param('taxclass');
+} else {
+ $query =~ /^(taxclass)?(\d+)$/
+ or die "Illegal taxnum (query $query)";
+ $taxclass = $1 ? 'taxclass' : '';
+ $taxnum = $2;
+ $delim = 'n';
+ $expansion = '';
+}
+
+my $cust_main_county = qsearchs('cust_main_county',{'taxnum'=>$taxnum})
+ or die "cust_main_county.taxnum $taxnum not found";
+die "Can't expand entry!" if $cust_main_county->getfield('county');
+
+my $p1 = popurl(1);
+print header("Tax Rate (expand)", menubar(
+ 'Main Menu' => popurl(2),
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+print <<END;
+ <FORM ACTION="${p1}process/cust_main_county-expand.cgi" METHOD=POST>
+ <INPUT TYPE="hidden" NAME="taxnum" VALUE="$taxnum">
+ <INPUT TYPE="hidden" NAME="taxclass" VALUE="$taxclass">
+ Separate by
+END
+print '<INPUT TYPE="radio" NAME="delim" VALUE="n"';
+print ' CHECKED' if $delim eq 'n';
+print '>line (broken on some browsers) or',
+ '<INPUT TYPE="radio" NAME="delim" VALUE="s"';
+print ' CHECKED' if $delim eq 's';
+print '>whitespace.';
+print <<END;
+ <BR><INPUT TYPE="submit" VALUE="Submit">
+ <BR><TEXTAREA NAME="expansion" ROWS=100>$expansion</TEXTAREA>
+ </FORM>
+ </CENTER>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_main_county.cgi b/httemplate/edit/cust_main_county.cgi
new file mode 100755
index 0000000..4bcfcbe
--- /dev/null
+++ b/httemplate/edit/cust_main_county.cgi
@@ -0,0 +1,98 @@
+<!-- mason kludge -->
+<%
+
+print header("Edit tax rates", menubar(
+ 'Main Menu' => popurl(2),
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+print qq!<FORM ACTION="!, popurl(1),
+ qq!process/cust_main_county.cgi" METHOD=POST>!, &table(), <<END;
+ <TR>
+ <TH><FONT SIZE=-1>Country</FONT></TH>
+ <TH><FONT SIZE=-1>State</FONT></TH>
+ <TH><FONT SIZE=-1>County</FONT></TH>
+ <TH><FONT SIZE=-1>Taxclass</FONT><BR><FONT SIZE=-2>(per-package classification)</FONT></TH>
+END
+
+if ( dbdef->table('cust_main_county')->column('taxname') ) {
+ print '<TH><FONT SIZE=-1>Tax name</FONT><BR><FONT SIZE=-2>(printed on invoices)</FONT></TH>';
+}
+
+print <<END;
+ <TH><FONT SIZE=-1>Tax</FONT></TH>
+ <TH><FONT SIZE=-1>Exempt<BR>per<BR>month</TH>
+END
+
+if ( dbdef->table('cust_main_county')->column('setuptax') ) {
+ print '<TH><FONT SIZE=-1>Setup<BR>fee<BR>exempt</TH>';
+}
+if ( dbdef->table('cust_main_county')->column('recurtax') ) {
+ print '<TH><FONT SIZE=-1>Recurring<BR>fee<BR>exempt</TH>';
+}
+
+print '</TR>';
+
+foreach my $cust_main_county ( sort { $a->country cmp $b->country
+ or $a->state cmp $b->state
+ or $a->county cmp $b->county
+ } qsearch('cust_main_county',{}) ) {
+ my($hashref)=$cust_main_county->hashref;
+ print <<END;
+ <TR>
+ <TD BGCOLOR="#ffffff">$hashref->{country}</TD>
+END
+
+ print "<TD", $hashref->{state}
+ ? ' BGCOLOR="#ffffff">'.$hashref->{state}
+ : ' BGCOLOR="#cccccc">(ALL)'
+ , "</TD>";
+
+ print "<TD", $hashref->{county}
+ ? ' BGCOLOR="#ffffff">'. $hashref->{county}
+ : ' BGCOLOR="#cccccc">(ALL)'
+ , "</TD>";
+
+ print "<TD", $hashref->{taxclass}
+ ? ' BGCOLOR="#ffffff">'. $hashref->{taxclass}
+ : ' BGCOLOR="#cccccc">(ALL)'
+ , "</TD>";
+
+ print qq!<TD><INPUT TYPE="text" NAME="taxname!, $hashref->{taxnum},
+ qq!" VALUE="!, $hashref->{taxname}, qq!"></TD>!
+ if dbdef->table('cust_main_county')->column('taxname');
+
+ print qq!<TD><TABLE><TR><TD><INPUT TYPE="text" NAME="tax!, $hashref->{taxnum},
+ qq!" VALUE="!, $hashref->{tax}, qq!" SIZE=6 MAXLENGTH=6></TD><TD>%</TD></TR></TABLE></TD>!;
+ print qq!<TD><TABLE><TR><TD>\$</TD><TD><INPUT TYPE="text" NAME="exempt_amount!, $hashref->{taxnum},
+ qq!" VALUE="!, $hashref->{exempt_amount}||0, qq!" SIZE=6></TD></TR></TABLE></TD>!;
+
+ print qq!<TD><INPUT TYPE="checkbox" NAME="setuptax!. $hashref->{taxnum}.
+ '" VALUE="Y"'.
+ ( $hashref->{setuptax} =~ /^Y$/i ? ' CHECKED' : '' ).
+ '></TD>'
+ if dbdef->table('cust_main_county')->column('setuptax');
+
+ print qq!<TD><INPUT TYPE="checkbox" NAME="recurtax!. $hashref->{taxnum}.
+ '" VALUE="Y"'.
+ ( $hashref->{recurtax} =~ /^Y$/i ? ' CHECKED' : '' ).
+ '></TD>'
+ if dbdef->table('cust_main_county')->column('recurtax');
+
+ print '</TR>';
+
+}
+
+print <<END;
+ </TABLE>
+ <INPUT TYPE="submit" VALUE="Apply changes">
+ </FORM>
+ </CENTER>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_pay.cgi b/httemplate/edit/cust_pay.cgi
new file mode 100755
index 0000000..f6ae7b2
--- /dev/null
+++ b/httemplate/edit/cust_pay.cgi
@@ -0,0 +1,129 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my($link, $linknum, $paid, $payby, $payinfo, $quickpay);
+if ( $cgi->param('error') ) {
+ $link = $cgi->param('link');
+ $linknum = $cgi->param('linknum');
+ $paid = $cgi->param('paid');
+ $payby = $cgi->param('payby');
+ $payinfo = $cgi->param('payinfo');
+ $quickpay = $cgi->param('quickpay');
+} elsif ($cgi->keywords) {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ $link = 'invnum';
+ $linknum = $1;
+ $paid = '';
+ $payby = 'BILL';
+ $payinfo = "";
+ $quickpay = '';
+} elsif ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+ $link = 'custnum';
+ $linknum = $1;
+ $paid = '';
+ $payby = 'BILL';
+ $payinfo = '';
+ $quickpay = $cgi->param('quickpay');
+} else {
+ die "illegal query ". $cgi->keywords;
+}
+my $_date = time;
+
+my $paybatch = "webui-$_date-$$-". rand() * 2**32;
+
+my $p1 = popurl(1);
+print header("Post payment", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT><BR><BR>"
+ if $cgi->param('error');
+
+print <<END, ntable("#cccccc",2);
+ <FORM ACTION="${p1}process/cust_pay.cgi" METHOD=POST>
+ <INPUT TYPE="hidden" NAME="link" VALUE="$link">
+ <INPUT TYPE="hidden" NAME="linknum" VALUE="$linknum">
+ <INPUT TYPE="hidden" NAME="quickpay" VALUE="$quickpay">
+END
+
+my $custnum;
+if ( $link eq 'invnum' ) {
+
+ my $cust_bill = qsearchs('cust_bill', { 'invnum' => $linknum } )
+ or die "unknown invnum $linknum";
+ print "Invoice #<B>$linknum</B>". ntable("#cccccc",2).
+ '<TR><TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff">'.
+ time2str("%D", $cust_bill->_date). '</TD></TR>'.
+ '<TR><TD ALIGN="right" VALIGN="top">Items</TD><TD BGCOLOR="#ffffff">';
+ foreach ( $cust_bill->cust_bill_pkg ) { #false laziness with FS::cust_bill
+ if ( $_->pkgnum ) {
+
+ my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
+ my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
+ my($pkg)=$part_pkg->pkg;
+
+ if ( $_->setup != 0 ) {
+ print "$pkg Setup<BR>"; # $money_char. sprintf("%10.2f",$_->setup);
+ print join('<BR>',
+ map { " ". $_->[0]. ": ". $_->[1] } $cust_pkg->labels
+ ). '<BR>';
+ }
+
+ if ( $_->recur != 0 ) {
+ print
+ "$pkg (" . time2str("%x",$_->sdate) . " - " .
+ time2str("%x",$_->edate) . ")<BR>";
+ #$money_char. sprintf("%10.2f",$_->recur)
+ print join('<BR>',
+ map { '--->'. $_->[0]. ": ". $_->[1] } $cust_pkg->labels
+ ). '<BR>';
+ }
+
+ } else { #pkgnum Tax
+ print "Tax<BR>" # $money_char. sprintf("%10.2f",$_->setup)
+ if $_->setup != 0;
+ }
+
+ }
+ print '</TD></TR></TABLE><BR><BR>';
+
+ $custnum = $cust_bill->custnum;
+
+} elsif ( $link eq 'custnum' ) {
+ $custnum = $linknum;
+}
+
+print small_custview($custnum, $conf->config('countrydefault'));
+
+print qq!<INPUT TYPE="hidden" NAME="_date" VALUE="$_date">!;
+print qq!<INPUT TYPE="hidden" NAME="payby" VALUE="$payby">!;
+
+print '<BR><BR>Payment'. ntable("#cccccc", 2).
+ '<TR><TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff">'.
+ time2str("%D",$_date). '</TD></TR>';
+
+print qq!<TR><TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">\$<INPUT TYPE="text" NAME="paid" VALUE="$paid" SIZE=8 MAXLENGTH=8></TD></TR>!;
+
+print qq!<TR><TD ALIGN="right">Payby</TD><TD BGCOLOR="#ffffff">$payby</TD></TR>!;
+
+#payinfo (check # now as payby="BILL" hardcoded.. what to do later?)
+print qq!<TR><TD ALIGN="right">Check #</TD><TD BGCOLOR="#ffffff"><INPUT TYPE="text" NAME="payinfo" VALUE="$payinfo"></TD></TR>!;
+
+print qq!<TR><TD ALIGN="right">Auto-apply<BR>to invoices</TD><TD><SELECT NAME="apply"><OPTION VALUE="yes" SELECTED>yes<OPTION>no</SELECT></TD>!;
+
+print "</TABLE>";
+
+#paybatch
+print qq!<INPUT TYPE="hidden" NAME="paybatch" VALUE="$paybatch">!;
+
+print <<END;
+<BR>
+<INPUT TYPE="submit" VALUE="Post payment">
+ </FORM>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_pkg.cgi b/httemplate/edit/cust_pkg.cgi
new file mode 100755
index 0000000..485d601
--- /dev/null
+++ b/httemplate/edit/cust_pkg.cgi
@@ -0,0 +1,117 @@
+<!-- mason kludge -->
+<%
+
+my %pkg = ();
+my %comment = ();
+my %all_pkg = ();
+my %all_comment = ();
+#foreach (qsearch('part_pkg', { 'disabled' => '' })) {
+# $pkg{ $_ -> getfield('pkgpart') } = $_->getfield('pkg');
+# $comment{ $_ -> getfield('pkgpart') } = $_->getfield('comment');
+#}
+foreach (qsearch('part_pkg', {} )) {
+ $all_pkg{ $_ -> getfield('pkgpart') } = $_->getfield('pkg');
+ $all_comment{ $_ -> getfield('pkgpart') } = $_->getfield('comment');
+ next if $_->disabled;
+ $pkg{ $_ -> getfield('pkgpart') } = $_->getfield('pkg');
+ $comment{ $_ -> getfield('pkgpart') } = $_->getfield('comment');
+}
+
+my($custnum, %remove_pkg);
+if ( $cgi->param('error') ) {
+ $custnum = $cgi->param('custnum');
+ %remove_pkg = map { $_ => 1 } $cgi->param('remove_pkg');
+} else {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ $custnum = $1;
+ %remove_pkg = ();
+}
+
+my $p1 = popurl(1);
+print header("Add/Edit Packages", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/cust_pkg.cgi" METHOD=POST>!;
+
+print qq!<INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">!;
+
+#current packages
+my @cust_pkg = qsearch('cust_pkg',{ 'custnum' => $custnum, 'cancel' => '' } );
+
+if (@cust_pkg) {
+ print <<END;
+Current packages - select to remove (services are moved to a new package below)
+<BR><BR>
+END
+
+ my $count = 0 ;
+ print qq!<TABLE>! ;
+ foreach (@cust_pkg) {
+ print '<TR>' if $count == 0;
+ my($pkgnum,$pkgpart)=( $_->getfield('pkgnum'), $_->getfield('pkgpart') );
+ print qq!<TD><INPUT TYPE="checkbox" NAME="remove_pkg" VALUE="$pkgnum"!;
+ print " CHECKED" if $remove_pkg{$pkgnum};
+ print qq!>$pkgnum: $all_pkg{$pkgpart} - $all_comment{$pkgpart}</TD>\n!;
+ $count ++ ;
+ if ($count == 2)
+ {
+ $count = 0 ;
+ print qq!</TR>\n! ;
+ }
+ }
+ print qq!</TABLE><BR><BR>!;
+}
+
+print <<END;
+Order new packages<BR><BR>
+END
+
+my $cust_main = qsearchs('cust_main',{'custnum'=>$custnum});
+my $agent = qsearchs('agent',{'agentnum'=> $cust_main->agentnum });
+
+my $count = 0;
+my $pkgparts = 0;
+print qq!<TABLE>!;
+foreach my $type_pkgs ( qsearch('type_pkgs',{'typenum'=> $agent->typenum }) ) {
+ $pkgparts++;
+ my($pkgpart)=$type_pkgs->pkgpart;
+ next unless exists $pkg{$pkgpart}; #skip disabled ones
+ print qq!<TR>! if ( $count == 0 );
+ my $value = $cgi->param("pkg$pkgpart") || 0;
+ print <<END;
+ <TD>
+ <INPUT TYPE="text" NAME="pkg$pkgpart" VALUE="$value" SIZE="2" MAXLENGTH="2">
+ $pkgpart: $pkg{$pkgpart} - $comment{$pkgpart}</TD>\n
+END
+ $count ++ ;
+ if ( $count == 2 ) {
+ print qq!</TR>\n! ;
+ $count = 0;
+ }
+}
+print qq!</TABLE>!;
+
+unless ( $pkgparts ) {
+ my $p2 = popurl(2);
+ my $typenum = $agent->typenum;
+ my $agent_type = qsearchs( 'agent_type', { 'typenum' => $typenum } );
+ my $atype = $agent_type->atype;
+ print <<END;
+(No <a href="${p2}browse/part_pkg.cgi">package definitions</a>, or agent type
+<a href="${p2}edit/agent_type.cgi?$typenum">$atype</a> not allowed to purchase
+any packages.)
+END
+}
+
+#submit
+print <<END;
+<P><INPUT TYPE="submit" VALUE="Order">
+ </FORM>
+ </BODY>
+</HTML>
+END
+%>
diff --git a/httemplate/edit/cust_refund.cgi b/httemplate/edit/cust_refund.cgi
new file mode 100755
index 0000000..8955c7c
--- /dev/null
+++ b/httemplate/edit/cust_refund.cgi
@@ -0,0 +1,94 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+my $custnum = $cgi->param('custnum');
+my $refund = $cgi->param('refund');
+my $payby = $cgi->param('payby');
+my $reason = $cgi->param('reason');
+
+my( $paynum, $cust_pay ) = ( '', '' );
+if ( $cgi->param('paynum') =~ /^(\d+)$/ ) {
+ $paynum = $1;
+ $cust_pay = qsearchs('cust_pay', { paynum=>$paynum } )
+ or die "unknown payment # $paynum";
+ $refund ||= $cust_pay->unrefunded;
+ if ( $custnum ) {
+ die "payment # $paynum is not for specified customer # $custnum"
+ unless $custnum == $cust_pay->custnum;
+ } else {
+ $custnum = $cust_pay->custnum;
+ }
+}
+die "no custnum or paynum specified!" unless $custnum;
+
+my $_date = time;
+
+my $p1 = popurl(1);
+
+print header('Refund '. ucfirst(lc($payby)). ' payment', '');
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+print <<END, small_custview($custnum, $conf->config('countrydefault'));
+ <FORM ACTION="${p1}process/cust_refund.cgi" METHOD=POST>
+ <INPUT TYPE="hidden" NAME="refundnum" VALUE="">
+ <INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">
+ <INPUT TYPE="hidden" NAME="paynum" VALUE="$paynum">
+ <INPUT TYPE="hidden" NAME="_date" VALUE="$_date">
+ <INPUT TYPE="hidden" NAME="payby" VALUE="$payby">
+ <INPUT TYPE="hidden" NAME="payinfo" VALUE="">
+ <INPUT TYPE="hidden" NAME="paybatch" VALUE="">
+ <INPUT TYPE="hidden" NAME="credited" VALUE="">
+ <BR>
+END
+
+if ( $cust_pay ) {
+
+ #false laziness w/FS/FS/cust_pay.pm
+ my $payby = $cust_pay->payby;
+ my $payinfo = $cust_pay->payinfo;
+ $payby =~ s/^BILL$/Check/ if $payinfo;
+ $payby =~ s/^CHEK$/Electronic check/;
+ $payinfo = $cust_pay->payinfo_masked if $payby eq 'CARD';
+
+ print '<BR>Payment'. ntable("#cccccc", 2).
+ '<TR><TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">$'.
+ $cust_pay->paid. '</TD></TR>'.
+ '<TR><TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff">'.
+ time2str("%D",$cust_pay->_date). '</TD></TR>'.
+ '<TR><TD ALIGN="right">Method</TD><TD BGCOLOR="#ffffff">'.
+ ucfirst(lc($payby)). ' # '. $payinfo. '</TD></TR>';
+ #false laziness w/FS/FS/cust_main::realtime_refund_bop
+ if ( $cust_pay->paybatch =~ /^(\w+):(\w+)(:(\w+))?$/ ) {
+ my ( $processor, $auth, $order_number ) = ( $1, $2, $4 );
+ print '<TR><TD ALIGN="right">Processor</TD><TD BGCOLOR="#ffffff">'.
+ $processor. '</TD></TR>';
+ print '<TR><TD ALIGN="right">Authorization</TD><TD BGCOLOR="#ffffff">'.
+ $auth. '</TD></TR>'
+ if length($auth);
+ print '<TR><TD ALIGN="right">Order number</TD><TD BGCOLOR="#ffffff">'.
+ $order_number. '</TD></TR>'
+ if length($order_number);
+ }
+ print '</TABLE>';
+}
+
+print '<BR>Refund'. ntable("#cccccc", 2).
+ '<TR><TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff">'.
+ time2str("%D",$_date). '</TD></TR>';
+
+print qq!<TR><TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">\$<INPUT TYPE="text" NAME="refund" VALUE="$refund" SIZE=8 MAXLENGTH=8></TD></TR>!;
+
+print qq!<TR><TD ALIGN="right">Reason</TD><TD BGCOLOR="#ffffff"><INPUT TYPE="text" NAME="reason" VALUE="$reason"></TD></TR>!;
+
+print <<END;
+</TABLE>
+<BR>
+<INPUT TYPE="submit" VALUE="Post refund">
+ </FORM>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/msgcat.cgi b/httemplate/edit/msgcat.cgi
new file mode 100755
index 0000000..ee9b1c6
--- /dev/null
+++ b/httemplate/edit/msgcat.cgi
@@ -0,0 +1,58 @@
+<!-- mason kludge -->
+<%
+
+print header("Edit Message catalog", menubar(
+# 'Main Menu' => $p,
+)), '<BR>';
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !. $cgi->param('error').
+ '</FONT><BR><BR>'
+ if $cgi->param('error');
+
+my $widget = new HTML::Widgets::SelectLayers(
+ 'selected_layer' => 'en_US',
+ 'options' => { 'en_US'=>'en_US' },
+ 'form_action' => 'process/msgcat.cgi',
+ 'layer_callback' => sub {
+ my $layer = shift;
+ my $html = qq!<INPUT TYPE="hidden" NAME="locale" VALUE="$layer">!.
+ "<BR>Messages for locale $layer<BR>". table().
+ "<TR><TH COLSPAN=2>Code</TH>".
+ "<TH>Message</TH>";
+ $html .= "<TH>en_US Message</TH>" unless $layer eq 'en_US';
+ $html .= '</TR>';
+
+ #foreach my $msgcat ( sort { $a->msgcode cmp $b->msgcode }
+ # qsearch('msgcat', { 'locale' => $layer } ) ) {
+ foreach my $msgcat ( qsearch('msgcat', { 'locale' => $layer } ) ) {
+ $html .=
+ '<TR><TD>'. $msgcat->msgnum. '</TD><TD>'. $msgcat->msgcode. '</TD>'.
+ '<TD><INPUT TYPE="text" SIZE=32 '.
+ qq! NAME="!. $msgcat->msgnum. '" '.
+ qq!VALUE="!. ($cgi->param($msgcat->msgnum)||$msgcat->msg). qq!"></TD>!;
+ unless ( $layer eq 'en_US' ) {
+ my $en_msgcat = qsearchs('msgcat', {
+ 'locale' => 'en_US',
+ 'msgcode' => $msgcat->msgcode,
+ } );
+ $html .= '<TD>'. $en_msgcat->msg. '</TD>';
+ }
+ $html .= '</TR>';
+ }
+
+ $html .= '</TABLE><BR><INPUT TYPE="submit" VALUE="Apply changes">';
+
+ $html;
+ },
+
+);
+
+print $widget->html;
+
+print <<END;
+ </TABLE>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/part_bill_event.cgi b/httemplate/edit/part_bill_event.cgi
new file mode 100755
index 0000000..2f99ca5
--- /dev/null
+++ b/httemplate/edit/part_bill_event.cgi
@@ -0,0 +1,288 @@
+<!-- mason kludge -->
+<%
+
+if ( $cgi->param('eventpart') && $cgi->param('eventpart') =~ /^(\d+)$/ ) {
+ $cgi->param('eventpart', $1);
+} else {
+ $cgi->param('eventpart', '');
+}
+
+my ($query) = $cgi->keywords;
+my $action = '';
+my $part_bill_event = '';
+if ( $cgi->param('error') ) {
+ $part_bill_event = new FS::part_bill_event ( {
+ map { $_, scalar($cgi->param($_)) } fields('part_bill_event')
+ } );
+}
+if ( $query && $query =~ /^(\d+)$/ ) {
+ $part_bill_event ||= qsearchs('part_bill_event',{'eventpart'=>$1});
+} else {
+ $part_bill_event ||= new FS::part_bill_event {};
+}
+$action ||= $part_bill_event->eventpart ? 'Edit' : 'Add';
+my $hashref = $part_bill_event->hashref;
+
+print header("$action Invoice Event Definition", menubar(
+ 'Main Menu' => popurl(2),
+ 'View all invoice events' => popurl(2). 'browse/part_bill_event.cgi',
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+print '<FORM ACTION="', popurl(1), 'process/part_bill_event.cgi" METHOD=POST>'.
+ '<INPUT TYPE="hidden" NAME="eventpart" VALUE="'.
+ $part_bill_event->eventpart .'">';
+print "Invoice Event #", $hashref->{eventpart} ? $hashref->{eventpart} : "(NEW)";
+
+print ntable("#cccccc",2), <<END;
+<TR><TD ALIGN="right">Payby</TD><TD><SELECT NAME="payby">
+END
+
+for (qw(CARD DCRD CHEK DCHK LECB BILL COMP)) {
+ print qq!<OPTION VALUE="$_"!;
+ if ($part_bill_event->payby eq $_) {
+ print " SELECTED>$_</OPTION>";
+ } else {
+ print ">$_</OPTION>";
+ }
+}
+
+my $days = $hashref->{seconds}/86400;
+
+print <<END;
+</SELECT></TD></TR>
+<TR><TD ALIGN="right">Event</TD><TD><INPUT TYPE="text" NAME="event" VALUE="$hashref->{event}"></TD></TR>
+<TR><TD ALIGN="right">After</TD><TD><INPUT TYPE="text" NAME="days" VALUE="$days"> days</TD></TR>
+END
+
+print '<TR><TD ALIGN="right">Disabled</TD><TD>';
+print '<INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"';
+print ' CHECKED' if $hashref->{disabled} eq "Y";
+print '>';
+print '</TD></TR>';
+
+print '<TR><TD ALIGN="right">Action</TD><TD>';
+
+#print ntable();
+
+sub select_pkgpart {
+ my $label = shift;
+ my $plandata = shift;
+ my %selected = map { $_=>1 } split(/,\s*/, $plandata->{$label});
+ qq(<SELECT NAME="$label" MULTIPLE>).
+ join("\n", map {
+ '<OPTION VALUE="'. $_->pkgpart. '"'.
+ ( $selected{$_->pkgpart} ? ' SELECTED' : '' ).
+ '>'. $_->pkg. ' - '. $_->comment
+ } qsearch('part_pkg', { 'disabled' => '' } ) ).
+ '</SELECT>';
+}
+
+sub select_agentnum {
+ my $plandata = shift;
+ my $agentnum = $plandata->{'agentnum'};
+ '<SELECT NAME="agentnum">'.
+ join("\n", map {
+ '<OPTION VALUE="'. $_->agentnum. '"'.
+ ( $_->agentnum == $agentnum ? ' SELECTED' : '' ).
+ '>'. $_->agent
+ } qsearch('agent', { 'disabled' => '' } ) ).
+ '</SELECT>';
+}
+
+#this is pretty kludgy right here.
+tie my %events, 'Tie::IxHash',
+
+ 'fee' => {
+ 'name' => 'Late fee',
+ 'code' => '$cust_main->charge( %%%charge%%%, \'%%%reason%%%\' );',
+ 'html' =>
+ 'Amount <INPUT TYPE="text" SIZE="7" NAME="charge" VALUE="%%%charge%%%">'.
+ '<BR>Reason <INPUT TYPE="text" NAME="reason" VALUE="%%%reason%%%">',
+ 'weight' => 10,
+ },
+ 'suspend' => {
+ 'name' => 'Suspend',
+ 'code' => '$cust_main->suspend();',
+ 'weight' => 10,
+ },
+ 'suspend-if-pkgpart' => {
+ 'name' => 'Suspend packages',
+ 'code' => '$cust_main->suspend_if_pkgpart(%%%if_pkgpart%%%);',
+ 'html' => sub { &select_pkgpart('if_pkgpart', @_) },
+ 'weight' => 10,
+ },
+ 'suspend-unless-pkgpart' => {
+ 'name' => 'Suspend packages except',
+ 'code' => '$cust_main->suspend_unless_pkgpart(%%%unless_pkgpart%%%);',
+ 'html' => sub { &select_pkgpart('unless_pkgpart', @_) },
+ 'weight' => 10,
+ },
+ 'cancel' => {
+ 'name' => 'Cancel',
+ 'code' => '$cust_main->cancel();',
+ 'weight' => 10,
+ },
+
+ 'addpost' => {
+ 'name' => 'Add postal invoicing',
+ 'code' => '$cust_main->invoicing_list_addpost(); "";',
+ 'weight' => 20,
+ },
+
+ 'comp' => {
+ 'name' => 'Pay invoice with a complimentary "payment"',
+ 'code' => '$cust_bill->comp();',
+ 'weight' => 30,
+ },
+
+ 'realtime-card' => {
+ 'name' => 'Run card with a <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a> realtime gateway',
+ 'code' => '$cust_bill->realtime_card();',
+ 'weight' => 30,
+ },
+
+ 'realtime-check' => {
+ 'name' => 'Run check with a <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a> realtime gateway',
+ 'code' => '$cust_bill->realtime_ach();',
+ 'weight' => 30,
+ },
+
+ 'realtime-lec' => {
+ 'name' => 'Run phone bill ("LEC") billing with a <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a> realtime gateway',
+ 'code' => '$cust_bill->realtime_lec();',
+ 'weight' => 30,
+ },
+
+ 'batch-card' => {
+ 'name' => 'Add card to the pending credit card batch',
+ 'code' => '$cust_bill->batch_card();',
+ 'weight' => 40,
+ },
+
+ 'send' => {
+ 'name' => 'Send invoice (email/print)',
+ 'code' => '$cust_bill->send();',
+ 'weight' => 50,
+ },
+
+ 'send_alternate' => {
+ 'name' => 'Send invoice (email/print) with alternate template',
+ 'code' => '$cust_bill->send(\'%%%templatename%%%\');',
+ 'html' =>
+ '<INPUT TYPE="text" NAME="templatename" VALUE="%%%templatename%%%">',
+ 'weight' => 50,
+ },
+
+ 'send_agent' => {
+ 'name' => 'Send invoice (email/print) ',
+ 'code' => '$cust_bill->send(\'%%%agent_templatename%%%\', %%%agentnum%%%, \'%%%agent_invoice_from%%%\');',
+ 'html' => sub {
+ '<TABLE BORDER=0>
+ <TR>
+ <TD ALIGN="right">only for agent </TD>
+ <TD>'. &select_agentnum(@_). '</TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">with template </TD>
+ <TD>
+ <INPUT TYPE="text" NAME="agent_templatename" VALUE="%%%agent_templatename%%%">
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">email From: </TD>
+ <TD>
+ <INPUT TYPE="text" NAME="agent_invoice_from" VALUE="%%%agent_invoice_from%%%">
+ </TD>
+ </TR>
+ </TABLE>';
+ },
+ 'weight' => 50,
+ },
+
+ 'send_csv_ftp' => {
+ 'name' => 'Upload CSV invoice data to an FTP server',
+ 'code' => '$cust_bill->send_csv( protocol => \'ftp\',
+ server => \'%%%ftpserver%%%\',
+ username => \'%%%ftpusername%%%\',
+ password => \'%%%ftppassword%%%\',
+ dir => \'%%%ftpdir%%%\' );',
+ 'html' =>
+ '<TABLE BORDER=0><TR><TD ALIGN="right">FTP server: </TD>'.
+ '<TD><INPUT TYPE="text" NAME="ftpserver" VALUE="%%%ftpserver%%%">'.
+ '</TD></TR>'.
+ '<TR><TD ALIGN="right">FTP username: </TD><TD>'.
+ '<INPUT TYPE="text" NAME="ftpusername" VALUE="%%%ftpusername%%%">'.
+ '</TD></TR>'.
+ '<TR><TD ALIGN="right">FTP password: </TD><TD>'.
+ '<INPUT TYPE="text" NAME="ftppassword" VALUE="%%%ftppassword%%%">'.
+ '</TD></TR>'.
+ '<TR><TD ALIGN="right">FTP directory: </TD>'.
+ '<TD><INPUT TYPE="text" NAME="ftpdir" VALUE="%%%ftpdir%%%">'.
+ '</TD></TR>'.
+ '</TABLE>',
+ 'weight' => 50,
+ },
+
+ 'bill' => {
+ 'name' => 'Generate invoices (normally only used with a <i>Late Fee</i> event)',
+ 'code' => '$cust_main->bill();',
+ 'weight' => 60,
+ },
+
+ 'apply' => {
+ 'name' => 'Apply unapplied payments and credits',
+ 'code' => '$cust_main->apply_payments; $cust_main->apply_credits; "";',
+ 'weight' => 70,
+ },
+
+ 'collect' => {
+ 'name' => 'Collect on invoices (normally only used with a <i>Late Fee</i> and <i>Generate Invoice</i> events)',
+ 'code' => '$cust_main->collect();',
+ 'weight' => 80,
+ },
+
+;
+
+foreach my $event ( keys %events ) {
+ my %plandata = map { /^(\w+) (.*)$/; ($1, $2); }
+ split(/\n/, $part_bill_event->plandata);
+ my $html = $events{$event}{html};
+ if ( ref($html) eq 'CODE' ) {
+ $html = &{$html}(\%plandata);
+ }
+ while ( $html =~ /%%%(\w+)%%%/ ) {
+ my $field = $1;
+ $html =~ s/%%%$field%%%/$plandata{$field}/;
+ }
+
+ print ntable( "#cccccc", 2).
+ qq!<TR><TD><INPUT TYPE="radio" NAME="plan_weight_eventcode" !;
+ print "CHECKED " if $event eq $part_bill_event->plan;
+ print qq!VALUE="!. $event. ":". $events{$event}{weight}. ":".
+ encode_entities($events{$event}{code}).
+ qq!">$events{$event}{name}</TD>!;
+ print '<TD>'. $html. '</TD>' if $html;
+ print qq!</TR>!;
+ print '</TABLE>';
+}
+
+#print '</TABLE>';
+
+print <<END;
+</TD></TR>
+</TABLE>
+END
+
+print qq!<INPUT TYPE="submit" VALUE="!,
+ $hashref->{eventpart} ? "Apply changes" : "Add invoice event",
+ qq!">!;
+%>
+
+ </FORM>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/edit/part_export.cgi b/httemplate/edit/part_export.cgi
new file mode 100644
index 0000000..b3d42bd
--- /dev/null
+++ b/httemplate/edit/part_export.cgi
@@ -0,0 +1,128 @@
+<!-- mason kludge -->
+<%
+
+#if ( $cgi->param('clone') && $cgi->param('clone') =~ /^(\d+)$/ ) {
+# $cgi->param('clone', $1);
+#} else {
+# $cgi->param('clone', '');
+#}
+
+my($query) = $cgi->keywords;
+my $action = '';
+my $part_export = '';
+if ( $cgi->param('error') ) {
+ $part_export = new FS::part_export ( {
+ map { $_, scalar($cgi->param($_)) } fields('part_export')
+ } );
+} elsif ( $query =~ /^(\d+)$/ ) {
+ $part_export = qsearchs('part_export', { 'exportnum' => $1 } );
+} else {
+ $part_export = new FS::part_export;
+}
+$action ||= $part_export->exportnum ? 'Edit' : 'Add';
+
+#my $exports = FS::part_export::export_info($svcdb);
+my $exports = FS::part_export::export_info();
+
+my %layers = map { $_ => "$_ - ". $exports->{$_}{desc} } keys %$exports;
+$layers{''}='';
+
+my $widget = new HTML::Widgets::SelectLayers(
+ 'selected_layer' => $part_export->exporttype,
+ 'options' => \%layers,
+ 'form_name' => 'dummy',
+ 'form_action' => 'process/part_export.cgi',
+ 'form_text' => [qw( exportnum machine )],
+# 'form_checkbox' => [qw()],
+ 'html_between' => "</TD></TR></TABLE>\n",
+ 'layer_callback' => sub {
+ my $layer = shift;
+ my $html = qq!<INPUT TYPE="hidden" NAME="exporttype" VALUE="$layer">!.
+ ntable("#cccccc",2);
+
+ $html .= '<TR><TD ALIGN="right">Description</TD><TD BGCOLOR=#ffffff>'.
+ $exports->{$layer}{notes}. '</TD></TR>'
+ if $layer;
+
+ foreach my $option ( keys %{$exports->{$layer}{options}} ) {
+ my $optinfo = $exports->{$layer}{options}{$option};
+ die "Retreived non-ref export info option from $layer export: $optinfo"
+ unless ref($optinfo);
+ my $label = $optinfo->{label};
+ my $type = defined($optinfo->{type}) ? $optinfo->{type} : 'text';
+ my $value = $cgi->param($option)
+ || ( $part_export->exportnum && $part_export->option($option) )
+ || ( (exists $optinfo->{default} && !$part_export->exportnum)
+ ? $optinfo->{default}
+ : ''
+ );
+ $html .= qq!<TR><TD ALIGN="right">$label</TD><TD>!;
+ if ( $type eq 'select' ) {
+ $html .= qq!<SELECT NAME="$option">!;
+ foreach my $select_option ( @{$optinfo->{options}} ) {
+ #if ( ref($select_option) ) {
+ #} else {
+ my $selected = $select_option eq $value ? ' SELECTED' : '';
+ $html .= qq!<OPTION VALUE="$select_option"$selected>!.
+ qq!$select_option</OPTION>!;
+ #}
+ }
+ $html .= '</SELECT>';
+ } elsif ( $type eq 'textarea' ) {
+ $html .= qq!<TEXTAREA NAME="$option" COLS=80 ROWS=8 WRAP="virtual">!.
+ encode_entities($value). '</TEXTAREA>';
+ } elsif ( $type eq 'text' ) {
+ $html .= qq!<INPUT TYPE="text" NAME="$option" VALUE="!.
+ encode_entities($value). '" SIZE=64>';
+ } elsif ( $type eq 'checkbox' ) {
+ $html .= qq!<INPUT TYPE="checkbox" NAME="$option" VALUE="1"!;
+ $html .= ' CHECKED' if $value;
+ $html .= '>';
+ } else {
+ $html .= "unknown type $type";
+ }
+ $html .= '</TD></TR>';
+ }
+ $html .= '</TABLE>';
+
+ $html .= '<INPUT TYPE="hidden" NAME="options" VALUE="'.
+ join(',', keys %{$exports->{$layer}{options}} ). '">';
+
+ $html .= '<INPUT TYPE="hidden" NAME="nodomain" VALUE="'.
+ $exports->{$layer}{nodomain}. '">';
+
+ $html .= '<INPUT TYPE="submit" VALUE="'.
+ ( $part_export->exportnum ? "Apply changes" : "Add export" ).
+ '">';
+
+ $html;
+ },
+);
+
+%>
+<%= header("$action Export", menubar(
+ 'Main Menu' => popurl(2),
+), ' onLoad="visualize()"')
+%>
+
+<% if ( $cgi->param('error') ) { %>
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <%= $cgi->param('error') %></FONT>
+ <BR><BR>
+<% } %>
+
+<FORM NAME="dummy">
+<INPUT TYPE="hidden" NAME="exportnum" VALUE="<%= $part_export->exportnum %>">
+
+<%= ntable("#cccccc",2) %>
+<TR>
+ <TD ALIGN="right">Export host</TD>
+ <TD>
+ <INPUT TYPE="text" NAME="machine" VALUE="<%= $part_export->machine %>">
+ </TD>
+</TR>
+<TR>
+ <TD ALIGN="right">Export</TD>
+ <TD><%= $widget->html %>
+</BODY>
+</HTML>
+
diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi
new file mode 100755
index 0000000..460f68b
--- /dev/null
+++ b/httemplate/edit/part_pkg.cgi
@@ -0,0 +1,627 @@
+<!-- mason kludge -->
+<%
+
+if ( $cgi->param('clone') && $cgi->param('clone') =~ /^(\d+)$/ ) {
+ $cgi->param('clone', $1);
+} else {
+ $cgi->param('clone', '');
+}
+if ( $cgi->param('pkgnum') && $cgi->param('pkgnum') =~ /^(\d+)$/ ) {
+ $cgi->param('pkgnum', $1);
+} else {
+ $cgi->param('pkgnum', '');
+}
+
+my ($query) = $cgi->keywords;
+my $action = '';
+my $part_pkg = '';
+if ( $cgi->param('error') ) {
+ $part_pkg = new FS::part_pkg ( {
+ map { $_, scalar($cgi->param($_)) } fields('part_pkg')
+ } );
+}
+if ( $cgi->param('clone') ) {
+ $action='Custom Pricing';
+ my $old_part_pkg =
+ qsearchs('part_pkg', { 'pkgpart' => $cgi->param('clone') } );
+ $part_pkg ||= $old_part_pkg->clone;
+ $part_pkg->disabled('Y');
+} elsif ( $query && $query =~ /^(\d+)$/ ) {
+ $part_pkg ||= qsearchs('part_pkg',{'pkgpart'=>$1});
+} else {
+ unless ( $part_pkg ) {
+ $part_pkg = new FS::part_pkg {};
+ $part_pkg->plan('flat');
+ }
+}
+unless ( $part_pkg->plan ) { #backwards-compat
+ $part_pkg->plan('flat');
+ $part_pkg->plandata("setup_fee=". $part_pkg->setup. "\n".
+ "recur_fee=". $part_pkg->recur. "\n");
+}
+$action ||= $part_pkg->pkgpart ? 'Edit' : 'Add';
+my $hashref = $part_pkg->hashref;
+
+
+print header("$action Package Definition", menubar(
+ 'Main Menu' => popurl(2),
+ 'View all packages' => popurl(2). 'browse/part_pkg.cgi',
+));
+#), ' onLoad="visualize()"');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+#print '<FORM ACTION="', popurl(1), 'process/part_pkg.cgi" METHOD=POST>';
+print '<FORM NAME="dummy">';
+
+#if ( $cgi->param('clone') ) {
+# print qq!<INPUT TYPE="hidden" NAME="clone" VALUE="!, $cgi->param('clone'), qq!">!;
+#}
+#if ( $cgi->param('pkgnum') ) {
+# print qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="!, $cgi->param('pkgnum'), qq!">!;
+#}
+#
+#print qq!<INPUT TYPE="hidden" NAME="pkgpart" VALUE="$hashref->{pkgpart}">!,
+print "Package Part #", $hashref->{pkgpart} ? $hashref->{pkgpart} : "(NEW)";
+
+#false laziness w/view/cust_main.cgi
+my %freq;
+tie %freq, 'Tie::IxHash',
+ '0' => '(no recurring fee)',
+ '1d' => 'daily',
+ '1w' => 'weekly',
+ '2w' => 'biweekly (every 2 weeks)',
+ '1' => 'monthly',
+ '2' => 'bimonthly (every 2 months)',
+ '3' => 'quarterly (every 3 months)',
+ '6' => 'semiannually (every 6 months)',
+ '12' => 'annually',
+ '24' => 'biannually (every 2 years)',
+;
+if ( $part_pkg->dbdef_table->column('freq')->type =~ /(int)/i ) {
+ delete $freq{$_} foreach grep { ! /^\d+$/ } keys %freq;
+}
+
+%>
+<%= ntable("#cccccc",2) %>
+ <TR>
+ <TD ALIGN="right">Package (customer-visible)</TD>
+ <TD>
+ <INPUT TYPE="text" NAME="pkg" SIZE=32 VALUE="<%= $part_pkg->pkg %>">
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Comment (customer-hidden)</TD>
+ <TD>
+ <INPUT TYPE="text" NAME="comment" SIZE=32 VALUE="<%=$part_pkg->comment%>">
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Recurring fee frequency </TD>
+ <TD>
+ <SELECT NAME="freq">
+ <% foreach my $freq ( keys %freq ) { %>
+ <OPTION VALUE="<%= $freq %>"<%= $freq eq $part_pkg->freq ? ' SELECTED' : '' %>><%= $freq{$freq} %>
+ <% } %>
+ </SELECT>
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Setup fee tax exempt</TD>
+ <TD>
+<%
+
+print '<INPUT TYPE="checkbox" NAME="setuptax" VALUE="Y"';
+print ' CHECKED' if $hashref->{setuptax} eq "Y";
+print '>';
+
+print <<END;
+</TD></TR>
+<TR><TD ALIGN="right">Recurring fee tax exempt</TD><TD>
+END
+
+print '<INPUT TYPE="checkbox" NAME="recurtax" VALUE="Y"';
+print ' CHECKED' if $hashref->{recurtax} eq "Y";
+print '>';
+
+print '</TD></TR>';
+
+my $conf = new FS::Conf;
+#false laziness w/ view/cust_main.cgi quick order
+if ( $conf->exists('enable_taxclasses') ) {
+ print '<TR><TD ALIGN="right">Tax class</TD><TD><SELECT NAME="taxclass">';
+ my $sth = dbh->prepare('SELECT DISTINCT taxclass FROM cust_main_county')
+ or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+ foreach my $taxclass ( map $_->[0], @{$sth->fetchall_arrayref} ) {
+ print qq!<OPTION VALUE="$taxclass"!;
+ print ' SELECTED' if $taxclass eq $hashref->{taxclass};
+ print qq!>$taxclass</OPTION>!;
+ }
+ print '</SELECT></TD></TR>';
+} else {
+ print
+ '<INPUT TYPE="hidden" NAME="taxclass" VALUE="'. $hashref->{taxclass}. '">';
+}
+
+print '<TR><TD ALIGN="right">Disable new orders</TD><TD>';
+print '<INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"';
+print ' CHECKED' if $hashref->{disabled} eq "Y";
+print '>';
+print '</TD></TR></TABLE>';
+
+my $thead = "\n\n". ntable('#cccccc', 2).
+ '<TR><TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Quan.</FONT></TH>';
+$thead .= '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Primary</FONT></TH>'
+ if dbdef->table('pkg_svc')->column('primary_svc');
+$thead .= '<TH BGCOLOR="#dcdcdc">Service</TH></TR>';
+
+#unless ( $cgi->param('clone') ) {
+#dunno why...
+unless ( 0 ) {
+ #print <<END, $thead;
+ print <<END, itable(), '<TR><TD VALIGN="top">', $thead;
+<BR><BR>Enter the quantity of each service this package includes.<BR><BR>
+END
+}
+
+my @fixups = ();
+my $count = 0;
+my $columns = 3;
+my @part_svc = qsearch( 'part_svc', { 'disabled' => '' } );
+foreach my $part_svc ( @part_svc ) {
+ my $svcpart = $part_svc->svcpart;
+ my $pkgpart = $cgi->param('clone') || $part_pkg->pkgpart;
+ my $pkg_svc = $pkgpart && qsearchs( 'pkg_svc', {
+ 'pkgpart' => $pkgpart,
+ 'svcpart' => $svcpart,
+ } ) || new FS::pkg_svc ( {
+ 'pkgpart' => $pkgpart,
+ 'svcpart' => $svcpart,
+ 'quantity' => 0,
+ 'primary_svc' => '',
+ });
+ #? #next unless $pkg_svc;
+
+ push @fixups, "pkg_svc$svcpart";
+
+ #unless ( defined ($cgi->param('clone')) && $cgi->param('clone') ) {
+ #dunno why...
+ unless ( 0 ) {
+ print '<TR>'; # if $count == 0 ;
+ print qq!<TD><INPUT TYPE="text" NAME="pkg_svc$svcpart" SIZE=4 MAXLENGTH=3 VALUE="!,
+ $cgi->param("pkg_svc$svcpart") || $pkg_svc->quantity || 0,
+ qq!"></TD>!;
+ if ( dbdef->table('pkg_svc')->column('primary_svc') ) {
+ print qq!<TD><INPUT TYPE="radio" NAME="pkg_svc_primary" VALUE="$svcpart"!;
+ print ' CHECKED' if $pkg_svc->primary_svc =~ /^Y/i;
+ print '></TD>';
+ }
+ print qq!<TD><A HREF="part_svc.cgi?!,$part_svc->svcpart,
+ qq!">!, $part_svc->getfield('svc'), "</A></TD></TR>";
+# print "</TABLE></TD><TD>$thead" if ++$count == int(scalar(@part_svc) / 2);
+ $count+=1;
+ foreach ( 1 .. $columns-1 ) {
+ print "</TABLE></TD><TD VALIGN=\"top\">$thead"
+ if $count == int( $_ * scalar(@part_svc) / $columns );
+ }
+ } else {
+ print qq!<INPUT TYPE="hidden" NAME="pkg_svc$svcpart" VALUE="!,
+ $cgi->param("pkg_svc$svcpart") || $pkg_svc->quantity || 0, qq!">\n!;
+ }
+}
+
+#unless ( $cgi->param('clone') ) {
+#dunno why...
+unless ( 0 ) {
+ print "</TR></TABLE></TD></TR></TABLE>";
+ #print "</TR></TABLE>";
+}
+
+foreach my $f ( qw( clone pkgnum ) ) {
+ print qq!<INPUT TYPE="hidden" NAME="$f" VALUE="!. $cgi->param($f). '">';
+}
+print '<INPUT TYPE="hidden" NAME="pkgpart" VALUE="'. $part_pkg->pkgpart. '">';
+
+# prolly should be in database
+tie my %plans, 'Tie::IxHash',
+ 'flat' => {
+ 'name' => 'Flat rate (anniversary billing)',
+ 'fields' => {
+ 'setup_fee' => { 'name' => 'Setup fee for this package',
+ 'default' => 0,
+ },
+ 'recur_fee' => { 'name' => 'Recurring fee for this package',
+ 'default' => 0,
+ },
+ },
+ 'fieldorder' => [ 'setup_fee', 'recur_fee' ],
+ 'setup' => 'what.setup_fee.value',
+ 'recur' => 'what.recur_fee.value',
+ },
+
+ 'flat_delayed' => {
+ 'name' => 'Free for X days, then flat rate (anniversary billing)',
+ 'fields' => {
+ 'free_days' => { 'name' => 'Initial free days',
+ 'default' => 0,
+ },
+ 'setup_fee' => { 'name' => 'Setup fee for this package',
+ 'default' => 0,
+ },
+ 'recur_fee' => { 'name' => 'Recurring fee for this package',
+ 'default' => 0,
+ },
+ },
+ 'fieldorder' => [ 'free_days', 'setup_fee', 'recur_fee' ],
+ 'setup' => '\'my $d = $cust_pkg->bill || $time; $d += 86400 * \' + what.free_days.value + \'; $cust_pkg->bill($d); $cust_pkg_mod_flag=1; \' + what.setup_fee.value',
+ 'recur' => 'what.recur_fee.value',
+ },
+
+ 'prorate' => {
+ 'name' => 'First partial month pro-rated, then flat-rate (1st of month billing)',
+ 'fields' => {
+ 'setup_fee' => { 'name' => 'Setup fee for this package',
+ 'default' => 0,
+ },
+ 'recur_fee' => { 'name' => 'Recurring fee for this package',
+ 'default' => 0,
+ },
+ },
+ 'fieldorder' => [ 'setup_fee', 'recur_fee' ],
+ 'setup' => 'what.setup_fee.value',
+ 'recur' => '\'my $mnow = $sdate; my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($sdate) )[0,1,2,3,4,5]; my $mstart = timelocal(0,0,0,1,$mon,$year); my $mend = timelocal(0,0,0,1, $mon == 11 ? 0 : $mon+1, $year+($mon==11)); $sdate = $mstart; ( $part_pkg->freq - 1 ) * \' + what.recur_fee.value + \' / $part_pkg->freq + \' + what.recur_fee.value + \' / $part_pkg->freq * ($mend-$mnow) / ($mend-$mstart) ; \'',
+ },
+
+ 'subscription' => {
+ 'name' => 'First partial month full charge, then flat-rate (1st of month billing)',
+ 'fields' => {
+ 'setup_fee' => { 'name' => 'Setup fee for this package',
+ 'default' => 0,
+ },
+ 'recur_fee' => { 'name' => 'Recurring fee for this package',
+ 'default' => 0,
+ },
+ },
+ 'fieldorder' => [ 'setup_fee', 'recur_fee' ],
+ 'setup' => 'what.setup_fee.value',
+ 'recur' => '\'my $mnow = $sdate; my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($sdate) )[0,1,2,3,4,5]; $sdate = timelocal(0,0,0,1,$mon,$year); \' + what.recur_fee.value',
+ },
+
+ 'flat_comission_cust' => {
+ 'name' => 'Flat rate with recurring commission per active customer',
+ 'fields' => {
+ 'setup_fee' => { 'name' => 'Setup fee for this package',
+ 'default' => 0,
+ },
+ 'recur_fee' => { 'name' => 'Recurring fee for this package',
+ 'default' => 0,
+ },
+ 'comission_amount' => { 'name' => 'Commission amount per month (per active customer)',
+ 'default' => 0,
+ },
+ 'comission_depth' => { 'name' => 'Number of layers',
+ 'default' => 1,
+ },
+ },
+ 'fieldorder' => [ 'setup_fee', 'recur_fee', 'comission_depth', 'comission_amount' ],
+ 'setup' => 'what.setup_fee.value',
+ 'recur' => '\'my $error = $cust_pkg->cust_main->credit( \' + what.comission_amount.value + \' * scalar($cust_pkg->cust_main->referral_cust_main_ncancelled(\' + what.comission_depth.value+ \')), "commission" ); die $error if $error; \' + what.recur_fee.value + \';\'',
+ },
+
+ 'flat_comission' => {
+ 'name' => 'Flat rate with recurring commission per (any) active package',
+ 'fields' => {
+ 'setup_fee' => { 'name' => 'Setup fee for this package',
+ 'default' => 0,
+ },
+ 'recur_fee' => { 'name' => 'Recurring fee for this package',
+ 'default' => 0,
+ },
+ 'comission_amount' => { 'name' => 'Commission amount per month (per active package)',
+ 'default' => 0,
+ },
+ 'comission_depth' => { 'name' => 'Number of layers',
+ 'default' => 1,
+ },
+ },
+ 'fieldorder' => [ 'setup_fee', 'recur_fee', 'comission_depth', 'comission_amount' ],
+ 'setup' => 'what.setup_fee.value',
+ 'recur' => '\'my $error = $cust_pkg->cust_main->credit( \' + what.comission_amount.value + \' * scalar($cust_pkg->cust_main->referral_cust_pkg(\' + what.comission_depth.value+ \')), "commission" ); die $error if $error; \' + what.recur_fee.value + \';\'',
+ },
+
+ 'flat_comission_pkg' => {
+ 'name' => 'Flat rate with recurring commission per (selected) active package',
+ 'fields' => {
+ 'setup_fee' => { 'name' => 'Setup fee for this package',
+ 'default' => 0,
+ },
+ 'recur_fee' => { 'name' => 'Recurring fee for this package',
+ 'default' => 0,
+ },
+ 'comission_amount' => { 'name' => 'Commission amount per month (per uncancelled package)',
+ 'default' => 0,
+ },
+ 'comission_depth' => { 'name' => 'Number of layers',
+ 'default' => 1,
+ },
+ 'comission_pkgpart' => { 'name' => 'Applicable packages<BR><FONT SIZE="-1">(hold <b>ctrl</b> to select multiple packages)</FONT>',
+ 'type' => 'select_multiple',
+ 'select_table' => 'part_pkg',
+ 'select_hash' => { 'disabled' => '' } ,
+ 'select_key' => 'pkgpart',
+ 'select_label' => 'pkg',
+ },
+ },
+ 'fieldorder' => [ 'setup_fee', 'recur_fee', 'comission_depth', 'comission_amount', 'comission_pkgpart' ],
+ 'setup' => 'what.setup_fee.value',
+ 'recur' => '""; var pkgparts = ""; for ( var c=0; c < document.flat_comission_pkg.comission_pkgpart.options.length; c++ ) { if (document.flat_comission_pkg.comission_pkgpart.options[c].selected) { pkgparts = pkgparts + document.flat_comission_pkg.comission_pkgpart.options[c].value + \', \'; } } what.recur.value = \'my $error = $cust_pkg->cust_main->credit( \' + what.comission_amount.value + \' * scalar( grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } ( \' + pkgparts + \' ) } $cust_pkg->cust_main->referral_cust_pkg(\' + what.comission_depth.value+ \')), "commission" ); die $error if $error; \' + what.recur_fee.value + \';\'',
+ },
+
+
+
+ 'sesmon_hour' => {
+ 'name' => 'Base charge plus charge per-hour from the session monitor',
+ 'fields' => {
+ 'setup_fee' => { 'name' => 'Setup fee for this package',
+ 'default' => 0,
+ },
+ 'recur_flat' => { 'name' => 'Base monthly charge for this package',
+ 'default' => 0,
+ },
+ 'recur_included_hours' => { 'name' => 'Hours included',
+ 'default' => 0,
+ },
+ 'recur_hourly_charge' => { 'name' => 'Additional charge per hour',
+ 'default' => 0,
+ },
+ },
+ 'fieldorder' => [ 'setup_fee', 'recur_flat', 'recur_included_hours', 'recur_hourly_charge' ],
+ 'setup' => 'what.setup_fee.value',
+ 'recur' => '\'my $hours = $cust_pkg->seconds_since($cust_pkg->bill || 0) / 3600 - \' + what.recur_included_hours.value + \'; $hours = 0 if $hours < 0; \' + what.recur_flat.value + \' + \' + what.recur_hourly_charge.value + \' * $hours;\'',
+ },
+
+ 'sesmon_minute' => {
+ 'name' => 'Base charge plus charge per-minute from the session monitor',
+ 'fields' => {
+ 'setup_fee' => { 'name' => 'Setup fee for this package',
+ 'default' => 0,
+ },
+ 'recur_flat' => { 'name' => 'Base monthly charge for this package',
+ 'default' => 0,
+ },
+ 'recur_included_min' => { 'name' => 'Minutes included',
+ 'default' => 0,
+ },
+ 'recur_minly_charge' => { 'name' => 'Additional charge per minute',
+ 'default' => 0,
+ },
+ },
+ 'fieldorder' => [ 'setup_fee', 'recur_flat', 'recur_included_min', 'recur_minly_charge' ],
+ 'setup' => 'what.setup_fee.value',
+ 'recur' => '\'my $min = $cust_pkg->seconds_since($cust_pkg->bill || 0) / 60 - \' + what.recur_included_min.value + \'; $min = 0 if $min < 0; \' + what.recur_flat.value + \' + \' + what.recur_minly_charge.value + \' * $min;\'',
+
+ },
+
+ 'sqlradacct_hour' => {
+ 'name' => 'Base charge plus charge per-hour (and for data) from an external sqlradius radacct table',
+ 'fields' => {
+ 'setup_fee' => { 'name' => 'Setup fee for this package',
+ 'default' => 0,
+ },
+ 'recur_flat' => { 'name' => 'Base monthly charge for this package',
+ 'default' => 0,
+ },
+ 'recur_included_hours' => { 'name' => 'Hours included',
+ 'default' => 0,
+ },
+ 'recur_hourly_charge' => { 'name' => 'Additional charge per hour',
+ 'default' => 0,
+ },
+ 'recur_included_input' => { 'name' => 'Input megabytes included',
+ 'default' => 0,
+ },
+ 'recur_input_charge' => { 'name' =>
+ 'Additional charge per input megabyte',
+ 'default' => 0,
+ },
+ 'recur_included_output' => { 'name' => 'Output megabytes included',
+ 'default' => 0,
+ },
+ 'recur_output_charge' => { 'name' =>
+ 'Additional charge per output megabyte',
+ 'default' => 0,
+ },
+ 'recur_included_total' => { 'name' =>
+ 'Total input+output megabytes included',
+ 'default' => 0,
+ },
+ 'recur_total_charge' => { 'name' =>
+ 'Additional charge per input+output megabyte',
+ 'default' => 0,
+ },
+ },
+ 'fieldorder' => [qw( setup_fee recur_flat recur_included_hours recur_hourly_charge recur_included_input recur_input_charge recur_included_output recur_output_charge recur_included_total recur_total_charge )],
+ 'setup' => 'what.setup_fee.value',
+ 'recur' => '\'my $last_bill = $cust_pkg->last_bill; my $hours = $cust_pkg->seconds_since_sqlradacct($last_bill, $sdate ) / 3600 - \' + what.recur_included_hours.value + \'; $hours = 0 if $hours < 0; my $input = $cust_pkg->attribute_since_sqlradacct($last_bill, $sdate, \"AcctInputOctets\" ) / 1048576; my $output = $cust_pkg->attribute_since_sqlradacct($last_bill, $sdate, \"AcctOutputOctets\" ) / 1048576; my $total = $input + $output - \' + what.recur_included_total.value + \'; $total = 0 if $total < 0; my $input = $input - \' + what.recur_included_input.value + \'; $input = 0 if $input < 0; my $output = $output - \' + what.recur_included_output.value + \'; $output = 0 if $output < 0; my $totalcharge = sprintf(\"%.2f\", \' + what.recur_total_charge.value + \' * $total); my $inputcharge = sprintf(\"%.2f\", \' + what.recur_input_charge.value + \' * $input); my $outputcharge = sprintf(\"%.2f\", \' + what.recur_output_charge.value + \' * $output); my $hourscharge = sprintf(\"%.2f\", \' + what.recur_hourly_charge.value + \' * $hours); if ( \' + what.recur_total_charge.value + \' > 0 ) { push @details, \"Last month\\\'s data \". sprintf(\"%.1f\", $total). \" megs: \\\$$totalcharge\" } if ( \' + what.recur_input_charge.value + \' > 0 ) { push @details, \"Last month\\\'s download \". sprintf(\"%.1f\", $input). \" megs: \\\$$inputcharge\" } if ( \' + what.recur_output_charge.value + \' > 0 ) { push @details, \"Last month\\\'s upload \". sprintf(\"%.1f\", $output). \" megs: \\\$$outputcharge\" } if ( \' + what.recur_hourly_charge.value + \' > 0 ) { push @details, \"Last month\\\'s time \". sprintf(\"%.1f\", $hours). \" hours: \\\$$hourscharge\"; } \' + what.recur_flat.value + \' + $hourscharge + $inputcharge + $outputcharge + $totalcharge ;\'',
+ },
+
+ 'sql_generic' => {
+ 'name' => 'Base charge plus a metered rate from a configurable SQL query',
+ 'fields' => {
+ 'setup_fee' => { 'name' => 'Setup fee for this package',
+ 'default' => 0,
+ },
+ 'recur_flat' => { 'name' => 'Base monthly charge for this package',
+ 'default' => 0,
+ },
+ 'recur_included' => { 'name' => 'Units included',
+ 'default' => 0,
+ },
+ 'recur_unit_charge' => { 'name' => 'Additional charge per unit',
+ 'default' => 0,
+ },
+ 'datasrc' => { 'name' => 'DBI data source',
+ 'default' => '',
+ },
+ 'db_username' => { 'name' => 'Database username',
+ 'default' => '',
+ },
+ 'db_password' => { 'name' => 'Database username',
+ 'default' => '',
+ },
+ 'query' => { 'name' => 'SQL query',
+ 'default' => '',
+ },
+ },
+ 'fieldorder' => [qw( setup_fee recur_flat recur_included recur_unit_charge datasrc db_username db_password query )],
+ 'setup' => 'what.setup_fee.value',
+ # 'recur' => '\'my $dbh = DBI->connect(\"\' + what.datasrc.value + \'\", \"\' + what.db_username.value + \'\") or die $DBI::errstr; \'',
+ 'recur' => '\'my $dbh = DBI->connect(\"\' + what.datasrc.value + \'\", \"\' + what.db_username.value + \'\", \"\' + what.db_password.value + \'\" ) or die $DBI::errstr; my $sth = $dbh->prepare(\"\' + what.query.value + \'\") or die $dbh->errstr; my $units = 0; foreach my $cust_svc ( grep { $_->part_svc->svcdb eq \"svc_domain\" } $cust_pkg->cust_svc ) { my $domain = $cust_svc->svc_x->domain; $sth->execute($domain) or die $sth->errstr; $units += $sth->fetchrow_arrayref->[0]; } $units -= \' + what.recur_included.value + \'; $units = 0 if $units < 0; \' + what.recur_flat.value + \' + $units * \' + what.recur_unit_charge.value + \';\'',
+ #'recur' => '\'my $dbh = DBI->connect("\' + what.datasrc.value + \'", "\' + what.db_username.value + \'", "\' what.db_password.value + \'" ) or die $DBI::errstr; my $sth = $dbh->prepare("\' + what.query.value + \'") or die $dbh->errstr; my $units = 0; foreach my $cust_svc ( grep { $_->part_svc->svcdb eq "svc_domain" } $cust_pkg->cust_svc ) { my $domain = $cust_svc->svc_x->domain; $sth->execute($domain) or die $sth->errstr; $units += $sth->fetchrow_arrayref->[0]; } $units -= \' + what.recur_included.value + \'; $units = 0 if $units < 0; \' + what.recur_flat.value + \' + $units * \' + what.recur_unit_charge + \';\'',
+ },
+
+
+
+ 'sql_external' => {
+ 'name' => 'Base charge plus additional fees for external services from a configurable SQL query',
+ 'fields' => {
+ 'setup_fee' => { 'name' => 'Setup fee for this package',
+ 'default' => 0,
+ },
+ 'recur_flat' => { 'name' => 'Base monthly charge for this package',
+ 'default' => 0,
+ },
+ 'datasrc' => { 'name' => 'DBI data source',
+ 'default' => '',
+ },
+ 'db_username' => { 'name' => 'Database username',
+ 'default' => '',
+ },
+ 'db_password' => { 'name' => 'Database password',
+ 'default' => '',
+ },
+ 'query' => { 'name' => 'SQL query',
+ 'default' => '',
+ },
+ },
+ 'fieldorder' => [qw( setup_fee recur_flat datasrc db_username db_password query )],
+ 'setup' => 'what.setup_fee.value',
+ 'recur' => q!'my $dbh = DBI->connect("' + what.datasrc.value + '", "' + what.db_username.value + '", "' + what.db_password.value + '" ) or die $DBI::errstr; my $sth = $dbh->prepare("' + what.query.value + '") or die $dbh->errstr; my $price = ' + what.recur_flat.value + '; foreach my $cust_svc ( grep { $_->part_svc->svcdb eq "svc_external" } $cust_pkg->cust_svc ){ my $id = $cust_svc->svc_x->id; $sth->execute($id) or die $sth->errstr; $price += $sth->fetchrow_arrayref->[0]; } $price;'!,
+
+ },
+
+;
+
+my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
+ split("\n", $part_pkg->plandata );
+
+tie my %options, 'Tie::IxHash', map { $_=>$plans{$_}->{'name'} } keys %plans;
+
+my @form_select = ();
+if ( $conf->exists('enable_taxclasses') ) {
+ push @form_select, 'taxclass';
+} else {
+ push @fixups, 'taxclass'; #hidden
+}
+
+my @form_radio = ();
+if ( dbdef->table('pkg_svc')->column('primary_svc') ) {
+ push @form_radio, 'pkg_svc_primary';
+}
+
+my $widget = new HTML::Widgets::SelectLayers(
+ 'selected_layer' => $part_pkg->plan,
+ 'options' => \%options,
+ 'form_name' => 'dummy',
+ 'form_action' => 'process/part_pkg.cgi',
+ 'form_text' => [ qw(pkg comment freq clone pkgnum pkgpart), @fixups ],
+ 'form_checkbox' => [ qw(setuptax recurtax disabled) ],
+ 'form_radio' => \@form_radio,
+ 'form_select' => \@form_select,
+ 'fixup_callback' => sub {
+ #my $ = @_;
+ my $html = '';
+ for my $p ( keys %plans ) {
+ $html .= "if ( what.plan.value == \"$p\" ) {
+ what.setup.value = $plans{$p}->{setup} ;
+ what.recur.value = $plans{$p}->{recur} ;
+ }\n";
+ }
+ $html;
+ },
+ 'layer_callback' => sub {
+ my $layer = shift;
+ my $html = qq!<INPUT TYPE="hidden" NAME="plan" VALUE="$layer">!.
+ ntable("#cccccc",2);
+ my $href = $plans{$layer}->{'fields'};
+ foreach my $field ( exists($plans{$layer}->{'fieldorder'})
+ ? @{$plans{$layer}->{'fieldorder'}}
+ : keys %{ $href }
+ ) {
+
+ $html .= '<TR><TD ALIGN="right">'. $href->{$field}{'name'}. '</TD><TD>';
+
+ if ( ! exists($href->{$field}{'type'}) ) {
+ $html .= qq!<INPUT TYPE="text" NAME="$field" VALUE="!.
+ ( exists($plandata{$field})
+ ? $plandata{$field}
+ : $href->{$field}{'default'} ).
+ qq!" onChange="fchanged(this)">!;
+ } elsif ( $href->{$field}{'type'} eq 'select_multiple' ) {
+ $html .= qq!<SELECT MULTIPLE NAME="$field" onChange="fchanged(this)">!;
+ foreach my $record (
+ qsearch( $href->{$field}{'select_table'},
+ $href->{$field}{'select_hash'} )
+ ) {
+ my $value = $record->getfield($href->{$field}{'select_key'});
+ $html .= qq!<OPTION VALUE="$value"!.
+ ( $plandata{$field} =~ /(^|, *)$value *(,|$)/
+ ? ' SELECTED'
+ : '' ).
+ '>'. $record->getfield($href->{$field}{'select_label'})
+ }
+ $html .= '</SELECT>';
+ }
+
+ $html .= '</TD></TR>';
+ }
+ $html .= '</TABLE>';
+
+ $html .= '<INPUT TYPE="hidden" NAME="plandata" VALUE="'.
+ join(',', keys %{ $href } ). '">'.
+ '<BR><BR>';
+
+ $html .= '<INPUT TYPE="submit" VALUE="'.
+ ( $hashref->{pkgpart} ? "Apply changes" : "Add package" ).
+ '" onClick="fchanged(this)">';
+
+ $html .= '<BR><BR>don\'t edit this unless you know what you\'re doing '.
+ '<INPUT TYPE="button" VALUE="refresh expressions" '.
+ 'onClick="fchanged(this)">'.
+ ntable("#cccccc",2).
+ '<TR><TD>'.
+ '<FONT SIZE="1">Setup expression<BR>'.
+ '<INPUT TYPE="text" NAME="setup" SIZE="160" VALUE="'.
+ encode_entities($hashref->{setup}). '" onLoad="fchanged(this)">'.
+ '</FONT><BR>'.
+ '<FONT SIZE="1">Recurring espression<BR>'.
+ '<INPUT TYPE="text" NAME="recur" SIZE="160" VALUE="'.
+ encode_entities($hashref->{recur}). '" onLoad="fchanged(this)">'.
+ '</FONT>'.
+ '</TR></TD>'.
+ '</TABLE>';
+
+ $html;
+
+ },
+);
+
+%>
+
+<BR>
+Price plan <%= $widget->html %>
+ </BODY>
+</HTML>
diff --git a/httemplate/edit/part_referral.cgi b/httemplate/edit/part_referral.cgi
new file mode 100755
index 0000000..f784dfa
--- /dev/null
+++ b/httemplate/edit/part_referral.cgi
@@ -0,0 +1,48 @@
+<!-- mason kludge -->
+<%
+
+my $part_referral;
+if ( $cgi->param('error') ) {
+ $part_referral = new FS::part_referral ( {
+ map { $_, scalar($cgi->param($_)) } fields('part_referral')
+ } );
+} elsif ( $cgi->keywords ) {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ $part_referral = qsearchs( 'part_referral', { 'refnum' => $1 } );
+} else { #adding
+ $part_referral = new FS::part_referral {};
+}
+my $action = $part_referral->refnum ? 'Edit' : 'Add';
+my $hashref = $part_referral->hashref;
+
+my $p1 = popurl(1);
+print header("$action Advertising source", menubar(
+ 'Main Menu' => popurl(2),
+ 'View all advertising sources' => popurl(2). "browse/part_referral.cgi",
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/part_referral.cgi" METHOD=POST>!;
+
+print qq!<INPUT TYPE="hidden" NAME="refnum" VALUE="$hashref->{refnum}">!;
+#print "Referral #", $hashref->{refnum} ? $hashref->{refnum} : "(NEW)";
+
+print <<END;
+Advertising source <INPUT TYPE="text" NAME="referral" SIZE=32 VALUE="$hashref->{referral}">
+END
+
+print qq!<BR><INPUT TYPE="submit" VALUE="!,
+ $hashref->{refnum} ? "Apply changes" : "Add advertising source",
+ qq!">!;
+
+print <<END;
+ </FORM>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/part_svc.cgi b/httemplate/edit/part_svc.cgi
new file mode 100755
index 0000000..befd9b2
--- /dev/null
+++ b/httemplate/edit/part_svc.cgi
@@ -0,0 +1,332 @@
+<%
+my $part_svc;
+my $clone = '';
+my $error = '';
+if ( $cgi->param('magic') eq 'process' ) {
+
+ my $svcpart = $cgi->param('svcpart');
+ my $old = qsearchs('part_svc', { 'svcpart' => $svcpart }) if $svcpart;
+
+ $cgi->param( 'svc_acct__usergroup',
+ join(',', $cgi->param('svc_acct__usergroup') ) );
+
+ my $new = new FS::part_svc ( {
+ map {
+ $_, scalar($cgi->param($_));
+ # } qw(svcpart svc svcdb)
+ } ( fields('part_svc'),
+ map { my $svcdb = $_;
+ my @fields = fields($svcdb);
+ push @fields, 'usergroup' if $svcdb eq 'svc_acct'; #kludge
+ map { ( $svcdb.'__'.$_, $svcdb.'__'.$_.'_flag' ) } @fields;
+ } grep defined( $FS::Record::dbdef->table($_) ),
+ qw( svc_acct svc_domain svc_forward svc_www svc_broadband )
+ )
+ } );
+
+ my %exportnums =
+ map { $_->exportnum => ( $cgi->param('exportnum'.$_->exportnum) || '') }
+ qsearch('part_export', {} );
+
+ if ( $svcpart ) {
+ $error = $new->replace($old, '1.3-COMPAT', [ 'usergroup' ], \%exportnums );
+ } else {
+ $error = $new->insert( [ 'usergroup' ], \%exportnums );
+ $svcpart = $new->getfield('svcpart');
+ }
+
+ unless ( $error ) { #no error, redirect
+ #print $cgi->redirect(popurl(3)."browse/part_svc.cgi");
+ print $cgi->redirect("${p}browse/part_svc.cgi");
+ myexit;
+ }
+
+ $part_svc = $new; #??
+ #$part_svc = new FS::part_svc ( {
+ # map { $_, scalar($cgi->param($_)) } fields('part_svc')
+ #} );
+
+} elsif ( $cgi->param('clone') && $cgi->param('clone') =~ /^(\d+)$/ ) {#clone
+ #$cgi->param('clone') =~ /^(\d+)$/ or die "malformed query: $query";
+ $part_svc = qsearchs('part_svc', { 'svcpart'=>$1 } )
+ or die "unknown svcpart: $1";
+ $clone = $part_svc->svcpart;
+ $part_svc->svcpart('');
+} elsif ( $cgi->keywords ) { #edit
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/ or die "malformed query: $query";
+ $part_svc=qsearchs('part_svc', { 'svcpart'=>$1 } )
+ or die "unknown svcpart: $1";
+} else { #adding
+ $part_svc = new FS::part_svc {};
+}
+
+my $action = $part_svc->svcpart ? 'Edit' : 'Add';
+my $hashref = $part_svc->hashref;
+# my $p_svcdb = $part_svc->svcdb || 'svc_acct';
+
+
+ #" onLoad=\"visualize()\""
+%>
+<!-- mason kludge -->
+<%= header("$action Service Definition",
+ menubar( 'Main Menu' => $p,
+ 'View all service definitions' => "${p}browse/part_svc.cgi"
+ ),
+ )
+%>
+
+<% if ( $error ) { %>
+<FONT SIZE="+1" COLOR="#ff0000">Error: <%= $error %></FONT>
+<% } %>
+
+<FORM NAME="dummy">
+
+ Service Part #<%= $part_svc->svcpart ? $part_svc->svcpart : "(NEW)" %>
+<BR><BR>
+Service <INPUT TYPE="text" NAME="svc" VALUE="<%= $hashref->{svc} %>"><BR>
+Disable new orders <INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<%= $hashref->{disabled} eq 'Y' ? ' CHECKED' : '' %>><BR>
+<INPUT TYPE="hidden" NAME="magic" VALUE="process">
+<INPUT TYPE="hidden" NAME="svcpart" VALUE="<%= $hashref->{svcpart} %>">
+<BR>
+Services are items you offer to your customers.
+<UL><LI>svc_acct - Shell accounts, POP mailboxes, SLIP/PPP and ISDN accounts
+ <LI>svc_domain - Domains
+ <LI>svc_forward - mail forwarding
+ <LI>svc_www - Virtual domain website
+ <LI>svc_broadband - Broadband/High-speed Internet service
+ <LI>svc_external - Externally-tracked service
+<!-- <LI>svc_charge - One-time charges (Partially unimplemented)
+ <LI>svc_wo - Work orders (Partially unimplemented)
+-->
+</UL>
+For the selected table, you can give fields default or fixed (unchangable)
+values. For example, a SLIP/PPP account may have a default (or perhaps fixed)
+<B>slipip</B> of <B>0.0.0.0</B>, while a POP mailbox will probably have a fixed
+blank <B>slipip</B> as well as a fixed shell something like <B>/bin/true</B> or
+<B>/usr/bin/passwd</B>.
+<BR><BR>
+
+<%
+
+my %vfields;
+
+#these might belong somewhere else for other user interfaces
+#pry need to eventually create stuff that's shared amount UIs
+my $conf = new FS::Conf;
+my %defs = (
+ 'svc_acct' => {
+ 'dir' => 'Home directory',
+ 'uid' => 'UID (set to fixed and blank for dial-only)',
+ 'slipip' => 'IP address (Set to fixed and blank to disable dialin, or, set a value to be exported to RADIUS Framed-IP-Address. Use the special value <code>0e0</code> [zero e zero] to enable export to RADIUS without a Framed-IP-Address.)',
+# 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
+ 'popnum' => {
+ desc => 'Access number',
+ type => 'select',
+ select_table => 'svc_acct_pop',
+ select_key => 'popnum',
+ select_label => 'city',
+ },
+ 'username' => {
+ desc => 'Username',
+ type => 'disabled',
+ },
+ 'quota' => '',
+ '_password' => 'Password',
+ 'gid' => 'GID (when blank, defaults to UID)',
+ 'shell' => {
+ desc =>'Shell (all service definitions should have a default or fixed shell that is present in the <b>shells</b> configuration file)',
+ type =>'select',
+ select_list => [ $conf->config('shells') ],
+ },
+ 'finger' => 'GECOS',
+ 'domsvc' => {
+ desc =>'svcnum from svc_domain',
+ type =>'select',
+ select_table => 'svc_domain',
+ select_key => 'svcnum',
+ select_label => 'domain',
+ },
+ 'usergroup' => {
+ desc =>'ICRADIUS/FreeRADIUS groups',
+ type =>'radius_usergroup_selector',
+ },
+ },
+ 'svc_domain' => {
+ 'domain' => 'Domain',
+ },
+ 'svc_forward' => {
+ 'srcsvc' => 'service from which mail is to be forwarded',
+ 'dstsvc' => 'service to which mail is to be forwarded',
+ 'dst' => 'someone@another.domain.com to use when dstsvc is 0',
+ },
+# 'svc_charge' => {
+# 'amount' => 'amount',
+# },
+# 'svc_wo' => {
+# 'worker' => 'Worker',
+# '_date' => 'Date',
+# },
+ 'svc_www' => {
+ #'recnum' => '',
+ #'usersvc' => '',
+ },
+ 'svc_broadband' => {
+ 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
+ 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
+ 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
+ 'blocknum' => 'Address block.',
+ },
+ 'svc_external' => {
+ #'id' => '',
+ #'title' => '',
+ },
+);
+
+ foreach my $svcdb (grep dbdef->table($_), keys %defs ) {
+ my $self = "FS::$svcdb"->new;
+ $vfields{$svcdb} = {};
+ foreach my $field ($self->virtual_fields) { # svc_Common::virtual_fields with a null svcpart returns all of them
+ my $pvf = $self->pvf($field);
+ my @list = $pvf->list;
+ if (scalar @list) {
+ $defs{$svcdb}->{$field} = { desc => $pvf->label,
+ type => 'select',
+ select_list => \@list };
+ } else {
+ $defs{$svcdb}->{$field} = $pvf->label;
+ } #endif
+ $vfields{$svcdb}->{$field} = $pvf;
+ warn "\$vfields{$svcdb}->{$field} = $pvf";
+ } #next $field
+ } #next $svcdb
+
+ my @dbs = $hashref->{svcdb}
+ ? ( $hashref->{svcdb} )
+ : qw( svc_acct svc_domain svc_forward svc_www svc_broadband svc_external );
+
+ tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } grep dbdef->table($_), @dbs;
+ my $widget = new HTML::Widgets::SelectLayers(
+ #'selected_layer' => $p_svcdb,
+ 'selected_layer' => $hashref->{svcdb} || 'svc_acct',
+ 'options' => \%svcdb,
+ 'form_name' => 'dummy',
+ #'form_action' => 'process/part_svc.cgi',
+ 'form_action' => 'part_svc.cgi', #self
+ 'form_text' => [ qw( magic svc svcpart ) ],
+ 'form_checkbox' => [ 'disabled' ],
+ 'layer_callback' => sub {
+ my $layer = shift;
+ my $html = qq!<INPUT TYPE="hidden" NAME="svcdb" VALUE="$layer">!;
+
+ my $columns = 3;
+ my $count = 0;
+ my @part_export =
+ map { qsearch( 'part_export', {exporttype => $_ } ) }
+ keys %{FS::part_export::export_info($layer)};
+ $html .= '<BR><BR>'. table().
+ table(). "<TR><TH COLSPAN=$columns>Exports</TH></TR><TR>";
+ foreach my $part_export ( @part_export ) {
+ $html .= '<TD><INPUT TYPE="checkbox"'.
+ ' NAME="exportnum'. $part_export->exportnum. '" VALUE="1" ';
+ $html .= 'CHECKED'
+ if ( $clone || $part_svc->svcpart ) #null svcpart search causing error
+ && qsearchs( 'export_svc', {
+ exportnum => $part_export->exportnum,
+ svcpart => $clone || $part_svc->svcpart });
+ $html .= '>'. $part_export->exportnum. ': '. $part_export->exporttype.
+ ' to '. $part_export->machine. '</TD>';
+ $count++;
+ $html .= '</TR><TR>' unless $count % $columns;
+ }
+ $html .= '</TR></TABLE><BR><BR>';
+
+ $html .= table(). "<TH>Field</TH><TH COLSPAN=2>Modifier</TH>";
+ #yucky kludge
+ my @fields = defined( $FS::Record::dbdef->table($layer) )
+ ? grep { $_ ne 'svcnum' } fields($layer)
+ : ();
+ push @fields, 'usergroup' if $layer eq 'svc_acct'; #kludge
+ $part_svc->svcpart($clone) if $clone; #haha, undone below
+ foreach my $field (@fields) {
+ my $part_svc_column = $part_svc->part_svc_column($field);
+ my $value = $error
+ ? $cgi->param("${layer}__${field}")
+ : $part_svc_column->columnvalue;
+ my $flag = $error
+ ? $cgi->param("${layer}__${field}_flag")
+ : $part_svc_column->columnflag;
+ my $def = $defs{$layer}{$field};
+ my $desc = ref($def) ? $def->{desc} : $def;
+
+ $html .= "<TR><TD>$field";
+ $html .= "- <FONT SIZE=-1>$desc</FONT>" if $desc;
+ $html .= "</TD>";
+ $flag = '' if ref($def) && $def->{type} eq 'disabled';
+ $html .=
+ qq!<TD><INPUT TYPE="radio" NAME="${layer}__${field}_flag" VALUE=""!.
+ ' CHECKED'x($flag eq ''). ">Off</TD>".
+ '<TD>';
+ unless ( ref($def) && $def->{type} eq 'disabled' ) {
+ $html .=
+ qq!<INPUT TYPE="radio" NAME="${layer}__${field}_flag" VALUE="D"!.
+ ' CHECKED'x($flag eq 'D'). ">Default ".
+ qq!<INPUT TYPE="radio" NAME="${layer}__${field}_flag" VALUE="F"!.
+ ' CHECKED'x($flag eq 'F'). ">Fixed ";
+ $html .= '<BR>';
+ }
+ if ( ref($def) ) {
+ if ( $def->{type} eq 'select' ) {
+ $html .= qq!<SELECT NAME="${layer}__${field}">!;
+ $html .= '<OPTION> </OPTION>' unless $value;
+ if ( $def->{select_table} ) {
+ foreach my $record ( qsearch( $def->{select_table}, {} ) ) {
+ my $rvalue = $record->getfield($def->{select_key});
+ $html .= qq!<OPTION VALUE="$rvalue"!.
+ ( $rvalue==$value ? ' SELECTED>' : '>' ).
+ $record->getfield($def->{select_label}). '</OPTION>';
+ } #next $record
+ } else { # select_list
+ foreach my $item ( @{$def->{select_list}} ) {
+ $html .= qq!<OPTION VALUE="$item"!.
+ ( $item eq $value ? ' SELECTED>' : '>' ).
+ $item. '</OPTION>';
+ } #next $item
+ } #endif
+ $html .= '</SELECT>';
+ } elsif ( $def->{type} eq 'radius_usergroup_selector' ) {
+ $html .= FS::svc_acct::radius_usergroup_selector(
+ [ split(',', $value) ], "${layer}__${field}" );
+ } elsif ( $def->{type} eq 'disabled' ) {
+ $html .=
+ qq!<INPUT TYPE="hidden" NAME="${layer}__${field}" VALUE="">!;
+ } else {
+ $html .= '<font color="#ff0000">unknown type'. $def->{type};
+ }
+ } else {
+ $html .=
+ qq!<INPUT TYPE="text" NAME="${layer}__${field}" VALUE="$value">!;
+ }
+
+ if($vfields{$layer}->{$field}) {
+ $html .= qq!<BR><INPUT TYPE="radio" NAME="${layer}__${field}_flag" VALUE="X"!.
+ ' CHECKED'x($flag eq 'X'). ">Excluded ";
+ }
+ $html .= "</TD></TR>\n";
+ }
+ $part_svc->svcpart('') if $clone; #undone
+ $html .= "</TABLE>";
+
+ $html .= '<BR><INPUT TYPE="submit" VALUE="'.
+ ($hashref->{svcpart} ? 'Apply changes' : 'Add service'). '">';
+
+ $html;
+
+ },
+ );
+
+%>
+Table <%= $widget->html %>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/edit/part_virtual_field.cgi b/httemplate/edit/part_virtual_field.cgi
new file mode 100644
index 0000000..fb10321
--- /dev/null
+++ b/httemplate/edit/part_virtual_field.cgi
@@ -0,0 +1,92 @@
+<!-- mason kludge -->
+<%
+my ($vfieldpart, $part_virtual_field);
+
+if ( $cgi->param('error') ) {
+ $part_virtual_field = new FS::part_virtual_field ( {
+ map { $_, scalar($cgi->param($_)) } fields('part_virtual_field')});
+ $vfieldpart = $part_virtual_field->vfieldpart;
+} else {
+ my($query) = $cgi->keywords;
+ if ( $query =~ /^(\d+)$/ ) { #editing
+ $vfieldpart=$1;
+ $part_virtual_field=qsearchs('part_virtual_field',
+ {'vfieldpart' => $vfieldpart})
+ or die "Unknown vfieldpart!";
+
+ } else { #adding
+ $part_virtual_field = new FS::part_virtual_field({});
+ }
+}
+my $action = $part_virtual_field->vfieldpart ? 'Edit' : 'Add';
+
+my $p1 = popurl(1);
+print header("$action Virtual Field Definition", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+%>
+<FORM ACTION="<%=$p1%>process/generic.cgi" METHOD="POST">
+
+<INPUT TYPE="hidden" NAME="table" VALUE="part_virtual_field">
+<INPUT TYPE="hidden" NAME="redirect_ok"
+ VALUE="<%=popurl(2)%>browse/part_virtual_field.cgi">
+<INPUT TYPE="hidden" NAME="vfieldpart" VALUE="<%=
+ $vfieldpart%>">
+Field #<B><%=$vfieldpart or "(NEW)"%></B><BR><BR>
+
+<%=ntable("#cccccc",2)%>
+ <TR>
+ <TD ALIGN="right">Name</TD>
+ <TD><INPUT TYPE="text" NAME="name" MAXLENGTH=15 VALUE="<%=
+ $part_virtual_field->name%>"></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Table</TD>
+ <TD><% if ($action eq 'Add') { %>
+ <SELECT SIZE=1 NAME="dbtable"><%
+ my $dbdef = dbdef; # ick
+ foreach my $dbtable (sort { $a cmp $b } $dbdef->tables) {
+ if ($dbtable !~ /^h_/
+ and $dbdef->table($dbtable)->primary_key) { %>
+ <OPTION VALUE="<%=$dbtable%>"><%=$dbtable%></OPTION><%
+ }
+ }
+ %></SELECT><%
+ } else { # Edit
+ %><%=$part_virtual_field->dbtable%>
+ <INPUT TYPE="hidden" NAME="dbtable" VALUE="<%=$part_virtual_field->dbtable%>">
+ <% } %>
+ </TD>
+ <TR>
+ <TD ALIGN="right">Label</TD>
+ <TD><INPUT TYPE="text" NAME="label" MAXLENGTH="20" VALUE="<%=
+ $part_virtual_field->label%>"></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Length</TD>
+ <TD><INPUT TYPE="text" NAME="length" MAXLENGTH=4 VALUE="<%=
+ $part_virtual_field->length%>"></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Check</TD>
+ <TD><TEXTAREA COLS="20" ROWS="4" NAME="check_block"><%=
+ $part_virtual_field->check_block%></TEXTAREA></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">List source</TD>
+ <TD><TEXTAREA COLS="20" ROWS="4" NAME="list_source"><%=
+ $part_virtual_field->list_source%></TEXTAREA></TD>
+ </TR>
+</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">
+
+</FORM>
+
+<BR><BR>
+<FONT SIZE=-2>If you don't understand what <I>check_block</I> and
+<I>list_source</I> mean, <B>LEAVE THEM BLANK</B>. We mean it.</FONT>
+
+
+</BODY>
+</HTML>
diff --git a/httemplate/edit/process/REAL_cust_pkg.cgi b/httemplate/edit/process/REAL_cust_pkg.cgi
new file mode 100755
index 0000000..3d697dd
--- /dev/null
+++ b/httemplate/edit/process/REAL_cust_pkg.cgi
@@ -0,0 +1,24 @@
+<%
+
+my $pkgnum = $cgi->param('pkgnum') or die;
+my $old = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+my %hash = $old->hash;
+$hash{'setup'} = $cgi->param('setup') ? str2time($cgi->param('setup')) : '';
+$hash{'bill'} = $cgi->param('bill') ? str2time($cgi->param('bill')) : '';
+$hash{'last_bill'} =
+ $cgi->param('last_bill') ? str2time($cgi->param('last_bill')) : '';
+$hash{'expire'} = $cgi->param('expire') ? str2time($cgi->param('expire')) : '';
+my $new = new FS::cust_pkg \%hash;
+
+my $error = $new->replace($old);
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "REAL_cust_pkg.cgi?". $cgi->query_string );
+} else {
+ my $custnum = $new->custnum;
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum".
+ "#cust_pkg$pkgnum" );
+}
+
+%>
diff --git a/httemplate/edit/process/addr_block/add.cgi b/httemplate/edit/process/addr_block/add.cgi
new file mode 100755
index 0000000..34d799c
--- /dev/null
+++ b/httemplate/edit/process/addr_block/add.cgi
@@ -0,0 +1,20 @@
+<%
+
+my $error = '';
+my $ip_gateway = $cgi->param('ip_gateway');
+my $ip_netmask = $cgi->param('ip_netmask');
+
+my $new = new FS::addr_block {
+ ip_gateway => $ip_gateway,
+ ip_netmask => $ip_netmask,
+ routernum => 0 };
+
+$error = $new->insert;
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(4). "browse/addr_block.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(4). "browse/addr_block.cgi");
+}
+%>
diff --git a/httemplate/edit/process/addr_block/allocate.cgi b/httemplate/edit/process/addr_block/allocate.cgi
new file mode 100755
index 0000000..85b0d7a
--- /dev/null
+++ b/httemplate/edit/process/addr_block/allocate.cgi
@@ -0,0 +1,25 @@
+<%
+my $error = '';
+my $blocknum = $cgi->param('blocknum');
+my $routernum = $cgi->param('routernum');
+
+my $addr_block = qsearchs('addr_block', { blocknum => $blocknum });
+my $router = qsearchs('router', { routernum => $routernum });
+
+if($addr_block) {
+ if ($router) {
+ $error = $addr_block->allocate($router);
+ } else {
+ $error = "Cannot find router with routernum $routernum";
+ }
+} else {
+ $error = "Cannot find block with blocknum $blocknum";
+}
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(4). "browse/addr_block.cgi?" . $cgi->query_string);
+} else {
+ print $cgi->redirect(popurl(4). "browse/addr_block.cgi");
+}
+%>
diff --git a/httemplate/edit/process/addr_block/deallocate.cgi b/httemplate/edit/process/addr_block/deallocate.cgi
new file mode 100755
index 0000000..cfb7ed0
--- /dev/null
+++ b/httemplate/edit/process/addr_block/deallocate.cgi
@@ -0,0 +1,24 @@
+<%
+my $error = '';
+my $blocknum = $cgi->param('blocknum');
+
+my $addr_block = qsearchs('addr_block', { blocknum => $blocknum });
+
+if($addr_block) {
+ my $router = $addr_block->router;
+ if ($router) {
+ $error = $addr_block->deallocate($router);
+ } else {
+ $error = "Block is not allocated to a router";
+ }
+} else {
+ $error = "Cannot find block with blocknum $blocknum";
+}
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(4). "browse/addr_block.cgi?" . $cgi->query_string);
+} else {
+ print $cgi->redirect(popurl(4). "browse/addr_block.cgi");
+}
+%>
diff --git a/httemplate/edit/process/addr_block/split.cgi b/httemplate/edit/process/addr_block/split.cgi
new file mode 100755
index 0000000..bb6d4ba
--- /dev/null
+++ b/httemplate/edit/process/addr_block/split.cgi
@@ -0,0 +1,19 @@
+<%
+my $error = '';
+my $blocknum = $cgi->param('blocknum');
+my $addr_block = qsearchs('addr_block', { blocknum => $blocknum });
+
+if ( $addr_block) {
+ $error = $addr_block->split_block;
+} else {
+ $error = "Unknown blocknum: $blocknum";
+}
+
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(4). "browse/addr_block.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(4). "browse/addr_block.cgi");
+}
+%>
diff --git a/httemplate/edit/process/agent.cgi b/httemplate/edit/process/agent.cgi
new file mode 100755
index 0000000..182eeab
--- /dev/null
+++ b/httemplate/edit/process/agent.cgi
@@ -0,0 +1,28 @@
+<%
+
+my $agentnum = $cgi->param('agentnum');
+
+my $old = qsearchs('agent',{'agentnum'=>$agentnum}) if $agentnum;
+
+my $new = new FS::agent ( {
+ map {
+ $_, scalar($cgi->param($_));
+ } fields('agent')
+} );
+
+my $error;
+if ( $agentnum ) {
+ $error=$new->replace($old);
+} else {
+ $error=$new->insert;
+ $agentnum=$new->getfield('agentnum');
+}
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "agent.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "browse/agent.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/agent_type.cgi b/httemplate/edit/process/agent_type.cgi
new file mode 100755
index 0000000..5165945
--- /dev/null
+++ b/httemplate/edit/process/agent_type.cgi
@@ -0,0 +1,55 @@
+<%
+
+my $typenum = $cgi->param('typenum');
+my $old = qsearchs('agent_type',{'typenum'=>$typenum}) if $typenum;
+
+my $new = new FS::agent_type ( {
+ map {
+ $_, scalar($cgi->param($_));
+ } fields('agent_type')
+} );
+
+my $error;
+if ( $typenum ) {
+ $error=$new->replace($old);
+} else {
+ $error=$new->insert;
+ $typenum=$new->getfield('typenum');
+}
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "agent_type.cgi?". $cgi->query_string );
+} else {
+
+ #false laziness w/ edit/process/part_svc.cgi
+ foreach my $part_pkg (qsearch('part_pkg',{})) {
+ my($pkgpart)=$part_pkg->getfield('pkgpart');
+
+ my($type_pkgs)=qsearchs('type_pkgs',{
+ 'typenum' => $typenum,
+ 'pkgpart' => $pkgpart,
+ });
+ if ( $type_pkgs && ! $cgi->param("pkgpart$pkgpart") ) {
+ my($d_type_pkgs)=$type_pkgs; #need to save $type_pkgs for below.
+ $error=$d_type_pkgs->delete;
+ die $error if $error;
+
+ } elsif ( $cgi->param("pkgpart$pkgpart")
+ && ! $type_pkgs
+ ) {
+ #ok to clobber it now (but bad form nonetheless?)
+ $type_pkgs=new FS::type_pkgs ({
+ 'typenum' => $typenum,
+ 'pkgpart' => $pkgpart,
+ });
+ $error= $type_pkgs->insert;
+ die $error if $error;
+ }
+
+ }
+
+ print $cgi->redirect(popurl(3). "browse/agent_type.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/cust_bill_pay.cgi b/httemplate/edit/process/cust_bill_pay.cgi
new file mode 100755
index 0000000..0025b16
--- /dev/null
+++ b/httemplate/edit/process/cust_bill_pay.cgi
@@ -0,0 +1,43 @@
+<%
+
+$cgi->param('paynum') =~ /^(\d*)$/ or die "Illegal paynum!";
+my $paynum = $1;
+
+my $cust_pay = qsearchs('cust_pay', { 'paynum' => $paynum } )
+ or die "No such paynum";
+
+my $cust_main = qsearchs('cust_main', { 'custnum' => $cust_pay->custnum } )
+ or die "Bogus credit: not attached to customer";
+
+my $custnum = $cust_main->custnum;
+
+my $new;
+if ($cgi->param('invnum') =~ /^Refund$/) {
+ $new = new FS::cust_refund ( {
+ 'reason' => 'Refunding payment', #enter reason in UI
+ 'refund' => $cgi->param('amount'),
+ 'payby' => 'BILL',
+ #'_date' => $cgi->param('_date'),
+ 'payinfo' => 'Cash', #enter payinfo in UI
+ 'paynum' => $paynum,
+ } );
+} else {
+ $new = new FS::cust_bill_pay ( {
+ map {
+ $_, scalar($cgi->param($_));
+ #} qw(custnum _date amount invnum)
+ } fields('cust_bill_pay')
+ } );
+}
+
+my $error = $new->insert;
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "cust_bill_pay.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+}
+
+
+%>
diff --git a/httemplate/edit/process/cust_credit.cgi b/httemplate/edit/process/cust_credit.cgi
new file mode 100755
index 0000000..85bfd44
--- /dev/null
+++ b/httemplate/edit/process/cust_credit.cgi
@@ -0,0 +1,26 @@
+<%
+
+$cgi->param('custnum') =~ /^(\d*)$/ or die "Illegal custnum!";
+my $custnum = $1;
+
+my $new = new FS::cust_credit ( {
+ map {
+ $_, scalar($cgi->param($_));
+ } fields('cust_credit')
+} );
+
+my $error = $new->insert;
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "cust_credit.cgi?". $cgi->query_string );
+} else {
+ if ( $cgi->param('apply') eq 'yes' ) {
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum })
+ or die "unknown custnum $custnum";
+ $cust_main->apply_credits;
+ }
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+}
+
+%>
diff --git a/httemplate/edit/process/cust_credit_bill.cgi b/httemplate/edit/process/cust_credit_bill.cgi
new file mode 100755
index 0000000..23e2e6c
--- /dev/null
+++ b/httemplate/edit/process/cust_credit_bill.cgi
@@ -0,0 +1,43 @@
+<%
+
+$cgi->param('crednum') =~ /^(\d*)$/ or die "Illegal crednum!";
+my $crednum = $1;
+
+my $cust_credit = qsearchs('cust_credit', { 'crednum' => $crednum } )
+ or die "No such crednum";
+
+my $cust_main = qsearchs('cust_main', { 'custnum' => $cust_credit->custnum } )
+ or die "Bogus credit: not attached to customer";
+
+my $custnum = $cust_main->custnum;
+
+my $new;
+if ($cgi->param('invnum') =~ /^Refund$/) {
+ $new = new FS::cust_refund ( {
+ 'reason' => $cust_credit->reason,
+ 'refund' => $cgi->param('amount'),
+ 'payby' => 'BILL',
+ #'_date' => $cgi->param('_date'),
+ 'payinfo' => 'Cash',
+ 'crednum' => $crednum,
+ } );
+} else {
+ $new = new FS::cust_credit_bill ( {
+ map {
+ $_, scalar($cgi->param($_));
+ #} qw(custnum _date amount invnum)
+ } fields('cust_credit_bill')
+ } );
+}
+
+my $error = $new->insert;
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "cust_credit_bill.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+}
+
+
+%>
diff --git a/httemplate/edit/process/cust_main.cgi b/httemplate/edit/process/cust_main.cgi
new file mode 100755
index 0000000..0b13ba9
--- /dev/null
+++ b/httemplate/edit/process/cust_main.cgi
@@ -0,0 +1,131 @@
+<%
+
+my $error = '';
+
+#unmunge stuff
+
+$cgi->param('tax','') unless defined $cgi->param('tax');
+
+$cgi->param('refnum', (split(/:/, ($cgi->param('refnum'))[0] ))[0] );
+
+my $payby = $cgi->param('payby');
+if ( $payby ) {
+ if ( $payby eq 'CHEK' || $payby eq 'DCHK' ) {
+ $cgi->param('payinfo',
+ $cgi->param($payby. '_payinfo1'). '@'. $cgi->param($payby. '_payinfo2') );
+ } else {
+ $cgi->param('payinfo', $cgi->param( $payby. '_payinfo' ) );
+ }
+ $cgi->param('paydate',
+ $cgi->param( $payby. '_month' ). '-'. $cgi->param( $payby. '_year' ) );
+ $cgi->param('payname', $cgi->param( $payby. '_payname' ) );
+ $cgi->param('paycvv', $cgi->param( $payby. '_paycvv' ) )
+ if defined $cgi->param( $payby. '_paycvv' );
+}
+
+my @invoicing_list = split( /\s*\,\s*/, $cgi->param('invoicing_list') );
+push @invoicing_list, 'POST' if $cgi->param('invoicing_list_POST');
+$cgi->param('invoicing_list', join(',', @invoicing_list) );
+
+
+#create new record object
+
+my $new = new FS::cust_main ( {
+ map {
+ $_, scalar($cgi->param($_))
+# } qw(custnum agentnum last first ss company address1 address2 city county
+# state zip daytime night fax payby payinfo paydate payname tax
+# otaker refnum)
+ } fields('cust_main')
+} );
+
+if ( defined($cgi->param('same')) && $cgi->param('same') eq "Y" ) {
+ $new->setfield("ship_$_", '') foreach qw(
+ last first company address1 address2 city county state zip
+ country daytime night fax
+ );
+}
+
+#perhaps this stuff should go to cust_main.pm
+my $cust_pkg = '';
+my $svc_acct = '';
+if ( $new->custnum eq '' ) {
+
+ if ( $cgi->param('pkgpart_svcpart') ) {
+ my $x = $cgi->param('pkgpart_svcpart');
+ $x =~ /^(\d+)_(\d+)$/ or die "illegal pkgpart_svcpart $x\n";
+ my($pkgpart, $svcpart) = ($1, $2);
+ #false laziness: copied from FS::cust_pkg::order (which should become a
+ #FS::cust_main method)
+ my(%part_pkg);
+ # generate %part_pkg
+ # $part_pkg{$pkgpart} is true iff $custnum may purchase $pkgpart
+ my $agent = qsearchs('agent',{'agentnum'=> $new->agentnum });
+ #my($type_pkgs);
+ #foreach $type_pkgs ( qsearch('type_pkgs',{'typenum'=> $agent->typenum }) ) {
+ # my($pkgpart)=$type_pkgs->pkgpart;
+ # $part_pkg{$pkgpart}++;
+ #}
+ # $pkgpart_href->{PKGPART} is true iff $custnum may purchase $pkgpart
+ my $pkgpart_href = $agent->pkgpart_hashref;
+ #eslaf
+
+ # this should wind up in FS::cust_pkg!
+ $error ||= "Agent ". $new->agentnum. " (type ". $agent->typenum. ") can't ".
+ "purchase pkgpart ". $pkgpart
+ #unless $part_pkg{ $pkgpart };
+ unless $pkgpart_href->{ $pkgpart };
+
+ $cust_pkg = new FS::cust_pkg ( {
+ #later 'custnum' => $custnum,
+ 'pkgpart' => $pkgpart,
+ } );
+ $error ||= $cust_pkg->check;
+
+ #$cust_svc = new FS::cust_svc ( { 'svcpart' => $svcpart } );
+
+ #$error ||= $cust_svc->check;
+
+ $svc_acct = new FS::svc_acct ( {
+ 'svcpart' => $svcpart,
+ 'username' => $cgi->param('username'),
+ '_password' => $cgi->param('_password'),
+ 'popnum' => $cgi->param('popnum'),
+ } );
+
+ my $y = $svc_acct->setdefault; # arguably should be in new method
+ $error ||= $y unless ref($y);
+ #and just in case you were silly
+ $svc_acct->svcpart($svcpart);
+ $svc_acct->username($cgi->param('username'));
+ $svc_acct->_password($cgi->param('_password'));
+ $svc_acct->popnum($cgi->param('popnum'));
+
+ $error ||= $svc_acct->check;
+
+ } elsif ( $cgi->param('username') ) { #good thing to catch
+ $error = "Can't assign username without a package!";
+ }
+
+ use Tie::RefHash;
+ tie my %hash, 'Tie::RefHash';
+ %hash = ( $cust_pkg => [ $svc_acct ] ) if $cust_pkg;
+ $error ||= $new->insert( \%hash, \@invoicing_list );
+} else { #create old record object
+ my $old = qsearchs( 'cust_main', { 'custnum' => $new->custnum } );
+ $error ||= "Old record not found!" unless $old;
+ if ( defined dbdef->table('cust_main')->column('paycvv')
+ && length($old->paycvv)
+ && $new->paycvv =~ /^\s*\*+\s*$/ ) {
+ $new->paycvv($old->paycvv);
+ }
+ $error ||= $new->replace($old, \@invoicing_list);
+}
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "cust_main.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?". $new->custnum);
+}
+%>
diff --git a/httemplate/edit/process/cust_main_county-collapse.cgi b/httemplate/edit/process/cust_main_county-collapse.cgi
new file mode 100755
index 0000000..5da9dea
--- /dev/null
+++ b/httemplate/edit/process/cust_main_county-collapse.cgi
@@ -0,0 +1,35 @@
+<%
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ or die "Illegal taxnum!";
+my $taxnum = $1;
+my $cust_main_county = qsearchs('cust_main_county', { 'taxnum' => $taxnum } )
+ or die "Unknown taxnum $taxnum";
+
+#really should do this in a .pm & start transaction
+
+foreach my $delete ( qsearch('cust_main_county', {
+ 'country' => $cust_main_county->country,
+ 'state' => $cust_main_county->state
+ } ) ) {
+# unless ( qsearch('cust_main',{
+# 'state' => $cust_main_county->getfield('state'),
+# 'county' => $cust_main_county->getfield('county'),
+# 'country' => $cust_main_county->getfield('country'),
+# } ) ) {
+ my $error = $delete->delete;
+ die $error if $error;
+# } else {
+ #should really fix the $cust_main record
+# }
+
+}
+
+$cust_main_county->taxnum('');
+$cust_main_county->county('');
+my $error = $cust_main_county->insert;
+die $error if $error;
+
+print $cgi->redirect(popurl(3). "browse/cust_main_county.cgi");
+
+%>
diff --git a/httemplate/edit/process/cust_main_county-expand.cgi b/httemplate/edit/process/cust_main_county-expand.cgi
new file mode 100755
index 0000000..a452711
--- /dev/null
+++ b/httemplate/edit/process/cust_main_county-expand.cgi
@@ -0,0 +1,58 @@
+<%
+
+$cgi->param('taxnum') =~ /^(\d+)$/ or die "Illegal taxnum!";
+my $taxnum = $1;
+my $cust_main_county = qsearchs('cust_main_county',{'taxnum'=>$taxnum})
+ or die ("Unknown taxnum!");
+
+my @expansion;
+if ( $cgi->param('delim') eq 'n' ) {
+ @expansion=split(/\n/,$cgi->param('expansion'));
+} elsif ( $cgi->param('delim') eq 's' ) {
+ @expansion=split(' ',$cgi->param('expansion'));
+} else {
+ die "Illegal delim!";
+}
+
+@expansion=map {
+ unless ( /^\s*([\w\- ]+)\s*$/ ) {
+ $cgi->param('error', "Illegal item in expansion");
+ print $cgi->redirect(popurl(2). "cust_main_county-expand.cgi?". $cgi->query_string );
+ myexit();
+ }
+ $1;
+} @expansion;
+
+foreach ( @expansion) {
+ my(%hash)=$cust_main_county->hash;
+ my($new)=new FS::cust_main_county \%hash;
+ $new->setfield('taxnum','');
+ if ( $cgi->param('taxclass') ) {
+ $new->setfield('taxclass', $_);
+ } elsif ( ! $cust_main_county->state ) {
+ $new->setfield('state',$_);
+ } else {
+ $new->setfield('county',$_);
+ }
+ #if (datasrc =~ m/Pg/)
+ #{
+ # $new->setfield('tax',0.0);
+ #}
+ my($error)=$new->insert;
+ die $error if $error;
+}
+
+unless ( qsearch( 'cust_main', {
+ 'state' => $cust_main_county->state,
+ 'county' => $cust_main_county->county,
+ 'country' => $cust_main_county->country,
+ } )
+ || ! @expansion
+) {
+ my($error)=($cust_main_county->delete);
+ die $error if $error;
+}
+
+print $cgi->redirect(popurl(3). "browse/cust_main_county.cgi");
+
+%>
diff --git a/httemplate/edit/process/cust_main_county.cgi b/httemplate/edit/process/cust_main_county.cgi
new file mode 100755
index 0000000..9287ed1
--- /dev/null
+++ b/httemplate/edit/process/cust_main_county.cgi
@@ -0,0 +1,30 @@
+<%
+
+foreach ( grep { /^tax\d+$/ } $cgi->param ) {
+ /^tax(\d+)$/ or die "Illegal form $_!";
+ my $taxnum = $1;
+ my $old = qsearchs('cust_main_county', { 'taxnum' => $taxnum })
+ or die "Couldn't find taxnum $taxnum!";
+ next unless $old->tax != $cgi->param("tax$taxnum")
+ || $old->exempt_amount != $cgi->param("exempt_amount$taxnum")
+ || $old->taxname ne $cgi->param("taxname$taxnum")
+ || $old->setuptax ne $cgi->param("setuptax$taxnum")
+ || $old->recurtax ne $cgi->param("recurtax$taxnum");
+ my %hash = $old->hash;
+ $hash{tax} = $cgi->param("tax$taxnum");
+ $hash{exempt_amount} = $cgi->param("exempt_amount$taxnum");
+ $hash{taxname} = $cgi->param("taxname$taxnum");
+ $hash{setuptax} = $cgi->param("setuptax$taxnum");
+ $hash{recurtax} = $cgi->param("recurtax$taxnum");
+ my $new = new FS::cust_main_county \%hash;
+ my $error = $new->replace($old);
+ if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "cust_main_county.cgi?". $cgi->query_string );
+ myexit();
+ }
+}
+
+print $cgi->redirect(popurl(3). "browse/cust_main_county.cgi");
+
+%>
diff --git a/httemplate/edit/process/cust_pay.cgi b/httemplate/edit/process/cust_pay.cgi
new file mode 100755
index 0000000..82442ae
--- /dev/null
+++ b/httemplate/edit/process/cust_pay.cgi
@@ -0,0 +1,39 @@
+<%
+
+$cgi->param('linknum') =~ /^(\d+)$/
+ or die "Illegal linknum: ". $cgi->param('linknum');
+my $linknum = $1;
+
+$cgi->param('link') =~ /^(custnum|invnum)$/
+ or die "Illegal link: ". $cgi->param('link');
+my $link = $1;
+
+my $new = new FS::cust_pay ( {
+ $link => $linknum,
+ map {
+ $_, scalar($cgi->param($_));
+ } qw(paid _date payby payinfo paybatch)
+ #} fields('cust_pay')
+} );
+
+my $error = $new->insert;
+
+if ($error) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). 'cust_pay.cgi?'. $cgi->query_string );
+} elsif ( $link eq 'invnum' ) {
+ print $cgi->redirect(popurl(3). "view/cust_bill.cgi?$linknum");
+} elsif ( $link eq 'custnum' ) {
+ if ( $cgi->param('apply') eq 'yes' ) {
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $linknum })
+ or die "unknown custnum $linknum";
+ $cust_main->apply_payments;
+ }
+ if ( $cgi->param('quickpay') eq 'yes' ) {
+ print $cgi->redirect(popurl(3). "search/cust_main-quickpay.html");
+ } else {
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?$linknum");
+ }
+}
+
+%>
diff --git a/httemplate/edit/process/cust_pkg.cgi b/httemplate/edit/process/cust_pkg.cgi
new file mode 100755
index 0000000..df8471c
--- /dev/null
+++ b/httemplate/edit/process/cust_pkg.cgi
@@ -0,0 +1,43 @@
+<%
+
+my $error = '';
+
+#untaint custnum
+$cgi->param('custnum') =~ /^(\d+)$/;
+my $custnum = $1;
+
+my @remove_pkgnums = map {
+ /^(\d+)$/ or die "Illegal remove_pkg value!";
+ $1;
+} $cgi->param('remove_pkg');
+
+my $error_redirect;
+my @pkgparts;
+if ( $cgi->param('new_pkgpart') =~ /^(\d+)$/ ) { #came from misc/change_pkg.cgi
+ $error_redirect = "misc/change_pkg.cgi";
+ @pkgparts = ($1);
+} else { #came from edit/cust_pkg.cgi
+ $error_redirect = "edit/cust_pkg.cgi";
+ foreach my $pkgpart ( map /^pkg(\d+)$/ ? $1 : (), $cgi->param ) {
+ if ( $cgi->param("pkg$pkgpart") =~ /^(\d+)$/ ) {
+ my $num_pkgs = $1;
+ while ( $num_pkgs-- ) {
+ push @pkgparts,$pkgpart;
+ }
+ } else {
+ $error = "Illegal quantity";
+ last;
+ }
+ }
+}
+
+$error ||= FS::cust_pkg::order($custnum,\@pkgparts,\@remove_pkgnums);
+
+if ($error) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(3). $error_redirect. '?'. $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+}
+
+%>
diff --git a/httemplate/edit/process/cust_refund.cgi b/httemplate/edit/process/cust_refund.cgi
new file mode 100755
index 0000000..7055d8e
--- /dev/null
+++ b/httemplate/edit/process/cust_refund.cgi
@@ -0,0 +1,42 @@
+<%
+
+$cgi->param('custnum') =~ /^(\d*)$/ or die "Illegal custnum!";
+my $custnum = $1;
+my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+ or die "unknown custnum $custnum";
+
+my $error = '';
+if ( $cgi->param('payby') =~ /^(CARD|CHEK)$/ ) {
+ my %payby2bop = (
+ 'CARD' => 'CC',
+ 'CHEK' => 'ECHECK',
+ );
+ my $bop = $payby2bop{$1};
+ $cgi->param('refund') =~ /^(\d*)(\.\d{2})?$/
+ or die "illegal refund amount ". $cgi->param('refund');
+ my $refund = "$1$2";
+ $cgi->param('paynum') =~ /^(\d*)$/ or die "Illegal paynum!";
+ my $paynum = $1;
+ my $reason = $cgi->param('reason');
+ $error = $cust_main->realtime_refund_bop( $bop, 'amount' => $refund,
+ 'paynum' => $paynum,
+ 'reason' => $reason, );
+} else {
+ die 'unimplemented';
+ #my $new = new FS::cust_refund ( {
+ # map {
+ # $_, scalar($cgi->param($_));
+ # } ( fields('cust_refund'), 'paynum' )
+ #} );
+ #$error = $new->insert;
+}
+
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "cust_refund.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+}
+
+%>
diff --git a/httemplate/edit/process/cust_svc.cgi b/httemplate/edit/process/cust_svc.cgi
new file mode 100644
index 0000000..187ede5
--- /dev/null
+++ b/httemplate/edit/process/cust_svc.cgi
@@ -0,0 +1,30 @@
+<%
+
+my $svcnum = $cgi->param('svcnum');
+
+my $old = qsearchs('cust_svc',{'svcnum'=>$svcnum}) if $svcnum;
+
+my $new = new FS::cust_svc ( {
+ map {
+ $_, scalar($cgi->param($_));
+ } fields('cust_svc')
+} );
+
+my $error;
+if ( $svcnum ) {
+ $error=$new->replace($old);
+} else {
+ $error=$new->insert;
+ $svcnum=$new->getfield('svcnum');
+}
+
+if ( $error ) {
+ #$cgi->param('error', $error);
+ #print $cgi->redirect(popurl(2). "cust_svc.cgi?". $cgi->query_string );
+ eidiot($error);
+} else {
+ my $svcdb = $new->part_svc->svcdb;
+ print $cgi->redirect(popurl(3). "view/$svcdb.cgi?$svcnum");
+}
+
+
diff --git a/httemplate/edit/process/domain_record.cgi b/httemplate/edit/process/domain_record.cgi
new file mode 100755
index 0000000..b8c3f62
--- /dev/null
+++ b/httemplate/edit/process/domain_record.cgi
@@ -0,0 +1,34 @@
+<%
+
+my $recnum = $cgi->param('recnum');
+
+my $old = qsearchs('agent',{'recnum'=>$recnum}) if $recnum;
+
+my $new = new FS::domain_record ( {
+ map {
+ $_, scalar($cgi->param($_));
+ } fields('domain_record')
+} );
+
+my $error;
+if ( $recnum ) {
+ $error=$new->replace($old);
+} else {
+ $error=$new->insert;
+ $recnum=$new->getfield('recnum');
+}
+
+if ( $error ) {
+# $cgi->param('error', $error);
+# print $cgi->redirect(popurl(2). "agent.cgi?". $cgi->query_string );
+ #no edit screen to send them back to
+%>
+<!-- mason kludge -->
+<%
+ eidiot($error);
+} else {
+ my $svcnum = $new->svcnum;
+ print $cgi->redirect(popurl(3). "view/svc_domain.cgi?$svcnum");
+}
+
+%>
diff --git a/httemplate/edit/process/generic.cgi b/httemplate/edit/process/generic.cgi
new file mode 100644
index 0000000..9c54feb
--- /dev/null
+++ b/httemplate/edit/process/generic.cgi
@@ -0,0 +1,70 @@
+<%
+
+# Welcome to generic.cgi.
+#
+# This script provides a generic edit/process/ backend for simple table
+# editing. All it knows how to do is take the values entered into
+# the script and insert them into the table specified by $cgi->param('table').
+# If there's an existing record with the same primary key, it will be
+# replaced. (Deletion will be added in the future.)
+#
+# Special cgi params for this script:
+# table: the name of the table to be edited. The script will die horribly
+# if it can't find the table.
+# redirect_ok: URL to be displayed after a successful edit. The value of
+# the record's primary key will be passed as a keyword.
+# Defaults to (freeside root)/view/$table.cgi.
+# redirect_error: URL to be displayed if there's an error. The original
+# query string, plus the error message, will be passed.
+# Defaults to $cgi->referer() (i.e. go back where you
+# came from).
+
+
+use FS::Record qw(qsearchs dbdef);
+use DBIx::DBSchema;
+use DBIx::DBSchema::Table;
+
+
+my $error;
+my $p2 = popurl(2);
+my $p3 = popurl(3);
+my $table = $cgi->param('table');
+my $dbdef = dbdef or die "Cannot fetch dbdef!";
+
+my $dbdef_table = $dbdef->table($table) or die "Cannot fetch schema for $table";
+
+my $pkey = $dbdef_table->primary_key or die "Cannot fetch pkey for $table";
+my $pkey_val = $cgi->param($pkey);
+
+
+#warn "new FS::Record ( $table, (hashref) )";
+my $new = FS::Record::new ( "FS::$table", {
+ map { $_, scalar($cgi->param($_)) } fields($table)
+} );
+
+#warn 'created $new of class '.ref($new);
+
+if($pkey_val and (my $old = qsearchs($table, { $pkey, $pkey_val} ))) {
+ # edit
+ $error = $new->replace($old);
+} else {
+ #add
+ $error = $new->insert;
+ $pkey_val = $new->getfield($pkey);
+ # New records usually don't have their primary keys set until after
+ # they've been checked/inserted, so grab the new $pkey_val so we can
+ # redirect to it.
+}
+
+my $redirect_ok = (($cgi->param('redirect_ok')) ?
+ $cgi->param('redirect_ok') : $p3."browse/generic.cgi?$table");
+my $redirect_error = (($cgi->param('redirect_error')) ?
+ $cgi->param('redirect_error') : $cgi->referer());
+
+if($error) {
+ $cgi->param('error', $error);
+ print $cgi->redirect($redirect_error . '?' . $cgi->query_string);
+} else {
+ print $cgi->redirect($redirect_ok);
+}
+%>
diff --git a/httemplate/edit/process/msgcat.cgi b/httemplate/edit/process/msgcat.cgi
new file mode 100644
index 0000000..1f94f66
--- /dev/null
+++ b/httemplate/edit/process/msgcat.cgi
@@ -0,0 +1,20 @@
+<%
+
+my $error;
+foreach my $param ( grep { /^\d+$/ } $cgi->param ) {
+ my $old = qsearchs('msgcat', { msgnum=>$param } );
+ next if $old->msg eq $cgi->param($param); #no need to update identical records
+ my $new = new FS::msgcat { $old->hash };
+ $new->msg($cgi->param($param));
+ $error = $new->replace($old);
+ last if $error;
+}
+
+if ( $error ) {
+ $cgi->param('error',$error);
+ print $cgi->redirect($p. "msgcat.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "browse/msgcat.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/part_bill_event.cgi b/httemplate/edit/process/part_bill_event.cgi
new file mode 100755
index 0000000..77dcd24
--- /dev/null
+++ b/httemplate/edit/process/part_bill_event.cgi
@@ -0,0 +1,54 @@
+<%
+
+my $eventpart = $cgi->param('eventpart');
+
+my $old = qsearchs('part_bill_event',{'eventpart'=>$eventpart}) if $eventpart;
+
+#s/days/seconds/
+$cgi->param('seconds', int( $cgi->param('days') * 86400 ) );
+
+my $error;
+if ( ! $cgi->param('plan_weight_eventcode') ) {
+ $error = "Must select an action";
+} else {
+
+ $cgi->param('plan_weight_eventcode') =~ /^([\w\-]+):(\d+):(.*)$/s
+ or die "illegal plan_weight_eventcode:".
+ $cgi->param('plan_weight_eventcode');
+ $cgi->param('plan', $1);
+ $cgi->param('weight', $2);
+ my $eventcode = $3;
+ my $plandata = '';
+ while ( $eventcode =~ /%%%(\w+)%%%/ ) {
+ my $field = $1;
+ my $value = join(', ', $cgi->param($field) );
+ $cgi->param($field, $value); #in case it errors out
+ $eventcode =~ s/%%%$field%%%/$value/;
+ $plandata .= "$field $value\n";
+ }
+ $cgi->param('eventcode', $eventcode);
+ $cgi->param('plandata', $plandata);
+
+ my $new = new FS::part_bill_event ( {
+ map {
+ $_, scalar($cgi->param($_));
+ } fields('part_bill_event'),
+ } );
+
+ if ( $eventpart ) {
+ $error = $new->replace($old);
+ } else {
+ $error = $new->insert;
+ $eventpart = $new->getfield('eventpart');
+ }
+}
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "part_bill_event.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3)."browse/part_bill_event.cgi");
+}
+
+%>
+
diff --git a/httemplate/edit/process/part_export.cgi b/httemplate/edit/process/part_export.cgi
new file mode 100644
index 0000000..fa009ed
--- /dev/null
+++ b/httemplate/edit/process/part_export.cgi
@@ -0,0 +1,39 @@
+<%
+
+my $exportnum = $cgi->param('exportnum');
+
+my $old = qsearchs('part_export', { 'exportnum'=>$exportnum } ) if $exportnum;
+
+#fixup options
+#warn join('-', split(',',$cgi->param('options')));
+my %options = map {
+ my $value = $cgi->param($_);
+ $value =~ s/\r\n/\n/g; #browsers? (textarea)
+ $_ => $value;
+} split(',', $cgi->param('options'));
+
+my $new = new FS::part_export ( {
+ map {
+ $_, scalar($cgi->param($_));
+ } fields('part_export')
+} );
+
+my $error;
+if ( $exportnum ) {
+ #warn $old;
+ #warn $exportnum;
+ #warn $new->machine;
+ $error = $new->replace($old,\%options);
+} else {
+ $error = $new->insert(\%options);
+# $exportnum = $new->exportnum;
+}
+
+if ( $error ) {
+ $cgi->param('error', $error );
+ print $cgi->redirect(popurl(2). "part_export.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "browse/part_export.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/part_pkg.cgi b/httemplate/edit/process/part_pkg.cgi
new file mode 100755
index 0000000..7eada7b
--- /dev/null
+++ b/httemplate/edit/process/part_pkg.cgi
@@ -0,0 +1,117 @@
+<%
+
+my $dbh = dbh;
+
+my $pkgpart = $cgi->param('pkgpart');
+
+my $old = qsearchs('part_pkg',{'pkgpart'=>$pkgpart}) if $pkgpart;
+
+#fixup plandata
+my $plandata = $cgi->param('plandata');
+my @plandata = split(',', $plandata);
+$cgi->param('plandata',
+ join('', map { "$_=". join(', ', $cgi->param($_)). "\n" } @plandata )
+);
+
+foreach (qw( setuptax recurtax disabled )) {
+ $cgi->param($_, '') unless defined $cgi->param($_);
+}
+
+my $new = new FS::part_pkg ( {
+ map {
+ $_, scalar($cgi->param($_));
+ } fields('part_pkg')
+} );
+
+#warn "setuptax: ". $new->setuptax;
+#warn "recurtax: ". $new->recurtax;
+
+#most of the stuff below should move to part_pkg.pm
+
+foreach my $part_svc ( qsearch('part_svc', {} ) ) {
+ my $quantity = $cgi->param('pkg_svc'. $part_svc->svcpart) || 0;
+ unless ( $quantity =~ /^(\d+)$/ ) {
+ $cgi->param('error', "Illegal quantity" );
+ print $cgi->redirect(popurl(2). "part_pkg.cgi?". $cgi->query_string );
+ myexit();
+ }
+}
+
+local $SIG{HUP} = 'IGNORE';
+local $SIG{INT} = 'IGNORE';
+local $SIG{QUIT} = 'IGNORE';
+local $SIG{TERM} = 'IGNORE';
+local $SIG{TSTP} = 'IGNORE';
+local $SIG{PIPE} = 'IGNORE';
+
+local $FS::UID::AutoCommit = 0;
+
+my $error;
+if ( $pkgpart ) {
+ $error = $new->replace($old);
+} else {
+ $error = $new->insert;
+ $pkgpart=$new->pkgpart;
+}
+if ( $error ) {
+ $dbh->rollback;
+ $cgi->param('error', $error );
+ print $cgi->redirect(popurl(2). "part_pkg.cgi?". $cgi->query_string );
+ myexit();
+}
+
+foreach my $part_svc (qsearch('part_svc',{})) {
+ my $quantity = $cgi->param('pkg_svc'. $part_svc->svcpart) || 0;
+ my $primary_svc =
+ $cgi->param('pkg_svc_primary') == $part_svc->svcpart ? 'Y' : '';
+ my $old_pkg_svc = qsearchs('pkg_svc', {
+ 'pkgpart' => $pkgpart,
+ 'svcpart' => $part_svc->svcpart,
+ } );
+ my $old_quantity = $old_pkg_svc ? $old_pkg_svc->quantity : 0;
+ my $old_primary_svc =
+ ( $old_pkg_svc && $old_pkg_svc->dbdef_table->column('primary_svc') )
+ ? $old_pkg_svc->primary_svc
+ : '';
+ next unless $old_quantity != $quantity || $old_primary_svc ne $primary_svc;
+
+ my $new_pkg_svc = new FS::pkg_svc( {
+ 'pkgpart' => $pkgpart,
+ 'svcpart' => $part_svc->svcpart,
+ 'quantity' => $quantity,
+ 'primary_svc' => $primary_svc,
+ } );
+ if ( $old_pkg_svc ) {
+ my $myerror = $new_pkg_svc->replace($old_pkg_svc);
+ if ( $myerror ) {
+ $dbh->rollback;
+ die $myerror;
+ }
+ } else {
+ my $myerror = $new_pkg_svc->insert;
+ if ( $myerror ) {
+ $dbh->rollback;
+ die $myerror;
+ }
+ }
+}
+
+unless ( $cgi->param('pkgnum') && $cgi->param('pkgnum') =~ /^(\d+)$/ ) {
+ $dbh->commit or die $dbh->errstr;
+ print $cgi->redirect(popurl(3). "browse/part_pkg.cgi");
+} else {
+ my($old_cust_pkg) = qsearchs( 'cust_pkg', { 'pkgnum' => $1 } );
+ my %hash = $old_cust_pkg->hash;
+ $hash{'pkgpart'} = $pkgpart;
+ my($new_cust_pkg) = new FS::cust_pkg \%hash;
+ my $myerror = $new_cust_pkg->replace($old_cust_pkg);
+ if ( $myerror ) {
+ $dbh->rollback;
+ die "Error modifying cust_pkg record: $myerror\n";
+ }
+
+ $dbh->commit or die $dbh->errstr;
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?". $new_cust_pkg->custnum);
+}
+
+%>
diff --git a/httemplate/edit/process/part_referral.cgi b/httemplate/edit/process/part_referral.cgi
new file mode 100755
index 0000000..fd2c015
--- /dev/null
+++ b/httemplate/edit/process/part_referral.cgi
@@ -0,0 +1,28 @@
+<%
+
+my $refnum = $cgi->param('refnum');
+
+my $new = new FS::part_referral ( {
+ map {
+ $_, scalar($cgi->param($_));
+ } fields('part_referral')
+} );
+
+my $error;
+if ( $refnum ) {
+ my $old = qsearchs( 'part_referral', { 'refnum' =>$ refnum } );
+ die "(Old) Record not found!" unless $old;
+ $error = $new->replace($old);
+} else {
+ $error = $new->insert;
+}
+$refnum=$new->refnum;
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "part_referral.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "browse/part_referral.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/quick-charge.cgi b/httemplate/edit/process/quick-charge.cgi
new file mode 100644
index 0000000..477f585
--- /dev/null
+++ b/httemplate/edit/process/quick-charge.cgi
@@ -0,0 +1,32 @@
+<%
+
+#untaint custnum
+$cgi->param('custnum') =~ /^(\d+)$/
+ or die 'illegal custnum '. $cgi->param('custnum');
+my $custnum = $1;
+
+$cgi->param('amount') =~ /^\s*(\d+(\.\d{1,2})?)\s*$/
+ or die 'illegal amount '. $cgi->param('amount');
+my $amount = $1;
+
+my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+ or die "unknown custnum $custnum";
+
+my $error = $cust_main->charge(
+ $amount,
+ $cgi->param('pkg'),
+ '$'. sprintf("%.2f",$amount),
+ $cgi->param('taxclass')
+);
+
+if ($error) {
+%>
+<!-- mason kludge -->
+<%
+ eidiot($error);
+} else {
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum" );
+}
+
+%>
+
diff --git a/httemplate/edit/process/quick-cust_pkg.cgi b/httemplate/edit/process/quick-cust_pkg.cgi
new file mode 100644
index 0000000..fd9e594
--- /dev/null
+++ b/httemplate/edit/process/quick-cust_pkg.cgi
@@ -0,0 +1,25 @@
+<%
+
+#untaint custnum
+$cgi->param('custnum') =~ /^(\d+)$/
+ or die 'illegal custnum '. $cgi->param('custnum');
+my $custnum = $1;
+$cgi->param('pkgpart') =~ /^(\d+)$/
+ or die 'illegal pkgpart '. $cgi->param('pkgpart');
+my $pkgpart = $1;
+
+my @cust_pkg = ();
+my $error = FS::cust_pkg::order($custnum, [ $pkgpart ], [], \@cust_pkg, );
+
+if ($error) {
+%>
+<!-- mason kludge -->
+<%
+ eidiot($error);
+} else {
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum".
+ "#cust_pkg". $cust_pkg[0]->pkgnum );
+}
+
+%>
+
diff --git a/httemplate/edit/process/router.cgi b/httemplate/edit/process/router.cgi
new file mode 100644
index 0000000..a2fa46d
--- /dev/null
+++ b/httemplate/edit/process/router.cgi
@@ -0,0 +1,67 @@
+<%
+
+local $FS::UID::AutoCommit=0;
+
+sub check {
+ my $error = shift;
+ if($error) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(3) . "edit/router.cgi?". $cgi->query_string);
+ dbh->rollback;
+ exit;
+ }
+}
+
+my $error = '';
+my $routernum = $cgi->param('routernum');
+my $routername = $cgi->param('routername');
+my $old = qsearchs('router', { routernum => $routernum });
+my @old_psr;
+
+my $new = new FS::router {
+ map {
+ ($_, scalar($cgi->param($_)));
+ } fields('router')
+};
+
+if($old) {
+ $error = $new->replace($old);
+} else {
+ $error = $new->insert;
+ $routernum = $new->routernum;
+}
+
+check($error);
+
+if ($old) {
+ @old_psr = $old->part_svc_router;
+ foreach my $psr (@old_psr) {
+ if($cgi->param('svcpart_'.$psr->svcpart) eq 'ON') {
+ # do nothing
+ } else {
+ $error = $psr->delete;
+ }
+ }
+ check($error);
+}
+
+foreach($cgi->param) {
+ if($cgi->param($_) eq 'ON' and /^svcpart_(\d+)$/) {
+ my $svcpart = $1;
+ if(grep {$_->svcpart == $svcpart} @old_psr) {
+ # do nothing
+ } else {
+ my $new_psr = new FS::part_svc_router { svcpart => $svcpart,
+ routernum => $routernum };
+ $error = $new_psr->insert;
+ }
+ check($error);
+ }
+}
+
+
+# Yay, everything worked!
+dbh->commit or die dbh->errstr;
+print $cgi->redirect(popurl(3). "browse/router.cgi");
+
+%>
diff --git a/httemplate/edit/process/svc_acct.cgi b/httemplate/edit/process/svc_acct.cgi
new file mode 100755
index 0000000..950a860
--- /dev/null
+++ b/httemplate/edit/process/svc_acct.cgi
@@ -0,0 +1,49 @@
+<%
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum = $1;
+
+my $old;
+if ( $svcnum ) {
+ $old = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
+ or die "fatal: can't find account (svcnum $svcnum)!";
+} else {
+ $old = '';
+}
+
+#unmunge popnum
+$cgi->param('popnum', (split(/:/, $cgi->param('popnum') ))[0] );
+
+#unmunge passwd
+if ( $cgi->param('_password') eq '*HIDDEN*' ) {
+ die "fatal: no previous account to recall hidden password from!" unless $old;
+ $cgi->param('_password',$old->getfield('_password'));
+}
+
+#unmunge usergroup
+$cgi->param('usergroup', [ $cgi->param('radius_usergroup') ] );
+
+my $new = new FS::svc_acct ( {
+ map {
+ $_, scalar($cgi->param($_));
+ #} qw(svcnum pkgnum svcpart username _password popnum uid gid finger dir
+ # shell quota slipip)
+ } ( fields('svc_acct'), qw( pkgnum svcpart usergroup ) )
+} );
+
+my $error;
+if ( $svcnum ) {
+ $error = $new->replace($old);
+} else {
+ $error = $new->insert;
+ $svcnum = $new->svcnum;
+}
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "svc_acct.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "view/svc_acct.cgi?" . $svcnum );
+}
+
+%>
diff --git a/httemplate/edit/process/svc_acct_pop.cgi b/httemplate/edit/process/svc_acct_pop.cgi
new file mode 100755
index 0000000..46ad74d
--- /dev/null
+++ b/httemplate/edit/process/svc_acct_pop.cgi
@@ -0,0 +1,28 @@
+<%
+
+my $popnum = $cgi->param('popnum');
+
+my $old = qsearchs('svc_acct_pop',{'popnum'=>$popnum}) if $popnum;
+
+my $new = new FS::svc_acct_pop ( {
+ map {
+ $_, scalar($cgi->param($_));
+ } fields('svc_acct_pop')
+} );
+
+my $error = '';
+if ( $popnum ) {
+ $error = $new->replace($old);
+} else {
+ $error = $new->insert;
+ $popnum=$new->getfield('popnum');
+}
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "svc_acct_pop.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "browse/svc_acct_pop.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/svc_broadband.cgi b/httemplate/edit/process/svc_broadband.cgi
new file mode 100644
index 0000000..4912a3a
--- /dev/null
+++ b/httemplate/edit/process/svc_broadband.cgi
@@ -0,0 +1,45 @@
+<%
+
+# If it's stupid but it works, it's not stupid.
+# -- U.S. Army
+
+local $FS::UID::AutoCommit = 0;
+my $dbh = FS::UID::dbh;
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum = $1;
+
+my $old;
+if ( $svcnum ) {
+ $old = qsearchs('svc_broadband', { 'svcnum' => $svcnum } )
+ or die "fatal: can't find broadband service (svcnum $svcnum)!";
+} else {
+ $old = '';
+}
+
+my $new = new FS::svc_broadband ( {
+ map {
+ ($_, scalar($cgi->param($_)));
+ } ( fields('svc_broadband'), qw( pkgnum svcpart ) )
+} );
+
+my $error;
+if ( $svcnum ) {
+ $error = $new->replace($old);
+} else {
+ $error = $new->insert;
+ $svcnum = $new->svcnum;
+}
+
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ $cgi->param('ip_addr', $new->ip_addr);
+ $dbh->rollback;
+ print $cgi->redirect(popurl(2). "svc_broadband.cgi?". $cgi->query_string );
+} else {
+ $dbh->commit or die $dbh->errstr;
+ print $cgi->redirect(popurl(3). "view/svc_broadband.cgi?" . $svcnum );
+}
+
+%>
diff --git a/httemplate/edit/process/svc_domain.cgi b/httemplate/edit/process/svc_domain.cgi
new file mode 100755
index 0000000..19f8eb4
--- /dev/null
+++ b/httemplate/edit/process/svc_domain.cgi
@@ -0,0 +1,31 @@
+<%
+
+#remove this to actually test the domains!
+$FS::svc_domain::whois_hack = 1;
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum = $1;
+
+my $new = new FS::svc_domain ( {
+ map {
+ $_, scalar($cgi->param($_));
+ #} qw(svcnum pkgnum svcpart domain action purpose)
+ } ( fields('svc_domain'), qw( pkgnum svcpart action purpose ) )
+} );
+
+my $error = '';
+if ($cgi->param('svcnum')) {
+ $error="Can't modify a domain!";
+} else {
+ $error=$new->insert;
+ $svcnum=$new->svcnum;
+}
+
+if ($error) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "svc_domain.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "view/svc_domain.cgi?$svcnum");
+}
+
+%>
diff --git a/httemplate/edit/process/svc_external.cgi b/httemplate/edit/process/svc_external.cgi
new file mode 100755
index 0000000..728cd21
--- /dev/null
+++ b/httemplate/edit/process/svc_external.cgi
@@ -0,0 +1,29 @@
+<%
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum =$1;
+
+my $old = qsearchs('svc_external',{'svcnum'=>$svcnum}) if $svcnum;
+
+my $new = new FS::svc_external ( {
+ map {
+ ($_, scalar($cgi->param($_)));
+ } ( fields('svc_external'), qw( pkgnum svcpart ) )
+} );
+
+my $error = '';
+if ( $svcnum ) {
+ $error = $new->replace($old);
+} else {
+ $error = $new->insert;
+ $svcnum = $new->getfield('svcnum');
+}
+
+if ($error) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "svc_external.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "view/svc_external.cgi?$svcnum");
+}
+
+%>
diff --git a/httemplate/edit/process/svc_forward.cgi b/httemplate/edit/process/svc_forward.cgi
new file mode 100755
index 0000000..bb066d8
--- /dev/null
+++ b/httemplate/edit/process/svc_forward.cgi
@@ -0,0 +1,29 @@
+<%
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum =$1;
+
+my $old = qsearchs('svc_forward',{'svcnum'=>$svcnum}) if $svcnum;
+
+my $new = new FS::svc_forward ( {
+ map {
+ ($_, scalar($cgi->param($_)));
+ } ( fields('svc_forward'), qw( pkgnum svcpart ) )
+} );
+
+my $error = '';
+if ( $svcnum ) {
+ $error = $new->replace($old);
+} else {
+ $error = $new->insert;
+ $svcnum = $new->getfield('svcnum');
+}
+
+if ($error) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "svc_forward.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "view/svc_forward.cgi?$svcnum");
+}
+
+%>
diff --git a/httemplate/edit/process/svc_www.cgi b/httemplate/edit/process/svc_www.cgi
new file mode 100644
index 0000000..4091314
--- /dev/null
+++ b/httemplate/edit/process/svc_www.cgi
@@ -0,0 +1,36 @@
+<%
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum = $1;
+
+my $old;
+if ( $svcnum ) {
+ $old = qsearchs('svc_www', { 'svcnum' => $svcnum } )
+ or die "fatal: can't find website (svcnum $svcnum)!";
+} else {
+ $old = '';
+}
+
+my $new = new FS::svc_www ( {
+ map {
+ ($_, scalar($cgi->param($_)));
+ #} qw(svcnum pkgnum svcpart recnum usersvc)
+ } ( fields('svc_www'), qw( pkgnum svcpart ) )
+} );
+
+my $error;
+if ( $svcnum ) {
+ $error = $new->replace($old);
+} else {
+ $error = $new->insert;
+ $svcnum = $new->svcnum;
+}
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "svc_www.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "view/svc_www.cgi?" . $svcnum );
+}
+
+%>
diff --git a/httemplate/edit/router.cgi b/httemplate/edit/router.cgi
new file mode 100755
index 0000000..a573c65
--- /dev/null
+++ b/httemplate/edit/router.cgi
@@ -0,0 +1,77 @@
+<HTML><BODY>
+
+<%
+
+my $router;
+if ( $cgi->keywords ) {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ $router = qsearchs('router', { routernum => $1 })
+ or print $cgi->redirect(popurl(2)."browse/router.cgi") ;
+} else {
+ $router = new FS::router ( {
+ map { $_, scalar($cgi->param($_)) } fields('router')
+ } );
+}
+
+my $routernum = $router->routernum;
+my $action = $routernum ? 'Edit' : 'Add';
+
+print header("$action Router", menubar(
+ 'Main Menu' => "$p",
+ 'View all routers' => "${p}browse/router.cgi",
+));
+
+my $p3 = popurl(3);
+
+if($cgi->param('error')) {
+%> <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+<% } %>
+
+<FORM ACTION="<%=popurl(1)%>process/router.cgi" METHOD=POST>
+ <INPUT TYPE="hidden" NAME="table" VALUE="router">
+ <INPUT TYPE="hidden" NAME="redirect_ok" VALUE="<%=$p3%>/browse/router.cgi">
+ <INPUT TYPE="hidden" NAME="redirect_error" VALUE="<%=$p3%>/edit/router.cgi">
+ <INPUT TYPE="hidden" NAME="routernum" VALUE="<%=$routernum%>">
+ <INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$router->svcnum%>">
+ Router #<%=$routernum or "(NEW)"%>
+
+<BR><BR>Name <INPUT TYPE="text" NAME="routername" SIZE=32 VALUE="<%=$router->routername%>">
+
+<BR><BR>
+Custom fields:
+<BR>
+<%=table() %>
+
+<%
+foreach my $field ($router->virtual_fields) {
+ print $router->pvf($field)->widget('HTML', 'edit',
+ $router->getfield($field));
+}
+%>
+</TABLE>
+
+
+<%
+unless ($router->svcnum) {
+%>
+<BR><BR>Select the service types available on this router<BR>
+<%
+
+ foreach my $part_svc ( qsearch('part_svc', { svcdb => 'svc_broadband',
+ disabled => '' }) ) {
+ %>
+ <BR>
+ <INPUT TYPE="checkbox" NAME="svcpart_<%=$part_svc->svcpart%>"<%=
+ qsearchs('part_svc_router', { svcpart => $part_svc->svcpart,
+ routernum => $routernum } ) ? ' CHECKED' : ''%> VALUE="ON">
+ <A HREF="<%=${p}%>edit/part_svc.cgi?<%=$part_svc->svcpart%>">
+ <%=$part_svc->svcpart%>: <%=$part_svc->svc%></A>
+ <% } %>
+
+<% } %>
+
+ <BR><BR><INPUT TYPE="submit" VALUE="Apply changes">
+ </FORM>
+</BODY></HTML>
+
diff --git a/httemplate/edit/svc_acct.cgi b/httemplate/edit/svc_acct.cgi
new file mode 100755
index 0000000..f1b8b80
--- /dev/null
+++ b/httemplate/edit/svc_acct.cgi
@@ -0,0 +1,301 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+my @shells = $conf->config('shells');
+
+my($svcnum, $pkgnum, $svcpart, $part_svc, $svc_acct, @groups);
+if ( $cgi->param('error') ) {
+ $svc_acct = new FS::svc_acct ( {
+ map { $_, scalar($cgi->param($_)) } fields('svc_acct')
+ } );
+ $svcnum = $svc_acct->svcnum;
+ $pkgnum = $cgi->param('pkgnum');
+ $svcpart = $cgi->param('svcpart');
+ $part_svc = qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
+ die "No part_svc entry for svcpart $svcpart!" unless $part_svc;
+ @groups = $cgi->param('radius_usergroup');
+} else {
+ my($query) = $cgi->keywords;
+ if ( $query =~ /^(\d+)$/ ) { #editing
+ $svcnum=$1;
+ $svc_acct=qsearchs('svc_acct',{'svcnum'=>$svcnum})
+ or die "Unknown (svc_acct) svcnum!";
+
+ my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+ or die "Unknown (cust_svc) svcnum!";
+
+ $pkgnum=$cust_svc->pkgnum;
+ $svcpart=$cust_svc->svcpart;
+
+ $part_svc = qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
+ die "No part_svc entry for svcpart $svcpart!" unless $part_svc;
+
+ @groups = $svc_acct->radius_groups;
+
+ } else { #adding
+
+ foreach $_ (split(/-/,$query)) {
+ $pkgnum=$1 if /^pkgnum(\d+)$/;
+ $svcpart=$1 if /^svcpart(\d+)$/;
+ }
+ $part_svc = qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
+ die "No part_svc entry for svcpart $svcpart!" unless $part_svc;
+
+ $svc_acct = new FS::svc_acct({svcpart => $svcpart});
+
+ $svcnum='';
+
+ #set gecos
+ my($cust_pkg)=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+ if ($cust_pkg) {
+ my($cust_main)=qsearchs('cust_main',{'custnum'=> $cust_pkg->custnum } );
+ unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
+ $svc_acct->setfield('finger',
+ $cust_main->getfield('first') . " " . $cust_main->getfield('last')
+ );
+ }
+ }
+
+ #set fixed and default fields from part_svc
+ foreach my $part_svc_column (
+ grep { $_->columnflag } $part_svc->all_part_svc_column
+ ) {
+ if ( $part_svc_column->columnname eq 'usergroup' ) {
+ @groups = split(',', $part_svc_column->columnvalue);
+ } else {
+ $svc_acct->setfield( $part_svc_column->columnname,
+ $part_svc_column->columnvalue,
+ );
+ }
+ }
+
+ }
+}
+
+#fixed radius groups always override & display
+if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
+ @groups = split(',', $part_svc->part_svc_column('usergroup')->columnvalue);
+}
+
+my $action = $svcnum ? 'Edit' : 'Add';
+
+my $svc = $part_svc->getfield('svc');
+
+my $otaker = getotaker;
+
+my $username = $svc_acct->username;
+my $password;
+if ( $svc_acct->_password ) {
+ if ( $conf->exists('showpasswords') || ! $svcnum ) {
+ $password = $svc_acct->_password;
+ } else {
+ $password = "*HIDDEN*";
+ }
+} else {
+ $password = '';
+}
+
+my $ulen = $conf->config('usernamemax')
+ || $svc_acct->dbdef_table->column('username')->length;
+my $ulen2 = $ulen+2;
+
+my $pmax = $conf->config('passwordmax') || 8;
+my $pmax2 = $pmax+2;
+
+my $p1 = popurl(1);
+print header("$action $svc account");
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT><BR><BR>"
+ if $cgi->param('error');
+
+print 'Service # '. ( $svcnum ? "<B>$svcnum</B>" : " (NEW)" ). '<BR>'.
+ 'Service: <B>'. $part_svc->svc. '</B><BR><BR>'.
+ <<END;
+ <FORM NAME="OneTrueForm" ACTION="${p1}process/svc_acct.cgi" METHOD=POST>
+ <INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">
+ <INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">
+ <INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">
+END
+
+print &ntable("#cccccc",2), <<END;
+<TR><TD ALIGN="right">Username</TD>
+<TD><INPUT TYPE="text" NAME="username" VALUE="$username" SIZE=$ulen2 MAXLENGTH=$ulen></TD></TR>
+<TR><TD ALIGN="right">Password</TD>
+<TD><INPUT TYPE="text" NAME="_password" VALUE="$password" SIZE=$pmax2 MAXLENGTH=$pmax>
+(blank to generate)</TD>
+</TR>
+END
+
+my $sec_phrase = $svc_acct->sec_phrase;
+if ( $conf->exists('security_phrase') ) {
+ print <<END;
+ <TR><TD ALIGN="right">Security phrase</TD>
+ <TD><INPUT TYPE="text" NAME="sec_phrase" VALUE="$sec_phrase" SIZE=32>
+ (for forgotten passwords)</TD>
+ </TD>
+END
+} else {
+ print qq!<INPUT TYPE="hidden" NAME="sec_phrase" VALUE="$sec_phrase">!;
+}
+
+#domain
+my $domsvc = $svc_acct->domsvc || 0;
+if ( $part_svc->part_svc_column('domsvc')->columnflag eq 'F' ) {
+ print qq!<INPUT TYPE="hidden" NAME="domsvc" VALUE="$domsvc">!;
+} else {
+ my %svc_domain = ();
+
+ if ( $domsvc ) {
+ my $svc_domain = qsearchs('svc_domain', { 'svcnum' => $domsvc, } );
+ if ( $svc_domain ) {
+ $svc_domain{$svc_domain->svcnum} = $svc_domain;
+ } else {
+ warn "unknown svc_domain.svcnum for svc_acct.domsvc: $domsvc";
+ }
+ }
+
+ if ( $part_svc->part_svc_column('domsvc')->columnflag eq 'D' ) {
+ my $svc_domain = qsearchs('svc_domain', {
+ 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue,
+ } );
+ if ( $svc_domain ) {
+ $svc_domain{$svc_domain->svcnum} = $svc_domain;
+ } else {
+ warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
+ $part_svc->part_svc_column('domsvc')->columnvalue;
+ }
+ }
+
+ my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $pkgnum } );
+ if ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
+ my @cust_svc =
+ map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
+ qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum } );
+ foreach my $cust_svc ( @cust_svc ) {
+ my $svc_domain =
+ qsearchs('svc_domain', { 'svcnum' => $cust_svc->svcnum } );
+ $svc_domain{$svc_domain->svcnum} = $svc_domain if $svc_domain;
+ }
+ } else {
+ %svc_domain = map { $_->svcnum => $_ } qsearch('svc_domain', {} );
+ }
+ print qq!<TR><TD ALIGN="right">Domain</TD>!.
+ qq!<TD><SELECT NAME="domsvc" SIZE=1>\n!;
+ foreach my $svcnum (
+ sort { $svc_domain{$a}->domain cmp $svc_domain{$b}->domain }
+ keys %svc_domain
+ ) {
+ my $svc_domain = $svc_domain{$svcnum};
+ print qq!<OPTION VALUE="!. $svc_domain->svcnum. qq!"!.
+ ( $svc_domain->svcnum == $domsvc ? ' SELECTED' : '' ).
+ '>'. $svc_domain->domain. "\n" ;
+ }
+ print "</SELECT></TD></TR>";
+}
+
+#pop
+my $popnum = $svc_acct->popnum || 0;
+if ( $part_svc->part_svc_column('popnum')->columnflag eq "F" ) {
+ print qq!<INPUT TYPE="hidden" NAME="popnum" VALUE="$popnum">!;
+} else {
+ print qq!<TR><TD ALIGN="right">Access number</TD>!.
+ qq!<TD>!. FS::svc_acct_pop::popselector($popnum). '</TD></TR>';
+}
+
+my($uid,$gid,$finger,$dir)=(
+ $svc_acct->uid,
+ $svc_acct->gid,
+ $svc_acct->finger,
+ $svc_acct->dir,
+);
+
+print <<END;
+<INPUT TYPE="hidden" NAME="uid" VALUE="$uid">
+<INPUT TYPE="hidden" NAME="gid" VALUE="$gid">
+END
+
+if ( !$finger && $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
+ print '<INPUT TYPE="hidden" NAME="finger" VALUE="">';
+} else {
+ print '<TR><TD ALIGN="right">GECOS</TD>'.
+ qq!<TD><INPUT TYPE="text" NAME="finger" VALUE="$finger"></TD></TR>!;
+}
+print qq!<INPUT TYPE="hidden" NAME="dir" VALUE="$dir">!;
+
+my $shell = $svc_acct->shell;
+if ( $part_svc->part_svc_column('shell')->columnflag eq "F"
+ || ( !$shell && $part_svc->part_svc_column('uid')->columnflag eq 'F' )
+ ) {
+ print qq!<INPUT TYPE="hidden" NAME="shell" VALUE="$shell">!;
+} else {
+ print qq!<TR><TD ALIGN="right">Shell</TD><TD><SELECT NAME="shell" SIZE=1>!;
+ my($etc_shell);
+ foreach $etc_shell (@shells) {
+ print "<OPTION", $etc_shell eq $shell ? ' SELECTED' : '', ">",
+ $etc_shell, "\n";
+ }
+ print "</SELECT></TD></TR>";
+}
+
+my($quota,$slipip)=(
+ $svc_acct->quota,
+ $svc_acct->slipip,
+);
+
+if ( $part_svc->part_svc_column('quota')->columnflag eq "F" )
+{
+ print qq!<INPUT TYPE="hidden" NAME="quota" VALUE="$quota">!;
+} else {
+ print <<END;
+ <TR><TD ALIGN="right">Quota:</TD>
+ <TD> <INPUT TYPE="text" NAME="quota" VALUE="$quota" ></TD>
+ </TR>
+END
+}
+
+if ( $part_svc->part_svc_column('slipip')->columnflag eq "F" ) {
+ print qq!<INPUT TYPE="hidden" NAME="slipip" VALUE="$slipip">!;
+} else {
+ print qq!<TR><TD ALIGN="right">IP</TD><TD><INPUT TYPE="text" NAME="slipip" VALUE="$slipip"></TD></TR>!;
+}
+
+foreach my $r ( grep { /^r(adius|[cr])_/ } fields('svc_acct') ) {
+ $r =~ /^^r(adius|[cr])_(.+)$/ or next; #?
+ my $a = $2;
+ if ( $part_svc->part_svc_column($r)->columnflag eq 'F' ) {
+ print qq!<INPUT TYPE="hidden" NAME="$r" VALUE="!.
+ $svc_acct->getfield($r). '">';
+ } else {
+ print qq!<TR><TD ALIGN="right">$FS::raddb::attrib{$a}</TD><TD><INPUT TYPE="text" NAME="$r" VALUE="!.
+ $svc_acct->getfield($r). '"></TD></TR>';
+ }
+}
+
+print '<TR><TD ALIGN="right">RADIUS groups</TD>';
+if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
+ print '<TD BGCOLOR="#ffffff">'. join('<BR>', @groups);
+} else {
+ print '<TD>'. &FS::svc_acct::radius_usergroup_selector( \@groups );
+}
+print '</TD></TR>';
+
+foreach my $field ($svc_acct->virtual_fields) {
+ if ( $part_svc->part_svc_column($field)->columnflag ne 'F' ) {
+ # If the flag is X, it won't even show up in $svc_acct->virtual_fields.
+ print $svc_acct->pvf($field)->widget('HTML', 'edit',
+ $svc_acct->getfield($field));
+ }
+}
+
+#submit
+print qq!</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">!;
+
+print <<END;
+ </FORM>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/svc_acct_pop.cgi b/httemplate/edit/svc_acct_pop.cgi
new file mode 100755
index 0000000..399502a
--- /dev/null
+++ b/httemplate/edit/svc_acct_pop.cgi
@@ -0,0 +1,56 @@
+<!-- mason kludge -->
+<%
+
+my $svc_acct_pop;
+if ( $cgi->param('error') ) {
+ $svc_acct_pop = new FS::svc_acct_pop ( {
+ map { $_, scalar($cgi->param($_)) } fields('svc_acct_pop')
+ } );
+} elsif ( $cgi->keywords ) { #editing
+ my($query)=$cgi->keywords;
+ $query =~ /^(\d+)$/;
+ $svc_acct_pop=qsearchs('svc_acct_pop',{'popnum'=>$1});
+} else { #adding
+ $svc_acct_pop = new FS::svc_acct_pop {};
+}
+my $action = $svc_acct_pop->popnum ? 'Edit' : 'Add';
+my $hashref = $svc_acct_pop->hashref;
+
+my $p1 = popurl(1);
+print header("$action Access Number", menubar(
+ 'Main Menu' => popurl(2),
+ 'View all Access Numbers' => popurl(2). "browse/svc_acct_pop.cgi",
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/svc_acct_pop.cgi" METHOD=POST>!;
+
+#display
+
+print qq!<INPUT TYPE="hidden" NAME="popnum" VALUE="$hashref->{popnum}">!,
+ "POP #", $hashref->{popnum} ? $hashref->{popnum} : "(NEW)";
+
+print <<END;
+<PRE>
+City <INPUT TYPE="text" NAME="city" SIZE=32 VALUE="$hashref->{city}">
+State <INPUT TYPE="text" NAME="state" SIZE=16 MAXLENGTH=16 VALUE="$hashref->{state}">
+Area Code <INPUT TYPE="text" NAME="ac" SIZE=4 MAXLENGTH=3 VALUE="$hashref->{ac}">
+Exchange <INPUT TYPE="text" NAME="exch" SIZE=4 MAXLENGTH=3 VALUE="$hashref->{exch}">
+Local <INPUT TYPE="text" NAME="loc" SIZE=5 MAXLENGTH=4 VALUE="$hashref->{loc}">
+</PRE>
+END
+
+print qq!<BR><INPUT TYPE="submit" VALUE="!,
+ $hashref->{popnum} ? "Apply changes" : "Add Access Number",
+ qq!">!;
+
+print <<END;
+ </FORM>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/svc_broadband.cgi b/httemplate/edit/svc_broadband.cgi
new file mode 100644
index 0000000..9e064c5
--- /dev/null
+++ b/httemplate/edit/svc_broadband.cgi
@@ -0,0 +1,175 @@
+<!-- mason kludge -->
+<%
+
+# If it's stupid but it works, it's still stupid.
+# -Kristian
+
+
+use HTML::Widgets::SelectLayers;
+use Tie::IxHash;
+
+my( $svcnum, $pkgnum, $svcpart, $part_svc, $svc_broadband );
+if ( $cgi->param('error') ) {
+ $svc_broadband = new FS::svc_broadband ( {
+ map { $_, scalar($cgi->param($_)) } fields('svc_broadband'), qw(svcpart)
+ } );
+ $svcnum = $svc_broadband->svcnum;
+ $pkgnum = $cgi->param('pkgnum');
+ $svcpart = $svc_broadband->svcpart;
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+} else {
+ my($query) = $cgi->keywords;
+ if ( $query =~ /^(\d+)$/ ) { #editing
+ $svcnum=$1;
+ $svc_broadband=qsearchs('svc_broadband',{'svcnum'=>$svcnum})
+ or die "Unknown (svc_broadband) svcnum!";
+
+ my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+ or die "Unknown (cust_svc) svcnum!";
+
+ $pkgnum=$cust_svc->pkgnum;
+ $svcpart=$cust_svc->svcpart;
+
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+
+ } else { #adding
+
+ foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
+ $pkgnum=$1 if /^pkgnum(\d+)$/;
+ $svcpart=$1 if /^svcpart(\d+)$/;
+ }
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+
+ $svc_broadband = new FS::svc_broadband({ svcpart => $svcpart });
+
+ $svcnum='';
+
+ #set fixed and default fields from part_svc
+ foreach my $part_svc_column (
+ grep { $_->columnflag } $part_svc->all_part_svc_column
+ ) {
+ $svc_broadband->setfield( $part_svc_column->columnname,
+ $part_svc_column->columnvalue,
+ );
+ }
+
+ }
+}
+my $action = $svc_broadband->svcnum ? 'Edit' : 'Add';
+
+if ($pkgnum) {
+
+ #Nothing?
+
+} elsif ( $action eq 'Edit' ) {
+
+ #Nothing?
+
+} else {
+ die "\$action eq Add, but \$pkgnum is null!\n";
+}
+
+my $p1 = popurl(1);
+
+my ($ip_addr, $speed_up, $speed_down, $blocknum) =
+ ($svc_broadband->ip_addr,
+ $svc_broadband->speed_up,
+ $svc_broadband->speed_down,
+ $svc_broadband->blocknum);
+
+%>
+
+<%=header("Broadband Service $action", '')%>
+
+<% if ($cgi->param('error')) { %>
+<FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT><BR>
+<% } %>
+
+Service #<B><%=$svcnum ? $svcnum : "(NEW)"%></B><BR><BR>
+
+<FORM ACTION="<%=${p1}%>process/svc_broadband.cgi" METHOD=POST>
+ <INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$svcnum%>">
+ <INPUT TYPE="hidden" NAME="pkgnum" VALUE="<%=$pkgnum%>">
+ <INPUT TYPE="hidden" NAME="svcpart" VALUE="<%=$svcpart%>">
+
+ <%=&ntable("#cccccc",2)%>
+ <TR>
+ <TD ALIGN="right">IP Address</TD>
+ <TD BGCOLOR="#ffffff">
+<% if ( $part_svc->part_svc_column('ip_addr')->columnflag eq 'F' ) { %>
+ <INPUT TYPE="hidden" NAME="ip_addr" VALUE="<%=$ip_addr%>"><%=$ip_addr%>
+<% } else { %>
+ <INPUT TYPE="text" NAME="ip_addr" VALUE="<%=$ip_addr%>">
+<% } %>
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Download speed</TD>
+ <TD BGCOLOR="#ffffff">
+<% if ( $part_svc->part_svc_column('speed_down')->columnflag eq 'F' ) { %>
+ <INPUT TYPE="hidden" NAME="speed_down" VALUE="<%=$speed_down%>"><%=$speed_down%>Kbps
+<% } else { %>
+ <INPUT TYPE="text" NAME="speed_down" SIZE=5 VALUE="<%=$speed_down%>">Kbps
+<% } %>
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Upload speed</TD>
+ <TD BGCOLOR="#ffffff">
+<% if ( $part_svc->part_svc_column('speed_up')->columnflag eq 'F' ) { %>
+ <INPUT TYPE="hidden" NAME="speed_up" VALUE="<%=$speed_up%>"><%=$speed_up%>Kbps
+<% } else { %>
+ <INPUT TYPE="text" NAME="speed_up" SIZE=5 VALUE="<%=$speed_up%>">Kbps
+<% } %>
+ </TD>
+ </TR>
+<% if ($action eq 'Add') { %>
+ <TR>
+ <TD ALIGN="right">Router/Block</TD>
+ <TD BGCOLOR="#ffffff">
+ <SELECT NAME="blocknum">
+<%
+ warn $svc_broadband->svcpart;
+ foreach my $router ($svc_broadband->allowed_routers) {
+ warn $router->routername;
+ foreach my $addr_block ($router->addr_block) {
+%>
+ <OPTION VALUE="<%=$addr_block->blocknum%>"<%=($addr_block->blocknum eq $blocknum) ? ' SELECTED' : ''%>>
+ <%=$router->routername%>:<%=$addr_block->ip_gateway%>/<%=$addr_block->ip_netmask%></OPTION>
+<%
+ }
+ }
+%>
+ </SELECT>
+ </TD>
+ </TR>
+<% } else { %>
+
+ <TR>
+ <TD ALIGN="right">Router/Block</TD>
+ <TD BGCOLOR="#ffffff">
+ <%=$svc_broadband->addr_block->router->routername%>:<%=$svc_broadband->addr_block->NetAddr%>
+ <INPUT TYPE="hidden" NAME="blocknum" VALUE="<%=$svc_broadband->blocknum%>">
+ </TD>
+ </TR>
+
+<% } %>
+
+<%
+foreach my $field ($svc_broadband->virtual_fields) {
+ if ( $part_svc->part_svc_column($field)->columnflag ne 'F' &&
+ $part_svc->part_svc_column($field)->columnflag ne 'X') {
+ print $svc_broadband->pvf($field)->widget('HTML', 'edit',
+ $svc_broadband->getfield($field));
+ }
+} %>
+ </TABLE>
+ <BR>
+ <INPUT TYPE="submit" NAME="submit" VALUE="Submit">
+</FORM>
+</BODY>
+</HTML>
+
diff --git a/httemplate/edit/svc_domain.cgi b/httemplate/edit/svc_domain.cgi
new file mode 100755
index 0000000..ca0e339
--- /dev/null
+++ b/httemplate/edit/svc_domain.cgi
@@ -0,0 +1,98 @@
+<!-- mason kludge -->
+<%
+
+my($svcnum, $pkgnum, $svcpart, $kludge_action, $purpose, $part_svc,
+ $svc_domain);
+if ( $cgi->param('error') ) {
+ $svc_domain = new FS::svc_domain ( {
+ map { $_, scalar($cgi->param($_)) } fields('svc_domain')
+ } );
+ $svcnum = $svc_domain->svcnum;
+ $pkgnum = $cgi->param('pkgnum');
+ $svcpart = $cgi->param('svcpart');
+ $kludge_action = $cgi->param('action');
+ $purpose = $cgi->param('purpose');
+ $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
+ die "No part_svc entry!" unless $part_svc;
+} else {
+ $kludge_action = '';
+ $purpose = '';
+ my($query) = $cgi->keywords;
+ if ( $query =~ /^(\d+)$/ ) { #editing
+ $svcnum=$1;
+ $svc_domain=qsearchs('svc_domain',{'svcnum'=>$svcnum})
+ or die "Unknown (svc_domain) svcnum!";
+
+ my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+ or die "Unknown (cust_svc) svcnum!";
+
+ $pkgnum=$cust_svc->pkgnum;
+ $svcpart=$cust_svc->svcpart;
+
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+
+ } else { #adding
+
+ $svc_domain = new FS::svc_domain({});
+
+ foreach $_ (split(/-/,$query)) {
+ $pkgnum=$1 if /^pkgnum(\d+)$/;
+ $svcpart=$1 if /^svcpart(\d+)$/;
+ }
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+
+ $svcnum='';
+
+ #set fixed and default fields from part_svc
+ foreach my $part_svc_column (
+ grep { $_->columnflag } $part_svc->all_part_svc_column
+ ) {
+ $svc_domain->setfield( $part_svc_column->columnname,
+ $part_svc_column->columnvalue,
+ );
+ }
+
+ }
+
+}
+my $action = $svcnum ? 'Edit' : 'Add';
+
+my $svc = $part_svc->getfield('svc');
+
+my $otaker = getotaker;
+
+my $domain = $svc_domain->domain;
+
+my $p1 = popurl(1);
+print header("$action $svc", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+print <<END;
+ <FORM ACTION="${p1}process/svc_domain.cgi" METHOD=POST>
+ <INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">
+ <INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">
+ <INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">
+END
+
+print qq!<INPUT TYPE="radio" NAME="action" VALUE="N"!;
+print ' CHECKED' if $kludge_action eq 'N';
+print qq!>New!;
+print qq!<BR><INPUT TYPE="radio" NAME="action" VALUE="M"!;
+print ' CHECKED' if $kludge_action eq 'M';
+print qq!>Transfer!;
+
+print <<END;
+<P>Domain <INPUT TYPE="text" NAME="domain" VALUE="$domain" SIZE=28 MAXLENGTH=63>
+<BR>Purpose/Description: <INPUT TYPE="text" NAME="purpose" VALUE="$purpose" SIZE=64>
+<P><INPUT TYPE="submit" VALUE="Submit">
+ </FORM>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/svc_external.cgi b/httemplate/edit/svc_external.cgi
new file mode 100644
index 0000000..bcfc85e
--- /dev/null
+++ b/httemplate/edit/svc_external.cgi
@@ -0,0 +1,105 @@
+<!-- mason kludge -->
+<%
+
+my( $svcnum, $pkgnum, $svcpart, $part_svc, $svc_external );
+if ( $cgi->param('error') ) {
+ $svc_external = new FS::svc_external ( {
+ map { $_, scalar($cgi->param($_)) } fields('svc_external')
+ } );
+ $svcnum = $svc_external->svcnum;
+ $pkgnum = $cgi->param('pkgnum');
+ $svcpart = $cgi->param('svcpart');
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+} else {
+ my($query) = $cgi->keywords;
+ if ( $query =~ /^(\d+)$/ ) { #editing
+ $svcnum=$1;
+ $svc_external=qsearchs('svc_external',{'svcnum'=>$svcnum})
+ or die "Unknown (svc_external) svcnum!";
+
+ my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+ or die "Unknown (cust_svc) svcnum!";
+
+ $pkgnum=$cust_svc->pkgnum;
+ $svcpart=$cust_svc->svcpart;
+
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+
+ } else { #adding
+
+ foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
+ $pkgnum=$1 if /^pkgnum(\d+)$/;
+ $svcpart=$1 if /^svcpart(\d+)$/;
+ }
+ $svc_external = new FS::svc_external { svcpart => $svcpart };
+
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+
+ $svcnum='';
+
+ #set fixed and default fields from part_svc
+ foreach my $part_svc_column (
+ grep { $_->columnflag } $part_svc->all_part_svc_column
+ ) {
+ $svc_external->setfield( $part_svc_column->columnname,
+ $part_svc_column->columnvalue,
+ );
+ }
+
+ }
+}
+my $action = $svc_external->svcnum ? 'Edit' : 'Add';
+
+my $p1 = popurl(1);
+print header("External service $action", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/svc_external.cgi" METHOD=POST>!;
+
+#display
+
+
+#svcnum
+print qq!<INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">!;
+print qq!Service #<B>!, $svcnum ? $svcnum : "(NEW)", "</B><BR><BR>";
+
+#pkgnum
+print qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!;
+
+#svcpart
+print qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!;
+
+my($id,$title)=(
+ $svc_external->id,
+ $svc_external->title,
+);
+
+print &ntable("#cccccc",2),
+ '<TR><TD ALIGN="right">External ID</TD><TD>'.
+ qq!<INPUT TYPE="text" NAME="id" VALUE="$id">!.
+ '</TD></TR>'.
+ '<TR><TD ALIGN="right">Title</TD><TD>'.
+ qq!<INPUT TYPE="text" NAME="title" VALUE="$title">!.
+ '</TD></TR>';
+
+foreach my $field ($svc_external->virtual_fields) {
+ if ( $part_svc->part_svc_column($field)->columnflag ne 'F' ) {
+ # If the flag is X, it won't even show up in $svc_acct->virtual_fields.
+ print $svc_external->pvf($field)->widget('HTML', 'edit',
+ $svc_external->getfield($field));
+ }
+}
+
+%>
+
+</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">
+ </FORM>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/edit/svc_forward.cgi b/httemplate/edit/svc_forward.cgi
new file mode 100755
index 0000000..2b9d35a
--- /dev/null
+++ b/httemplate/edit/svc_forward.cgi
@@ -0,0 +1,177 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my($svcnum, $pkgnum, $svcpart, $part_svc, $svc_forward);
+if ( $cgi->param('error') ) {
+ $svc_forward = new FS::svc_forward ( {
+ map { $_, scalar($cgi->param($_)) } fields('svc_forward')
+ } );
+ $svcnum = $svc_forward->svcnum;
+ $pkgnum = $cgi->param('pkgnum');
+ $svcpart = $cgi->param('svcpart');
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+} else {
+
+ my($query) = $cgi->keywords;
+
+ if ( $query =~ /^(\d+)$/ ) { #editing
+ $svcnum=$1;
+ $svc_forward=qsearchs('svc_forward',{'svcnum'=>$svcnum})
+ or die "Unknown (svc_forward) svcnum!";
+
+ my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+ or die "Unknown (cust_svc) svcnum!";
+
+ $pkgnum=$cust_svc->pkgnum;
+ $svcpart=$cust_svc->svcpart;
+
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+
+ } else { #adding
+
+ $svc_forward = new FS::svc_forward({});
+
+ foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
+ $pkgnum=$1 if /^pkgnum(\d+)$/;
+ $svcpart=$1 if /^svcpart(\d+)$/;
+ }
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+
+ $svcnum='';
+
+ #set fixed and default fields from part_svc
+ foreach my $part_svc_column (
+ grep { $_->columnflag } $part_svc->all_part_svc_column
+ ) {
+ $svc_forward->setfield( $part_svc_column->columnname,
+ $part_svc_column->columnvalue,
+ );
+ }
+ }
+
+}
+my $action = $svc_forward->svcnum ? 'Edit' : 'Add';
+
+my %email;
+
+#starting with those currently attached
+foreach my $method (qw( srcsvc_acct dstsvc_acct )) {
+ my $svc_acct = $svc_forward->$method();
+ $email{$svc_acct->svcnum} = $svc_acct->email if $svc_acct;
+}
+
+if ($pkgnum) {
+
+ #find all possible user svcnums (and emails)
+
+ #and including the rest for this customer
+ my($u_part_svc,@u_acct_svcparts);
+ foreach $u_part_svc ( qsearch('part_svc',{'svcdb'=>'svc_acct'}) ) {
+ push @u_acct_svcparts,$u_part_svc->getfield('svcpart');
+ }
+
+ my($cust_pkg)=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+ my($custnum)=$cust_pkg->getfield('custnum');
+ my($i_cust_pkg);
+ foreach $i_cust_pkg ( qsearch('cust_pkg',{'custnum'=>$custnum}) ) {
+ my($cust_pkgnum)=$i_cust_pkg->getfield('pkgnum');
+ my($acct_svcpart);
+ foreach $acct_svcpart (@u_acct_svcparts) { #now find the corresponding
+ #record(s) in cust_svc ( for this
+ #pkgnum ! )
+ foreach my $i_cust_svc (
+ qsearch( 'cust_svc', { 'pkgnum' => $cust_pkgnum,
+ 'svcpart' => $acct_svcpart } )
+ ) {
+ my $svc_acct =
+ qsearchs( 'svc_acct', { 'svcnum' => $i_cust_svc->svcnum } );
+ $email{$svc_acct->svcnum} = $svc_acct->email;
+ }
+ }
+ }
+
+} elsif ( $action eq 'Add' ) {
+ die "\$action eq Add, but \$pkgnum is null!\n";
+}
+
+my($srcsvc,$dstsvc,$dst)=(
+ $svc_forward->srcsvc,
+ $svc_forward->dstsvc,
+ $svc_forward->dst,
+);
+my $src = $svc_forward->dbdef_table->column('src') ? $svc_forward->src : '';
+
+#display
+
+%>
+
+<%= header("Mail Forward $action") %>
+
+<% if ( $cgi->param('error') ) { %>
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <%= $cgi->param('error') %></FONT>
+ <BR><BR>
+<% } %>
+
+Service #<%= $svcnum ? "<B>$svcnum</B>" : " (NEW)" %><BR>
+Service: <B><%= $part_svc->svc %></B><BR><BR>
+
+<FORM ACTION="process/svc_forward.cgi" METHOD="POST">
+<INPUT TYPE="hidden" NAME="svcnum" VALUE="<%= $svcnum %>">
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<%= $pkgnum %>">
+<INPUT TYPE="hidden" NAME="svcpart" VALUE="<%= $svcpart %>">
+
+<SCRIPT TYPE="text/javascript">
+function srcchanged(what) {
+ if ( what.options[what.selectedIndex].value == 0 ) {
+ what.form.src.disabled = false;
+ what.form.src.style.backgroundColor = "white";
+ } else {
+ what.form.src.disabled = true;
+ what.form.src.style.backgroundColor = "lightgrey";
+ }
+}
+function dstchanged(what) {
+ if ( what.options[what.selectedIndex].value == 0 ) {
+ what.form.dst.disabled = false;
+ what.form.dst.style.backgroundColor = "white";
+ } else {
+ what.form.dst.disabled = true;
+ what.form.dst.style.backgroundColor = "lightgrey";
+ }
+}
+</SCRIPT>
+
+<%= ntable("#cccccc",2) %>
+<TR><TD ALIGN="right">Email to</TD>
+<TD><SELECT NAME="srcsvc" SIZE=1 onChange="srcchanged(this)">
+<% foreach $_ (keys %email) { %>
+ <OPTION<%= $_ eq $srcsvc ? " SELECTED" : "" %> VALUE="<%= $_ %>"><%= $email{$_} %></OPTION>
+<% } %>
+<% if ( $svc_forward->dbdef_table->column('src') ) { %>
+ <OPTION <%= $src ? 'SELECTED' : '' %> VALUE="0">(other email address)</OPTION>
+<% } %>
+</SELECT>
+<% if ( $svc_forward->dbdef_table->column('src') ) { %>
+<INPUT TYPE="text" NAME="src" VALUE="<%= $src %>" <%= ( $src || !scalar(%email) ) ? '' : 'DISABLED STYLE="background-color: lightgrey"' %>>
+<% } %>
+</TD></TR>
+
+<TR><TD ALIGN="right">Forwards to</TD>
+<TD><SELECT NAME="dstsvc" SIZE=1 onChange="dstchanged(this)">
+<% foreach $_ (keys %email) { %>
+ <OPTION<%= $_ eq $dstsvc ? " SELECTED" : "" %> VALUE="<%= $_ %>"><%= $email{$_} %></OPTION>
+<% } %>
+<OPTION <%= $dst ? 'SELECTED' : '' %> VALUE="0">(other email address)</OPTION>
+</SELECT>
+<INPUT TYPE="text" NAME="dst" VALUE="<%= $dst %>" <%= ( $dst || !scalar(%email) ) ? '' : 'DISABLED STYLE="background-color: lightgrey"' %>>
+</TD></TR>
+ </TABLE>
+<BR><INPUT TYPE="submit" VALUE="Submit">
+</FORM>
+ </BODY>
+</HTML>
diff --git a/httemplate/edit/svc_www.cgi b/httemplate/edit/svc_www.cgi
new file mode 100644
index 0000000..4989bb6
--- /dev/null
+++ b/httemplate/edit/svc_www.cgi
@@ -0,0 +1,221 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my( $svcnum, $pkgnum, $svcpart, $part_svc, $svc_www );
+if ( $cgi->param('error') ) {
+ $svc_www = new FS::svc_www ( {
+ map { $_, scalar($cgi->param($_)) } fields('svc_www')
+ } );
+ $svcnum = $svc_www->svcnum;
+ $pkgnum = $cgi->param('pkgnum');
+ $svcpart = $cgi->param('svcpart');
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+} else {
+ my($query) = $cgi->keywords;
+ if ( $query =~ /^(\d+)$/ ) { #editing
+ $svcnum=$1;
+ $svc_www=qsearchs('svc_www',{'svcnum'=>$svcnum})
+ or die "Unknown (svc_www) svcnum!";
+
+ my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+ or die "Unknown (cust_svc) svcnum!";
+
+ $pkgnum=$cust_svc->pkgnum;
+ $svcpart=$cust_svc->svcpart;
+
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+
+ } else { #adding
+
+ foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
+ $pkgnum=$1 if /^pkgnum(\d+)$/;
+ $svcpart=$1 if /^svcpart(\d+)$/;
+ }
+ $svc_www = new FS::svc_www { svcpart => $svcpart };
+
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+
+ $svcnum='';
+
+ #set fixed and default fields from part_svc
+ foreach my $part_svc_column (
+ grep { $_->columnflag } $part_svc->all_part_svc_column
+ ) {
+ $svc_www->setfield( $part_svc_column->columnname,
+ $part_svc_column->columnvalue,
+ );
+ }
+
+ }
+}
+my $action = $svc_www->svcnum ? 'Edit' : 'Add';
+
+my( %svc_acct, %arec );
+if ($pkgnum) {
+
+ my @u_acct_svcparts;
+ foreach my $svcpart (
+ map { $_->svcpart } qsearch( 'part_svc', { 'svcdb' => 'svc_acct' } )
+ ) {
+ next if $conf->exists('svc_www-usersvc_svcpart')
+ && ! grep { $svcpart == $_ }
+ $conf->config('svc_www-usersvc_svcpart');
+ push @u_acct_svcparts, $svcpart;
+ }
+
+ my($cust_pkg)=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+ my($custnum)=$cust_pkg->getfield('custnum');
+ my($i_cust_pkg);
+ foreach $i_cust_pkg ( qsearch('cust_pkg',{'custnum'=>$custnum}) ) {
+ my($cust_pkgnum)=$i_cust_pkg->getfield('pkgnum');
+ my($acct_svcpart);
+ foreach $acct_svcpart (@u_acct_svcparts) { #now find the corresponding
+ #record(s) in cust_svc ( for this
+ #pkgnum ! )
+ my($i_cust_svc);
+ foreach $i_cust_svc ( qsearch('cust_svc',{'pkgnum'=>$cust_pkgnum,'svcpart'=>$acct_svcpart}) ) {
+ my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$i_cust_svc->getfield('svcnum')});
+ $svc_acct{$svc_acct->getfield('svcnum')}=
+ $svc_acct->cust_svc->part_svc->svc. ': '. $svc_acct->email;
+ }
+ }
+ }
+
+
+ my($d_part_svc,@d_acct_svcparts);
+ foreach $d_part_svc ( qsearch('part_svc',{'svcdb'=>'svc_domain'}) ) {
+ push @d_acct_svcparts,$d_part_svc->getfield('svcpart');
+ }
+
+ foreach $i_cust_pkg ( qsearch( 'cust_pkg', { 'custnum' => $custnum } ) ) {
+ my $cust_pkgnum = $i_cust_pkg->pkgnum;
+
+ foreach my $acct_svcpart (@d_acct_svcparts) {
+
+ foreach my $i_cust_svc (
+ qsearch( 'cust_svc', { 'pkgnum' => $cust_pkgnum,
+ 'svcpart' => $acct_svcpart } )
+ ) {
+ my $svc_domain =
+ qsearchs( 'svc_domain', { 'svcnum' => $i_cust_svc->svcnum } );
+
+ my $extra_sql = "AND ( rectype = 'A' OR rectype = 'CNAME' )";
+ unless ( $conf->exists('svc_www-enable_subdomains') ) {
+ $extra_sql .= " AND ( reczone = '\@' OR reczone = '".
+ $svc_domain->domain. ".' )";
+ }
+
+ foreach my $domain_rec (
+ qsearch( 'domain_record',
+ {
+ 'svcnum' => $svc_domain->svcnum,
+ },
+ '',
+ $extra_sql,
+ )
+ ) {
+ $arec{$domain_rec->recnum} = $domain_rec->zone;
+ }
+
+ if ( $conf->exists('svc_www-enable_subdomains') ) {
+ $arec{'www.'. $svc_domain->domain} = 'www.'. $svc_domain->domain
+ unless qsearchs( 'domain_record', {
+ svcnum => $svc_domain->svcnum,
+ reczone => 'www',
+ } )
+ || qsearchs( 'domain_record', {
+ svcnum => $svc_domain->svcnum,
+ reczone => 'www.'.$svc-domain->domain.'.',
+ } );
+ }
+
+ $arec{'@.'. $svc_domain->domain} = $svc_domain->domain
+ unless qsearchs('domain_record', {
+ svcnum => $svc_domain->svcnum,
+ reczone => '@',
+ } )
+ || qsearchs('domain_record', {
+ svcnum => $svc_domain->svcnum,
+ reczone => $svc_domain->domain.'.',
+ } );
+
+ }
+
+ }
+ }
+
+} elsif ( $action eq 'Edit' ) {
+
+ my($domain_rec) = qsearchs('domain_record', { 'recnum'=>$svc_www->recnum });
+ $arec{$svc_www->recnum} = join '.', $domain_rec->recdata, $domain_rec->reczone;
+
+} else {
+ die "\$action eq Add, but \$pkgnum is null!\n";
+}
+
+
+my $p1 = popurl(1);
+print header("Web Hosting $action", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/svc_www.cgi" METHOD=POST>!;
+
+#display
+
+
+
+#svcnum
+print qq!<INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">!;
+print qq!Service #<B>!, $svcnum ? $svcnum : "(NEW)", "</B><BR><BR>";
+
+#pkgnum
+print qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!;
+
+#svcpart
+print qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!;
+
+my($recnum,$usersvc)=(
+ $svc_www->recnum,
+ $svc_www->usersvc,
+);
+
+print &ntable("#cccccc",2),
+ '<TR><TD ALIGN="right">Zone</TD><TD><SELECT NAME="recnum" SIZE=1>';
+foreach $_ (keys %arec) {
+ print "<OPTION", $_ eq $recnum ? " SELECTED" : "",
+ qq! VALUE="$_">$arec{$_}!;
+}
+print "</SELECT></TD></TR>";
+
+print '<TR><TD ALIGN="right">Username</TD><TD><SELECT NAME="usersvc" SIZE=1>';
+foreach $_ (keys %svc_acct) {
+ print "<OPTION", ($_ eq $usersvc) ? " SELECTED" : "",
+ qq! VALUE="$_">$svc_acct{$_}!;
+}
+print "</SELECT></TD></TR>";
+
+foreach my $field ($svc_www->virtual_fields) {
+ if ( $part_svc->part_svc_column($field)->columnflag ne 'F' ) {
+ # If the flag is X, it won't even show up in $svc_acct->virtual_fields.
+ print $svc_www->pvf($field)->widget('HTML', 'edit',
+ $svc_www->getfield($field));
+ }
+}
+
+print '</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">';
+
+print <<END;
+
+ </FORM>
+ </BODY>
+</HTML>
+END
+%>
diff --git a/httemplate/elements/calendar-en.js b/httemplate/elements/calendar-en.js
new file mode 100644
index 0000000..e9d6a22
--- /dev/null
+++ b/httemplate/elements/calendar-en.js
@@ -0,0 +1,123 @@
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mishoo@infoiasi.ro>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ "Sunday");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Sun",
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun");
+
+// full month names
+Calendar._MN = new Array
+("January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "About the calendar";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2003\n" + // don't translate this this ;-)
+"For latest version visit: http://dynarch.com/mishoo/calendar.epl\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "Prev. year (hold for menu)";
+Calendar._TT["PREV_MONTH"] = "Prev. month (hold for menu)";
+Calendar._TT["GO_TODAY"] = "Go Today";
+Calendar._TT["NEXT_MONTH"] = "Next month (hold for menu)";
+Calendar._TT["NEXT_YEAR"] = "Next year (hold for menu)";
+Calendar._TT["SEL_DATE"] = "Select date";
+Calendar._TT["DRAG_TO_MOVE"] = "Drag to move";
+Calendar._TT["PART_TODAY"] = " (today)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "Display %s first";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Close";
+Calendar._TT["TODAY"] = "Today";
+Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "wk";
+Calendar._TT["TIME"] = "Time:";
diff --git a/httemplate/elements/calendar-setup.js b/httemplate/elements/calendar-setup.js
new file mode 100644
index 0000000..55e22b9
--- /dev/null
+++ b/httemplate/elements/calendar-setup.js
@@ -0,0 +1,181 @@
+/* Copyright Mihai Bazon, 2002, 2003 | http://dynarch.com/mishoo/
+ * ---------------------------------------------------------------------------
+ *
+ * The DHTML Calendar
+ *
+ * Details and latest version at:
+ * http://dynarch.com/mishoo/calendar.epl
+ *
+ * This script is distributed under the GNU Lesser General Public License.
+ * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html
+ *
+ * This file defines helper functions for setting up the calendar. They are
+ * intended to help non-programmers get a working calendar on their site
+ * quickly. This script should not be seen as part of the calendar. It just
+ * shows you what one can do with the calendar, while in the same time
+ * providing a quick and simple method for setting it up. If you need
+ * exhaustive customization of the calendar creation process feel free to
+ * modify this code to suit your needs (this is recommended and much better
+ * than modifying calendar.js itself).
+ */
+
+// $Id: calendar-setup.js,v 1.4 2004-09-22 11:04:41 ivan Exp $
+
+/**
+ * This function "patches" an input field (or other element) to use a calendar
+ * widget for date selection.
+ *
+ * The "params" is a single object that can have the following properties:
+ *
+ * prop. name | description
+ * -------------------------------------------------------------------------------------------------
+ * inputField | the ID of an input field to store the date
+ * displayArea | the ID of a DIV or other element to show the date
+ * button | ID of a button or other element that will trigger the calendar
+ * eventName | event that will trigger the calendar, without the "on" prefix (default: "click")
+ * ifFormat | date format that will be stored in the input field
+ * daFormat | the date format that will be used to display the date in displayArea
+ * singleClick | (true/false) wether the calendar is in single click mode or not (default: true)
+ * firstDay | numeric: 0 to 6. "0" means display Sunday first, "1" means display Monday first, etc.
+ * align | alignment (default: "Br"); if you don't know what's this see the calendar documentation
+ * range | array with 2 elements. Default: [1900, 2999] -- the range of years available
+ * weekNumbers | (true/false) if it's true (default) the calendar will display week numbers
+ * flat | null or element ID; if not null the calendar will be a flat calendar having the parent with the given ID
+ * flatCallback | function that receives a JS Date object and returns an URL to point the browser to (for flat calendar)
+ * disableFunc | function that receives a JS Date object and should return true if that date has to be disabled in the calendar
+ * onSelect | function that gets called when a date is selected. You don't _have_ to supply this (the default is generally okay)
+ * onClose | function that gets called when the calendar is closed. [default]
+ * onUpdate | function that gets called after the date is updated in the input field. Receives a reference to the calendar.
+ * date | the date that the calendar will be initially displayed to
+ * showsTime | default: false; if true the calendar will include a time selector
+ * timeFormat | the time format; can be "12" or "24", default is "12"
+ * electric | if true (default) then given fields/date areas are updated for each move; otherwise they're updated only on close
+ * step | configures the step of the years in drop-down boxes; default: 2
+ * position | configures the calendar absolute position; default: null
+ * cache | if "true" (but default: "false") it will reuse the same calendar object, where possible
+ * showOthers | if "true" (but default: "false") it will show days from other months too
+ *
+ * None of them is required, they all have default values. However, if you
+ * pass none of "inputField", "displayArea" or "button" you'll get a warning
+ * saying "nothing to setup".
+ */
+Calendar.setup = function (params) {
+ function param_default(pname, def) { if (typeof params[pname] == "undefined") { params[pname] = def; } };
+
+ param_default("inputField", null);
+ param_default("displayArea", null);
+ param_default("button", null);
+ param_default("eventName", "click");
+ param_default("ifFormat", "%Y/%m/%d");
+ param_default("daFormat", "%Y/%m/%d");
+ param_default("singleClick", true);
+ param_default("disableFunc", null);
+ param_default("dateStatusFunc", params["disableFunc"]); // takes precedence if both are defined
+ param_default("firstDay", 0); // defaults to "Sunday" first
+ param_default("align", "Br");
+ param_default("range", [1900, 2999]);
+ param_default("weekNumbers", true);
+ param_default("flat", null);
+ param_default("flatCallback", null);
+ param_default("onSelect", null);
+ param_default("onClose", null);
+ param_default("onUpdate", null);
+ param_default("date", null);
+ param_default("showsTime", false);
+ param_default("timeFormat", "24");
+ param_default("electric", true);
+ param_default("step", 2);
+ param_default("position", null);
+ param_default("cache", false);
+ param_default("showOthers", false);
+
+ var tmp = ["inputField", "displayArea", "button"];
+ for (var i in tmp) {
+ if (typeof params[tmp[i]] == "string") {
+ params[tmp[i]] = document.getElementById(params[tmp[i]]);
+ }
+ }
+ if (!(params.flat || params.inputField || params.displayArea || params.button)) {
+ alert("Calendar.setup:\n Nothing to setup (no fields found). Please check your code");
+ return false;
+ }
+
+ function onSelect(cal) {
+ var p = cal.params;
+ var update = (cal.dateClicked || p.electric);
+ if (update && p.flat) {
+ if (typeof p.flatCallback == "function")
+ p.flatCallback(cal);
+ else
+ alert("No flatCallback given -- doing nothing.");
+ return false;
+ }
+ if (update && p.inputField) {
+ p.inputField.value = cal.date.print(p.ifFormat);
+ if (typeof p.inputField.onchange == "function")
+ p.inputField.onchange();
+ }
+ if (update && p.displayArea)
+ p.displayArea.innerHTML = cal.date.print(p.daFormat);
+ if (update && p.singleClick && cal.dateClicked)
+ cal.callCloseHandler();
+ if (update && typeof p.onUpdate == "function")
+ p.onUpdate(cal);
+ };
+
+ if (params.flat != null) {
+ if (typeof params.flat == "string")
+ params.flat = document.getElementById(params.flat);
+ if (!params.flat) {
+ alert("Calendar.setup:\n Flat specified but can't find parent.");
+ return false;
+ }
+ var cal = new Calendar(params.firstDay, params.date, params.onSelect || onSelect);
+ cal.showsTime = params.showsTime;
+ cal.time24 = (params.timeFormat == "24");
+ cal.params = params;
+ cal.weekNumbers = params.weekNumbers;
+ cal.setRange(params.range[0], params.range[1]);
+ cal.setDateStatusHandler(params.dateStatusFunc);
+ cal.create(params.flat);
+ cal.show();
+ return false;
+ }
+
+ var triggerEl = params.button || params.displayArea || params.inputField;
+ triggerEl["on" + params.eventName] = function() {
+ var dateEl = params.inputField || params.displayArea;
+ var dateFmt = params.inputField ? params.ifFormat : params.daFormat;
+ var mustCreate = false;
+ var cal = window.calendar;
+ if (!(cal && params.cache)) {
+ window.calendar = cal = new Calendar(params.firstDay,
+ params.date,
+ params.onSelect || onSelect,
+ params.onClose || function(cal) { cal.hide(); });
+ cal.showsTime = params.showsTime;
+ cal.time24 = (params.timeFormat == "24");
+ cal.weekNumbers = params.weekNumbers;
+ mustCreate = true;
+ } else {
+ if (params.date)
+ cal.setDate(params.date);
+ cal.hide();
+ }
+ cal.showsOtherMonths = params.showOthers;
+ cal.yearStep = params.step;
+ cal.setRange(params.range[0], params.range[1]);
+ cal.params = params;
+ cal.setDateStatusHandler(params.dateStatusFunc);
+ cal.setDateFormat(dateFmt);
+ if (mustCreate)
+ cal.create();
+ cal.parseDate(dateEl.value || dateEl.innerHTML);
+ cal.refresh();
+ if (!params.position)
+ cal.showAtElement(params.button || params.displayArea || params.inputField, params.align);
+ else
+ cal.showAt(params.position[0], params.position[1]);
+ return false;
+ };
+};
diff --git a/httemplate/elements/calendar-win2k-2.css b/httemplate/elements/calendar-win2k-2.css
new file mode 100644
index 0000000..6001cfa
--- /dev/null
+++ b/httemplate/elements/calendar-win2k-2.css
@@ -0,0 +1,270 @@
+/* The main calendar widget. DIV containing a table. */
+
+.calendar {
+ position: relative;
+ display: none;
+ border-top: 2px solid #fff;
+ border-right: 2px solid #000;
+ border-bottom: 2px solid #000;
+ border-left: 2px solid #fff;
+ font-size: 11px;
+ color: #000;
+ cursor: default;
+ background: #d4c8d0;
+ font-family: tahoma,verdana,sans-serif;
+}
+
+.calendar table {
+ border-top: 1px solid #000;
+ border-right: 1px solid #fff;
+ border-bottom: 1px solid #fff;
+ border-left: 1px solid #000;
+ font-size: 11px;
+ color: #000;
+ cursor: default;
+ background: #d4c8d0;
+ font-family: tahoma,verdana,sans-serif;
+}
+
+/* Header part -- contains navigation buttons and day names. */
+
+.calendar .button { /* "<<", "<", ">", ">>" buttons have this class */
+ text-align: center;
+ padding: 1px;
+ border-top: 1px solid #fff;
+ border-right: 1px solid #000;
+ border-bottom: 1px solid #000;
+ border-left: 1px solid #fff;
+}
+
+.calendar .nav {
+ background: transparent url(menuarrow.gif) no-repeat 100% 100%;
+}
+
+.calendar thead .title { /* This holds the current "month, year" */
+ font-weight: bold;
+ padding: 1px;
+ border: 1px solid #000;
+ background: #847880;
+ color: #fff;
+ text-align: center;
+}
+
+.calendar thead .headrow { /* Row <TR> containing navigation buttons */
+}
+
+.calendar thead .daynames { /* Row <TR> containing the day names */
+}
+
+.calendar thead .name { /* Cells <TD> containing the day names */
+ border-bottom: 1px solid #000;
+ padding: 2px;
+ text-align: center;
+ background: #f4e8f0;
+}
+
+.calendar thead .weekend { /* How a weekend day name shows in header */
+ color: #f00;
+}
+
+.calendar thead .hilite { /* How do the buttons in header appear when hover */
+ border-top: 2px solid #fff;
+ border-right: 2px solid #000;
+ border-bottom: 2px solid #000;
+ border-left: 2px solid #fff;
+ padding: 0px;
+ background-color: #e4d8e0;
+}
+
+.calendar thead .active { /* Active (pressed) buttons in header */
+ padding: 2px 0px 0px 2px;
+ border-top: 1px solid #000;
+ border-right: 1px solid #fff;
+ border-bottom: 1px solid #fff;
+ border-left: 1px solid #000;
+ background-color: #c4b8c0;
+}
+
+/* The body part -- contains all the days in month. */
+
+.calendar tbody .day { /* Cells <TD> containing month days dates */
+ width: 2em;
+ text-align: right;
+ padding: 2px 4px 2px 2px;
+}
+.calendar tbody .day.othermonth {
+ font-size: 80%;
+ color: #aaa;
+}
+.calendar tbody .day.othermonth.oweekend {
+ color: #faa;
+}
+
+.calendar table .wn {
+ padding: 2px 3px 2px 2px;
+ border-right: 1px solid #000;
+ background: #f4e8f0;
+}
+
+.calendar tbody .rowhilite td {
+ background: #e4d8e0;
+}
+
+.calendar tbody .rowhilite td.wn {
+ background: #d4c8d0;
+}
+
+.calendar tbody td.hilite { /* Hovered cells <TD> */
+ padding: 1px 3px 1px 1px;
+ border-top: 1px solid #fff;
+ border-right: 1px solid #000;
+ border-bottom: 1px solid #000;
+ border-left: 1px solid #fff;
+}
+
+.calendar tbody td.active { /* Active (pressed) cells <TD> */
+ padding: 2px 2px 0px 2px;
+ border-top: 1px solid #000;
+ border-right: 1px solid #fff;
+ border-bottom: 1px solid #fff;
+ border-left: 1px solid #000;
+}
+
+.calendar tbody td.selected { /* Cell showing selected date */
+ font-weight: bold;
+ border-top: 1px solid #000;
+ border-right: 1px solid #fff;
+ border-bottom: 1px solid #fff;
+ border-left: 1px solid #000;
+ padding: 2px 2px 0px 2px;
+ background: #e4d8e0;
+}
+
+.calendar tbody td.weekend { /* Cells showing weekend days */
+ color: #f00;
+}
+
+.calendar tbody td.today { /* Cell showing today date */
+ font-weight: bold;
+ color: #00f;
+}
+
+.calendar tbody .disabled { color: #999; }
+
+.calendar tbody .emptycell { /* Empty cells (the best is to hide them) */
+ visibility: hidden;
+}
+
+.calendar tbody .emptyrow { /* Empty row (some months need less than 6 rows) */
+ display: none;
+}
+
+/* The footer part -- status bar and "Close" button */
+
+.calendar tfoot .footrow { /* The <TR> in footer (only one right now) */
+}
+
+.calendar tfoot .ttip { /* Tooltip (status bar) cell <TD> */
+ background: #f4e8f0;
+ padding: 1px;
+ border: 1px solid #000;
+ background: #847880;
+ color: #fff;
+ text-align: center;
+}
+
+.calendar tfoot .hilite { /* Hover style for buttons in footer */
+ border-top: 1px solid #fff;
+ border-right: 1px solid #000;
+ border-bottom: 1px solid #000;
+ border-left: 1px solid #fff;
+ padding: 1px;
+ background: #e4d8e0;
+}
+
+.calendar tfoot .active { /* Active (pressed) style for buttons in footer */
+ padding: 2px 0px 0px 2px;
+ border-top: 1px solid #000;
+ border-right: 1px solid #fff;
+ border-bottom: 1px solid #fff;
+ border-left: 1px solid #000;
+}
+
+/* Combo boxes (menus that display months/years for direct selection) */
+
+.calendar .combo {
+ position: absolute;
+ display: none;
+ width: 4em;
+ top: 0px;
+ left: 0px;
+ cursor: default;
+ border-top: 1px solid #fff;
+ border-right: 1px solid #000;
+ border-bottom: 1px solid #000;
+ border-left: 1px solid #fff;
+ background: #e4d8e0;
+ font-size: 90%;
+ padding: 1px;
+}
+
+.calendar .combo .label,
+.calendar .combo .label-IEfix {
+ text-align: center;
+ padding: 1px;
+}
+
+.calendar .combo .label-IEfix {
+ width: 4em;
+}
+
+.calendar .combo .active {
+ background: #d4c8d0;
+ padding: 0px;
+ border-top: 1px solid #000;
+ border-right: 1px solid #fff;
+ border-bottom: 1px solid #fff;
+ border-left: 1px solid #000;
+}
+
+.calendar .combo .hilite {
+ background: #408;
+ color: #fea;
+}
+
+.calendar td.time {
+ border-top: 1px solid #000;
+ padding: 1px 0px;
+ text-align: center;
+ background-color: #f4f0e8;
+}
+
+.calendar td.time .hour,
+.calendar td.time .minute,
+.calendar td.time .ampm {
+ padding: 0px 3px 0px 4px;
+ border: 1px solid #889;
+ font-weight: bold;
+ background-color: #fff;
+}
+
+.calendar td.time .ampm {
+ text-align: center;
+}
+
+.calendar td.time .colon {
+ padding: 0px 2px 0px 3px;
+ font-weight: bold;
+}
+
+.calendar td.time span.hilite {
+ border-color: #000;
+ background-color: #766;
+ color: #fff;
+}
+
+.calendar td.time span.active {
+ border-color: #f00;
+ background-color: #000;
+ color: #0f0;
+}
diff --git a/httemplate/elements/calendar.js b/httemplate/elements/calendar.js
new file mode 100644
index 0000000..ec18d80
--- /dev/null
+++ b/httemplate/elements/calendar.js
@@ -0,0 +1,1715 @@
+/* Copyright Mihai Bazon, 2002, 2003 | http://dynarch.com/mishoo/
+ * ------------------------------------------------------------------
+ *
+ * The DHTML Calendar, version 0.9.6 "Keep cool but don't freeze"
+ *
+ * Details and latest version at:
+ * http://dynarch.com/mishoo/calendar.epl
+ *
+ * This script is distributed under the GNU Lesser General Public License.
+ * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html
+ */
+
+// $Id: calendar.js,v 1.4 2004-09-22 11:04:41 ivan Exp $
+
+/** The Calendar object constructor. */
+Calendar = function (firstDayOfWeek, dateStr, onSelected, onClose) {
+ // member variables
+ this.activeDiv = null;
+ this.currentDateEl = null;
+ this.getDateStatus = null;
+ this.timeout = null;
+ this.onSelected = onSelected || null;
+ this.onClose = onClose || null;
+ this.dragging = false;
+ this.hidden = false;
+ this.minYear = 1970;
+ this.maxYear = 2050;
+ this.dateFormat = Calendar._TT["DEF_DATE_FORMAT"];
+ this.ttDateFormat = Calendar._TT["TT_DATE_FORMAT"];
+ this.isPopup = true;
+ this.weekNumbers = true;
+ this.firstDayOfWeek = firstDayOfWeek; // 0 for Sunday, 1 for Monday, etc.
+ this.showsOtherMonths = false;
+ this.dateStr = dateStr;
+ this.ar_days = null;
+ this.showsTime = false;
+ this.time24 = true;
+ this.yearStep = 2;
+ // HTML elements
+ this.table = null;
+ this.element = null;
+ this.tbody = null;
+ this.firstdayname = null;
+ // Combo boxes
+ this.monthsCombo = null;
+ this.yearsCombo = null;
+ this.hilitedMonth = null;
+ this.activeMonth = null;
+ this.hilitedYear = null;
+ this.activeYear = null;
+ // Information
+ this.dateClicked = false;
+
+ // one-time initializations
+ if (typeof Calendar._SDN == "undefined") {
+ // table of short day names
+ if (typeof Calendar._SDN_len == "undefined")
+ Calendar._SDN_len = 3;
+ var ar = new Array();
+ for (var i = 8; i > 0;) {
+ ar[--i] = Calendar._DN[i].substr(0, Calendar._SDN_len);
+ }
+ Calendar._SDN = ar;
+ // table of short month names
+ if (typeof Calendar._SMN_len == "undefined")
+ Calendar._SMN_len = 3;
+ ar = new Array();
+ for (var i = 12; i > 0;) {
+ ar[--i] = Calendar._MN[i].substr(0, Calendar._SMN_len);
+ }
+ Calendar._SMN = ar;
+ }
+};
+
+// ** constants
+
+/// "static", needed for event handlers.
+Calendar._C = null;
+
+/// detect a special case of "web browser"
+Calendar.is_ie = ( /msie/i.test(navigator.userAgent) &&
+ !/opera/i.test(navigator.userAgent) );
+
+Calendar.is_ie5 = ( Calendar.is_ie && /msie 5\.0/i.test(navigator.userAgent) );
+
+/// detect Opera browser
+Calendar.is_opera = /opera/i.test(navigator.userAgent);
+
+/// detect KHTML-based browsers
+Calendar.is_khtml = /Konqueror|Safari|KHTML/i.test(navigator.userAgent);
+
+// BEGIN: UTILITY FUNCTIONS; beware that these might be moved into a separate
+// library, at some point.
+
+Calendar.getAbsolutePos = function(el) {
+ var SL = 0, ST = 0;
+ var is_div = /^div$/i.test(el.tagName);
+ if (is_div && el.scrollLeft)
+ SL = el.scrollLeft;
+ if (is_div && el.scrollTop)
+ ST = el.scrollTop;
+ var r = { x: el.offsetLeft - SL, y: el.offsetTop - ST };
+ if (el.offsetParent) {
+ var tmp = this.getAbsolutePos(el.offsetParent);
+ r.x += tmp.x;
+ r.y += tmp.y;
+ }
+ return r;
+};
+
+Calendar.isRelated = function (el, evt) {
+ var related = evt.relatedTarget;
+ if (!related) {
+ var type = evt.type;
+ if (type == "mouseover") {
+ related = evt.fromElement;
+ } else if (type == "mouseout") {
+ related = evt.toElement;
+ }
+ }
+ while (related) {
+ if (related == el) {
+ return true;
+ }
+ related = related.parentNode;
+ }
+ return false;
+};
+
+Calendar.removeClass = function(el, className) {
+ if (!(el && el.className)) {
+ return;
+ }
+ var cls = el.className.split(" ");
+ var ar = new Array();
+ for (var i = cls.length; i > 0;) {
+ if (cls[--i] != className) {
+ ar[ar.length] = cls[i];
+ }
+ }
+ el.className = ar.join(" ");
+};
+
+Calendar.addClass = function(el, className) {
+ Calendar.removeClass(el, className);
+ el.className += " " + className;
+};
+
+Calendar.getElement = function(ev) {
+ if (Calendar.is_ie) {
+ return window.event.srcElement;
+ } else {
+ return ev.currentTarget;
+ }
+};
+
+Calendar.getTargetElement = function(ev) {
+ if (Calendar.is_ie) {
+ return window.event.srcElement;
+ } else {
+ return ev.target;
+ }
+};
+
+Calendar.stopEvent = function(ev) {
+ ev || (ev = window.event);
+ if (Calendar.is_ie) {
+ ev.cancelBubble = true;
+ ev.returnValue = false;
+ } else {
+ ev.preventDefault();
+ ev.stopPropagation();
+ }
+ return false;
+};
+
+Calendar.addEvent = function(el, evname, func) {
+ if (el.attachEvent) { // IE
+ el.attachEvent("on" + evname, func);
+ } else if (el.addEventListener) { // Gecko / W3C
+ el.addEventListener(evname, func, true);
+ } else {
+ el["on" + evname] = func;
+ }
+};
+
+Calendar.removeEvent = function(el, evname, func) {
+ if (el.detachEvent) { // IE
+ el.detachEvent("on" + evname, func);
+ } else if (el.removeEventListener) { // Gecko / W3C
+ el.removeEventListener(evname, func, true);
+ } else {
+ el["on" + evname] = null;
+ }
+};
+
+Calendar.createElement = function(type, parent) {
+ var el = null;
+ if (document.createElementNS) {
+ // use the XHTML namespace; IE won't normally get here unless
+ // _they_ "fix" the DOM2 implementation.
+ el = document.createElementNS("http://www.w3.org/1999/xhtml", type);
+ } else {
+ el = document.createElement(type);
+ }
+ if (typeof parent != "undefined") {
+ parent.appendChild(el);
+ }
+ return el;
+};
+
+// END: UTILITY FUNCTIONS
+
+// BEGIN: CALENDAR STATIC FUNCTIONS
+
+/** Internal -- adds a set of events to make some element behave like a button. */
+Calendar._add_evs = function(el) {
+ with (Calendar) {
+ addEvent(el, "mouseover", dayMouseOver);
+ addEvent(el, "mousedown", dayMouseDown);
+ addEvent(el, "mouseout", dayMouseOut);
+ if (is_ie) {
+ addEvent(el, "dblclick", dayMouseDblClick);
+ el.setAttribute("unselectable", true);
+ }
+ }
+};
+
+Calendar.findMonth = function(el) {
+ if (typeof el.month != "undefined") {
+ return el;
+ } else if (typeof el.parentNode.month != "undefined") {
+ return el.parentNode;
+ }
+ return null;
+};
+
+Calendar.findYear = function(el) {
+ if (typeof el.year != "undefined") {
+ return el;
+ } else if (typeof el.parentNode.year != "undefined") {
+ return el.parentNode;
+ }
+ return null;
+};
+
+Calendar.showMonthsCombo = function () {
+ var cal = Calendar._C;
+ if (!cal) {
+ return false;
+ }
+ var cal = cal;
+ var cd = cal.activeDiv;
+ var mc = cal.monthsCombo;
+ if (cal.hilitedMonth) {
+ Calendar.removeClass(cal.hilitedMonth, "hilite");
+ }
+ if (cal.activeMonth) {
+ Calendar.removeClass(cal.activeMonth, "active");
+ }
+ var mon = cal.monthsCombo.getElementsByTagName("div")[cal.date.getMonth()];
+ Calendar.addClass(mon, "active");
+ cal.activeMonth = mon;
+ var s = mc.style;
+ s.display = "block";
+ if (cd.navtype < 0)
+ s.left = cd.offsetLeft + "px";
+ else {
+ var mcw = mc.offsetWidth;
+ if (typeof mcw == "undefined")
+ // Konqueror brain-dead techniques
+ mcw = 50;
+ s.left = (cd.offsetLeft + cd.offsetWidth - mcw) + "px";
+ }
+ s.top = (cd.offsetTop + cd.offsetHeight) + "px";
+};
+
+Calendar.showYearsCombo = function (fwd) {
+ var cal = Calendar._C;
+ if (!cal) {
+ return false;
+ }
+ var cal = cal;
+ var cd = cal.activeDiv;
+ var yc = cal.yearsCombo;
+ if (cal.hilitedYear) {
+ Calendar.removeClass(cal.hilitedYear, "hilite");
+ }
+ if (cal.activeYear) {
+ Calendar.removeClass(cal.activeYear, "active");
+ }
+ cal.activeYear = null;
+ var Y = cal.date.getFullYear() + (fwd ? 1 : -1);
+ var yr = yc.firstChild;
+ var show = false;
+ for (var i = 12; i > 0; --i) {
+ if (Y >= cal.minYear && Y <= cal.maxYear) {
+ yr.firstChild.data = Y;
+ yr.year = Y;
+ yr.style.display = "block";
+ show = true;
+ } else {
+ yr.style.display = "none";
+ }
+ yr = yr.nextSibling;
+ Y += fwd ? cal.yearStep : -cal.yearStep;
+ }
+ if (show) {
+ var s = yc.style;
+ s.display = "block";
+ if (cd.navtype < 0)
+ s.left = cd.offsetLeft + "px";
+ else {
+ var ycw = yc.offsetWidth;
+ if (typeof ycw == "undefined")
+ // Konqueror brain-dead techniques
+ ycw = 50;
+ s.left = (cd.offsetLeft + cd.offsetWidth - ycw) + "px";
+ }
+ s.top = (cd.offsetTop + cd.offsetHeight) + "px";
+ }
+};
+
+// event handlers
+
+Calendar.tableMouseUp = function(ev) {
+ var cal = Calendar._C;
+ if (!cal) {
+ return false;
+ }
+ if (cal.timeout) {
+ clearTimeout(cal.timeout);
+ }
+ var el = cal.activeDiv;
+ if (!el) {
+ return false;
+ }
+ var target = Calendar.getTargetElement(ev);
+ ev || (ev = window.event);
+ Calendar.removeClass(el, "active");
+ if (target == el || target.parentNode == el) {
+ Calendar.cellClick(el, ev);
+ }
+ var mon = Calendar.findMonth(target);
+ var date = null;
+ if (mon) {
+ date = new Date(cal.date);
+ if (mon.month != date.getMonth()) {
+ date.setMonth(mon.month);
+ cal.setDate(date);
+ cal.dateClicked = false;
+ cal.callHandler();
+ }
+ } else {
+ var year = Calendar.findYear(target);
+ if (year) {
+ date = new Date(cal.date);
+ if (year.year != date.getFullYear()) {
+ date.setFullYear(year.year);
+ cal.setDate(date);
+ cal.dateClicked = false;
+ cal.callHandler();
+ }
+ }
+ }
+ with (Calendar) {
+ removeEvent(document, "mouseup", tableMouseUp);
+ removeEvent(document, "mouseover", tableMouseOver);
+ removeEvent(document, "mousemove", tableMouseOver);
+ cal._hideCombos();
+ _C = null;
+ return stopEvent(ev);
+ }
+};
+
+Calendar.tableMouseOver = function (ev) {
+ var cal = Calendar._C;
+ if (!cal) {
+ return;
+ }
+ var el = cal.activeDiv;
+ var target = Calendar.getTargetElement(ev);
+ if (target == el || target.parentNode == el) {
+ Calendar.addClass(el, "hilite active");
+ Calendar.addClass(el.parentNode, "rowhilite");
+ } else {
+ if (typeof el.navtype == "undefined" || (el.navtype != 50 && (el.navtype == 0 || Math.abs(el.navtype) > 2)))
+ Calendar.removeClass(el, "active");
+ Calendar.removeClass(el, "hilite");
+ Calendar.removeClass(el.parentNode, "rowhilite");
+ }
+ ev || (ev = window.event);
+ if (el.navtype == 50 && target != el) {
+ var pos = Calendar.getAbsolutePos(el);
+ var w = el.offsetWidth;
+ var x = ev.clientX;
+ var dx;
+ var decrease = true;
+ if (x > pos.x + w) {
+ dx = x - pos.x - w;
+ decrease = false;
+ } else
+ dx = pos.x - x;
+
+ if (dx < 0) dx = 0;
+ var range = el._range;
+ var current = el._current;
+ var count = Math.floor(dx / 10) % range.length;
+ for (var i = range.length; --i >= 0;)
+ if (range[i] == current)
+ break;
+ while (count-- > 0)
+ if (decrease) {
+ if (--i < 0)
+ i = range.length - 1;
+ } else if ( ++i >= range.length )
+ i = 0;
+ var newval = range[i];
+ el.firstChild.data = newval;
+
+ cal.onUpdateTime();
+ }
+ var mon = Calendar.findMonth(target);
+ if (mon) {
+ if (mon.month != cal.date.getMonth()) {
+ if (cal.hilitedMonth) {
+ Calendar.removeClass(cal.hilitedMonth, "hilite");
+ }
+ Calendar.addClass(mon, "hilite");
+ cal.hilitedMonth = mon;
+ } else if (cal.hilitedMonth) {
+ Calendar.removeClass(cal.hilitedMonth, "hilite");
+ }
+ } else {
+ if (cal.hilitedMonth) {
+ Calendar.removeClass(cal.hilitedMonth, "hilite");
+ }
+ var year = Calendar.findYear(target);
+ if (year) {
+ if (year.year != cal.date.getFullYear()) {
+ if (cal.hilitedYear) {
+ Calendar.removeClass(cal.hilitedYear, "hilite");
+ }
+ Calendar.addClass(year, "hilite");
+ cal.hilitedYear = year;
+ } else if (cal.hilitedYear) {
+ Calendar.removeClass(cal.hilitedYear, "hilite");
+ }
+ } else if (cal.hilitedYear) {
+ Calendar.removeClass(cal.hilitedYear, "hilite");
+ }
+ }
+ return Calendar.stopEvent(ev);
+};
+
+Calendar.tableMouseDown = function (ev) {
+ if (Calendar.getTargetElement(ev) == Calendar.getElement(ev)) {
+ return Calendar.stopEvent(ev);
+ }
+};
+
+Calendar.calDragIt = function (ev) {
+ var cal = Calendar._C;
+ if (!(cal && cal.dragging)) {
+ return false;
+ }
+ var posX;
+ var posY;
+ if (Calendar.is_ie) {
+ posY = window.event.clientY + document.body.scrollTop;
+ posX = window.event.clientX + document.body.scrollLeft;
+ } else {
+ posX = ev.pageX;
+ posY = ev.pageY;
+ }
+ cal.hideShowCovered();
+ var st = cal.element.style;
+ st.left = (posX - cal.xOffs) + "px";
+ st.top = (posY - cal.yOffs) + "px";
+ return Calendar.stopEvent(ev);
+};
+
+Calendar.calDragEnd = function (ev) {
+ var cal = Calendar._C;
+ if (!cal) {
+ return false;
+ }
+ cal.dragging = false;
+ with (Calendar) {
+ removeEvent(document, "mousemove", calDragIt);
+ removeEvent(document, "mouseup", calDragEnd);
+ tableMouseUp(ev);
+ }
+ cal.hideShowCovered();
+};
+
+Calendar.dayMouseDown = function(ev) {
+ var el = Calendar.getElement(ev);
+ if (el.disabled) {
+ return false;
+ }
+ var cal = el.calendar;
+ cal.activeDiv = el;
+ Calendar._C = cal;
+ if (el.navtype != 300) with (Calendar) {
+ if (el.navtype == 50) {
+ el._current = el.firstChild.data;
+ addEvent(document, "mousemove", tableMouseOver);
+ } else
+ addEvent(document, Calendar.is_ie5 ? "mousemove" : "mouseover", tableMouseOver);
+ addClass(el, "hilite active");
+ addEvent(document, "mouseup", tableMouseUp);
+ } else if (cal.isPopup) {
+ cal._dragStart(ev);
+ }
+ if (el.navtype == -1 || el.navtype == 1) {
+ if (cal.timeout) clearTimeout(cal.timeout);
+ cal.timeout = setTimeout("Calendar.showMonthsCombo()", 250);
+ } else if (el.navtype == -2 || el.navtype == 2) {
+ if (cal.timeout) clearTimeout(cal.timeout);
+ cal.timeout = setTimeout((el.navtype > 0) ? "Calendar.showYearsCombo(true)" : "Calendar.showYearsCombo(false)", 250);
+ } else {
+ cal.timeout = null;
+ }
+ return Calendar.stopEvent(ev);
+};
+
+Calendar.dayMouseDblClick = function(ev) {
+ Calendar.cellClick(Calendar.getElement(ev), ev || window.event);
+ if (Calendar.is_ie) {
+ document.selection.empty();
+ }
+};
+
+Calendar.dayMouseOver = function(ev) {
+ var el = Calendar.getElement(ev);
+ if (Calendar.isRelated(el, ev) || Calendar._C || el.disabled) {
+ return false;
+ }
+ if (el.ttip) {
+ if (el.ttip.substr(0, 1) == "_") {
+ el.ttip = el.caldate.print(el.calendar.ttDateFormat) + el.ttip.substr(1);
+ }
+ el.calendar.tooltips.firstChild.data = el.ttip;
+ }
+ if (el.navtype != 300) {
+ Calendar.addClass(el, "hilite");
+ if (el.caldate) {
+ Calendar.addClass(el.parentNode, "rowhilite");
+ }
+ }
+ return Calendar.stopEvent(ev);
+};
+
+Calendar.dayMouseOut = function(ev) {
+ with (Calendar) {
+ var el = getElement(ev);
+ if (isRelated(el, ev) || _C || el.disabled) {
+ return false;
+ }
+ removeClass(el, "hilite");
+ if (el.caldate) {
+ removeClass(el.parentNode, "rowhilite");
+ }
+ el.calendar.tooltips.firstChild.data = _TT["SEL_DATE"];
+ return stopEvent(ev);
+ }
+};
+
+/**
+ * A generic "click" handler :) handles all types of buttons defined in this
+ * calendar.
+ */
+Calendar.cellClick = function(el, ev) {
+ var cal = el.calendar;
+ var closing = false;
+ var newdate = false;
+ var date = null;
+ if (typeof el.navtype == "undefined") {
+ Calendar.removeClass(cal.currentDateEl, "selected");
+ Calendar.addClass(el, "selected");
+ closing = (cal.currentDateEl == el);
+ if (!closing) {
+ cal.currentDateEl = el;
+ }
+ cal.date = new Date(el.caldate);
+ date = cal.date;
+ newdate = true;
+ // a date was clicked
+ if (!(cal.dateClicked = !el.otherMonth))
+ cal._init(cal.firstDayOfWeek, date);
+ } else {
+ if (el.navtype == 200) {
+ Calendar.removeClass(el, "hilite");
+ cal.callCloseHandler();
+ return;
+ }
+ date = (el.navtype == 0) ? new Date() : new Date(cal.date);
+ // unless "today" was clicked, we assume no date was clicked so
+ // the selected handler will know not to close the calenar when
+ // in single-click mode.
+ // cal.dateClicked = (el.navtype == 0);
+ cal.dateClicked = false;
+ var year = date.getFullYear();
+ var mon = date.getMonth();
+ function setMonth(m) {
+ var day = date.getDate();
+ var max = date.getMonthDays(m);
+ if (day > max) {
+ date.setDate(max);
+ }
+ date.setMonth(m);
+ };
+ switch (el.navtype) {
+ case 400:
+ Calendar.removeClass(el, "hilite");
+ var text = Calendar._TT["ABOUT"];
+ if (typeof text != "undefined") {
+ text += cal.showsTime ? Calendar._TT["ABOUT_TIME"] : "";
+ } else {
+ // FIXME: this should be removed as soon as lang files get updated!
+ text = "Help and about box text is not translated into this language.\n" +
+ "If you know this language and you feel generous please update\n" +
+ "the corresponding file in \"lang\" subdir to match calendar-en.js\n" +
+ "and send it back to <mishoo@infoiasi.ro> to get it into the distribution ;-)\n\n" +
+ "Thank you!\n" +
+ "http://dynarch.com/mishoo/calendar.epl\n";
+ }
+ alert(text);
+ return;
+ case -2:
+ if (year > cal.minYear) {
+ date.setFullYear(year - 1);
+ }
+ break;
+ case -1:
+ if (mon > 0) {
+ setMonth(mon - 1);
+ } else if (year-- > cal.minYear) {
+ date.setFullYear(year);
+ setMonth(11);
+ }
+ break;
+ case 1:
+ if (mon < 11) {
+ setMonth(mon + 1);
+ } else if (year < cal.maxYear) {
+ date.setFullYear(year + 1);
+ setMonth(0);
+ }
+ break;
+ case 2:
+ if (year < cal.maxYear) {
+ date.setFullYear(year + 1);
+ }
+ break;
+ case 100:
+ cal.setFirstDayOfWeek(el.fdow);
+ return;
+ case 50:
+ var range = el._range;
+ var current = el.firstChild.data;
+ for (var i = range.length; --i >= 0;)
+ if (range[i] == current)
+ break;
+ if (ev && ev.shiftKey) {
+ if (--i < 0)
+ i = range.length - 1;
+ } else if ( ++i >= range.length )
+ i = 0;
+ var newval = range[i];
+ el.firstChild.data = newval;
+ cal.onUpdateTime();
+ return;
+ case 0:
+ // TODAY will bring us here
+ if ((typeof cal.getDateStatus == "function") && cal.getDateStatus(date, date.getFullYear(), date.getMonth(), date.getDate())) {
+ // remember, "date" was previously set to new
+ // Date() if TODAY was clicked; thus, it
+ // contains today date.
+ return false;
+ }
+ break;
+ }
+ if (!date.equalsTo(cal.date)) {
+ cal.setDate(date);
+ newdate = true;
+ }
+ }
+ if (newdate) {
+ cal.callHandler();
+ }
+ if (closing) {
+ Calendar.removeClass(el, "hilite");
+ cal.callCloseHandler();
+ }
+};
+
+// END: CALENDAR STATIC FUNCTIONS
+
+// BEGIN: CALENDAR OBJECT FUNCTIONS
+
+/**
+ * This function creates the calendar inside the given parent. If _par is
+ * null than it creates a popup calendar inside the BODY element. If _par is
+ * an element, be it BODY, then it creates a non-popup calendar (still
+ * hidden). Some properties need to be set before calling this function.
+ */
+Calendar.prototype.create = function (_par) {
+ var parent = null;
+ if (! _par) {
+ // default parent is the document body, in which case we create
+ // a popup calendar.
+ parent = document.getElementsByTagName("body")[0];
+ this.isPopup = true;
+ } else {
+ parent = _par;
+ this.isPopup = false;
+ }
+ this.date = this.dateStr ? new Date(this.dateStr) : new Date();
+
+ var table = Calendar.createElement("table");
+ this.table = table;
+ table.cellSpacing = 0;
+ table.cellPadding = 0;
+ table.calendar = this;
+ Calendar.addEvent(table, "mousedown", Calendar.tableMouseDown);
+
+ var div = Calendar.createElement("div");
+ this.element = div;
+ div.className = "calendar";
+ if (this.isPopup) {
+ div.style.position = "absolute";
+ div.style.display = "none";
+ }
+ div.appendChild(table);
+
+ var thead = Calendar.createElement("thead", table);
+ var cell = null;
+ var row = null;
+
+ var cal = this;
+ var hh = function (text, cs, navtype) {
+ cell = Calendar.createElement("td", row);
+ cell.colSpan = cs;
+ cell.className = "button";
+ if (navtype != 0 && Math.abs(navtype) <= 2)
+ cell.className += " nav";
+ Calendar._add_evs(cell);
+ cell.calendar = cal;
+ cell.navtype = navtype;
+ if (text.substr(0, 1) != "&") {
+ cell.appendChild(document.createTextNode(text));
+ }
+ else {
+ // FIXME: dirty hack for entities
+ cell.innerHTML = text;
+ }
+ return cell;
+ };
+
+ row = Calendar.createElement("tr", thead);
+ var title_length = 6;
+ (this.isPopup) && --title_length;
+ (this.weekNumbers) && ++title_length;
+
+ hh("?", 1, 400).ttip = Calendar._TT["INFO"];
+ this.title = hh("", title_length, 300);
+ this.title.className = "title";
+ if (this.isPopup) {
+ this.title.ttip = Calendar._TT["DRAG_TO_MOVE"];
+ this.title.style.cursor = "move";
+ hh("&#x00d7;", 1, 200).ttip = Calendar._TT["CLOSE"];
+ }
+
+ row = Calendar.createElement("tr", thead);
+ row.className = "headrow";
+
+ this._nav_py = hh("&#x00ab;", 1, -2);
+ this._nav_py.ttip = Calendar._TT["PREV_YEAR"];
+
+ this._nav_pm = hh("&#x2039;", 1, -1);
+ this._nav_pm.ttip = Calendar._TT["PREV_MONTH"];
+
+ this._nav_now = hh(Calendar._TT["TODAY"], this.weekNumbers ? 4 : 3, 0);
+ this._nav_now.ttip = Calendar._TT["GO_TODAY"];
+
+ this._nav_nm = hh("&#x203a;", 1, 1);
+ this._nav_nm.ttip = Calendar._TT["NEXT_MONTH"];
+
+ this._nav_ny = hh("&#x00bb;", 1, 2);
+ this._nav_ny.ttip = Calendar._TT["NEXT_YEAR"];
+
+ // day names
+ row = Calendar.createElement("tr", thead);
+ row.className = "daynames";
+ if (this.weekNumbers) {
+ cell = Calendar.createElement("td", row);
+ cell.className = "name wn";
+ cell.appendChild(document.createTextNode(Calendar._TT["WK"]));
+ }
+ for (var i = 7; i > 0; --i) {
+ cell = Calendar.createElement("td", row);
+ cell.appendChild(document.createTextNode(""));
+ if (!i) {
+ cell.navtype = 100;
+ cell.calendar = this;
+ Calendar._add_evs(cell);
+ }
+ }
+ this.firstdayname = (this.weekNumbers) ? row.firstChild.nextSibling : row.firstChild;
+ this._displayWeekdays();
+
+ var tbody = Calendar.createElement("tbody", table);
+ this.tbody = tbody;
+
+ for (i = 6; i > 0; --i) {
+ row = Calendar.createElement("tr", tbody);
+ if (this.weekNumbers) {
+ cell = Calendar.createElement("td", row);
+ cell.appendChild(document.createTextNode(""));
+ }
+ for (var j = 7; j > 0; --j) {
+ cell = Calendar.createElement("td", row);
+ cell.appendChild(document.createTextNode(""));
+ cell.calendar = this;
+ Calendar._add_evs(cell);
+ }
+ }
+
+ if (this.showsTime) {
+ row = Calendar.createElement("tr", tbody);
+ row.className = "time";
+
+ cell = Calendar.createElement("td", row);
+ cell.className = "time";
+ cell.colSpan = 2;
+ cell.innerHTML = Calendar._TT["TIME"] || "&nbsp;";
+
+ cell = Calendar.createElement("td", row);
+ cell.className = "time";
+ cell.colSpan = this.weekNumbers ? 4 : 3;
+
+ (function(){
+ function makeTimePart(className, init, range_start, range_end) {
+ var part = Calendar.createElement("span", cell);
+ part.className = className;
+ part.appendChild(document.createTextNode(init));
+ part.calendar = cal;
+ part.ttip = Calendar._TT["TIME_PART"];
+ part.navtype = 50;
+ part._range = [];
+ if (typeof range_start != "number")
+ part._range = range_start;
+ else {
+ for (var i = range_start; i <= range_end; ++i) {
+ var txt;
+ if (i < 10 && range_end >= 10) txt = '0' + i;
+ else txt = '' + i;
+ part._range[part._range.length] = txt;
+ }
+ }
+ Calendar._add_evs(part);
+ return part;
+ };
+ var hrs = cal.date.getHours();
+ var mins = cal.date.getMinutes();
+ var t12 = !cal.time24;
+ var pm = (hrs > 12);
+ if (t12 && pm) hrs -= 12;
+ var H = makeTimePart("hour", hrs, t12 ? 1 : 0, t12 ? 12 : 23);
+ var span = Calendar.createElement("span", cell);
+ span.appendChild(document.createTextNode(":"));
+ span.className = "colon";
+ var M = makeTimePart("minute", mins, 0, 59);
+ var AP = null;
+ cell = Calendar.createElement("td", row);
+ cell.className = "time";
+ cell.colSpan = 2;
+ if (t12)
+ AP = makeTimePart("ampm", pm ? "pm" : "am", ["am", "pm"]);
+ else
+ cell.innerHTML = "&nbsp;";
+
+ cal.onSetTime = function() {
+ var hrs = this.date.getHours();
+ var mins = this.date.getMinutes();
+ var pm = (hrs > 12);
+ if (pm && t12) hrs -= 12;
+ H.firstChild.data = (hrs < 10) ? ("0" + hrs) : hrs;
+ M.firstChild.data = (mins < 10) ? ("0" + mins) : mins;
+ if (t12)
+ AP.firstChild.data = pm ? "pm" : "am";
+ };
+
+ cal.onUpdateTime = function() {
+ var date = this.date;
+ var h = parseInt(H.firstChild.data, 10);
+ if (t12) {
+ if (/pm/i.test(AP.firstChild.data) && h < 12)
+ h += 12;
+ else if (/am/i.test(AP.firstChild.data) && h == 12)
+ h = 0;
+ }
+ var d = date.getDate();
+ var m = date.getMonth();
+ var y = date.getFullYear();
+ date.setHours(h);
+ date.setMinutes(parseInt(M.firstChild.data, 10));
+ date.setFullYear(y);
+ date.setMonth(m);
+ date.setDate(d);
+ this.dateClicked = false;
+ this.callHandler();
+ };
+ })();
+ } else {
+ this.onSetTime = this.onUpdateTime = function() {};
+ }
+
+ var tfoot = Calendar.createElement("tfoot", table);
+
+ row = Calendar.createElement("tr", tfoot);
+ row.className = "footrow";
+
+ cell = hh(Calendar._TT["SEL_DATE"], this.weekNumbers ? 8 : 7, 300);
+ cell.className = "ttip";
+ if (this.isPopup) {
+ cell.ttip = Calendar._TT["DRAG_TO_MOVE"];
+ cell.style.cursor = "move";
+ }
+ this.tooltips = cell;
+
+ div = Calendar.createElement("div", this.element);
+ this.monthsCombo = div;
+ div.className = "combo";
+ for (i = 0; i < Calendar._MN.length; ++i) {
+ var mn = Calendar.createElement("div");
+ mn.className = Calendar.is_ie ? "label-IEfix" : "label";
+ mn.month = i;
+ mn.appendChild(document.createTextNode(Calendar._SMN[i]));
+ div.appendChild(mn);
+ }
+
+ div = Calendar.createElement("div", this.element);
+ this.yearsCombo = div;
+ div.className = "combo";
+ for (i = 12; i > 0; --i) {
+ var yr = Calendar.createElement("div");
+ yr.className = Calendar.is_ie ? "label-IEfix" : "label";
+ yr.appendChild(document.createTextNode(""));
+ div.appendChild(yr);
+ }
+
+ this._init(this.firstDayOfWeek, this.date);
+ parent.appendChild(this.element);
+};
+
+/** keyboard navigation, only for popup calendars */
+Calendar._keyEvent = function(ev) {
+ if (!window.calendar) {
+ return false;
+ }
+ (Calendar.is_ie) && (ev = window.event);
+ var cal = window.calendar;
+ var act = (Calendar.is_ie || ev.type == "keypress");
+ if (ev.ctrlKey) {
+ switch (ev.keyCode) {
+ case 37: // KEY left
+ act && Calendar.cellClick(cal._nav_pm);
+ break;
+ case 38: // KEY up
+ act && Calendar.cellClick(cal._nav_py);
+ break;
+ case 39: // KEY right
+ act && Calendar.cellClick(cal._nav_nm);
+ break;
+ case 40: // KEY down
+ act && Calendar.cellClick(cal._nav_ny);
+ break;
+ default:
+ return false;
+ }
+ } else switch (ev.keyCode) {
+ case 32: // KEY space (now)
+ Calendar.cellClick(cal._nav_now);
+ break;
+ case 27: // KEY esc
+ act && cal.callCloseHandler();
+ break;
+ case 37: // KEY left
+ case 38: // KEY up
+ case 39: // KEY right
+ case 40: // KEY down
+ if (act) {
+ var date = cal.date.getDate() - 1;
+ var el = cal.currentDateEl;
+ var ne = null;
+ var prev = (ev.keyCode == 37) || (ev.keyCode == 38);
+ switch (ev.keyCode) {
+ case 37: // KEY left
+ (--date >= 0) && (ne = cal.ar_days[date]);
+ break;
+ case 38: // KEY up
+ date -= 7;
+ (date >= 0) && (ne = cal.ar_days[date]);
+ break;
+ case 39: // KEY right
+ (++date < cal.ar_days.length) && (ne = cal.ar_days[date]);
+ break;
+ case 40: // KEY down
+ date += 7;
+ (date < cal.ar_days.length) && (ne = cal.ar_days[date]);
+ break;
+ }
+ if (!ne) {
+ if (prev) {
+ Calendar.cellClick(cal._nav_pm);
+ } else {
+ Calendar.cellClick(cal._nav_nm);
+ }
+ date = (prev) ? cal.date.getMonthDays() : 1;
+ el = cal.currentDateEl;
+ ne = cal.ar_days[date - 1];
+ }
+ Calendar.removeClass(el, "selected");
+ Calendar.addClass(ne, "selected");
+ cal.date = new Date(ne.caldate);
+ cal.callHandler();
+ cal.currentDateEl = ne;
+ }
+ break;
+ case 13: // KEY enter
+ if (act) {
+ cal.callHandler();
+ cal.hide();
+ }
+ break;
+ default:
+ return false;
+ }
+ return Calendar.stopEvent(ev);
+};
+
+/**
+ * (RE)Initializes the calendar to the given date and firstDayOfWeek
+ */
+Calendar.prototype._init = function (firstDayOfWeek, date) {
+ var today = new Date();
+ this.table.style.visibility = "hidden";
+ var year = date.getFullYear();
+ if (year < this.minYear) {
+ year = this.minYear;
+ date.setFullYear(year);
+ } else if (year > this.maxYear) {
+ year = this.maxYear;
+ date.setFullYear(year);
+ }
+ this.firstDayOfWeek = firstDayOfWeek;
+ this.date = new Date(date);
+ var month = date.getMonth();
+ var mday = date.getDate();
+ var no_days = date.getMonthDays();
+
+ // calendar voodoo for computing the first day that would actually be
+ // displayed in the calendar, even if it's from the previous month.
+ // WARNING: this is magic. ;-)
+ date.setDate(1);
+ var day1 = (date.getDay() - this.firstDayOfWeek) % 7;
+ if (day1 < 0)
+ day1 += 7;
+ date.setDate(-day1);
+ date.setDate(date.getDate() + 1);
+
+ var row = this.tbody.firstChild;
+ var MN = Calendar._SMN[month];
+ var ar_days = new Array();
+ var weekend = Calendar._TT["WEEKEND"];
+ for (var i = 0; i < 6; ++i, row = row.nextSibling) {
+ var cell = row.firstChild;
+ if (this.weekNumbers) {
+ cell.className = "day wn";
+ cell.firstChild.data = date.getWeekNumber();
+ cell = cell.nextSibling;
+ }
+ row.className = "daysrow";
+ var hasdays = false;
+ for (var j = 0; j < 7; ++j, cell = cell.nextSibling, date.setDate(date.getDate() + 1)) {
+ var iday = date.getDate();
+ var wday = date.getDay();
+ cell.className = "day";
+ var current_month = (date.getMonth() == month);
+ if (!current_month) {
+ if (this.showsOtherMonths) {
+ cell.className += " othermonth";
+ cell.otherMonth = true;
+ } else {
+ cell.className = "emptycell";
+ cell.innerHTML = "&nbsp;";
+ cell.disabled = true;
+ continue;
+ }
+ } else {
+ cell.otherMonth = false;
+ hasdays = true;
+ }
+ cell.disabled = false;
+ cell.firstChild.data = iday;
+ if (typeof this.getDateStatus == "function") {
+ var status = this.getDateStatus(date, year, month, iday);
+ if (status === true) {
+ cell.className += " disabled";
+ cell.disabled = true;
+ } else {
+ if (/disabled/i.test(status))
+ cell.disabled = true;
+ cell.className += " " + status;
+ }
+ }
+ if (!cell.disabled) {
+ ar_days[ar_days.length] = cell;
+ cell.caldate = new Date(date);
+ cell.ttip = "_";
+ if (current_month && iday == mday) {
+ cell.className += " selected";
+ this.currentDateEl = cell;
+ }
+ if (date.getFullYear() == today.getFullYear() &&
+ date.getMonth() == today.getMonth() &&
+ iday == today.getDate()) {
+ cell.className += " today";
+ cell.ttip += Calendar._TT["PART_TODAY"];
+ }
+ if (weekend.indexOf(wday.toString()) != -1) {
+ cell.className += cell.otherMonth ? " oweekend" : " weekend";
+ }
+ }
+ }
+ if (!(hasdays || this.showsOtherMonths))
+ row.className = "emptyrow";
+ }
+ this.ar_days = ar_days;
+ this.title.firstChild.data = Calendar._MN[month] + ", " + year;
+ this.onSetTime();
+ this.table.style.visibility = "visible";
+ // PROFILE
+ // this.tooltips.firstChild.data = "Generated in " + ((new Date()) - today) + " ms";
+};
+
+/**
+ * Calls _init function above for going to a certain date (but only if the
+ * date is different than the currently selected one).
+ */
+Calendar.prototype.setDate = function (date) {
+ if (!date.equalsTo(this.date)) {
+ this._init(this.firstDayOfWeek, date);
+ }
+};
+
+/**
+ * Refreshes the calendar. Useful if the "disabledHandler" function is
+ * dynamic, meaning that the list of disabled date can change at runtime.
+ * Just * call this function if you think that the list of disabled dates
+ * should * change.
+ */
+Calendar.prototype.refresh = function () {
+ this._init(this.firstDayOfWeek, this.date);
+};
+
+/** Modifies the "firstDayOfWeek" parameter (pass 0 for Synday, 1 for Monday, etc.). */
+Calendar.prototype.setFirstDayOfWeek = function (firstDayOfWeek) {
+ this._init(firstDayOfWeek, this.date);
+ this._displayWeekdays();
+};
+
+/**
+ * Allows customization of what dates are enabled. The "unaryFunction"
+ * parameter must be a function object that receives the date (as a JS Date
+ * object) and returns a boolean value. If the returned value is true then
+ * the passed date will be marked as disabled.
+ */
+Calendar.prototype.setDateStatusHandler = Calendar.prototype.setDisabledHandler = function (unaryFunction) {
+ this.getDateStatus = unaryFunction;
+};
+
+/** Customization of allowed year range for the calendar. */
+Calendar.prototype.setRange = function (a, z) {
+ this.minYear = a;
+ this.maxYear = z;
+};
+
+/** Calls the first user handler (selectedHandler). */
+Calendar.prototype.callHandler = function () {
+ if (this.onSelected) {
+ this.onSelected(this, this.date.print(this.dateFormat));
+ }
+};
+
+/** Calls the second user handler (closeHandler). */
+Calendar.prototype.callCloseHandler = function () {
+ if (this.onClose) {
+ this.onClose(this);
+ }
+ this.hideShowCovered();
+};
+
+/** Removes the calendar object from the DOM tree and destroys it. */
+Calendar.prototype.destroy = function () {
+ var el = this.element.parentNode;
+ el.removeChild(this.element);
+ Calendar._C = null;
+ window.calendar = null;
+};
+
+/**
+ * Moves the calendar element to a different section in the DOM tree (changes
+ * its parent).
+ */
+Calendar.prototype.reparent = function (new_parent) {
+ var el = this.element;
+ el.parentNode.removeChild(el);
+ new_parent.appendChild(el);
+};
+
+// This gets called when the user presses a mouse button anywhere in the
+// document, if the calendar is shown. If the click was outside the open
+// calendar this function closes it.
+Calendar._checkCalendar = function(ev) {
+ if (!window.calendar) {
+ return false;
+ }
+ var el = Calendar.is_ie ? Calendar.getElement(ev) : Calendar.getTargetElement(ev);
+ for (; el != null && el != calendar.element; el = el.parentNode);
+ if (el == null) {
+ // calls closeHandler which should hide the calendar.
+ window.calendar.callCloseHandler();
+ return Calendar.stopEvent(ev);
+ }
+};
+
+/** Shows the calendar. */
+Calendar.prototype.show = function () {
+ var rows = this.table.getElementsByTagName("tr");
+ for (var i = rows.length; i > 0;) {
+ var row = rows[--i];
+ Calendar.removeClass(row, "rowhilite");
+ var cells = row.getElementsByTagName("td");
+ for (var j = cells.length; j > 0;) {
+ var cell = cells[--j];
+ Calendar.removeClass(cell, "hilite");
+ Calendar.removeClass(cell, "active");
+ }
+ }
+ this.element.style.display = "block";
+ this.hidden = false;
+ if (this.isPopup) {
+ window.calendar = this;
+ Calendar.addEvent(document, "keydown", Calendar._keyEvent);
+ Calendar.addEvent(document, "keypress", Calendar._keyEvent);
+ Calendar.addEvent(document, "mousedown", Calendar._checkCalendar);
+ }
+ this.hideShowCovered();
+};
+
+/**
+ * Hides the calendar. Also removes any "hilite" from the class of any TD
+ * element.
+ */
+Calendar.prototype.hide = function () {
+ if (this.isPopup) {
+ Calendar.removeEvent(document, "keydown", Calendar._keyEvent);
+ Calendar.removeEvent(document, "keypress", Calendar._keyEvent);
+ Calendar.removeEvent(document, "mousedown", Calendar._checkCalendar);
+ }
+ this.element.style.display = "none";
+ this.hidden = true;
+ this.hideShowCovered();
+};
+
+/**
+ * Shows the calendar at a given absolute position (beware that, depending on
+ * the calendar element style -- position property -- this might be relative
+ * to the parent's containing rectangle).
+ */
+Calendar.prototype.showAt = function (x, y) {
+ var s = this.element.style;
+ s.left = x + "px";
+ s.top = y + "px";
+ this.show();
+};
+
+/** Shows the calendar near a given element. */
+Calendar.prototype.showAtElement = function (el, opts) {
+ var self = this;
+ var p = Calendar.getAbsolutePos(el);
+ if (!opts || typeof opts != "string") {
+ this.showAt(p.x, p.y + el.offsetHeight);
+ return true;
+ }
+ function fixPosition(box) {
+ if (box.x < 0)
+ box.x = 0;
+ if (box.y < 0)
+ box.y = 0;
+ var cp = document.createElement("div");
+ var s = cp.style;
+ s.position = "absolute";
+ s.right = s.bottom = s.width = s.height = "0px";
+ document.body.appendChild(cp);
+ var br = Calendar.getAbsolutePos(cp);
+ document.body.removeChild(cp);
+ if (Calendar.is_ie) {
+ br.y += document.body.scrollTop;
+ br.x += document.body.scrollLeft;
+ } else {
+ br.y += window.scrollY;
+ br.x += window.scrollX;
+ }
+ var tmp = box.x + box.width - br.x;
+ if (tmp > 0) box.x -= tmp;
+ tmp = box.y + box.height - br.y;
+ if (tmp > 0) box.y -= tmp;
+ };
+ this.element.style.display = "block";
+ Calendar.continuation_for_the_fucking_khtml_browser = function() {
+ var w = self.element.offsetWidth;
+ var h = self.element.offsetHeight;
+ self.element.style.display = "none";
+ var valign = opts.substr(0, 1);
+ var halign = "l";
+ if (opts.length > 1) {
+ halign = opts.substr(1, 1);
+ }
+ // vertical alignment
+ switch (valign) {
+ case "T": p.y -= h; break;
+ case "B": p.y += el.offsetHeight; break;
+ case "C": p.y += (el.offsetHeight - h) / 2; break;
+ case "t": p.y += el.offsetHeight - h; break;
+ case "b": break; // already there
+ }
+ // horizontal alignment
+ switch (halign) {
+ case "L": p.x -= w; break;
+ case "R": p.x += el.offsetWidth; break;
+ case "C": p.x += (el.offsetWidth - w) / 2; break;
+ case "r": p.x += el.offsetWidth - w; break;
+ case "l": break; // already there
+ }
+ p.width = w;
+ p.height = h + 40;
+ self.monthsCombo.style.display = "none";
+ fixPosition(p);
+ self.showAt(p.x, p.y);
+ };
+ if (Calendar.is_khtml)
+ setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()", 10);
+ else
+ Calendar.continuation_for_the_fucking_khtml_browser();
+};
+
+/** Customizes the date format. */
+Calendar.prototype.setDateFormat = function (str) {
+ this.dateFormat = str;
+};
+
+/** Customizes the tooltip date format. */
+Calendar.prototype.setTtDateFormat = function (str) {
+ this.ttDateFormat = str;
+};
+
+/**
+ * Tries to identify the date represented in a string. If successful it also
+ * calls this.setDate which moves the calendar to the given date.
+ */
+Calendar.prototype.parseDate = function (str, fmt) {
+ var y = 0;
+ var m = -1;
+ var d = 0;
+ var a = str.split(/\W+/);
+ if (!fmt) {
+ fmt = this.dateFormat;
+ }
+ var b = fmt.match(/%./g);
+ var i = 0, j = 0;
+ var hr = 0;
+ var min = 0;
+ for (i = 0; i < a.length; ++i) {
+ if (!a[i])
+ continue;
+ switch (b[i]) {
+ case "%d":
+ case "%e":
+ d = parseInt(a[i], 10);
+ break;
+
+ case "%m":
+ m = parseInt(a[i], 10) - 1;
+ break;
+
+ case "%Y":
+ case "%y":
+ y = parseInt(a[i], 10);
+ (y < 100) && (y += (y > 29) ? 1900 : 2000);
+ break;
+
+ case "%b":
+ case "%B":
+ for (j = 0; j < 12; ++j) {
+ if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { m = j; break; }
+ }
+ break;
+
+ case "%H":
+ case "%I":
+ case "%k":
+ case "%l":
+ hr = parseInt(a[i], 10);
+ break;
+
+ case "%P":
+ case "%p":
+ if (/pm/i.test(a[i]) && hr < 12)
+ hr += 12;
+ break;
+
+ case "%M":
+ min = parseInt(a[i], 10);
+ break;
+ }
+ }
+ if (y != 0 && m != -1 && d != 0) {
+ this.setDate(new Date(y, m, d, hr, min, 0));
+ return;
+ }
+ y = 0; m = -1; d = 0;
+ for (i = 0; i < a.length; ++i) {
+ if (a[i].search(/[a-zA-Z]+/) != -1) {
+ var t = -1;
+ for (j = 0; j < 12; ++j) {
+ if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { t = j; break; }
+ }
+ if (t != -1) {
+ if (m != -1) {
+ d = m+1;
+ }
+ m = t;
+ }
+ } else if (parseInt(a[i], 10) <= 12 && m == -1) {
+ m = a[i]-1;
+ } else if (parseInt(a[i], 10) > 31 && y == 0) {
+ y = parseInt(a[i], 10);
+ (y < 100) && (y += (y > 29) ? 1900 : 2000);
+ } else if (d == 0) {
+ d = a[i];
+ }
+ }
+ if (y == 0) {
+ var today = new Date();
+ y = today.getFullYear();
+ }
+ if (m != -1 && d != 0) {
+ this.setDate(new Date(y, m, d, hr, min, 0));
+ }
+};
+
+Calendar.prototype.hideShowCovered = function () {
+ var self = this;
+ Calendar.continuation_for_the_fucking_khtml_browser = function() {
+ function getVisib(obj){
+ var value = obj.style.visibility;
+ if (!value) {
+ if (document.defaultView && typeof (document.defaultView.getComputedStyle) == "function") { // Gecko, W3C
+ if (!Calendar.is_khtml)
+ value = document.defaultView.
+ getComputedStyle(obj, "").getPropertyValue("visibility");
+ else
+ value = '';
+ } else if (obj.currentStyle) { // IE
+ value = obj.currentStyle.visibility;
+ } else
+ value = '';
+ }
+ return value;
+ };
+
+ var tags = new Array("applet", "iframe", "select");
+ var el = self.element;
+
+ var p = Calendar.getAbsolutePos(el);
+ var EX1 = p.x;
+ var EX2 = el.offsetWidth + EX1;
+ var EY1 = p.y;
+ var EY2 = el.offsetHeight + EY1;
+
+ for (var k = tags.length; k > 0; ) {
+ var ar = document.getElementsByTagName(tags[--k]);
+ var cc = null;
+
+ for (var i = ar.length; i > 0;) {
+ cc = ar[--i];
+
+ p = Calendar.getAbsolutePos(cc);
+ var CX1 = p.x;
+ var CX2 = cc.offsetWidth + CX1;
+ var CY1 = p.y;
+ var CY2 = cc.offsetHeight + CY1;
+
+ if (self.hidden || (CX1 > EX2) || (CX2 < EX1) || (CY1 > EY2) || (CY2 < EY1)) {
+ if (!cc.__msh_save_visibility) {
+ cc.__msh_save_visibility = getVisib(cc);
+ }
+ cc.style.visibility = cc.__msh_save_visibility;
+ } else {
+ if (!cc.__msh_save_visibility) {
+ cc.__msh_save_visibility = getVisib(cc);
+ }
+ cc.style.visibility = "hidden";
+ }
+ }
+ }
+ };
+ if (Calendar.is_khtml)
+ setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()", 10);
+ else
+ Calendar.continuation_for_the_fucking_khtml_browser();
+};
+
+/** Internal function; it displays the bar with the names of the weekday. */
+Calendar.prototype._displayWeekdays = function () {
+ var fdow = this.firstDayOfWeek;
+ var cell = this.firstdayname;
+ var weekend = Calendar._TT["WEEKEND"];
+ for (var i = 0; i < 7; ++i) {
+ cell.className = "day name";
+ var realday = (i + fdow) % 7;
+ if (i) {
+ cell.ttip = Calendar._TT["DAY_FIRST"].replace("%s", Calendar._DN[realday]);
+ cell.navtype = 100;
+ cell.calendar = this;
+ cell.fdow = realday;
+ Calendar._add_evs(cell);
+ }
+ if (weekend.indexOf(realday.toString()) != -1) {
+ Calendar.addClass(cell, "weekend");
+ }
+ cell.firstChild.data = Calendar._SDN[(i + fdow) % 7];
+ cell = cell.nextSibling;
+ }
+};
+
+/** Internal function. Hides all combo boxes that might be displayed. */
+Calendar.prototype._hideCombos = function () {
+ this.monthsCombo.style.display = "none";
+ this.yearsCombo.style.display = "none";
+};
+
+/** Internal function. Starts dragging the element. */
+Calendar.prototype._dragStart = function (ev) {
+ if (this.dragging) {
+ return;
+ }
+ this.dragging = true;
+ var posX;
+ var posY;
+ if (Calendar.is_ie) {
+ posY = window.event.clientY + document.body.scrollTop;
+ posX = window.event.clientX + document.body.scrollLeft;
+ } else {
+ posY = ev.clientY + window.scrollY;
+ posX = ev.clientX + window.scrollX;
+ }
+ var st = this.element.style;
+ this.xOffs = posX - parseInt(st.left);
+ this.yOffs = posY - parseInt(st.top);
+ with (Calendar) {
+ addEvent(document, "mousemove", calDragIt);
+ addEvent(document, "mouseup", calDragEnd);
+ }
+};
+
+// BEGIN: DATE OBJECT PATCHES
+
+/** Adds the number of days array to the Date object. */
+Date._MD = new Array(31,28,31,30,31,30,31,31,30,31,30,31);
+
+/** Constants used for time computations */
+Date.SECOND = 1000 /* milliseconds */;
+Date.MINUTE = 60 * Date.SECOND;
+Date.HOUR = 60 * Date.MINUTE;
+Date.DAY = 24 * Date.HOUR;
+Date.WEEK = 7 * Date.DAY;
+
+/** Returns the number of days in the current month */
+Date.prototype.getMonthDays = function(month) {
+ var year = this.getFullYear();
+ if (typeof month == "undefined") {
+ month = this.getMonth();
+ }
+ if (((0 == (year%4)) && ( (0 != (year%100)) || (0 == (year%400)))) && month == 1) {
+ return 29;
+ } else {
+ return Date._MD[month];
+ }
+};
+
+/** Returns the number of day in the year. */
+Date.prototype.getDayOfYear = function() {
+ var now = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0);
+ var then = new Date(this.getFullYear(), 0, 0, 0, 0, 0);
+ var time = now - then;
+ return Math.floor(time / Date.DAY);
+};
+
+/** Returns the number of the week in year, as defined in ISO 8601. */
+Date.prototype.getWeekNumber = function() {
+ var d = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0);
+ var DoW = d.getDay();
+ d.setDate(d.getDate() - (DoW + 6) % 7 + 3); // Nearest Thu
+ var ms = d.valueOf(); // GMT
+ d.setMonth(0);
+ d.setDate(4); // Thu in Week 1
+ return Math.round((ms - d.valueOf()) / (7 * 864e5)) + 1;
+};
+
+/** Checks dates equality (ignores time) */
+Date.prototype.equalsTo = function(date) {
+ return ((this.getFullYear() == date.getFullYear()) &&
+ (this.getMonth() == date.getMonth()) &&
+ (this.getDate() == date.getDate()) &&
+ (this.getHours() == date.getHours()) &&
+ (this.getMinutes() == date.getMinutes()));
+};
+
+/** Prints the date in a string according to the given format. */
+Date.prototype.print = function (str) {
+ var m = this.getMonth();
+ var d = this.getDate();
+ var y = this.getFullYear();
+ var wn = this.getWeekNumber();
+ var w = this.getDay();
+ var s = {};
+ var hr = this.getHours();
+ var pm = (hr >= 12);
+ var ir = (pm) ? (hr - 12) : hr;
+ var dy = this.getDayOfYear();
+ if (ir == 0)
+ ir = 12;
+ var min = this.getMinutes();
+ var sec = this.getSeconds();
+ s["%a"] = Calendar._SDN[w]; // abbreviated weekday name [FIXME: I18N]
+ s["%A"] = Calendar._DN[w]; // full weekday name
+ s["%b"] = Calendar._SMN[m]; // abbreviated month name [FIXME: I18N]
+ s["%B"] = Calendar._MN[m]; // full month name
+ // FIXME: %c : preferred date and time representation for the current locale
+ s["%C"] = 1 + Math.floor(y / 100); // the century number
+ s["%d"] = (d < 10) ? ("0" + d) : d; // the day of the month (range 01 to 31)
+ s["%e"] = d; // the day of the month (range 1 to 31)
+ // FIXME: %D : american date style: %m/%d/%y
+ // FIXME: %E, %F, %G, %g, %h (man strftime)
+ s["%H"] = (hr < 10) ? ("0" + hr) : hr; // hour, range 00 to 23 (24h format)
+ s["%I"] = (ir < 10) ? ("0" + ir) : ir; // hour, range 01 to 12 (12h format)
+ s["%j"] = (dy < 100) ? ((dy < 10) ? ("00" + dy) : ("0" + dy)) : dy; // day of the year (range 001 to 366)
+ s["%k"] = hr; // hour, range 0 to 23 (24h format)
+ s["%l"] = ir; // hour, range 1 to 12 (12h format)
+ s["%m"] = (m < 9) ? ("0" + (1+m)) : (1+m); // month, range 01 to 12
+ s["%M"] = (min < 10) ? ("0" + min) : min; // minute, range 00 to 59
+ s["%n"] = "\n"; // a newline character
+ s["%p"] = pm ? "PM" : "AM";
+ s["%P"] = pm ? "pm" : "am";
+ // FIXME: %r : the time in am/pm notation %I:%M:%S %p
+ // FIXME: %R : the time in 24-hour notation %H:%M
+ s["%s"] = Math.floor(this.getTime() / 1000);
+ s["%S"] = (sec < 10) ? ("0" + sec) : sec; // seconds, range 00 to 59
+ s["%t"] = "\t"; // a tab character
+ // FIXME: %T : the time in 24-hour notation (%H:%M:%S)
+ s["%U"] = s["%W"] = s["%V"] = (wn < 10) ? ("0" + wn) : wn;
+ s["%u"] = w + 1; // the day of the week (range 1 to 7, 1 = MON)
+ s["%w"] = w; // the day of the week (range 0 to 6, 0 = SUN)
+ // FIXME: %x : preferred date representation for the current locale without the time
+ // FIXME: %X : preferred time representation for the current locale without the date
+ s["%y"] = ('' + y).substr(2, 2); // year without the century (range 00 to 99)
+ s["%Y"] = y; // year with the century
+ s["%%"] = "%"; // a literal '%' character
+
+ var re = /%./g;
+ if (!Calendar.is_ie5)
+ return str.replace(re, function (par) { return s[par] || par; });
+
+ var a = str.match(re);
+ for (var i = 0; i < a.length; i++) {
+ var tmp = s[a[i]];
+ if (tmp) {
+ re = new RegExp(a[i], 'g');
+ str = str.replace(re, tmp);
+ }
+ }
+
+ return str;
+};
+
+Date.prototype.__msh_oldSetFullYear = Date.prototype.setFullYear;
+Date.prototype.setFullYear = function(y) {
+ var d = new Date(this);
+ d.__msh_oldSetFullYear(y);
+ if (d.getMonth() != this.getMonth())
+ this.setDate(28);
+ this.__msh_oldSetFullYear(y);
+};
+
+// END: DATE OBJECT PATCHES
+
+
+// global object that remembers the calendar
+window.calendar = null;
diff --git a/httemplate/elements/calendar_stripped.js b/httemplate/elements/calendar_stripped.js
new file mode 100644
index 0000000..6a8e326
--- /dev/null
+++ b/httemplate/elements/calendar_stripped.js
@@ -0,0 +1,12 @@
+/* Copyright Mihai Bazon, 2002, 2003 | http://dynarch.com/mishoo/
+ * ------------------------------------------------------------------
+ *
+ * The DHTML Calendar, version 0.9.6 "Keep cool but don't freeze"
+ *
+ * Details and latest version at:
+ * http://dynarch.com/mishoo/calendar.epl
+ *
+ * This script is distributed under the GNU Lesser General Public License.
+ * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html
+ */
+ Calendar=function(firstDayOfWeek,dateStr,onSelected,onClose){this.activeDiv=null;this.currentDateEl=null;this.getDateStatus=null;this.timeout=null;this.onSelected=onSelected||null;this.onClose=onClose||null;this.dragging=false;this.hidden=false;this.minYear=1970;this.maxYear=2050;this.dateFormat=Calendar._TT["DEF_DATE_FORMAT"];this.ttDateFormat=Calendar._TT["TT_DATE_FORMAT"];this.isPopup=true;this.weekNumbers=true;this.firstDayOfWeek=firstDayOfWeek;this.showsOtherMonths=false;this.dateStr=dateStr;this.ar_days=null;this.showsTime=false;this.time24=true;this.yearStep=2;this.table=null;this.element=null;this.tbody=null;this.firstdayname=null;this.monthsCombo=null;this.yearsCombo=null;this.hilitedMonth=null;this.activeMonth=null;this.hilitedYear=null;this.activeYear=null;this.dateClicked=false;if(typeof Calendar._SDN=="undefined"){if(typeof Calendar._SDN_len=="undefined")Calendar._SDN_len=3;var ar=new Array();for(var i=8;i>0;){ar[--i]=Calendar._DN[i].substr(0,Calendar._SDN_len);}Calendar._SDN=ar;if(typeof Calendar._SMN_len=="undefined")Calendar._SMN_len=3;ar=new Array();for(var i=12;i>0;){ar[--i]=Calendar._MN[i].substr(0,Calendar._SMN_len);}Calendar._SMN=ar;}};Calendar._C=null;Calendar.is_ie=(/msie/i.test(navigator.userAgent)&&!/opera/i.test(navigator.userAgent));Calendar.is_ie5=(Calendar.is_ie&&/msie 5\.0/i.test(navigator.userAgent));Calendar.is_opera=/opera/i.test(navigator.userAgent);Calendar.is_khtml=/Konqueror|Safari|KHTML/i.test(navigator.userAgent);Calendar.getAbsolutePos=function(el){var SL=0,ST=0;var is_div=/^div$/i.test(el.tagName);if(is_div&&el.scrollLeft)SL=el.scrollLeft;if(is_div&&el.scrollTop)ST=el.scrollTop;var r={x:el.offsetLeft-SL,y:el.offsetTop-ST};if(el.offsetParent){var tmp=this.getAbsolutePos(el.offsetParent);r.x+=tmp.x;r.y+=tmp.y;}return r;};Calendar.isRelated=function(el,evt){var related=evt.relatedTarget;if(!related){var type=evt.type;if(type=="mouseover"){related=evt.fromElement;}else if(type=="mouseout"){related=evt.toElement;}}while(related){if(related==el){return true;}related=related.parentNode;}return false;};Calendar.removeClass=function(el,className){if(!(el&&el.className)){return;}var cls=el.className.split(" ");var ar=new Array();for(var i=cls.length;i>0;){if(cls[--i]!=className){ar[ar.length]=cls[i];}}el.className=ar.join(" ");};Calendar.addClass=function(el,className){Calendar.removeClass(el,className);el.className+=" "+className;};Calendar.getElement=function(ev){if(Calendar.is_ie){return window.event.srcElement;}else{return ev.currentTarget;}};Calendar.getTargetElement=function(ev){if(Calendar.is_ie){return window.event.srcElement;}else{return ev.target;}};Calendar.stopEvent=function(ev){ev||(ev=window.event);if(Calendar.is_ie){ev.cancelBubble=true;ev.returnValue=false;}else{ev.preventDefault();ev.stopPropagation();}return false;};Calendar.addEvent=function(el,evname,func){if(el.attachEvent){el.attachEvent("on"+evname,func);}else if(el.addEventListener){el.addEventListener(evname,func,true);}else{el["on"+evname]=func;}};Calendar.removeEvent=function(el,evname,func){if(el.detachEvent){el.detachEvent("on"+evname,func);}else if(el.removeEventListener){el.removeEventListener(evname,func,true);}else{el["on"+evname]=null;}};Calendar.createElement=function(type,parent){var el=null;if(document.createElementNS){el=document.createElementNS("http://www.w3.org/1999/xhtml",type);}else{el=document.createElement(type);}if(typeof parent!="undefined"){parent.appendChild(el);}return el;};Calendar._add_evs=function(el){with(Calendar){addEvent(el,"mouseover",dayMouseOver);addEvent(el,"mousedown",dayMouseDown);addEvent(el,"mouseout",dayMouseOut);if(is_ie){addEvent(el,"dblclick",dayMouseDblClick);el.setAttribute("unselectable",true);}}};Calendar.findMonth=function(el){if(typeof el.month!="undefined"){return el;}else if(typeof el.parentNode.month!="undefined"){return el.parentNode;}return null;};Calendar.findYear=function(el){if(typeof el.year!="undefined"){return el;}else if(typeof el.parentNode.year!="undefined"){return el.parentNode;}return null;};Calendar.showMonthsCombo=function(){var cal=Calendar._C;if(!cal){return false;}var cal=cal;var cd=cal.activeDiv;var mc=cal.monthsCombo;if(cal.hilitedMonth){Calendar.removeClass(cal.hilitedMonth,"hilite");}if(cal.activeMonth){Calendar.removeClass(cal.activeMonth,"active");}var mon=cal.monthsCombo.getElementsByTagName("div")[cal.date.getMonth()];Calendar.addClass(mon,"active");cal.activeMonth=mon;var s=mc.style;s.display="block";if(cd.navtype<0)s.left=cd.offsetLeft+"px";else{var mcw=mc.offsetWidth;if(typeof mcw=="undefined")mcw=50;s.left=(cd.offsetLeft+cd.offsetWidth-mcw)+"px";}s.top=(cd.offsetTop+cd.offsetHeight)+"px";};Calendar.showYearsCombo=function(fwd){var cal=Calendar._C;if(!cal){return false;}var cal=cal;var cd=cal.activeDiv;var yc=cal.yearsCombo;if(cal.hilitedYear){Calendar.removeClass(cal.hilitedYear,"hilite");}if(cal.activeYear){Calendar.removeClass(cal.activeYear,"active");}cal.activeYear=null;var Y=cal.date.getFullYear()+(fwd?1:-1);var yr=yc.firstChild;var show=false;for(var i=12;i>0;--i){if(Y>=cal.minYear&&Y<=cal.maxYear){yr.firstChild.data=Y;yr.year=Y;yr.style.display="block";show=true;}else{yr.style.display="none";}yr=yr.nextSibling;Y+=fwd?cal.yearStep:-cal.yearStep;}if(show){var s=yc.style;s.display="block";if(cd.navtype<0)s.left=cd.offsetLeft+"px";else{var ycw=yc.offsetWidth;if(typeof ycw=="undefined")ycw=50;s.left=(cd.offsetLeft+cd.offsetWidth-ycw)+"px";}s.top=(cd.offsetTop+cd.offsetHeight)+"px";}};Calendar.tableMouseUp=function(ev){var cal=Calendar._C;if(!cal){return false;}if(cal.timeout){clearTimeout(cal.timeout);}var el=cal.activeDiv;if(!el){return false;}var target=Calendar.getTargetElement(ev);ev||(ev=window.event);Calendar.removeClass(el,"active");if(target==el||target.parentNode==el){Calendar.cellClick(el,ev);}var mon=Calendar.findMonth(target);var date=null;if(mon){date=new Date(cal.date);if(mon.month!=date.getMonth()){date.setMonth(mon.month);cal.setDate(date);cal.dateClicked=false;cal.callHandler();}}else{var year=Calendar.findYear(target);if(year){date=new Date(cal.date);if(year.year!=date.getFullYear()){date.setFullYear(year.year);cal.setDate(date);cal.dateClicked=false;cal.callHandler();}}}with(Calendar){removeEvent(document,"mouseup",tableMouseUp);removeEvent(document,"mouseover",tableMouseOver);removeEvent(document,"mousemove",tableMouseOver);cal._hideCombos();_C=null;return stopEvent(ev);}};Calendar.tableMouseOver=function(ev){var cal=Calendar._C;if(!cal){return;}var el=cal.activeDiv;var target=Calendar.getTargetElement(ev);if(target==el||target.parentNode==el){Calendar.addClass(el,"hilite active");Calendar.addClass(el.parentNode,"rowhilite");}else{if(typeof el.navtype=="undefined"||(el.navtype!=50&&(el.navtype==0||Math.abs(el.navtype)>2)))Calendar.removeClass(el,"active");Calendar.removeClass(el,"hilite");Calendar.removeClass(el.parentNode,"rowhilite");}ev||(ev=window.event);if(el.navtype==50&&target!=el){var pos=Calendar.getAbsolutePos(el);var w=el.offsetWidth;var x=ev.clientX;var dx;var decrease=true;if(x>pos.x+w){dx=x-pos.x-w;decrease=false;}else dx=pos.x-x;if(dx<0)dx=0;var range=el._range;var current=el._current;var count=Math.floor(dx/10)%range.length;for(var i=range.length;--i>=0;)if(range[i]==current)break;while(count-->0)if(decrease){if(--i<0)i=range.length-1;}else if(++i>=range.length)i=0;var newval=range[i];el.firstChild.data=newval;cal.onUpdateTime();}var mon=Calendar.findMonth(target);if(mon){if(mon.month!=cal.date.getMonth()){if(cal.hilitedMonth){Calendar.removeClass(cal.hilitedMonth,"hilite");}Calendar.addClass(mon,"hilite");cal.hilitedMonth=mon;}else if(cal.hilitedMonth){Calendar.removeClass(cal.hilitedMonth,"hilite");}}else{if(cal.hilitedMonth){Calendar.removeClass(cal.hilitedMonth,"hilite");}var year=Calendar.findYear(target);if(year){if(year.year!=cal.date.getFullYear()){if(cal.hilitedYear){Calendar.removeClass(cal.hilitedYear,"hilite");}Calendar.addClass(year,"hilite");cal.hilitedYear=year;}else if(cal.hilitedYear){Calendar.removeClass(cal.hilitedYear,"hilite");}}else if(cal.hilitedYear){Calendar.removeClass(cal.hilitedYear,"hilite");}}return Calendar.stopEvent(ev);};Calendar.tableMouseDown=function(ev){if(Calendar.getTargetElement(ev)==Calendar.getElement(ev)){return Calendar.stopEvent(ev);}};Calendar.calDragIt=function(ev){var cal=Calendar._C;if(!(cal&&cal.dragging)){return false;}var posX;var posY;if(Calendar.is_ie){posY=window.event.clientY+document.body.scrollTop;posX=window.event.clientX+document.body.scrollLeft;}else{posX=ev.pageX;posY=ev.pageY;}cal.hideShowCovered();var st=cal.element.style;st.left=(posX-cal.xOffs)+"px";st.top=(posY-cal.yOffs)+"px";return Calendar.stopEvent(ev);};Calendar.calDragEnd=function(ev){var cal=Calendar._C;if(!cal){return false;}cal.dragging=false;with(Calendar){removeEvent(document,"mousemove",calDragIt);removeEvent(document,"mouseup",calDragEnd);tableMouseUp(ev);}cal.hideShowCovered();};Calendar.dayMouseDown=function(ev){var el=Calendar.getElement(ev);if(el.disabled){return false;}var cal=el.calendar;cal.activeDiv=el;Calendar._C=cal;if(el.navtype!=300)with(Calendar){if(el.navtype==50){el._current=el.firstChild.data;addEvent(document,"mousemove",tableMouseOver);}else addEvent(document,Calendar.is_ie5?"mousemove":"mouseover",tableMouseOver);addClass(el,"hilite active");addEvent(document,"mouseup",tableMouseUp);}else if(cal.isPopup){cal._dragStart(ev);}if(el.navtype==-1||el.navtype==1){if(cal.timeout)clearTimeout(cal.timeout);cal.timeout=setTimeout("Calendar.showMonthsCombo()",250);}else if(el.navtype==-2||el.navtype==2){if(cal.timeout)clearTimeout(cal.timeout);cal.timeout=setTimeout((el.navtype>0)?"Calendar.showYearsCombo(true)":"Calendar.showYearsCombo(false)",250);}else{cal.timeout=null;}return Calendar.stopEvent(ev);};Calendar.dayMouseDblClick=function(ev){Calendar.cellClick(Calendar.getElement(ev),ev||window.event);if(Calendar.is_ie){document.selection.empty();}};Calendar.dayMouseOver=function(ev){var el=Calendar.getElement(ev);if(Calendar.isRelated(el,ev)||Calendar._C||el.disabled){return false;}if(el.ttip){if(el.ttip.substr(0,1)=="_"){el.ttip=el.caldate.print(el.calendar.ttDateFormat)+el.ttip.substr(1);}el.calendar.tooltips.firstChild.data=el.ttip;}if(el.navtype!=300){Calendar.addClass(el,"hilite");if(el.caldate){Calendar.addClass(el.parentNode,"rowhilite");}}return Calendar.stopEvent(ev);};Calendar.dayMouseOut=function(ev){with(Calendar){var el=getElement(ev);if(isRelated(el,ev)||_C||el.disabled){return false;}removeClass(el,"hilite");if(el.caldate){removeClass(el.parentNode,"rowhilite");}el.calendar.tooltips.firstChild.data=_TT["SEL_DATE"];return stopEvent(ev);}};Calendar.cellClick=function(el,ev){var cal=el.calendar;var closing=false;var newdate=false;var date=null;if(typeof el.navtype=="undefined"){Calendar.removeClass(cal.currentDateEl,"selected");Calendar.addClass(el,"selected");closing=(cal.currentDateEl==el);if(!closing){cal.currentDateEl=el;}cal.date=new Date(el.caldate);date=cal.date;newdate=true;if(!(cal.dateClicked=!el.otherMonth))cal._init(cal.firstDayOfWeek,date);}else{if(el.navtype==200){Calendar.removeClass(el,"hilite");cal.callCloseHandler();return;}date=(el.navtype==0)?new Date():new Date(cal.date);cal.dateClicked=false;var year=date.getFullYear();var mon=date.getMonth();function setMonth(m){var day=date.getDate();var max=date.getMonthDays(m);if(day>max){date.setDate(max);}date.setMonth(m);};switch(el.navtype){case 400:Calendar.removeClass(el,"hilite");var text=Calendar._TT["ABOUT"];if(typeof text!="undefined"){text+=cal.showsTime?Calendar._TT["ABOUT_TIME"]:"";}else{text="Help and about box text is not translated into this language.\n"+"If you know this language and you feel generous please update\n"+"the corresponding file in \"lang\" subdir to match calendar-en.js\n"+"and send it back to <mishoo@infoiasi.ro> to get it into the distribution ;-)\n\n"+"Thank you!\n"+"http://dynarch.com/mishoo/calendar.epl\n";}alert(text);return;case-2:if(year>cal.minYear){date.setFullYear(year-1);}break;case-1:if(mon>0){setMonth(mon-1);}else if(year-->cal.minYear){date.setFullYear(year);setMonth(11);}break;case 1:if(mon<11){setMonth(mon+1);}else if(year<cal.maxYear){date.setFullYear(year+1);setMonth(0);}break;case 2:if(year<cal.maxYear){date.setFullYear(year+1);}break;case 100:cal.setFirstDayOfWeek(el.fdow);return;case 50:var range=el._range;var current=el.firstChild.data;for(var i=range.length;--i>=0;)if(range[i]==current)break;if(ev&&ev.shiftKey){if(--i<0)i=range.length-1;}else if(++i>=range.length)i=0;var newval=range[i];el.firstChild.data=newval;cal.onUpdateTime();return;case 0:if((typeof cal.getDateStatus=="function")&&cal.getDateStatus(date,date.getFullYear(),date.getMonth(),date.getDate())){return false;}break;}if(!date.equalsTo(cal.date)){cal.setDate(date);newdate=true;}}if(newdate){cal.callHandler();}if(closing){Calendar.removeClass(el,"hilite");cal.callCloseHandler();}};Calendar.prototype.create=function(_par){var parent=null;if(!_par){parent=document.getElementsByTagName("body")[0];this.isPopup=true;}else{parent=_par;this.isPopup=false;}this.date=this.dateStr?new Date(this.dateStr):new Date();var table=Calendar.createElement("table");this.table=table;table.cellSpacing=0;table.cellPadding=0;table.calendar=this;Calendar.addEvent(table,"mousedown",Calendar.tableMouseDown);var div=Calendar.createElement("div");this.element=div;div.className="calendar";if(this.isPopup){div.style.position="absolute";div.style.display="none";}div.appendChild(table);var thead=Calendar.createElement("thead",table);var cell=null;var row=null;var cal=this;var hh=function(text,cs,navtype){cell=Calendar.createElement("td",row);cell.colSpan=cs;cell.className="button";if(navtype!=0&&Math.abs(navtype)<=2)cell.className+=" nav";Calendar._add_evs(cell);cell.calendar=cal;cell.navtype=navtype;if(text.substr(0,1)!="&"){cell.appendChild(document.createTextNode(text));}else{cell.innerHTML=text;}return cell;};row=Calendar.createElement("tr",thead);var title_length=6;(this.isPopup)&&--title_length;(this.weekNumbers)&&++title_length;hh("?",1,400).ttip=Calendar._TT["INFO"];this.title=hh("",title_length,300);this.title.className="title";if(this.isPopup){this.title.ttip=Calendar._TT["DRAG_TO_MOVE"];this.title.style.cursor="move";hh("&#x00d7;",1,200).ttip=Calendar._TT["CLOSE"];}row=Calendar.createElement("tr",thead);row.className="headrow";this._nav_py=hh("&#x00ab;",1,-2);this._nav_py.ttip=Calendar._TT["PREV_YEAR"];this._nav_pm=hh("&#x2039;",1,-1);this._nav_pm.ttip=Calendar._TT["PREV_MONTH"];this._nav_now=hh(Calendar._TT["TODAY"],this.weekNumbers?4:3,0);this._nav_now.ttip=Calendar._TT["GO_TODAY"];this._nav_nm=hh("&#x203a;",1,1);this._nav_nm.ttip=Calendar._TT["NEXT_MONTH"];this._nav_ny=hh("&#x00bb;",1,2);this._nav_ny.ttip=Calendar._TT["NEXT_YEAR"];row=Calendar.createElement("tr",thead);row.className="daynames";if(this.weekNumbers){cell=Calendar.createElement("td",row);cell.className="name wn";cell.appendChild(document.createTextNode(Calendar._TT["WK"]));}for(var i=7;i>0;--i){cell=Calendar.createElement("td",row);cell.appendChild(document.createTextNode(""));if(!i){cell.navtype=100;cell.calendar=this;Calendar._add_evs(cell);}}this.firstdayname=(this.weekNumbers)?row.firstChild.nextSibling:row.firstChild;this._displayWeekdays();var tbody=Calendar.createElement("tbody",table);this.tbody=tbody;for(i=6;i>0;--i){row=Calendar.createElement("tr",tbody);if(this.weekNumbers){cell=Calendar.createElement("td",row);cell.appendChild(document.createTextNode(""));}for(var j=7;j>0;--j){cell=Calendar.createElement("td",row);cell.appendChild(document.createTextNode(""));cell.calendar=this;Calendar._add_evs(cell);}}if(this.showsTime){row=Calendar.createElement("tr",tbody);row.className="time";cell=Calendar.createElement("td",row);cell.className="time";cell.colSpan=2;cell.innerHTML=Calendar._TT["TIME"]||"&nbsp;";cell=Calendar.createElement("td",row);cell.className="time";cell.colSpan=this.weekNumbers?4:3;(function(){function makeTimePart(className,init,range_start,range_end){var part=Calendar.createElement("span",cell);part.className=className;part.appendChild(document.createTextNode(init));part.calendar=cal;part.ttip=Calendar._TT["TIME_PART"];part.navtype=50;part._range=[];if(typeof range_start!="number")part._range=range_start;else{for(var i=range_start;i<=range_end;++i){var txt;if(i<10&&range_end>=10)txt='0'+i;else txt=''+i;part._range[part._range.length]=txt;}}Calendar._add_evs(part);return part;};var hrs=cal.date.getHours();var mins=cal.date.getMinutes();var t12=!cal.time24;var pm=(hrs>12);if(t12&&pm)hrs-=12;var H=makeTimePart("hour",hrs,t12?1:0,t12?12:23);var span=Calendar.createElement("span",cell);span.appendChild(document.createTextNode(":"));span.className="colon";var M=makeTimePart("minute",mins,0,59);var AP=null;cell=Calendar.createElement("td",row);cell.className="time";cell.colSpan=2;if(t12)AP=makeTimePart("ampm",pm?"pm":"am",["am","pm"]);else cell.innerHTML="&nbsp;";cal.onSetTime=function(){var hrs=this.date.getHours();var mins=this.date.getMinutes();var pm=(hrs>12);if(pm&&t12)hrs-=12;H.firstChild.data=(hrs<10)?("0"+hrs):hrs;M.firstChild.data=(mins<10)?("0"+mins):mins;if(t12)AP.firstChild.data=pm?"pm":"am";};cal.onUpdateTime=function(){var date=this.date;var h=parseInt(H.firstChild.data,10);if(t12){if(/pm/i.test(AP.firstChild.data)&&h<12)h+=12;else if(/am/i.test(AP.firstChild.data)&&h==12)h=0;}var d=date.getDate();var m=date.getMonth();var y=date.getFullYear();date.setHours(h);date.setMinutes(parseInt(M.firstChild.data,10));date.setFullYear(y);date.setMonth(m);date.setDate(d);this.dateClicked=false;this.callHandler();};})();}else{this.onSetTime=this.onUpdateTime=function(){};}var tfoot=Calendar.createElement("tfoot",table);row=Calendar.createElement("tr",tfoot);row.className="footrow";cell=hh(Calendar._TT["SEL_DATE"],this.weekNumbers?8:7,300);cell.className="ttip";if(this.isPopup){cell.ttip=Calendar._TT["DRAG_TO_MOVE"];cell.style.cursor="move";}this.tooltips=cell;div=Calendar.createElement("div",this.element);this.monthsCombo=div;div.className="combo";for(i=0;i<Calendar._MN.length;++i){var mn=Calendar.createElement("div");mn.className=Calendar.is_ie?"label-IEfix":"label";mn.month=i;mn.appendChild(document.createTextNode(Calendar._SMN[i]));div.appendChild(mn);}div=Calendar.createElement("div",this.element);this.yearsCombo=div;div.className="combo";for(i=12;i>0;--i){var yr=Calendar.createElement("div");yr.className=Calendar.is_ie?"label-IEfix":"label";yr.appendChild(document.createTextNode(""));div.appendChild(yr);}this._init(this.firstDayOfWeek,this.date);parent.appendChild(this.element);};Calendar._keyEvent=function(ev){if(!window.calendar){return false;}(Calendar.is_ie)&&(ev=window.event);var cal=window.calendar;var act=(Calendar.is_ie||ev.type=="keypress");if(ev.ctrlKey){switch(ev.keyCode){case 37:act&&Calendar.cellClick(cal._nav_pm);break;case 38:act&&Calendar.cellClick(cal._nav_py);break;case 39:act&&Calendar.cellClick(cal._nav_nm);break;case 40:act&&Calendar.cellClick(cal._nav_ny);break;default:return false;}}else switch(ev.keyCode){case 32:Calendar.cellClick(cal._nav_now);break;case 27:act&&cal.callCloseHandler();break;case 37:case 38:case 39:case 40:if(act){var date=cal.date.getDate()-1;var el=cal.currentDateEl;var ne=null;var prev=(ev.keyCode==37)||(ev.keyCode==38);switch(ev.keyCode){case 37:(--date>=0)&&(ne=cal.ar_days[date]);break;case 38:date-=7;(date>=0)&&(ne=cal.ar_days[date]);break;case 39:(++date<cal.ar_days.length)&&(ne=cal.ar_days[date]);break;case 40:date+=7;(date<cal.ar_days.length)&&(ne=cal.ar_days[date]);break;}if(!ne){if(prev){Calendar.cellClick(cal._nav_pm);}else{Calendar.cellClick(cal._nav_nm);}date=(prev)?cal.date.getMonthDays():1;el=cal.currentDateEl;ne=cal.ar_days[date-1];}Calendar.removeClass(el,"selected");Calendar.addClass(ne,"selected");cal.date=new Date(ne.caldate);cal.callHandler();cal.currentDateEl=ne;}break;case 13:if(act){cal.callHandler();cal.hide();}break;default:return false;}return Calendar.stopEvent(ev);};Calendar.prototype._init=function(firstDayOfWeek,date){var today=new Date();this.table.style.visibility="hidden";var year=date.getFullYear();if(year<this.minYear){year=this.minYear;date.setFullYear(year);}else if(year>this.maxYear){year=this.maxYear;date.setFullYear(year);}this.firstDayOfWeek=firstDayOfWeek;this.date=new Date(date);var month=date.getMonth();var mday=date.getDate();var no_days=date.getMonthDays();date.setDate(1);var day1=(date.getDay()-this.firstDayOfWeek)%7;if(day1<0)day1+=7;date.setDate(-day1);date.setDate(date.getDate()+1);var row=this.tbody.firstChild;var MN=Calendar._SMN[month];var ar_days=new Array();var weekend=Calendar._TT["WEEKEND"];for(var i=0;i<6;++i,row=row.nextSibling){var cell=row.firstChild;if(this.weekNumbers){cell.className="day wn";cell.firstChild.data=date.getWeekNumber();cell=cell.nextSibling;}row.className="daysrow";var hasdays=false;for(var j=0;j<7;++j,cell=cell.nextSibling,date.setDate(date.getDate()+1)){var iday=date.getDate();var wday=date.getDay();cell.className="day";var current_month=(date.getMonth()==month);if(!current_month){if(this.showsOtherMonths){cell.className+=" othermonth";cell.otherMonth=true;}else{cell.className="emptycell";cell.innerHTML="&nbsp;";cell.disabled=true;continue;}}else{cell.otherMonth=false;hasdays=true;}cell.disabled=false;cell.firstChild.data=iday;if(typeof this.getDateStatus=="function"){var status=this.getDateStatus(date,year,month,iday);if(status===true){cell.className+=" disabled";cell.disabled=true;}else{if(/disabled/i.test(status))cell.disabled=true;cell.className+=" "+status;}}if(!cell.disabled){ar_days[ar_days.length]=cell;cell.caldate=new Date(date);cell.ttip="_";if(current_month&&iday==mday){cell.className+=" selected";this.currentDateEl=cell;}if(date.getFullYear()==today.getFullYear()&&date.getMonth()==today.getMonth()&&iday==today.getDate()){cell.className+=" today";cell.ttip+=Calendar._TT["PART_TODAY"];}if(weekend.indexOf(wday.toString())!=-1){cell.className+=cell.otherMonth?" oweekend":" weekend";}}}if(!(hasdays||this.showsOtherMonths))row.className="emptyrow";}this.ar_days=ar_days;this.title.firstChild.data=Calendar._MN[month]+", "+year;this.onSetTime();this.table.style.visibility="visible";};Calendar.prototype.setDate=function(date){if(!date.equalsTo(this.date)){this._init(this.firstDayOfWeek,date);}};Calendar.prototype.refresh=function(){this._init(this.firstDayOfWeek,this.date);};Calendar.prototype.setFirstDayOfWeek=function(firstDayOfWeek){this._init(firstDayOfWeek,this.date);this._displayWeekdays();};Calendar.prototype.setDateStatusHandler=Calendar.prototype.setDisabledHandler=function(unaryFunction){this.getDateStatus=unaryFunction;};Calendar.prototype.setRange=function(a,z){this.minYear=a;this.maxYear=z;};Calendar.prototype.callHandler=function(){if(this.onSelected){this.onSelected(this,this.date.print(this.dateFormat));}};Calendar.prototype.callCloseHandler=function(){if(this.onClose){this.onClose(this);}this.hideShowCovered();};Calendar.prototype.destroy=function(){var el=this.element.parentNode;el.removeChild(this.element);Calendar._C=null;window.calendar=null;};Calendar.prototype.reparent=function(new_parent){var el=this.element;el.parentNode.removeChild(el);new_parent.appendChild(el);};Calendar._checkCalendar=function(ev){if(!window.calendar){return false;}var el=Calendar.is_ie?Calendar.getElement(ev):Calendar.getTargetElement(ev);for(;el!=null&&el!=calendar.element;el=el.parentNode);if(el==null){window.calendar.callCloseHandler();return Calendar.stopEvent(ev);}};Calendar.prototype.show=function(){var rows=this.table.getElementsByTagName("tr");for(var i=rows.length;i>0;){var row=rows[--i];Calendar.removeClass(row,"rowhilite");var cells=row.getElementsByTagName("td");for(var j=cells.length;j>0;){var cell=cells[--j];Calendar.removeClass(cell,"hilite");Calendar.removeClass(cell,"active");}}this.element.style.display="block";this.hidden=false;if(this.isPopup){window.calendar=this;Calendar.addEvent(document,"keydown",Calendar._keyEvent);Calendar.addEvent(document,"keypress",Calendar._keyEvent);Calendar.addEvent(document,"mousedown",Calendar._checkCalendar);}this.hideShowCovered();};Calendar.prototype.hide=function(){if(this.isPopup){Calendar.removeEvent(document,"keydown",Calendar._keyEvent);Calendar.removeEvent(document,"keypress",Calendar._keyEvent);Calendar.removeEvent(document,"mousedown",Calendar._checkCalendar);}this.element.style.display="none";this.hidden=true;this.hideShowCovered();};Calendar.prototype.showAt=function(x,y){var s=this.element.style;s.left=x+"px";s.top=y+"px";this.show();};Calendar.prototype.showAtElement=function(el,opts){var self=this;var p=Calendar.getAbsolutePos(el);if(!opts||typeof opts!="string"){this.showAt(p.x,p.y+el.offsetHeight);return true;}function fixPosition(box){if(box.x<0)box.x=0;if(box.y<0)box.y=0;var cp=document.createElement("div");var s=cp.style;s.position="absolute";s.right=s.bottom=s.width=s.height="0px";document.body.appendChild(cp);var br=Calendar.getAbsolutePos(cp);document.body.removeChild(cp);if(Calendar.is_ie){br.y+=document.body.scrollTop;br.x+=document.body.scrollLeft;}else{br.y+=window.scrollY;br.x+=window.scrollX;}var tmp=box.x+box.width-br.x;if(tmp>0)box.x-=tmp;tmp=box.y+box.height-br.y;if(tmp>0)box.y-=tmp;};this.element.style.display="block";Calendar.continuation_for_the_fucking_khtml_browser=function(){var w=self.element.offsetWidth;var h=self.element.offsetHeight;self.element.style.display="none";var valign=opts.substr(0,1);var halign="l";if(opts.length>1){halign=opts.substr(1,1);}switch(valign){case "T":p.y-=h;break;case "B":p.y+=el.offsetHeight;break;case "C":p.y+=(el.offsetHeight-h)/2;break;case "t":p.y+=el.offsetHeight-h;break;case "b":break;}switch(halign){case "L":p.x-=w;break;case "R":p.x+=el.offsetWidth;break;case "C":p.x+=(el.offsetWidth-w)/2;break;case "r":p.x+=el.offsetWidth-w;break;case "l":break;}p.width=w;p.height=h+40;self.monthsCombo.style.display="none";fixPosition(p);self.showAt(p.x,p.y);};if(Calendar.is_khtml)setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()",10);else Calendar.continuation_for_the_fucking_khtml_browser();};Calendar.prototype.setDateFormat=function(str){this.dateFormat=str;};Calendar.prototype.setTtDateFormat=function(str){this.ttDateFormat=str;};Calendar.prototype.parseDate=function(str,fmt){var y=0;var m=-1;var d=0;var a=str.split(/\W+/);if(!fmt){fmt=this.dateFormat;}var b=fmt.match(/%./g);var i=0,j=0;var hr=0;var min=0;for(i=0;i<a.length;++i){if(!a[i])continue;switch(b[i]){case "%d":case "%e":d=parseInt(a[i],10);break;case "%m":m=parseInt(a[i],10)-1;break;case "%Y":case "%y":y=parseInt(a[i],10);(y<100)&&(y+=(y>29)?1900:2000);break;case "%b":case "%B":for(j=0;j<12;++j){if(Calendar._MN[j].substr(0,a[i].length).toLowerCase()==a[i].toLowerCase()){m=j;break;}}break;case "%H":case "%I":case "%k":case "%l":hr=parseInt(a[i],10);break;case "%P":case "%p":if(/pm/i.test(a[i])&&hr<12)hr+=12;break;case "%M":min=parseInt(a[i],10);break;}}if(y!=0&&m!=-1&&d!=0){this.setDate(new Date(y,m,d,hr,min,0));return;}y=0;m=-1;d=0;for(i=0;i<a.length;++i){if(a[i].search(/[a-zA-Z]+/)!=-1){var t=-1;for(j=0;j<12;++j){if(Calendar._MN[j].substr(0,a[i].length).toLowerCase()==a[i].toLowerCase()){t=j;break;}}if(t!=-1){if(m!=-1){d=m+1;}m=t;}}else if(parseInt(a[i],10)<=12&&m==-1){m=a[i]-1;}else if(parseInt(a[i],10)>31&&y==0){y=parseInt(a[i],10);(y<100)&&(y+=(y>29)?1900:2000);}else if(d==0){d=a[i];}}if(y==0){var today=new Date();y=today.getFullYear();}if(m!=-1&&d!=0){this.setDate(new Date(y,m,d,hr,min,0));}};Calendar.prototype.hideShowCovered=function(){var self=this;Calendar.continuation_for_the_fucking_khtml_browser=function(){function getVisib(obj){var value=obj.style.visibility;if(!value){if(document.defaultView&&typeof(document.defaultView.getComputedStyle)=="function"){if(!Calendar.is_khtml)value=document.defaultView. getComputedStyle(obj,"").getPropertyValue("visibility");else value='';}else if(obj.currentStyle){value=obj.currentStyle.visibility;}else value='';}return value;};var tags=new Array("applet","iframe","select");var el=self.element;var p=Calendar.getAbsolutePos(el);var EX1=p.x;var EX2=el.offsetWidth+EX1;var EY1=p.y;var EY2=el.offsetHeight+EY1;for(var k=tags.length;k>0;){var ar=document.getElementsByTagName(tags[--k]);var cc=null;for(var i=ar.length;i>0;){cc=ar[--i];p=Calendar.getAbsolutePos(cc);var CX1=p.x;var CX2=cc.offsetWidth+CX1;var CY1=p.y;var CY2=cc.offsetHeight+CY1;if(self.hidden||(CX1>EX2)||(CX2<EX1)||(CY1>EY2)||(CY2<EY1)){if(!cc.__msh_save_visibility){cc.__msh_save_visibility=getVisib(cc);}cc.style.visibility=cc.__msh_save_visibility;}else{if(!cc.__msh_save_visibility){cc.__msh_save_visibility=getVisib(cc);}cc.style.visibility="hidden";}}}};if(Calendar.is_khtml)setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()",10);else Calendar.continuation_for_the_fucking_khtml_browser();};Calendar.prototype._displayWeekdays=function(){var fdow=this.firstDayOfWeek;var cell=this.firstdayname;var weekend=Calendar._TT["WEEKEND"];for(var i=0;i<7;++i){cell.className="day name";var realday=(i+fdow)%7;if(i){cell.ttip=Calendar._TT["DAY_FIRST"].replace("%s",Calendar._DN[realday]);cell.navtype=100;cell.calendar=this;cell.fdow=realday;Calendar._add_evs(cell);}if(weekend.indexOf(realday.toString())!=-1){Calendar.addClass(cell,"weekend");}cell.firstChild.data=Calendar._SDN[(i+fdow)%7];cell=cell.nextSibling;}};Calendar.prototype._hideCombos=function(){this.monthsCombo.style.display="none";this.yearsCombo.style.display="none";};Calendar.prototype._dragStart=function(ev){if(this.dragging){return;}this.dragging=true;var posX;var posY;if(Calendar.is_ie){posY=window.event.clientY+document.body.scrollTop;posX=window.event.clientX+document.body.scrollLeft;}else{posY=ev.clientY+window.scrollY;posX=ev.clientX+window.scrollX;}var st=this.element.style;this.xOffs=posX-parseInt(st.left);this.yOffs=posY-parseInt(st.top);with(Calendar){addEvent(document,"mousemove",calDragIt);addEvent(document,"mouseup",calDragEnd);}};Date._MD=new Array(31,28,31,30,31,30,31,31,30,31,30,31);Date.SECOND=1000;Date.MINUTE=60*Date.SECOND;Date.HOUR=60*Date.MINUTE;Date.DAY=24*Date.HOUR;Date.WEEK=7*Date.DAY;Date.prototype.getMonthDays=function(month){var year=this.getFullYear();if(typeof month=="undefined"){month=this.getMonth();}if(((0==(year%4))&&((0!=(year%100))||(0==(year%400))))&&month==1){return 29;}else{return Date._MD[month];}};Date.prototype.getDayOfYear=function(){var now=new Date(this.getFullYear(),this.getMonth(),this.getDate(),0,0,0);var then=new Date(this.getFullYear(),0,0,0,0,0);var time=now-then;return Math.floor(time/Date.DAY);};Date.prototype.getWeekNumber=function(){var d=new Date(this.getFullYear(),this.getMonth(),this.getDate(),0,0,0);var DoW=d.getDay();d.setDate(d.getDate()-(DoW+6)%7+3);var ms=d.valueOf();d.setMonth(0);d.setDate(4);return Math.round((ms-d.valueOf())/(7*864e5))+1;};Date.prototype.equalsTo=function(date){return((this.getFullYear()==date.getFullYear())&&(this.getMonth()==date.getMonth())&&(this.getDate()==date.getDate())&&(this.getHours()==date.getHours())&&(this.getMinutes()==date.getMinutes()));};Date.prototype.print=function(str){var m=this.getMonth();var d=this.getDate();var y=this.getFullYear();var wn=this.getWeekNumber();var w=this.getDay();var s={};var hr=this.getHours();var pm=(hr>=12);var ir=(pm)?(hr-12):hr;var dy=this.getDayOfYear();if(ir==0)ir=12;var min=this.getMinutes();var sec=this.getSeconds();s["%a"]=Calendar._SDN[w];s["%A"]=Calendar._DN[w];s["%b"]=Calendar._SMN[m];s["%B"]=Calendar._MN[m];s["%C"]=1+Math.floor(y/100);s["%d"]=(d<10)?("0"+d):d;s["%e"]=d;s["%H"]=(hr<10)?("0"+hr):hr;s["%I"]=(ir<10)?("0"+ir):ir;s["%j"]=(dy<100)?((dy<10)?("00"+dy):("0"+dy)):dy;s["%k"]=hr;s["%l"]=ir;s["%m"]=(m<9)?("0"+(1+m)):(1+m);s["%M"]=(min<10)?("0"+min):min;s["%n"]="\n";s["%p"]=pm?"PM":"AM";s["%P"]=pm?"pm":"am";s["%s"]=Math.floor(this.getTime()/1000);s["%S"]=(sec<10)?("0"+sec):sec;s["%t"]="\t";s["%U"]=s["%W"]=s["%V"]=(wn<10)?("0"+wn):wn;s["%u"]=w+1;s["%w"]=w;s["%y"]=(''+y).substr(2,2);s["%Y"]=y;s["%%"]="%";var re=/%./g;if(!Calendar.is_ie5)return str.replace(re,function(par){return s[par]||par;});var a=str.match(re);for(var i=0;i<a.length;i++){var tmp=s[a[i]];if(tmp){re=new RegExp(a[i],'g');str=str.replace(re,tmp);}}return str;};Date.prototype.__msh_oldSetFullYear=Date.prototype.setFullYear;Date.prototype.setFullYear=function(y){var d=new Date(this);d.__msh_oldSetFullYear(y);if(d.getMonth()!=this.getMonth())this.setDate(28);this.__msh_oldSetFullYear(y);};window.calendar=null; \ No newline at end of file
diff --git a/httemplate/elements/header.html b/httemplate/elements/header.html
new file mode 100644
index 0000000..10e4e40
--- /dev/null
+++ b/httemplate/elements/header.html
@@ -0,0 +1,21 @@
+<%
+ my($title, $menubar) = ( shift, shift );
+ my $etc = @_ ? shift : ''; #$etc is for things like onLoad= etc.
+ my $head = @_ ? shift : ''; #$head is for things that go in the <HEAD> section
+%>
+ <HTML>
+ <HEAD>
+ <TITLE>
+ <%= $title %>
+ </TITLE>
+ <META HTTP-Equiv="Cache-Control" Content="no-cache">
+ <META HTTP-Equiv="Pragma" Content="no-cache">
+ <META HTTP-Equiv="Expires" Content="0">
+ <%= $head %>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8"<%= $etc %>>
+ <FONT SIZE=6>
+ <%= $title %>
+ </FONT>
+ <BR><BR>
+ <%= $menubar ? "$menubar<BR><BR>" : '' %>
diff --git a/httemplate/elements/menubar.html b/httemplate/elements/menubar.html
new file mode 100644
index 0000000..87a5031
--- /dev/null
+++ b/httemplate/elements/menubar.html
@@ -0,0 +1,8 @@
+<%
+ my($item, $url, @html);
+ while (@_) {
+ ($item, $url) = splice(@_,0,2);
+ push @html, qq!<A HREF="$url">$item</A>!;
+ }
+%>
+<%= join(' | ', @html) %>
diff --git a/httemplate/elements/pager.html b/httemplate/elements/pager.html
new file mode 100644
index 0000000..0510d32
--- /dev/null
+++ b/httemplate/elements/pager.html
@@ -0,0 +1,42 @@
+<%
+
+ my %opt = @_;
+
+ my $pager = '';
+ if ( $opt{'total'} != $opt{'num_rows'} && $opt{'maxrecords'} ) {
+ unless ( $opt{'offset'} == 0 ) {
+ $cgi->param('offset', $opt{'offset'} - $opt{'maxrecords'});
+%>
+
+ <A HREF="<%= $cgi->self_url %>"><B><FONT SIZE="+1">Previous</FONT></B></A>
+
+<%
+ }
+ my $page = 0;
+ for ( my $poff = 0; $poff < $opt{'total'}; $poff += $opt{'maxrecords'} ) {
+ $page++;
+ if ( $opt{'offset'} == $poff ) {
+%>
+
+ <FONT SIZE="+2"><%= $page %></FONT>
+
+<%
+ } else {
+ $cgi->param('offset', $poff);
+%>
+
+ <A HREF="<%= $cgi->self_url %>"><%= $page %></A>
+
+<%
+ }
+ }
+ unless ( $opt{'offset'} + $opt{'maxrecords'} > $opt{'total'} ) {
+ $cgi->param('offset', $opt{'offset'} + $opt{'maxrecords'});
+%>
+
+ <A HREF="<%= $cgi->self_url %>"><B><FONT SIZE="+1">Next</FONT></B></A>
+
+<%
+ }
+ }
+%>
diff --git a/httemplate/elements/small_custview.html b/httemplate/elements/small_custview.html
new file mode 100644
index 0000000..1e8ae73
--- /dev/null
+++ b/httemplate/elements/small_custview.html
@@ -0,0 +1,2 @@
+<% my $conf = new FS::Conf; %>
+<%= small_custview( shift, shift || $conf->config('countrydefault') ) %>
diff --git a/httemplate/elements/table.html b/httemplate/elements/table.html
new file mode 100644
index 0000000..3b61087
--- /dev/null
+++ b/httemplate/elements/table.html
@@ -0,0 +1,8 @@
+<%
+ my $color = shift;
+ if ( $color ) {
+%>
+ <TABLE BGCOLOR="<%= $color %>" BORDER=1 WIDTH="100%" CELLSPACING=0 CELLPADDING=2 BORDERCOLOR="#999999">
+<% } else { %>
+ <TABLE BORDER=1 CELLSPACING=0 CELLPADDING=2 BORDERCOLOR="#999999">
+<% } %>
diff --git a/httemplate/graph/money_time-graph.cgi b/httemplate/graph/money_time-graph.cgi
new file mode 100755
index 0000000..bb3d23a
--- /dev/null
+++ b/httemplate/graph/money_time-graph.cgi
@@ -0,0 +1,68 @@
+<%
+
+#my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
+my ($curmon,$curyear) = (localtime(time))[4,5];
+
+#find first month
+my $syear = $cgi->param('syear') || 1899+$curyear;
+my $smonth = $cgi->param('smonth') || $curmon+1;
+
+#find last month
+my $eyear = $cgi->param('eyear') || 1900+$curyear;
+my $emonth = $cgi->param('emonth') || $curmon+1;
+#if ( $emonth++>12 ) { $emonth-=12; $eyear++; }
+
+#my @labels;
+#my %data;
+
+my @items = qw( invoiced netsales credits payments receipts );
+my %label = (
+ 'invoiced' => 'Gross Sales (invoiced)',
+ 'netsales' => 'Net Sales (invoiced - applied credits)',
+ 'credits' => 'Credits',
+ 'payments' => 'Gross Receipts (payments)',
+ 'receipts' => 'Net Receipts/Cashflow (payments - refunds)',
+);
+my %color = (
+ 'invoiced' => [ 153, 153, 255 ], #light blue
+ 'netsales' => [ 0, 0, 204 ], #blue
+ 'credits' => [ 204, 0, 0 ], #red
+ 'payments' => [ 153, 204, 153 ], #light green
+ 'receipts' => [ 0, 204, 0 ], #green
+);
+
+my $report = new FS::Report::Table::Monthly (
+ 'items' => \@items,
+ 'start_month' => $smonth,
+ 'start_year' => $syear,
+ 'end_month' => $emonth,
+ 'end_year' => $eyear,
+);
+my %data = %{$report->data};
+
+#my $chart = Chart::LinesPoints->new(1024,480);
+#my $chart = Chart::LinesPoints->new(768,480);
+my $chart = Chart::LinesPoints->new(976,384);
+
+my $d = 0;
+$chart->set(
+ #'min_val' => 0,
+ 'legend' => 'bottom',
+ 'colors' => { ( map { 'dataset'.$d++ => $color{$_} } @items ),
+ #'grey_background' => [ 211, 211, 211 ],
+ 'grey_background' => 'white',
+ 'background' => [ 0xe8, 0xe8, 0xe8 ], #grey
+ },
+ #'grey_background' => 'false',
+ 'legend_labels' => [ map { $label{$_} } @items ],
+ 'brush_size' => 4,
+ #'pt_size' => 12,
+);
+
+my @data = map { $data{$_} } ( 'label', @items );
+
+http_header('Content-Type' => 'image/png' );
+
+$chart->_set_colors();
+
+%><%= $chart->scalar_png(\@data) %>
diff --git a/httemplate/graph/money_time.cgi b/httemplate/graph/money_time.cgi
new file mode 100644
index 0000000..1c7d542
--- /dev/null
+++ b/httemplate/graph/money_time.cgi
@@ -0,0 +1,125 @@
+<!-- mason kludge -->
+<%
+
+#my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
+my ($curmon,$curyear) = (localtime(time))[4,5];
+
+#find first month
+my $syear = $cgi->param('syear') || 1899+$curyear;
+my $smonth = $cgi->param('smonth') || $curmon+1;
+
+#find last month
+my $eyear = $cgi->param('eyear') || 1900+$curyear;
+my $emonth = $cgi->param('emonth') || $curmon+1;
+
+%>
+
+<HTML>
+ <HEAD>
+ <TITLE>Sales, Credits and Receipts Summary</TITLE>
+ </HEAD>
+<BODY BGCOLOR="#e8e8e8">
+<IMG SRC="money_time-graph.cgi?<%= $cgi->query_string %>" WIDTH="976" HEIGHT="384">
+<BR>
+
+<%= table('e8e8e8') %>
+<%
+
+my @items = qw( invoiced netsales credits payments receipts );
+my %label = (
+ 'invoiced' => 'Gross Sales',
+ 'netsales' => 'Net Sales',
+ 'credits' => 'Credits',
+ 'payments' => 'Gross Receipts',
+ 'receipts' => 'Net Receipts',
+);
+my %color = (
+ 'invoiced' => '9999ff', #light blue
+ 'netsales' => '0000cc', #blue
+ 'credits' => 'cc0000', #red
+ 'payments' => '99cc99', #light green
+ 'receipts' => '00cc00', #green
+);
+my %link = (
+ 'invoiced' => "${p}search/cust_bill.html?",
+ 'credits' => "${p}search/cust_credit.html?",
+ 'payments' => "${p}search/cust_pay.cgi?magic=_date;",
+);
+
+my $report = new FS::Report::Table::Monthly (
+ 'items' => \@items,
+ 'start_month' => $smonth,
+ 'start_year' => $syear,
+ 'end_month' => $emonth,
+ 'end_year' => $eyear,
+);
+my $data = $report->data;
+
+my @mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
+
+%>
+
+<TR><TD></TD>
+<% foreach my $column ( @{$data->{label}} ) {
+ #$column =~ s/^(\d+)\//$mon[$1-1]<BR>/e;
+ $column =~ s/^(\d+)\//$mon[$1-1]<BR>/;
+ %>
+ <TH><%= $column %></TH>
+<% } %>
+</TR>
+
+<% foreach my $row (@items) { %>
+ <TR><TH><FONT COLOR="#<%= $color{$row} %>"><%= $label{$row} %></FONT></TH>
+ <% my $link = exists($link{$row})
+ ? qq(<A HREF="$link{$row})
+ : '';
+ my @speriod = @{$data->{speriod}};
+ my @eperiod = @{$data->{eperiod}};
+ %>
+ <% foreach my $column ( @{$data->{$row}} ) { %>
+ <TD ALIGN="right" BGCOLOR="#ffffff">
+ <%= $link ? $link. 'begin='. shift(@speriod). ';end='. shift(@eperiod). '">' : '' %><FONT COLOR="#<%= $color{$row} %>">$<%= sprintf("%.2f", $column) %></FONT><%= $link ? '</A>' : '' %>
+ </TD>
+ <% } %>
+ </TR>
+<% } %>
+</TABLE>
+
+<BR>
+<FORM METHOD="POST">
+<!--
+<INPUT TYPE="checkbox" NAME="ar">
+ Accounts receivable (invoices - applied credits)<BR>
+<INPUT TYPE="checkbox" NAME="charged">
+ Just Invoices<BR>
+<INPUT TYPE="checkbox" NAME="defer">
+ Accounts receivable, with deferred revenue (invoices - applied credits, with charges for annual/semi-annual/quarterly/etc. services deferred over applicable time period) (there has got to be a shorter description for this)<BR>
+<INPUT TYPE="checkbox" NAME="cash">
+ Cashflow (payments - refunds)<BR>
+<BR>
+-->
+From <SELECT NAME="smonth">
+<% foreach my $mon ( 1..12 ) { %>
+<OPTION VALUE="<%= $mon %>"<%= $mon == $smonth ? ' SELECTED' : '' %>><%= $mon[$mon-1] %>
+<% } %>
+</SELECT>
+<SELECT NAME="syear">
+<% foreach my $y ( 1999 .. 2010 ) { %>
+<OPTION VALUE="<%= $y %>"<%= $y == $syear ? ' SELECTED' : '' %>><%= $y %>
+<% } %>
+</SELECT>
+ to <SELECT NAME="emonth">
+<% foreach my $mon ( 1..12 ) { %>
+<OPTION VALUE="<%= $mon %>"<%= $mon == $emonth ? ' SELECTED' : '' %>><%= $mon[$mon-1] %>
+<% } %>
+</SELECT>
+<SELECT NAME="eyear">
+<% foreach my $y ( 1999 .. 2010 ) { %>
+<OPTION VALUE="<%= $y %>"<%= $y == $eyear ? ' SELECTED' : '' %>><%= $y %>
+<% } %>
+</SELECT>
+
+<INPUT TYPE="submit" VALUE="Redisplay">
+</FORM>
+</BODY>
+</HTML>
diff --git a/httemplate/images/ach.png b/httemplate/images/ach.png
new file mode 100644
index 0000000..fdcd5e6
--- /dev/null
+++ b/httemplate/images/ach.png
Binary files differ
diff --git a/httemplate/images/calendar.png b/httemplate/images/calendar.png
new file mode 100644
index 0000000..1632661
--- /dev/null
+++ b/httemplate/images/calendar.png
Binary files differ
diff --git a/httemplate/images/cvv2.png b/httemplate/images/cvv2.png
new file mode 100644
index 0000000..4610dcb
--- /dev/null
+++ b/httemplate/images/cvv2.png
Binary files differ
diff --git a/httemplate/images/cvv2_amex.png b/httemplate/images/cvv2_amex.png
new file mode 100644
index 0000000..21c36a0
--- /dev/null
+++ b/httemplate/images/cvv2_amex.png
Binary files differ
diff --git a/httemplate/images/small-logo.png b/httemplate/images/small-logo.png
new file mode 100644
index 0000000..a8fe807
--- /dev/null
+++ b/httemplate/images/small-logo.png
Binary files differ
diff --git a/httemplate/index.html b/httemplate/index.html
new file mode 100644
index 0000000..98a28ab
--- /dev/null
+++ b/httemplate/index.html
@@ -0,0 +1,212 @@
+<HTML>
+ <HEAD>
+ <TITLE>
+ Freeside Main Menu
+ </TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#FFFFFF">
+ <table width="100%">
+ <tr><td>
+ <IMG BORDER=0 ALT="freeside" SRC="images/small-logo.png">
+ </td><td valign="top">
+ <font color="#7f007b" size=7></font>
+ </td><td align=right valign=bottom>
+ version %%%VERSION%%%
+ <BR><A HREF="http://www.sisd.com/freeside">Freeside&nbsp;home&nbsp;page</A>
+ <BR><A HREF="docs/">Documentation</A>
+ </td></tr>
+ </table>
+
+<BR>
+[<A NAME="customer_service" style="background-color: #cccccc"> Sales / Customer service </A>]
+[ <A HREF="#bookkeeping">Bookkeeping / Collections</A> ]
+[ <A HREF="#reports">Reports</A> ]
+[ <A HREF="#sysadmin">Sysadmin</A> ]
+ <TABLE CELLSPACING=2 CELLPADDING=0 BORDER=0" WIDTH="100%" BGCOLOR="#eeeeee">
+ <TR><TH BGCOLOR="#cccccc">Sales / Customer service</TH></TR>
+ <TR><TD>
+ <BR><FONT SIZE="+1"><A HREF="edit/cust_main.cgi">New Customer</A></FONT>
+ <BR>
+ <BR><FORM ACTION="search/cust_main.cgi" METHOD="POST"><INPUT TYPE="hidden" NAME="custnum_on" VALUE="1">Customer # <INPUT TYPE="text" NAME="custnum_text"><INPUT TYPE="submit" VALUE="Search"> or <A HREF="search/cust_main.cgi?browse=custnum">all customers by customer number</A></FORM>
+ <FORM ACTION="search/cust_main.cgi" METHOD="POST"><INPUT TYPE="hidden" NAME="last_on" VALUE="1">Last name <INPUT TYPE="text" NAME="last_text"><SELECT NAME="last_type"><OPTION SELECTED VALUE="All">(all)</OPTION><OPTION>Fuzzy<OPTION>Substring</OPTION><OPTION>Exact</OPTION></SELECT><INPUT TYPE="submit" VALUE="Search"> or <A HREF="search/cust_main.cgi?browse=last">all customers by last name</A></FORM>
+ <FORM ACTION="search/cust_main.cgi" METHOD="POST"><INPUT TYPE="hidden" NAME="company_on" VALUE="1">Company <INPUT TYPE="text" NAME="company_text"><SELECT NAME="company_type"><OPTION SELECTED VALUE="All">(all)</OPTION><OPTION>Fuzzy<OPTION>Substring</OPTION><OPTION>Exact</OPTION></SELECT><INPUT TYPE="submit" VALUE="Search"> or <A HREF="search/cust_main.cgi?browse=company">all customers by company</A></FORM>
+<!-- <FORM ACTION="search/cust_main.cgi" METHOD="POST"><INPUT TYPE="hidden" NAME="address2_on" VALUE="1">Unit <INPUT TYPE="text" NAME="address2_text"><INPUT TYPE="submit" VALUE="Search"></FORM>-->
+ <FORM ACTION="search/cust_main.cgi" METHOD="POST"><INPUT TYPE="hidden" NAME="phone_on" VALUE="1">Phone # <INPUT TYPE="text" NAME="phone_text"><INPUT TYPE="submit" VALUE="Search"></FORM>
+ <BR><FORM ACTION="search/svc_acct.cgi" METHOD="POST">Username <INPUT TYPE="text" NAME="username"><SELECT NAME="username_type"><OPTION VALUE="All">(all)</OPTION><OPTION>Fuzzy</OPTION><OPTION>Substring</OPTION><OPTION SELECTED>Exact</OPTION></SELECT><INPUT TYPE="submit" VALUE="Search"> or <A HREF="search/svc_acct.cgi?username">all accounts by username</A> or <A HREF="search/svc_acct.cgi?uid">uid</A></FORM>
+ <BR><FORM ACTION="search/svc_domain.cgi" METHOD="POST">Domain <INPUT TYPE="text" NAME="domain"><INPUT TYPE="submit" VALUE="Search"> or <A HREF="search/svc_domain.cgi?domain">all domains</A></FORM>
+ <BR><A HREF="search/svc_forward.cgi?svcnum">all mail forwards by svcnum</A><BR>
+ <BR><A HREF="search/svc_www.cgi?svcnum">all virtual hosts by svcnum</A><BR>
+ <BR><A HREF="search/svc_external.cgi?svcnum">all external services by svcnum</A><BR>
+ <BR>
+ </TD></TR>
+ </TABLE>
+
+
+
+ <BR><BR><BR>
+
+
+[ <A HREF="#customer_service">Sales / Customer service</A> ]
+[<A NAME="bookkeeping" style="background-color: #cccccc"> Bookkeeping / Collections </A>]
+[ <A HREF="#reports">Reports</A> ]
+[ <A HREF="#sysadmin">Sysadmin</A> ]
+ <TABLE CELLSPACING=2 CELLPADDING=0 BORDER=0 WIDTH="100%" BGCOLOR="#eeeeee">
+ <TR><TH BGCOLOR="#cccccc">Bookkeeping / Collections</TH></TR>
+ <TR><TD>
+ <BR><A HREF="search/cust_main-quickpay.html">Quick payment entry</A>
+ <BR>
+ <BR><FORM ACTION="search/cust_main.cgi" METHOD="POST">Credit card # <INPUT TYPE="hidden" NAME="card_on" VALUE="1"><INPUT TYPE="text" NAME="card"><INPUT TYPE="submit" VALUE="Search"></FORM>
+ <FORM ACTION="search/cust_bill.html" METHOD="POST">Invoice # <INPUT TYPE="text" NAME="invnum" SIZE="8"><INPUT TYPE="submit" VALUE="Search"></FORM>
+ <FORM ACTION="search/cust_pay.cgi" METHOD="POST">Check # <INPUT TYPE="text" NAME="payinfo" SIZE="8"><INPUT TYPE="hidden" NAME="payby" VALUE="BILL"><INPUT TYPE="submit" VALUE="Search"></FORM>
+ <BR><A HREF="browse/cust_pay_batch.cgi">View pending credit card batch</A> <BR><BR><A HREF="search/cust_pkg_report.cgi">Packages (by next bill date range)</A>
+ <BR><BR>Invoice reports
+ <UL>
+ <LI><a href="search/cust_bill_event.html">Invoice event errors (failed credit cards, procesoor or printer problems, etc.)</a>
+ <LI>open invoices (<A HREF="search/cust_bill.html?OPEN_invnum">by invoice number</A>) (<A HREF="search/cust_bill.html?OPEN_date">by date</A>) (<A HREF="search/cust_bill.html?OPEN_custnum">by customer number</A>)
+ <LI>15 day open invoices (<A HREF="search/cust_bill.html?OPEN15_invnum">by invoice number</A>) (<A HREF="search/cust_bill.html?OPEN15_date">by date</A>) (<A HREF="search/cust_bill.html?OPEN15_custnum">by customer number</A>)
+ <LI>30 day open invoices (<A HREF="search/cust_bill.html?OPEN30_invnum">by invoice number</A>) (<A HREF="search/cust_bill.html?OPEN30_date">by date</A>) (<A HREF="search/cust_bill.html?OPEN30_custnum">by customer number</A>)
+ <LI>60 day open invoices (<A HREF="search/cust_bill.html?OPEN60_invnum">by invoice number</A>) (<A HREF="search/cust_bill.html?OPEN60_date">by date</A>) (<A HREF="search/cust_bill.html?OPEN60_custnum">by customer number</A>)
+ <LI>90 day open invoices (<A HREF="search/cust_bill.html?OPEN90_invnum">by invoice number</A>) (<A HREF="search/cust_bill.html?OPEN90_date">by date</A>) (<A HREF="search/cust_bill.html?OPEN90_custnum">by customer number</A>)
+ <LI>120 day open invoices (<A HREF="search/cust_bill.html?OPEN120_invnum">by invoice number</A>) (<A HREF="search/cust_bill.html?OPEN120_date">by date</A>) (<A HREF="search/cust_bill.html?OPEN120_custnum">by customer number</A>)
+ <LI>all invoices (<A HREF="search/cust_bill.html?invnum">by invoice number</A>) (<A HREF="search/cust_bill.html?date">by date</A>) (<A HREF="search/cust_bill.html?custnum">by customer number</A>)
+ </UL>
+ <A HREF="search/report_cust_pay.html">Payment report (by type and/or date range)</A>
+ <BR><BR><A HREF="search/report_cust_credit.html">Credit report (by employee and/or date range)</A>
+ <BR><BR><A HREF="graph/money_time.cgi">Sales, Credits and Receipts Summary</A>
+ <BR><BR><A HREF="search/report_receivables.cgi">Accounts Receivable Aging Summary</A>
+ <BR><BR><A HREF="search/report_prepaid_income.html">Prepaid Income (Unearned Revenue) Report</A>
+ <BR><BR><A HREF="search/report_tax.html">Sales Tax Liability Report</A>
+ <BR><BR>
+ <CENTER><HR WIDTH="94%" NOSHADE></CENTER><BR>
+ <A NAME="admin">Administration</a>
+ <ul>
+ <LI><A HREF="browse/part_pkg.cgi">View/Edit package definitions</A>
+ - One or more services are grouped together into a package and
+ given pricing information. Customers purchase packages, not
+ services.
+<!-- <LI><A HREF="browse/agent_type.cgi">View/Edit agent types</A>
+ - Agent types define groups of package definitions that you can
+ then assign to particular agents.
+ <LI><A HREF="browse/agent.cgi">View/Edit agents</A>
+ - Agents are resellers of your service. Agents may be limited
+ to a subset of your full offerings (via their type).
+-->
+ <LI><A HREF="browse/cust_main_county.cgi">View/Edit locales and tax rates</A>
+ - Change tax rates, or break down a country into states, or a state
+ into counties and assign different tax rates to each.
+ <LI><A HREF="browse/part_bill_event.cgi">View/Edit invoice events</A> - Actions for overdue invoices
+ </ul>
+ <BR>
+ </TD></TR>
+ </TABLE>
+
+
+
+ <BR><BR><BR>
+
+
+
+[ <A HREF="#customer_service">Sales / Customer service</A> ]
+[ <A HREF="#bookkeeping">Bookkeeping / Collections</A> ]
+[<A NAME="reports" style="background-color: #cccccc"> Reports </A>]
+[ <A HREF="#sysadmin">Sysadmin</A> ]
+ <TABLE CELLSPACING=2 CELLPADDING=0 BORDER=0 WIDTH="100%" BGCOLOR="#eeeeee">
+ <TR><TH BGCOLOR="#cccccc">Reports</TH></TR>
+ <TR><TD>
+ <BR>
+ <A HREF="search/sqlradius.html">RADIUS sessions</A><BR><BR>
+ Auditing pre-Freeside services with no customer record
+ <UL>
+ <LI>unlinked accounts (<A HREF="search/svc_acct.cgi?UN_svcnum">by service number</A>) (<A HREF="search/svc_acct.cgi?UN_username">by username</A>) (<A HREF="search/svc_acct.cgi?UN_uid">by uid</A>)
+<!-- <LI>unlinked mail forwards (<A HREF="search/svc_forward.cgi?UN_svcnum">by service number</A>) (by ?)) -->
+ <LI>unlinked domains (<A HREF="search/svc_domain.cgi?UN_svcnum">by service number</A>) (<A HREF="search/svc_domain.cgi?UN_domain">by domain</A>)
+ <LI>unlinked externals (<A HREF="search/svc_external.cgi?UN_svcnum">by service number</A>) (<A HREF="search/svc_external.cgi?UN_id">by id</A>)
+ </UL>
+ Packages
+ <UL>
+ <LI><A HREF="search/cust_pkg.cgi?pkgnum">all packages (by package number)</A>
+ <LI><A HREF="search/cust_pkg.cgi?magic=suspended">suspended packages (by package number)</A>
+ <LI><A HREF="search/cust_pkg.cgi?APKG_pkgnum">packages with unconfigured services (by package number)</A>
+ <LI><A HREF="search/cust_pkg_report.cgi">packages (by next bill date range)</A>
+ </UL>
+ <A HREF="browse/part_pkg.cgi?active=1">Package definitions (by number of active packages)</A><BR><BR>
+ <A HREF="browse/part_svc.cgi?active=1">Service definitions (by number of active services)</A><BR><BR>
+ Customers
+ <UL>
+ <LI><A HREF="search/cust_main-otaker.cgi">Search customers by ordering employee</A>
+ </UL>
+ <FORM ACTION="search/sql.html" METHOD="POST">SQL query: <TT>SELECT </TT><INPUT TYPE="text" NAME="sql" SIZE=32><INPUT TYPE="submit" VALUE="Query"></FORM>
+
+ <BR>
+ </TD></TR>
+ </TABLE>
+
+
+
+ <BR><BR><BR>
+
+
+[ <A HREF="#customer_service">Sales / Customer service</A> ]
+[ <A HREF="#bookkeeping">Bookkeeping / Collections</A> ]
+[ <A HREF="#reports">Reports</A> ]
+[<A NAME="sysadmin" style="background-color: #cccccc"> Sysadmin </A>]
+ <TABLE CELLSPACING=2 CELLPADDING=0 BORDER=0 WIDTH="100%" BGCOLOR="#eeeeee">
+ <TR><TH BGCOLOR="#cccccc">Sysadmin</TH></TR>
+ <TR><TD>
+ <BR>
+ <!-- <BR>View active NAS ports:
+ <A HREF="browse/nas.cgi">session server</A>
+ <!-- or <A HREF="browse/nas-sqlradius.cgi">RADIUS</A>
+ <BR> -->
+ <A HREF="browse/queue.cgi">View pending job queue</A>
+ <BR><A HREF="misc/cust_main-import.cgi">Batch import customers from CSV file</A>
+ <BR><A HREF="misc/cust_main-import_charges.cgi">Batch import charges from CSV file</A>
+ <BR><A HREF="misc/dump.cgi">Download database dump</A>
+ <BR><BR><CENTER><HR WIDTH="94%" NOSHADE></CENTER><BR>
+ <A NAME="config" HREF="config/config-view.cgi">Configuration</a><!-- - <font size="+2" color="#ff0000">start here</font> -->
+ <BR><BR><A NAME="admin">Administration</a>
+ <ul>
+ <LI><A HREF="browse/part_export.cgi">View/Edit exports</A>
+ - Provisioning services to external machines, databases and APIs.
+ <LI><A HREF="browse/part_svc.cgi">View/Edit service definitions</A>
+ - Services are items you offer to your customers.
+ <LI><A HREF="browse/part_pkg.cgi">View/Edit package definitions</A>
+ - One or more services are grouped together into a package and
+ given pricing information. Customers purchase packages, not
+ services.
+ <LI><A HREF="browse/agent_type.cgi">View/Edit agent types</A>
+ - Agent types define groups of package definitions that you can
+ then assign to particular agents.
+ <LI><A HREF="browse/agent.cgi">View/Edit agents</A>
+ - Agents are resellers of your service. Agents may be limited
+ to a subset of your full offerings (via their type).
+ <LI><A HREF="browse/part_referral.cgi">View/Edit advertising sources</A>
+ - Where a customer heard about your service. Tracked for
+ informational purposes.
+ <LI><A HREF="browse/cust_main_county.cgi">View/Edit locales and tax rates</A>
+ - Change tax rates, or break down a country into states, or a state
+ into counties and assign different tax rates to each.
+ <LI><A HREF="browse/svc_acct_pop.cgi">View/Edit access numbers</A>
+ - Points of Presence
+ <LI><A HREF="browse/part_bill_event.cgi">View/Edit invoice events</A> - Actions for overdue invoices
+ <LI><A HREF="browse/msgcat.cgi">View/Edit message catalog</A> - Change error messages and other customizable labels.
+ <LI><A HREF="browse/part_virtual_field.cgi">View/Edit virtual fields</A>
+ - Locally defined fields
+ <LI><A HREF="browse/router.cgi">View/Edit routers</A>
+ - Broadband access routers
+ <LI><A HREF="browse/addr_block.cgi">View/Edit address blocks</A>
+ - Manage address blocks and block assignments to broadband routers.
+ </ul>
+ <BR>
+ </TD></TR>
+ </TABLE>
+ <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+ <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+ <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+ <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+ <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+ <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+ <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+ <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+ </BODY>
+</HTML>
diff --git a/httemplate/misc/bill.cgi b/httemplate/misc/bill.cgi
new file mode 100755
index 0000000..44d85b8
--- /dev/null
+++ b/httemplate/misc/bill.cgi
@@ -0,0 +1,38 @@
+<%
+
+#untaint custnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d*)$/;
+my $custnum = $1;
+my $cust_main = qsearchs('cust_main',{'custnum'=>$custnum});
+die "Can't find customer!\n" unless $cust_main;
+
+my $error = $cust_main->bill(
+# 'time'=>$time
+ );
+#&eidiot($error) if $error;
+
+unless ( $error ) {
+ $cust_main->apply_payments;
+ $cust_main->apply_credits;
+
+ $error = $cust_main->collect(
+ # 'invoice-time'=>$time,
+ #'batch_card'=> 'yes',
+ #'batch_card'=> 'no',
+ #'report_badcard'=> 'yes',
+ #'retry_card' => 'yes',
+ 'retry' => 'yes',
+ );
+}
+#&eidiot($error) if $error;
+
+if ( $error ) {
+%>
+<!-- mason kludge -->
+<%
+ &idiot($error);
+} else {
+ print $cgi->redirect(popurl(2). "view/cust_main.cgi?$custnum");
+}
+%>
diff --git a/httemplate/misc/cancel-unaudited.cgi b/httemplate/misc/cancel-unaudited.cgi
new file mode 100755
index 0000000..43e439b
--- /dev/null
+++ b/httemplate/misc/cancel-unaudited.cgi
@@ -0,0 +1,34 @@
+<%
+
+my $dbh = dbh;
+
+#untaint svcnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+
+#my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$svcnum});
+#die "Unknown svcnum!" unless $svc_acct;
+
+my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$svcnum});
+die "Unknown svcnum!" unless $cust_svc;
+my $cust_pkg = $cust_svc->cust_pkg;
+if ( $cust_pkg ) {
+ &eidiot( 'This account has already been audited. Cancel the '.
+ qq!<A HREF="${p}view/cust_main.cgi?!. $cust_pkg->custnum.
+ '#cust_pkg'. $cust_pkg->pkgnum. '">'.
+ 'package</A> instead.');
+}
+
+my $error = $cust_svc->cancel;
+
+if ( $error ) {
+ %>
+<!-- mason kludge -->
+<%
+ &eidiot($error);
+} else {
+ print $cgi->redirect(popurl(2));
+}
+
+%>
diff --git a/httemplate/misc/cancel_pkg.cgi b/httemplate/misc/cancel_pkg.cgi
new file mode 100755
index 0000000..0487677
--- /dev/null
+++ b/httemplate/misc/cancel_pkg.cgi
@@ -0,0 +1,15 @@
+<%
+
+#untaint pkgnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal pkgnum";
+my $pkgnum = $1;
+
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+
+my $error = $cust_pkg->cancel;
+eidiot($error) if $error;
+
+print $cgi->redirect($p. "view/cust_main.cgi?".$cust_pkg->getfield('custnum'));
+
+%>
diff --git a/httemplate/misc/catchall.cgi b/httemplate/misc/catchall.cgi
new file mode 100755
index 0000000..3402b61
--- /dev/null
+++ b/httemplate/misc/catchall.cgi
@@ -0,0 +1,133 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my($svc_domain, $svcnum, $pkgnum, $svcpart, $part_svc);
+if ( $cgi->param('error') ) {
+ $svc_domain = new FS::svc_domain ( {
+ map { $_, scalar($cgi->param($_)) } fields('svc_domain')
+ } );
+ $svcnum = $svc_domain->svcnum;
+ $pkgnum = $cgi->param('pkgnum');
+ $svcpart = $cgi->param('svcpart');
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+} else {
+ my($query) = $cgi->keywords;
+ if ( $query =~ /^(\d+)$/ ) { #editing
+ $svcnum=$1;
+ $svc_domain=qsearchs('svc_domain',{'svcnum'=>$svcnum})
+ or die "Unknown (svc_domain) svcnum!";
+
+ my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+ or die "Unknown (cust_svc) svcnum!";
+
+ $pkgnum=$cust_svc->pkgnum;
+ $svcpart=$cust_svc->svcpart;
+
+ $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+ die "No part_svc entry!" unless $part_svc;
+
+ } else {
+
+ die "Invalid (svc_domain) svcnum!";
+
+ }
+}
+
+my %email;
+if ($pkgnum) {
+
+ #find all possible user svcnums (and emails)
+
+ #starting with that currently attached
+ if ($svc_domain->catchall) {
+ my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$svc_domain->catchall});
+ $email{$svc_domain->catchall} = $svc_acct->email;
+ }
+
+ #and including the rest for this customer
+ my($u_part_svc,@u_acct_svcparts);
+ foreach $u_part_svc ( qsearch('part_svc',{'svcdb'=>'svc_acct'}) ) {
+ push @u_acct_svcparts,$u_part_svc->getfield('svcpart');
+ }
+
+ my($cust_pkg)=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+ my($custnum)=$cust_pkg->getfield('custnum');
+ my($i_cust_pkg);
+ foreach $i_cust_pkg ( qsearch('cust_pkg',{'custnum'=>$custnum}) ) {
+ my($cust_pkgnum)=$i_cust_pkg->getfield('pkgnum');
+ my($acct_svcpart);
+ foreach $acct_svcpart (@u_acct_svcparts) { #now find the corresponding
+ #record(s) in cust_svc ( for this
+ #pkgnum ! )
+ my($i_cust_svc);
+ foreach $i_cust_svc ( qsearch('cust_svc',{'pkgnum'=>$cust_pkgnum,'svcpart'=>$acct_svcpart}) ) {
+ my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$i_cust_svc->getfield('svcnum')});
+ $email{$svc_acct->getfield('svcnum')}=$svc_acct->email;
+ }
+ }
+ }
+
+} else {
+
+ my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$svc_domain->catchall});
+ $email{$svc_domain->catchall} = $svc_acct->email;
+}
+
+# add an absence of a catchall
+$email{''} = "(none)";
+
+my $p1 = popurl(1);
+print header("Domain Catchall Edit", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/catchall.cgi" METHOD=POST>!;
+
+#display
+
+ #formatting
+ print "<PRE>";
+
+#svcnum
+print qq!<INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">!;
+print qq!Service #<FONT SIZE=+1><B>!, $svcnum ? $svcnum : " (NEW)", "</B></FONT>";
+
+#pkgnum
+print qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!;
+
+#svcpart
+print qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!;
+
+my($domain,$catchall)=(
+ $svc_domain->domain,
+ $svc_domain->catchall,
+);
+
+print qq!<INPUT TYPE="hidden" NAME="domain" VALUE="$domain">!;
+
+#catchall
+print qq!\n\nMail to <I>(anything)</I>@<B>$domain</B> forwards to <SELECT NAME="catchall" SIZE=1>!;
+foreach $_ (keys %email) {
+ print "<OPTION", $_ eq $catchall ? " SELECTED" : "",
+ qq! VALUE="$_">$email{$_}!;
+}
+print "</SELECT>";
+
+ #formatting
+ print "</PRE>\n";
+
+print qq!<CENTER><INPUT TYPE="submit" VALUE="Submit"></CENTER>!;
+
+print <<END;
+
+ </FORM>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/misc/change_pkg.cgi b/httemplate/misc/change_pkg.cgi
new file mode 100755
index 0000000..5346fd9
--- /dev/null
+++ b/httemplate/misc/change_pkg.cgi
@@ -0,0 +1,66 @@
+<!-- mason kludge -->
+<%
+
+my $pkgnum;
+if ( $cgi->param('error') ) {
+ #$custnum = $cgi->param('custnum');
+ #%remove_pkg = map { $_ => 1 } $cgi->param('remove_pkg');
+ $pkgnum = ($cgi->param('remove_pkg'))[0];
+} else {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ #$custnum = $1;
+ $pkgnum = $1;
+ #%remove_pkg = ();
+}
+
+my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $pkgnum } )
+ or die "unknown pkgnum $pkgnum";
+my $custnum = $cust_pkg->custnum;
+
+my $conf = new FS::Conf;
+
+my $p1 = popurl(1);
+
+my $cust_main = $cust_pkg->cust_main
+ or die "can't get cust_main record for custnum ". $cust_pkg->custnum.
+ " ( pkgnum ". cust_pkg->pkgnum. ")";
+my $agent = $cust_main->agent;
+
+print header("Change Package", menubar(
+ "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+ 'Main Menu' => $p,
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT><BR><BR>"
+ if $cgi->param('error');
+
+my $part_pkg = $cust_pkg->part_pkg;
+
+print small_custview( $cust_main, $conf->config('countrydefault') ).
+ qq!<FORM ACTION="${p}edit/process/cust_pkg.cgi" METHOD=POST>!.
+ qq!<INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">!.
+ qq!<INPUT TYPE="hidden" NAME="remove_pkg" VALUE="$pkgnum">!.
+ '<BR>Current package: '. $part_pkg->pkg. ' - '. $part_pkg->comment.
+ qq!<BR>New package: <SELECT NAME="new_pkgpart"><OPTION VALUE=0></OPTION>!;
+
+foreach my $part_pkg (
+ grep { ! $_->disabled && $_->pkgpart != $cust_pkg->pkgpart }
+ map { $_->part_pkg } $agent->agent_type->type_pkgs
+) {
+ my $pkgpart = $part_pkg->pkgpart;
+ print qq!<OPTION VALUE="$pkgpart"!;
+ print ' SELECTED' if $cgi->param('error')
+ && $cgi->param('new_pkgpart') == $pkgpart;
+ print qq!>$pkgpart: !. $part_pkg->pkg. ' - '. $part_pkg->comment. '</OPTION>';
+}
+
+print <<END;
+</SELECT>
+<BR><BR><INPUT TYPE="submit" VALUE="Change package">
+ </FORM>
+ </BODY>
+</HTML>
+END
+%>
diff --git a/httemplate/misc/cust_main-cancel.cgi b/httemplate/misc/cust_main-cancel.cgi
new file mode 100755
index 0000000..257c338
--- /dev/null
+++ b/httemplate/misc/cust_main-cancel.cgi
@@ -0,0 +1,16 @@
+<%
+
+#untaint custnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal custnum";
+my $custnum = $1;
+
+my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+
+my @errors = $cust_main->cancel;
+eidiot(join(' / ', @errors)) if scalar(@errors);
+
+#print $cgi->redirect($p. "view/cust_main.cgi?". $cust_main->custnum);
+print $cgi->redirect($p);
+
+%>
diff --git a/httemplate/misc/cust_main-import.cgi b/httemplate/misc/cust_main-import.cgi
new file mode 100644
index 0000000..6b36f47
--- /dev/null
+++ b/httemplate/misc/cust_main-import.cgi
@@ -0,0 +1,51 @@
+<!-- mason kludge -->
+<%= header('Batch Customer Import') %>
+<FORM ACTION="process/cust_main-import.cgi" METHOD="post" ENCTYPE="multipart/form-data">
+Import a CSV file containing customer records.<BR><BR>
+Default file format is CSV, with the following field order: <i>cust_pkg.setup, dayphone, first, last, address1, address2, city, state, zip, comments</i><BR><BR>
+
+<%
+ #false laziness with edit/cust_main.cgi
+ my @agents = qsearch( 'agent', {} );
+ die "No agents created!" unless @agents;
+ my $agentnum = $agents[0]->agentnum; #default to first
+
+ if ( scalar(@agents) == 1 ) {
+%>
+ <INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>">
+<% } else { %>
+ <BR><BR>Agent <SELECT NAME="agentnum" SIZE="1">
+ <% foreach my $agent (sort { $a->agent cmp $b->agent } @agents) { %>
+ <OPTION VALUE="<%= $agent->agentnum %>" <%= " SELECTED"x($agent->agentnum==$agentnum) %>><%= $agent->agent %></OPTION>
+ <% } %>
+ </SELECT><BR><BR>
+<% } %>
+
+<%
+ my @referrals = qsearch('part_referral',{});
+ die "No advertising sources created!" unless @referrals;
+ my $refnum = $referrals[0]->refnum; #default to first
+
+ if ( scalar(@referrals) == 1 ) {
+%>
+ <INPUT TYPE="hidden" NAME="refnum" VALUE="<%= $refnum %>">
+<% } else { %>
+ <BR><BR>Advertising source <SELECT NAME="refnum" SIZE="1">
+ <% foreach my $referral ( sort { $a->referral <=> $b->referral } @referrals) { %>
+ <OPTION VALUE="<%= $referral->refnum %>" <%= " SELECTED"x($referral->refnum==$refnum) %>><%= $referral->refnum %>: <%= $referral->referral %></OPTION>
+ <% } %>
+ </SELECT><BR><BR>
+<% } %>
+
+ First package: <SELECT NAME="pkgpart"><OPTION VALUE="">(none)</OPTION>
+<% foreach my $part_pkg ( qsearch('part_pkg',{'disabled'=>'' }) ) { %>
+ <OPTION VALUE="<%= $part_pkg->pkgpart %>"><%= $part_pkg->pkg. ' - '. $part_pkg->comment %></OPTION>
+<% } %>
+</SELECT><BR><BR>
+
+ CSV Filename: <INPUT TYPE="file" NAME="csvfile"><BR><BR>
+ <INPUT TYPE="submit" VALUE="Import">
+ </FORM>
+ </BODY>
+<HTML>
+
diff --git a/httemplate/misc/cust_main-import_charges.cgi b/httemplate/misc/cust_main-import_charges.cgi
new file mode 100644
index 0000000..0822b9e
--- /dev/null
+++ b/httemplate/misc/cust_main-import_charges.cgi
@@ -0,0 +1,14 @@
+<!-- mason kludge -->
+<%= header('Batch Customer Charge') %>
+<FORM ACTION="process/cust_main-import_charges.cgi" METHOD="post" ENCTYPE="multipart/form-data">
+Import a CSV file containing customer charges.<BR><BR>
+Default file format is CSV, with the following field order: <i>custnum, amount, description</i><BR><BR>
+If <i>amount</i> is negative, a credit will be applied instead.<BR><BR>
+<BR><BR>
+
+ CSV Filename: <INPUT TYPE="file" NAME="csvfile"><BR><BR>
+ <INPUT TYPE="submit" VALUE="Import">
+ </FORM>
+ </BODY>
+<HTML>
+
diff --git a/httemplate/misc/delete-cust_credit.cgi b/httemplate/misc/delete-cust_credit.cgi
new file mode 100755
index 0000000..30de04d
--- /dev/null
+++ b/httemplate/misc/delete-cust_credit.cgi
@@ -0,0 +1,16 @@
+<%
+
+#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;
+eidiot($error) if $error;
+
+print $cgi->redirect($p. "view/cust_main.cgi?". $custnum);
+
+%>
diff --git a/httemplate/misc/delete-cust_pay.cgi b/httemplate/misc/delete-cust_pay.cgi
new file mode 100755
index 0000000..3efd918
--- /dev/null
+++ b/httemplate/misc/delete-cust_pay.cgi
@@ -0,0 +1,16 @@
+<%
+
+#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;
+eidiot($error) if $error;
+
+print $cgi->redirect($p. "view/cust_main.cgi?". $custnum);
+
+%>
diff --git a/httemplate/misc/delete-customer.cgi b/httemplate/misc/delete-customer.cgi
new file mode 100755
index 0000000..4302317
--- /dev/null
+++ b/httemplate/misc/delete-customer.cgi
@@ -0,0 +1,60 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+die "Customer deletions not enabled" unless $conf->exists('deletecustomers');
+
+my($custnum, $new_custnum);
+if ( $cgi->param('error') ) {
+ $custnum = $cgi->param('custnum');
+ $new_custnum = $cgi->param('new_custnum');
+} else {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/ or die "Illegal query: $query";
+ $custnum = $1;
+ $new_custnum = '';
+}
+my $cust_main = qsearchs( 'cust_main', { 'custnum' => $custnum } )
+ or die "Customer not found: $custnum";
+
+print header('Delete customer');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+
+print
+ qq!<form action="!, popurl(1), qq!process/delete-customer.cgi" method=post>!,
+ qq!<input type="hidden" name="custnum" value="$custnum">!;
+
+if ( qsearch('cust_pkg', { 'custnum' => $custnum, 'cancel' => '' } ) ) {
+ print "Move uncancelled packages to customer number ",
+ qq!<input type="text" name="new_custnum" value="$new_custnum"><br><br>!;
+}
+
+print <<END;
+This will <b>completely remove</b> all traces of this customer record. This
+is <B>not</B> what you want if this is a real customer who has simply
+canceled service with you. For that, cancel all of the customer's packages.
+(you can optionally hide cancelled customers with the <a href="../config/config-view.cgi#hidecancelledcustomers">hidecancelledcustomers</a> configuration option)
+<br>
+<br>Are you <b>absolutely sure</b> you want to delete this customer?
+<br><input type="submit" value="Yes">
+</form></body></html>
+END
+
+#Deleting a customer you have financial records on (i.e. credits) is
+#typically considered fraudulant bookkeeping. Remember, deleting
+#customers should ONLY be used for completely bogus records. You should
+#NOT delete real customers who simply discontinue service.
+#
+#For real customers who simply discontinue service, cancel all of the
+#customer's packages. Customers with all cancelled packages are not
+#billed. There is no need to take further action to prevent billing on
+#customers with all cancelled packages.
+#
+#Also see the "hidecancelledcustomers" and "hidecancelledpackages"
+#configuration options, which will allow you to surpress the display of
+#cancelled customers and packages, respectively.
+
+%>
diff --git a/httemplate/misc/delete-domain_record.cgi b/httemplate/misc/delete-domain_record.cgi
new file mode 100755
index 0000000..dcc2d50
--- /dev/null
+++ b/httemplate/misc/delete-domain_record.cgi
@@ -0,0 +1,15 @@
+<%
+
+#untaint recnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal recnum";
+my $recnum = $1;
+
+my $domain_record = qsearchs('domain_record',{'recnum'=>$recnum});
+
+my $error = $domain_record->delete;
+eidiot($error) if $error;
+
+print $cgi->redirect($p. "view/svc_domain.cgi?". $domain_record->svcnum);
+
+%>
diff --git a/httemplate/misc/delete-part_export.cgi b/httemplate/misc/delete-part_export.cgi
new file mode 100755
index 0000000..7c4ab8b
--- /dev/null
+++ b/httemplate/misc/delete-part_export.cgi
@@ -0,0 +1,15 @@
+<%
+
+#untaint exportnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal exportnum";
+my $exportnum = $1;
+
+my $part_export = qsearchs('part_export',{'exportnum'=>$exportnum});
+
+my $error = $part_export->delete;
+eidiot($error) if $error;
+
+print $cgi->redirect($p. "browse/part_export.cgi");
+
+%>
diff --git a/httemplate/misc/download-batch.cgi b/httemplate/misc/download-batch.cgi
new file mode 100644
index 0000000..306ef5d
--- /dev/null
+++ b/httemplate/misc/download-batch.cgi
@@ -0,0 +1,16 @@
+<%
+
+#http_header('Content-Type' => 'text/comma-separated-values' ); #IE chokes
+http_header('Content-Type' => 'text/plain' );
+
+for my $cust_pay_batch ( sort { $a->paybatchnum <=> $b->paybatchnum }
+ qsearch('cust_pay_batch', {} )
+) {
+
+$cust_pay_batch->exp =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+my( $mon, $y ) = ( $2, $1 );
+$mon = "0$mon" if $mon < 10;
+my $exp = "$mon$y";
+
+%>,,,,<%= $cust_pay_batch->cardnum %>,<%= $exp %>,<%= $cust_pay_batch->amount %>,<%= $cust_pay_batch->paybatchnum %>
+<% } %>
diff --git a/httemplate/misc/dump.cgi b/httemplate/misc/dump.cgi
new file mode 100644
index 0000000..dc1323b
--- /dev/null
+++ b/httemplate/misc/dump.cgi
@@ -0,0 +1,19 @@
+<%
+ if ( driver_name =~ /^Pg$/ ) {
+ my $dbname = (split(':', datasrc))[2];
+ if ( $dbname =~ /[;=]/ ) {
+ my %elements = map { /^(\w+)=(.*)$/; $1=>$2 } split(';', $dbname);
+ $dbname = $elements{'dbname'};
+ }
+ open(DUMP,"pg_dump $dbname |");
+ } else {
+ eidiot "don't (yet) know how to dump ". driver_name. " databases\n";
+ }
+
+ http_header('Content-Type' => 'text/plain' );
+
+ while (<DUMP>) {
+ print $_;
+ }
+ close DUMP;
+%>
diff --git a/httemplate/misc/email-invoice.cgi b/httemplate/misc/email-invoice.cgi
new file mode 100755
index 0000000..a560a18
--- /dev/null
+++ b/httemplate/misc/email-invoice.cgi
@@ -0,0 +1,23 @@
+<%
+
+my $conf = new FS::Conf;
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d*)$/;
+my $invnum = $1;
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+die "Can't find invoice!\n" unless $cust_bill;
+
+my $error = send_email(
+ 'from' => $cust_bill->_agent_invoice_from || $conf->config('invoice_from'),
+ 'to' => [ grep { $_ ne 'POST' } $cust_bill->cust_main->invoicing_list ],
+ 'subject' => 'Invoice',
+ 'body' => [ $cust_bill->print_text ],
+);
+eidiot($error) if $error;
+
+my $custnum = $cust_bill->getfield('custnum');
+print $cgi->redirect("${p}view/cust_main.cgi?$custnum");
+
+%>
diff --git a/httemplate/misc/expire_pkg.cgi b/httemplate/misc/expire_pkg.cgi
new file mode 100755
index 0000000..b59674a
--- /dev/null
+++ b/httemplate/misc/expire_pkg.cgi
@@ -0,0 +1,55 @@
+<!-- mason kludge -->
+<%
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $pkgnum = $1;
+
+#get package record
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+die "Unknown pkgnum $pkgnum" unless $cust_pkg;
+my $part_pkg = $cust_pkg->part_pkg;
+
+my $custnum = $cust_pkg->getfield('custnum');
+
+my $date = $cust_pkg->expire ? time2str('%D', $cust_pkg->expire) : '';
+
+%>
+
+<%= header('Expire package', menubar(
+ "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+ 'Main Menu' => popurl(2)
+)) %>
+
+<LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+<SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
+
+<%= $pkgnum %>: <%= $part_pkg->pkg. ' - '. $part_pkg->comment %>
+
+<FORM NAME="formname" ACTION="process/expire_pkg.cgi" METHOD="post">
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<%= $pkgnum %>">
+<TABLE>
+ <TR>
+ <TD>Cancel package on </TD>
+ <TD><INPUT TYPE="text" NAME="date" ID="expire_date" VALUE="<%= $date %>">
+ <IMG SRC="<%= $p %>images/calendar.png" ID="expire_button" STYLE="cursor:pointer" TITLE="Select date">
+ <BR><I>m/d/y</I>
+ </TD>
+ </TR>
+</TABLE>
+
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "expire_date",
+ ifFormat: "%m/%d/%Y",
+ button: "expire_button",
+ align: "BR"
+ });
+</SCRIPT>
+
+<INPUT TYPE="submit" VALUE="Cancel later">
+</FORM>
+</BODY>
+</HTML>
diff --git a/httemplate/misc/link.cgi b/httemplate/misc/link.cgi
new file mode 100755
index 0000000..18cd378
--- /dev/null
+++ b/httemplate/misc/link.cgi
@@ -0,0 +1,74 @@
+<!-- mason kludge -->
+<%
+
+my %link_field = (
+ 'svc_acct' => 'username',
+ 'svc_domain' => 'domain',
+);
+
+my %link_field2 = (
+ 'svc_acct' => { label => 'Domain',
+ field => 'domsvc',
+ type => 'select',
+ select_table => 'svc_domain',
+ select_key => 'svcnum',
+ select_label => 'domain'
+ },
+);
+
+my($query) = $cgi->keywords;
+my($pkgnum, $svcpart) = ('', '');
+foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
+ $pkgnum=$1 if /^pkgnum(\d+)$/;
+ $svcpart=$1 if /^svcpart(\d+)$/;
+}
+
+my $part_svc = qsearchs('part_svc',{'svcpart'=>$svcpart});
+my $svc = $part_svc->getfield('svc');
+my $svcdb = $part_svc->getfield('svcdb');
+my $link_field = $link_field{$svcdb};
+my $link_field2 = $link_field2{$svcdb};
+
+%>
+
+<%= header("Link to existing $svc") %>
+<FORM ACTION="<%= popurl(1) %>process/link.cgi" METHOD=POST>
+
+<% if ( $link_field ) { %>
+ <INPUT TYPE="hidden" NAME="svcnum" VALUE="">
+ <INPUT TYPE="hidden" NAME="link_field" VALUE="<%= $link_field %>">
+ <%= $link_field %> of existing service: <INPUT TYPE="text" NAME="link_value">
+ <BR>
+ <% if ( $link_field2 ) { %>
+ <INPUT TYPE="hidden" NAME="link_field2" VALUE="<%= $link_field2->{field} %>">
+ <%= $link_field2->{'label'} %> of existing service:
+ <% if ( $link_field2->{'type'} eq 'select' ) { %>
+ <% if ( $link_field2->{'select_table'} ) { %>
+ <SELECT NAME="link_value2">
+ <OPTION> </OPTION>
+ <% foreach my $r ( qsearch( $link_field2->{'select_table'}, {})) { %>
+ <% my $key = $link_field2->{'select_key'}; %>
+ <% my $label = $link_field2->{'select_label'}; %>
+ <OPTION VALUE="<%= $r->$key() %>"><%= $r->$label() %></OPTION>
+ <% } %>
+ </SELECT>
+ <% } else { %>
+ Don't know how to process secondary link field for <%= $svcdb %>
+ (type=>select but no select_table)
+ <% } %>
+ <% } else { %>
+ Don't know how to process secondary link field for <%= $svcdb %>
+ (unknown type <%= $link_field2->{'type'} %>)
+ <% } %>
+ <BR>
+ <% } %>
+<% } else { %>
+ Service # of existing service: <INPUT TYPE="text" NAME="svcnum" VALUE="">
+<% } %>
+
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<%= $pkgnum %>">
+<INPUT TYPE="hidden" NAME="svcpart" VALUE="<%= $svcpart %>">
+<BR><INPUT TYPE="submit" VALUE="Link">
+ </FORM>
+ </BODY>
+</HTML>
diff --git a/httemplate/misc/meta-import.cgi b/httemplate/misc/meta-import.cgi
new file mode 100644
index 0000000..2f3b738
--- /dev/null
+++ b/httemplate/misc/meta-import.cgi
@@ -0,0 +1,64 @@
+<!-- mason kludge -->
+<%= header('Import') %>
+<FORM ACTION="process/meta-import.cgi" METHOD="post" ENCTYPE="multipart/form-data">
+Import data from a DBI data source<BR><BR>
+
+<%
+ #false laziness with edit/cust_main.cgi
+ my @agents = qsearch( 'agent', {} );
+ die "No agents created!" unless @agents;
+ my $agentnum = $agents[0]->agentnum; #default to first
+
+ if ( scalar(@agents) == 1 ) {
+%>
+ <INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>">
+<% } else { %>
+ <BR><BR>Agent <SELECT NAME="agentnum" SIZE="1">
+ <% foreach my $agent (sort { $a->agent cmp $b->agent } @agents) { %>
+ <OPTION VALUE="<%= $agent->agentnum %>" <%= " SELECTED"x($agent->agentnum==$agentnum) %>><%= $agent->agent %></OPTION>
+ <% } %>
+ </SELECT><BR><BR>
+<% } %>
+
+<%
+ my @referrals = qsearch('part_referral',{});
+ die "No advertising sources created!" unless @referrals;
+ my $refnum = $referrals[0]->refnum; #default to first
+
+ if ( scalar(@referrals) == 1 ) {
+%>
+ <INPUT TYPE="hidden" NAME="refnum" VALUE="<%= $refnum %>">
+<% } else { %>
+ <BR><BR>Advertising source <SELECT NAME="refnum" SIZE="1">
+ <% foreach my $referral ( sort { $a->referral <=> $b->referral } @referrals) { %>
+ <OPTION VALUE="<%= $referral->refnum %>" <%= " SELECTED"x($referral->refnum==$refnum) %>><%= $referral->refnum %>: <%= $referral->referral %></OPTION>
+ <% } %>
+ </SELECT><BR><BR>
+<% } %>
+
+ First package: <SELECT NAME="pkgpart"><OPTION VALUE="">(none)</OPTION>
+<% foreach my $part_pkg ( qsearch('part_pkg',{'disabled'=>'' }) ) { %>
+ <OPTION VALUE="<%= $part_pkg->pkgpart %>"><%= $part_pkg->pkg. ' - '. $part_pkg->comment %></OPTION>
+<% } %>
+</SELECT><BR><BR>
+
+ <table>
+ <tr>
+ <td align="right">DBI data source: </td>
+ <td><INPUT TYPE="text" NAME="data_source"></td>
+ </tr>
+ <tr>
+ <td align="right">DBI username: </td>
+ <td><INPUT TYPE="text" NAME="username"></td>
+ </tr>
+ <tr>
+ <td align="right">DBI password: </td>
+ <td><INPUT TYPE="text" NAME="password"></td>
+ </tr>
+ </table>
+ <INPUT TYPE="submit" VALUE="Import">
+
+ </FORM>
+ </BODY>
+<HTML>
+
diff --git a/httemplate/misc/payment.cgi b/httemplate/misc/payment.cgi
new file mode 100644
index 0000000..02c6c54
--- /dev/null
+++ b/httemplate/misc/payment.cgi
@@ -0,0 +1,209 @@
+<%
+ my %type = ( 'CARD' => 'credit card',
+ 'CHEK' => 'electronic check (ACH)',
+ );
+
+ $cgi->param('payby') =~ /^(CARD|CHEK)$/
+ or die "unknown payby ". $cgi->param('payby');
+ my $payby = $1;
+
+ $cgi->param('custnum') =~ /^(\d+)$/
+ or die "illegal custnum ". $cgi->param('custnum');
+ my $custnum = $1;
+
+ my $cust_main = qsearchs( 'cust_main', { 'custnum'=>$custnum } );
+ die "unknown custnum $custnum" unless $cust_main;
+
+ my $balance = $cust_main->balance;
+
+ my $payinfo = '';
+
+ #false laziness w/selfservice make_payment.html shortcut for one-country
+ my $conf = new FS::Conf;
+ my %states = map { $_->state => 1 }
+ qsearch('cust_main_county', {
+ 'country' => $conf->config('defaultcountry') || 'US'
+ } );
+ my @states = sort { $a cmp $b } keys %states;
+
+ my $paybatch = "webui-payment-". time. "-$$-". rand() * 2**32;
+
+%>
+<%= include( '/elements/header.html', "Process $type{$payby} payment" ) %>
+<%= include( '/elements/small_custview.html', $cust_main ) %>
+<FORM NAME="OneTrueForm" ACTION="process/payment.cgi" METHOD="POST" onSubmit="document.OneTrueForm.process.disabled=true">
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<%= $custnum %>">
+<INPUT TYPE="hidden" NAME="payby" VALUE="<%= $payby %>">
+<INPUT TYPE="hidden" NAME="paybatch" VALUE="<%= $paybatch %>">
+<SCRIPT>
+var mywindow = -1;
+function myopen(filename,windowname,properties) {
+ myclose();
+ mywindow = window.open(filename,windowname,properties);
+}
+function myclose() {
+ if ( mywindow != -1 )
+ mywindow.close();
+ mywindow = -1;
+}
+var achwindow = -1;
+function achopen(filename,windowname,properties) {
+ achclose();
+ achwindow = window.open(filename,windowname,properties);
+}
+function achclose() {
+ if ( achwindow != -1 )
+ achwindow.close();
+ achwindow = -1;
+}
+</SCRIPT>
+<% #include( '/elements/table.html', '#cccccc' ) %>
+<%= ntable('#cccccc') %>
+ <TR>
+ <TD ALIGN="right">Payment amount</TD>
+ <TD>
+ <TABLE><TR><TD BGCOLOR="#ffffff">
+ $<INPUT TYPE="text" NAME="amount" SIZE=8 VALUE="<%= $balance > 0 ? sprintf("%.2f", $balance) : '' %>">
+ </TD></TR></TABLE>
+ </TD>
+ </TR>
+<% if ( $payby eq 'CARD' ) {
+ my( $payinfo, $paycvv, $month, $year ) = ( '', '', '', '' );
+ my $payname = $cust_main->first. ' '. $cust_main->getfield('last');
+ my $address1 = $cust_main->address1;
+ my $address2 = $cust_main->address2;
+ my $city = $cust_main->city;
+ my $state = $cust_main->state;
+ my $zip = $cust_main->zip;
+ if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
+ $payinfo = $cust_main->payinfo;
+ $paycvv = $cust_main->paycvv;
+ ( $month, $year ) = $cust_main->paydate_monthyear;
+ $payname = $cust_main->payname if $cust_main->payname;
+ }
+%>
+ <TR>
+ <TD ALIGN="right">Card&nbsp;number</TD>
+ <TD>
+ <TABLE>
+ <TR>
+ <TD>
+ <INPUT TYPE="text" NAME="payinfo" SIZE=20 MAXLENGTH=19 VALUE="<%=$payinfo%>"> </TD>
+ <TD>Exp.</TD>
+ <TD>
+ <SELECT NAME="month">
+ <% for ( ( map "0$_", 1 .. 9 ), 10 .. 12 ) { %>
+ <OPTION<%= $_ == $month ? ' SELECTED' : '' %>><%= $_ %>
+ <% } %>
+ </SELECT>
+ </TD>
+ <TD> / </TD>
+ <TD>
+ <SELECT NAME="year">
+ <% my @a = localtime; for ( $a[5]+1900 .. $a[5]+1915 ) { %>
+ <OPTION<%= $_ == $year ? ' SELECTED' : '' %>><%= $_ %>
+ <% } %>
+ </SELECT>
+ </TD>
+ </TR>
+ </TABLE>
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">CVV2</TD>
+ <TD><INPUT TYPE="text" NAME="paycvv" VALUE="<%= $paycvv %>" SIZE=4 MAXLENGTH=4>
+ (<A HREF="javascript:myopen('../docs/cvv2.html','cvv2','toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=480,height=288')">help</A>)
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Exact&nbsp;name&nbsp;on&nbsp;card</TD>
+ <TD><INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="payname" VALUE="<%=$payname%>"></TD>
+ </TR><TR>
+ <TD ALIGN="right">Card&nbsp;billing&nbsp;address</TD>
+ <TD>
+ <INPUT TYPE="text" SIZE=40 MAXLENGTH=80 NAME="address1" VALUE="<%=$address1%>">
+ </TD>
+ </TR><TR>
+ <TD ALIGN="right">Address&nbsp;line&nbsp;2</TD>
+ <TD>
+ <INPUT TYPE="text" SIZE=40 MAXLENGTH=80 NAME="address2" VALUE="<%=$address2%>">
+ </TD>
+ </TR><TR>
+ <TD ALIGN="right">City</TD>
+ <TD>
+ <TABLE>
+ <TR>
+ <TD>
+ <INPUT TYPE="text" NAME="city" SIZE="12" MAXLENGTH=80 VALUE="<%=$city%>">
+ </TD>
+ <TD>State</TD>
+ <TD>
+ <SELECT NAME="state">
+ <% for ( @states ) { %>
+ <OPTION<%= $_ eq $state ? ' SELECTED' : '' %>><%= $_ %>
+ <% } %>
+ </SELECT>
+ </TD>
+ <TD>Zip</TD>
+ <TD>
+ <INPUT TYPE="text" NAME="zip" SIZE=11 MAXLENGTH=10 VALUE="<%=$zip%>">
+ </TD>
+ </TR>
+ </TABLE>
+ </TD>
+ </TR>
+
+<% } elsif ( $payby eq 'CHEK' ) {
+ my( $payinfo1, $payinfo2, $payname, $ss ) = ( '', '', '', '' );
+ if ( $cust_main->payby =~ /^(CHEK|DCHK)$/ ) {
+ $cust_main->payinfo =~ /^(\d+)\@(\d+)$/
+ or die "unparsable payinfo ". $cust_main->payinfo;
+ ($payinfo1, $payinfo2) = ($1, $2);
+ $payname = $cust_main->payname;
+ $ss = $cust_main->ss;
+ }
+%>
+ <INPUT TYPE="hidden" NAME="month" VALUE="12">
+ <INPUT TYPE="hidden" NAME="year" VALUE="2037">
+ <TR>
+ <TD ALIGN="right">Account&nbsp;number</TD>
+ <TD><INPUT TYPE="text" SIZE=10 NAME="payinfo1" VALUE="<%=$payinfo1%>"></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">ABA/Routing&nbsp;number</TD>
+ <TD>
+ <INPUT TYPE="text" SIZE=10 MAXLENGTH=9 NAME="payinfo2" VALUE="<%=$payinfo2%>">
+ (<A HREF="javascript:achopen('../docs/ach.html','ach','toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=384,height=256')">help</A>)
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Bank&nbsp;name</TD>
+ <TD><INPUT TYPE="text" NAME="payname" VALUE="<%=$payname%>"></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">
+ Account&nbsp;holder<BR>
+ Social&nbsp;security&nbsp;or&nbsp;tax&nbspID&nbsp;#
+ </TD>
+ <TD><INPUT TYPE="text" NAME="ss" VALUE="<%=$ss%>"></TD>
+ </TR>
+
+<% } %>
+
+<TR>
+ <TD COLSPAN=2>
+ <INPUT TYPE="checkbox" CHECKED NAME="save" VALUE="1">
+ Remember this information
+ </TD>
+</TR><TR>
+ <TD COLSPAN=2>
+ <INPUT TYPE="checkbox"<%= ( ( $payby eq 'CARD' && $cust_main->payby ne 'DCRD' ) || ( $payby eq 'CHEK' && $cust_main->payby eq 'CHEK' ) ) ? ' CHECKED' : '' %> NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }">
+ Charge future payments to this <%= $type{$payby} %> automatically
+ </TD>
+</TR>
+</TABLE>
+<BR>
+<INPUT TYPE="submit" NAME="process" VALUE="Process payment">
+</FORM>
+</BODY>
+</HTML>
diff --git a/httemplate/misc/print-invoice.cgi b/httemplate/misc/print-invoice.cgi
new file mode 100755
index 0000000..144f615
--- /dev/null
+++ b/httemplate/misc/print-invoice.cgi
@@ -0,0 +1,29 @@
+<%
+
+my $conf = new FS::Conf;
+my $lpr = $conf->config('lpr');
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d*)$/;
+my $invnum = $1;
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+die "Can't find invoice!\n" unless $cust_bill;
+
+ open(LPR,"|$lpr") or die "Can't open $lpr: $!";
+
+ if ( $conf->exists('invoice_latex') ) {
+ print LPR $cust_bill->print_ps; #( date )
+ } else {
+ print LPR $cust_bill->print_text; #( date )
+ }
+
+ close LPR
+ or die $! ? "Error closing $lpr: $!"
+ : "Exit status $? from $lpr";
+
+my $custnum = $cust_bill->getfield('custnum');
+
+print $cgi->redirect("${p}view/cust_main.cgi?$custnum");
+
+%>
diff --git a/httemplate/misc/process/catchall.cgi b/httemplate/misc/process/catchall.cgi
new file mode 100755
index 0000000..44a63f9
--- /dev/null
+++ b/httemplate/misc/process/catchall.cgi
@@ -0,0 +1,33 @@
+<%
+
+$FS::svc_domain::whois_hack=1;
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum =$1;
+
+my $old = qsearchs('svc_domain',{'svcnum'=>$svcnum}) if $svcnum;
+
+my $new = new FS::svc_domain ( {
+ map {
+ ($_, scalar($cgi->param($_)));
+ } ( fields('svc_domain'), qw( pkgnum svcpart ) )
+} );
+
+$new->setfield('action' => 'M');
+
+my $error;
+if ( $svcnum ) {
+ $error = $new->replace($old);
+} else {
+ $error = $new->insert;
+ $svcnum = $new->getfield('svcnum');
+}
+
+if ($error) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "catchall.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "view/svc_domain.cgi?$svcnum");
+}
+
+%>
diff --git a/httemplate/misc/process/cust_main-import.cgi b/httemplate/misc/process/cust_main-import.cgi
new file mode 100644
index 0000000..9e1adce
--- /dev/null
+++ b/httemplate/misc/process/cust_main-import.cgi
@@ -0,0 +1,30 @@
+<%
+
+ my $fh = $cgi->upload('csvfile');
+ #warn $cgi;
+ #warn $fh;
+
+ my $error = defined($fh)
+ ? FS::cust_main::batch_import( {
+ filehandle => $fh,
+ agentnum => scalar($cgi->param('agentnum')),
+ refnum => scalar($cgi->param('refnum')),
+ pkgpart => scalar($cgi->param('pkgpart')),
+ 'fields' => [qw( cust_pkg.setup dayphone first last address1 address2
+ city state zip comments )],
+ } )
+ : 'No file';
+
+ if ( $error ) {
+ %>
+ <!-- mason kludge -->
+ <%
+ eidiot($error);
+# $cgi->param('error', $error);
+# print $cgi->redirect( "${p}cust_main-import.cgi
+ } else {
+ %>
+ <!-- mason kludge -->
+ <%= header('Import sucessful') %> <%
+ }
+%>
diff --git a/httemplate/misc/process/cust_main-import_charges.cgi b/httemplate/misc/process/cust_main-import_charges.cgi
new file mode 100644
index 0000000..14df1bd
--- /dev/null
+++ b/httemplate/misc/process/cust_main-import_charges.cgi
@@ -0,0 +1,26 @@
+<%
+
+ my $fh = $cgi->upload('csvfile');
+ #warn $cgi;
+ #warn $fh;
+
+ my $error = defined($fh)
+ ? FS::cust_main::batch_charge( {
+ filehandle => $fh,
+ 'fields' => [qw( custnum amount pkg )],
+ } )
+ : 'No file';
+
+ if ( $error ) {
+ %>
+ <!-- mason kludge -->
+ <%
+ eidiot($error);
+# $cgi->param('error', $error);
+# print $cgi->redirect( "${p}cust_main-import_charges.cgi
+ } else {
+ %>
+ <!-- mason kludge -->
+ <%= header('Import sucessful') %> <%
+ }
+%>
diff --git a/httemplate/misc/process/delete-customer.cgi b/httemplate/misc/process/delete-customer.cgi
new file mode 100755
index 0000000..16bdbae
--- /dev/null
+++ b/httemplate/misc/process/delete-customer.cgi
@@ -0,0 +1,29 @@
+<%
+
+my $conf = new FS::Conf;
+die "Customer deletions not enabled" unless $conf->exists('deletecustomers');
+
+$cgi->param('custnum') =~ /^(\d+)$/;
+my $custnum = $1;
+my $new_custnum;
+if ( $cgi->param('new_custnum') ) {
+ $cgi->param('new_custnum') =~ /^(\d+)$/
+ or die "Illegal new customer number: ". $cgi->param('new_custnum');
+ $new_custnum = $1;
+} else {
+ $new_custnum = '';
+}
+my $cust_main = qsearchs( 'cust_main', { 'custnum' => $custnum } )
+ or die "Customer not found: $custnum";
+
+my $error = $cust_main->delete($new_custnum);
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "delete-customer.cgi?". $cgi->query_string );
+} elsif ( $new_custnum ) {
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?$new_custnum");
+} else {
+ print $cgi->redirect(popurl(3));
+}
+%>
diff --git a/httemplate/misc/process/expire_pkg.cgi b/httemplate/misc/process/expire_pkg.cgi
new file mode 100755
index 0000000..dc35592
--- /dev/null
+++ b/httemplate/misc/process/expire_pkg.cgi
@@ -0,0 +1,25 @@
+<%
+
+#untaint date & pkgnum
+
+my $date;
+if ( $cgi->param('date') ) {
+ str2time($cgi->param('date')) =~ /^(\d+)$/ or die "Illegal date";
+ $date=$1;
+} else {
+ $date='';
+}
+
+$cgi->param('pkgnum') =~ /^(\d+)$/ or die "Illegal pkgnum";
+my $pkgnum = $1;
+
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+my %hash = $cust_pkg->hash;
+$hash{expire}=$date;
+my $new = new FS::cust_pkg ( \%hash );
+my $error = $new->replace($cust_pkg);
+&eidiot($error) if $error;
+
+print $cgi->redirect(popurl(3). "view/cust_main.cgi?".$cust_pkg->getfield('custnum'));
+
+%>
diff --git a/httemplate/misc/process/link.cgi b/httemplate/misc/process/link.cgi
new file mode 100755
index 0000000..acdd1ad
--- /dev/null
+++ b/httemplate/misc/process/link.cgi
@@ -0,0 +1,57 @@
+<%
+
+$cgi->param('pkgnum') =~ /^(\d+)$/;
+my $pkgnum = $1;
+$cgi->param('svcpart') =~ /^(\d+)$/;
+my $svcpart = $1;
+$cgi->param('svcnum') =~ /^(\d*)$/;
+my $svcnum = $1;
+
+unless ( $svcnum ) {
+ my $part_svc = qsearchs('part_svc',{'svcpart'=>$svcpart});
+ my $svcdb = $part_svc->getfield('svcdb');
+ $cgi->param('link_field') =~ /^(\w+)$/;
+ my $link_field = $1;
+ my %search = ( $link_field => $cgi->param('link_value') );
+ if ( $cgi->param('link_field2') =~ /^(\w+)$/ ) {
+ $search{$1} = $cgi->param('link_value2');
+ }
+ my $svc_x = ( sort { ($b->cust_svc->pkgnum > 0) <=> ($a->cust_svc->pkgnum > 0)
+ or ($b->cust_svc->svcpart == $svcpart)
+ <=> ($a->cust_svc->svcpart == $svcpart)
+ }
+ qsearch( $svcdb, \%search )
+ )[0];
+ eidiot("$link_field not found!") unless $svc_x;
+ $svcnum = $svc_x->svcnum;
+}
+
+my $old = qsearchs('cust_svc',{'svcnum'=>$svcnum});
+die "svcnum not found!" unless $old;
+my $conf = new FS::Conf;
+my($error, $new);
+if ( $old->pkgnum && ! $conf->exists('legacy_link-steal') ) {
+ $error = "svcnum $svcnum already linked to package ". $old->pkgnum;
+} else {
+ $new = new FS::cust_svc ({
+ 'svcnum' => $svcnum,
+ 'pkgnum' => $pkgnum,
+ 'svcpart' => $svcpart,
+ });
+
+ $error = $new->replace($old);
+}
+
+unless ($error) {
+ #no errors, so let's view this customer.
+ my $custnum = $new->cust_pkg->custnum;
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum".
+ "#cust_pkg$pkgnum" );
+} else {
+%>
+<!-- mason kludge -->
+<%
+ idiot($error);
+}
+
+%>
diff --git a/httemplate/misc/process/meta-import.cgi b/httemplate/misc/process/meta-import.cgi
new file mode 100644
index 0000000..59d236f
--- /dev/null
+++ b/httemplate/misc/process/meta-import.cgi
@@ -0,0 +1,178 @@
+<!-- mason kludge -->
+<%= header('Map tables') %>
+
+<SCRIPT>
+var gSafeOnload = new Array();
+var gSafeOnsubmit = new Array();
+window.onload = SafeOnload;
+function SafeAddOnLoad(f) {
+ gSafeOnload[gSafeOnload.length] = f;
+}
+function SafeOnload() {
+ for (var i=0;i<gSafeOnload.length;i++)
+ gSafeOnload[i]();
+}
+function SafeAddOnSubmit(f) {
+ gSafeOnsubmit[gSafeOnsubmit.length] = f;
+}
+function SafeOnsubmit() {
+ for (var i=0;i<gSafeOnsubmit.length;i++)
+ gSafeOnsubmit[i]();
+}
+</SCRIPT>
+
+<FORM NAME="OneTrueForm" METHOD="POST" ACTION="meta-import.cgi">
+
+<%
+ #use DBIx::DBSchema;
+ my $schema = new_native DBIx::DBSchema
+ map { $cgi->param($_) } qw( data_source username password );
+ foreach my $field (qw( data_source username password )) { %>
+ <INPUT TYPE="hidden" NAME=<%= $field %> VALUE="<%= $cgi->param($field) %>">
+ <% }
+
+ my %schema;
+ use Tie::DxHash;
+ tie %schema, 'Tie::DxHash';
+ if ( $cgi->param('schema') ) {
+ my $schema_string = $cgi->param('schema');
+ %> <INPUT TYPE="hidden" NAME="schema" VALUE="<%=$schema_string%>"> <%
+ %schema = map { /^\s*(\w+)\s*=>\s*(\w+)\s*$/
+ or die "guru meditation #420: $_";
+ ( $1 => $2 );
+ }
+ split( /\n/, $schema_string );
+ }
+
+ #first page
+ unless ( $cgi->param('magic') ) { %>
+
+ <INPUT TYPE="hidden" NAME="magic" VALUE="process">
+ <%= hashmaker('schema', [ $schema->tables ],
+ [ grep !/^h_/, dbdef->tables ], ) %>
+ <br><INPUT TYPE="submit" VALUE="done">
+ <%
+
+ #second page
+ } elsif ( $cgi->param('magic') eq 'process' ) { %>
+
+ <INPUT TYPE="hidden" NAME="magic" VALUE="process2">
+ <%
+
+ my %unique;
+ foreach my $table ( keys %schema ) {
+
+ my @from_columns = $schema->table($table)->columns;
+ my @fs_columns = dbdef->table($schema{$table})->columns;
+
+ %>
+ <%= hashmaker( $table.'__'.$unique{$table}++,
+ \@from_columns => \@fs_columns,
+ $table => $schema{$table}, ) %>
+ <br><hr><br>
+ <%
+
+ }
+
+ %>
+ <br><INPUT TYPE="submit" VALUE="done">
+ <%
+
+ #third (results)
+ } elsif ( $cgi->param('magic') eq 'process2' ) {
+
+ print "<pre>\n";
+
+ my %unique;
+ foreach my $table ( keys %schema ) {
+ ( my $spaces = $table ) =~ s/./ /g;
+ print "'$table' => { 'table' => '$schema{$table}',\n".
+ #(length($table) x ' '). " 'map' => {\n";
+ "$spaces 'map' => {\n";
+ my %map = map { /^\s*(\w+)\s*=>\s*(\w+)\s*$/
+ or die "guru meditation #420: $_";
+ ( $1 => $2 );
+ }
+ split( /\n/, $cgi->param($table.'__'.$unique{$table}++) );
+ foreach ( keys %map ) {
+ print "$spaces '$_' => '$map{$_}',\n";
+ }
+ print "$spaces },\n";
+ print "$spaces },\n";
+
+ }
+ print "\n</pre>";
+
+ } else {
+ warn "unrecognized magic: ". $cgi->param('magic');
+ }
+
+ %>
+</FORM>
+</BODY>
+</HTML>
+
+ <%
+ #hashmaker widget
+ sub hashmaker {
+ my($name, $from, $to, $labelfrom, $labelto) = @_;
+ my $fromsize = scalar(@$from);
+ my $tosize = scalar(@$to);
+ "<TABLE><TR><TH>$labelfrom</TH><TH>$labelto</TH></TR><TR><TD>".
+ qq!<SELECT NAME="${name}_from" SIZE=$fromsize>\n!.
+ join("\n", map { qq!<OPTION VALUE="$_">$_</OPTION>! } sort { $a cmp $b } @$from ).
+ "</SELECT>\n<BR>".
+ qq!<INPUT TYPE="button" VALUE="refill" onClick="repack_${name}_from()">!.
+ '</TD><TD>'.
+ qq!<SELECT NAME="${name}_to" SIZE=$tosize>\n!.
+ join("\n", map { qq!<OPTION VALUE="$_">$_</OPTION>! } sort { $a cmp $b } @$to ).
+ "</SELECT>\n<BR>".
+ qq!<INPUT TYPE="button" VALUE="refill" onClick="repack_${name}_to()">!.
+ '</TD></TR>'.
+ '<TR><TD COLSPAN=2>'.
+ qq!<INPUT TYPE="button" VALUE="map" onClick="toke_$name(this.form)">!.
+ '</TD></TR><TR><TD COLSPAN=2>'.
+ qq!<TEXTAREA NAME="$name" COLS=80 ROWS=8></TEXTAREA>!.
+ '</TD></TR></TABLE>'.
+ "<script>
+ function toke_$name() {
+ fromObject = document.OneTrueForm.${name}_from;
+ for (var i=fromObject.options.length-1;i>-1;i--) {
+ if (fromObject.options[i].selected)
+ fromname = deleteOption_$name(fromObject,i);
+ }
+ toObject = document.OneTrueForm.${name}_to;
+ for (var i=toObject.options.length-1;i>-1;i--) {
+ if (toObject.options[i].selected)
+ toname = deleteOption_$name(toObject,i);
+ }
+ document.OneTrueForm.$name.value = document.OneTrueForm.$name.value + fromname + ' => ' + toname + '\\n';
+ }
+ function deleteOption_$name(object,index) {
+ value = object.options[index].value;
+ object.options[index] = null;
+ return value;
+ }
+ function repack_${name}_from() {
+ var object = document.OneTrueForm.${name}_from;
+ object.options.length = 0;
+ ". join("\n",
+ map { "addOption_$name(object, '$_');\n" }
+ ( sort { $a cmp $b } @$from ) ). "
+ }
+ function repack_${name}_to() {
+ var object = document.OneTrueForm.${name}_to;
+ object.options.length = 0;
+ ". join("\n",
+ map { "addOption_$name(object, '$_');\n" }
+ ( sort { $a cmp $b } @$to ) ). "
+ }
+ function addOption_$name(object,value) {
+ var length = object.length;
+ object.options[length] = new Option(value, value, false, false);
+ }
+ </script>".
+ '';
+ }
+
+%>
diff --git a/httemplate/misc/process/payment.cgi b/httemplate/misc/process/payment.cgi
new file mode 100644
index 0000000..fa0ede8
--- /dev/null
+++ b/httemplate/misc/process/payment.cgi
@@ -0,0 +1,148 @@
+<%
+
+#some false laziness w/MyAccount::process_payment
+
+$cgi->param('custnum') =~ /^(\d+)$/
+ or die "illegal custnum ". $cgi->param('custnum');
+my $custnum = $1;
+
+my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+die "unknown custnum $custnum" unless $cust_main;
+
+$cgi->param('amount') =~ /^\s*(\d*(\.\d\d)?)\s*$/
+ or eidiot "illegal amount ". $cgi->param('amount');
+my $amount = $1;
+eidiot "amount <= 0" unless $amount > 0;
+
+$cgi->param('year') =~ /^(\d+)$/
+ or die "illegal year ". $cgi->param('year');
+my $year = $1;
+
+$cgi->param('month') =~ /^(\d+)$/
+ or die "illegal month ". $cgi->param('month');
+my $month = $1;
+
+$cgi->param('payby') =~ /^(CARD|CHEK)$/
+ or die "illegal payby ". $cgi->param('payby');
+my $payby = $1;
+my %payby2bop = (
+ 'CARD' => 'CC',
+ 'CHEK' => 'ECHECK',
+);
+my %payby2fields = (
+ 'CARD' => [ qw( address1 address2 city state zip ) ],
+ 'CHEK' => [ qw( ss ) ],
+);
+my %type = ( 'CARD' => 'credit card',
+ 'CHEK' => 'electronic check (ACH)',
+ );
+
+$cgi->param('payname') =~ /^([\w \,\.\-\']+)$/
+ or eidiot gettext('illegal_name'). " payname: ". $cgi->param('payname');
+my $payname = $1;
+
+$cgi->param('paybatch') =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
+ or eidiot gettext('illegal_text'). " paybatch: ". $cgi->param('paybatch');
+my $paybatch = $1;
+
+my $payinfo;
+my $paycvv = '';
+if ( $payby eq 'CHEK' ) {
+
+ $cgi->param('payinfo1') =~ /^(\d+)$/
+ or eidiot "illegal account number ". $cgi->param('payinfo1');
+ my $payinfo1 = $1;
+ $cgi->param('payinfo2') =~ /^(\d+)$/
+ or eidiot "illegal ABA/routing number ". $cgi->param('payinfo2');
+ my $payinfo2 = $1;
+ $payinfo = $payinfo1. '@'. $payinfo2;
+
+} elsif ( $payby eq 'CARD' ) {
+
+ $payinfo = $cgi->param('payinfo');
+ $payinfo =~ s/\D//g;
+ $payinfo =~ /^(\d{13,16})$/
+ or eidiot gettext('invalid_card'); # . ": ". $self->payinfo;
+ $payinfo = $1;
+ validate($payinfo)
+ or eidiot gettext('invalid_card'); # . ": ". $self->payinfo;
+ eidiot gettext('unknown_card_type')
+ if cardtype($payinfo) eq "Unknown";
+
+ if ( defined $cust_main->dbdef_table->column('paycvv') ) {
+ if ( length($cgi->param('paycvv') ) ) {
+ if ( cardtype($payinfo) eq 'American Express card' ) {
+ $cgi->param('paycvv') =~ /^(\d{4})$/
+ or eidiot "CVV2 (CID) for American Express cards is four digits.";
+ $paycvv = $1;
+ } else {
+ $cgi->param('paycvv') =~ /^(\d{3})$/
+ or eidiot "CVV2 (CVC2/CID) is three digits.";
+ $paycvv = $1;
+ }
+ }
+ }
+
+} else {
+ die "unknown payby $payby";
+}
+
+my $error = $cust_main->realtime_bop( $payby2bop{$payby}, $amount,
+ 'quiet' => 1,
+ 'payinfo' => $payinfo,
+ 'paydate' => "$year-$month-01",
+ 'payname' => $payname,
+ 'paybatch' => $paybatch,
+ 'paycvv' => $paycvv,
+ map { $_ => $cgi->param($_) } @{$payby2fields{$payby}}
+);
+eidiot($error) if $error;
+
+$cust_main->apply_payments;
+
+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' => $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
+ my $conf = new FS::Conf;
+ if ( $payby eq 'CARD' &&
+ grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save') ) {
+ $new->set( 'paycvv' => $paycvv );
+ } else {
+ $new->set( 'paycvv' => '');
+ }
+
+ $new->set( $_ => $cgi->param($_) ) foreach @{$payby2fields{$payby}};
+
+ my $error = $new->replace($cust_main);
+ eidiot "payment processed sucessfully, but error saving info: $error"
+ if $error;
+ $cust_main = $new;
+}
+
+#success!
+
+%>
+<%= include( '/elements/header.html', ucfirst($type{$payby}). ' processing sucessful',
+ include('/elements/menubar.html',
+ 'Main menu' => popurl(3),
+ "View this customer (#$custnum)" =>
+ popurl(3). "view/cust_main.cgi?$custnum",
+ ),
+
+ )
+%>
+<%= include( '/elements/small_custview.html', $cust_main ) %>
+</BODY>
+</HTML>
diff --git a/httemplate/misc/queue.cgi b/httemplate/misc/queue.cgi
new file mode 100644
index 0000000..ce9c8fb
--- /dev/null
+++ b/httemplate/misc/queue.cgi
@@ -0,0 +1,47 @@
+<%
+
+$cgi->param('action') =~ /^(new|del|(retry|remove) selected)$/
+ or die "Illegal action";
+my $action = $1;
+
+my $job;
+if ( $action eq 'new' || $action eq 'del' ) {
+ $cgi->param('jobnum') =~ /^(\d+)$/ or die "Illegal jobnum";
+ my $jobnum = $1;
+ $job = qsearchs('queue', { 'jobnum' => $1 })
+ or die "unknown jobnum $jobnum - ".
+ "it probably completed normally or was removed by another user";
+}
+
+if ( $action eq 'new' ) {
+ my %hash = $job->hash;
+ $hash{'status'} = 'new';
+ $hash{'statustext'} = '';
+ my $new = new FS::queue \%hash;
+ my $error = $new->replace($job);
+ die $error if $error;
+} elsif ( $action eq 'del' ) {
+ my $error = $job->delete;
+ die $error if $error;
+} elsif ( $action =~ /^(retry|remove) selected$/ ) {
+ foreach my $jobnum (
+ map { /^jobnum(\d+)$/; $1; } grep /^jobnum\d+$/, $cgi->param
+ ) {
+ my $job = qsearchs('queue', { 'jobnum' => $jobnum });
+ if ( $action eq 'retry selected' && $job ) { #new
+ my %hash = $job->hash;
+ $hash{'status'} = 'new';
+ $hash{'statustext'} = '';
+ my $new = new FS::queue \%hash;
+ my $error = $new->replace($job);
+ die $error if $error;
+ } elsif ( $action eq 'remove selected' && $job ) { #del
+ my $error = $job->delete;
+ die $error if $error;
+ }
+ }
+}
+
+print $cgi->redirect(popurl(2). "browse/queue.cgi");
+
+%>
diff --git a/httemplate/misc/susp_pkg.cgi b/httemplate/misc/susp_pkg.cgi
new file mode 100755
index 0000000..4a19fa8
--- /dev/null
+++ b/httemplate/misc/susp_pkg.cgi
@@ -0,0 +1,15 @@
+<%
+
+#untaint pkgnum
+my ($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal pkgnum";
+my $pkgnum = $1;
+
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+
+my $error = $cust_pkg->suspend;
+&eidiot($error) if $error;
+
+print $cgi->redirect(popurl(2). "view/cust_main.cgi?".$cust_pkg->getfield('custnum'));
+
+%>
diff --git a/httemplate/misc/unapply-cust_credit.cgi b/httemplate/misc/unapply-cust_credit.cgi
new file mode 100755
index 0000000..c658d2a
--- /dev/null
+++ b/httemplate/misc/unapply-cust_credit.cgi
@@ -0,0 +1,18 @@
+<%
+
+#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;
+
+foreach my $cust_credit_bill ( $cust_credit->cust_credit_bill ) {
+ my $error = $cust_credit_bill->delete;
+ eidiot($error) if $error;
+}
+
+print $cgi->redirect($p. "view/cust_main.cgi?". $custnum);
+
+%>
diff --git a/httemplate/misc/unapply-cust_pay.cgi b/httemplate/misc/unapply-cust_pay.cgi
new file mode 100755
index 0000000..28643ef
--- /dev/null
+++ b/httemplate/misc/unapply-cust_pay.cgi
@@ -0,0 +1,18 @@
+<%
+
+#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;
+
+foreach my $cust_bill_pay ( $cust_pay->cust_bill_pay ) {
+ my $error = $cust_bill_pay->delete;
+ eidiot($error) if $error;
+}
+
+print $cgi->redirect($p. "view/cust_main.cgi?". $custnum);
+
+%>
diff --git a/httemplate/misc/unprovision.cgi b/httemplate/misc/unprovision.cgi
new file mode 100755
index 0000000..3c92a4e
--- /dev/null
+++ b/httemplate/misc/unprovision.cgi
@@ -0,0 +1,29 @@
+<%
+
+my $dbh = dbh;
+
+#untaint svcnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+
+#my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$svcnum});
+#die "Unknown svcnum!" unless $svc_acct;
+
+my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$svcnum});
+die "Unknown svcnum!" unless $cust_svc;
+
+my $custnum = $cust_svc->cust_pkg->custnum;
+
+my $error = $cust_svc->cancel;
+
+if ( $error ) {
+ %>
+<!-- mason kludge -->
+<%
+ &eidiot($error);
+} else {
+ print $cgi->redirect(popurl(2)."view/cust_main.cgi?$custnum");
+}
+
+%>
diff --git a/httemplate/misc/unsusp_pkg.cgi b/httemplate/misc/unsusp_pkg.cgi
new file mode 100755
index 0000000..5008729
--- /dev/null
+++ b/httemplate/misc/unsusp_pkg.cgi
@@ -0,0 +1,15 @@
+<%
+
+#untaint pkgnum
+my ($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal pkgnum";
+my $pkgnum = $1;
+
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+
+my $error = $cust_pkg->unsuspend;
+&eidiot($error) if $error;
+
+print $cgi->redirect(popurl(2). "view/cust_main.cgi?".$cust_pkg->getfield('custnum'));
+
+%>
diff --git a/httemplate/misc/upload-batch.cgi b/httemplate/misc/upload-batch.cgi
new file mode 100644
index 0000000..5d01501
--- /dev/null
+++ b/httemplate/misc/upload-batch.cgi
@@ -0,0 +1,30 @@
+<%
+
+ my $fh = $cgi->upload('batch_results');
+ my $filename = $cgi->param('batch_results');
+ $filename =~ /^(.*[\/\\])?([^\/\\]+)$/
+ or die "unparsable filename: $filename\n";
+ my $paybatch = $2;
+
+ my $error = defined($fh)
+ ? FS::cust_pay_batch::import_results( {
+ 'filehandle' => $fh,
+ 'format' => $cgi->param('format'),
+ 'paybatch' => $paybatch,
+ } )
+ : 'No file';
+
+ if ( $error ) {
+ %>
+ <!-- mason kludge -->
+ <%
+ eidiot($error);
+# $cgi->param('error', $error);
+# print $cgi->redirect( "${p}cust_main-import.cgi
+ } else {
+ %>
+ <!-- mason kludge -->
+ <%= header('Batch results upload sucessful') %> <%
+ }
+%>
+
diff --git a/httemplate/misc/void-cust_pay.cgi b/httemplate/misc/void-cust_pay.cgi
new file mode 100755
index 0000000..4eec608
--- /dev/null
+++ b/httemplate/misc/void-cust_pay.cgi
@@ -0,0 +1,16 @@
+<%
+
+#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->void;
+eidiot($error) if $error;
+
+print $cgi->redirect($p. "view/cust_main.cgi?". $custnum);
+
+%>
diff --git a/httemplate/misc/whois.cgi b/httemplate/misc/whois.cgi
new file mode 100644
index 0000000..dd7851d
--- /dev/null
+++ b/httemplate/misc/whois.cgi
@@ -0,0 +1,25 @@
+<%
+ my $svcnum = $cgi->param('svcnum');
+ my $custnum = $cgi->param('custnum');
+ my $domain = $cgi->param('domain');
+
+%>
+<%= header("Whois $domain", menubar(
+ ( $custnum
+ ? ( "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+ )
+ : ()
+ ),
+ "View this domain (#$svcnum)" => "${p}view/svc_domain.cgi?$svcnum",
+ "Main menu" => $p,
+)) %>
+<% my $whois = eval { whois($domain) };
+ if ( $@ ) {
+ ( $whois = $@ ) =~ s/ at \/.*Net\/Whois\/Raw\.pm line \d+.*$//s;
+ } else {
+ $whois =~ s/^\n+//;
+ }
+%>
+<PRE><%= $whois %></PRE>
+</BODY>
+</HTML>
diff --git a/httemplate/search/cust_bill.cgi b/httemplate/search/cust_bill.cgi
new file mode 100755
index 0000000..5b0538c
--- /dev/null
+++ b/httemplate/search/cust_bill.cgi
@@ -0,0 +1,165 @@
+<%
+
+my $conf = new FS::Conf;
+my $maxrecords = $conf->config('maxsearchrecordsperpage');
+
+my $orderby = ''; #removeme
+
+my $limit = '';
+$limit .= "LIMIT $maxrecords" if $maxrecords;
+
+my $offset = $cgi->param('offset') || 0;
+$limit .= " OFFSET $offset" if $offset;
+
+my($total, $tot_amount, $tot_balance);
+
+my(@cust_bill);
+if ( $cgi->keywords ) {
+ my($query) = $cgi->keywords;
+ my $owed = "charged - ( select coalesce(sum(amount),0) from cust_bill_pay
+ where cust_bill_pay.invnum = cust_bill.invnum )
+ - ( select coalesce(sum(amount),0) from cust_credit_bill
+ where cust_credit_bill.invnum = cust_bill.invnum )";
+ my @where;
+ if ( $query =~ /^(OPEN(\d*)_)?(invnum|date|custnum)$/ ) {
+ my($open, $days, $field) = ($1, $2, $3);
+ $field = "_date" if $field eq 'date';
+ $orderby = "ORDER BY cust_bill.$field";
+ push @where, "0 != $owed" if $open;
+ push @where, "cust_bill._date < ". (time-86400*$days) if $days;
+ } else {
+ die "unknown query string $query";
+ }
+
+ my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
+
+ my $statement = "SELECT COUNT(*), sum(charged), sum($owed)
+ FROM cust_bill $extra_sql";
+ my $sth = dbh->prepare($statement) or die dbh->errstr. " doing $statement";
+ $sth->execute or die "Error executing \"$statement\": ". $sth->errstr;
+
+ ( $total, $tot_amount, $tot_balance ) = @{$sth->fetchrow_arrayref};
+
+ @cust_bill = qsearch(
+ 'cust_bill',
+ {},
+ "cust_bill.*, $owed as owed",
+ "$extra_sql $orderby $limit"
+ );
+} else {
+ $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/;
+ my $invnum = $2;
+ @cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum } );
+ $total = scalar(@cust_bill);
+}
+
+#if ( scalar(@cust_bill) == 1 ) {
+if ( $total == 1 ) {
+ my $invnum = $cust_bill[0]->invnum;
+ print $cgi->redirect(popurl(2). "view/cust_bill.cgi?$invnum"); #redirect
+} elsif ( scalar(@cust_bill) == 0 ) {
+%>
+<!-- mason kludge -->
+<%
+ eidiot("Invoice not found.");
+} else {
+%>
+<!-- mason kludge -->
+<%
+
+ #begin pager
+ my $pager = '';
+ if ( $total != scalar(@cust_bill) && $maxrecords ) {
+ unless ( $offset == 0 ) {
+ $cgi->param('offset', $offset - $maxrecords);
+ $pager .= '<A HREF="'. $cgi->self_url.
+ '"><B><FONT SIZE="+1">Previous</FONT></B></A> ';
+ }
+ my $poff;
+ my $page;
+ for ( $poff = 0; $poff < $total; $poff += $maxrecords ) {
+ $page++;
+ if ( $offset == $poff ) {
+ $pager .= qq!<FONT SIZE="+2">$page</FONT> !;
+ } else {
+ $cgi->param('offset', $poff);
+ $pager .= qq!<A HREF="!. $cgi->self_url. qq!">$page</A> !;
+ }
+ }
+ unless ( $offset + $maxrecords > $total ) {
+ $cgi->param('offset', $offset + $maxrecords);
+ $pager .= '<A HREF="'. $cgi->self_url.
+ '"><B><FONT SIZE="+1">Next</FONT></B></A> ';
+ }
+ }
+ #end pager
+
+ print header("Invoice Search Results", menubar(
+ 'Main Menu', popurl(2)
+ )).
+ "$total matching invoices found<BR>".
+ "\$$tot_balance total balance<BR>".
+ "\$$tot_amount total amount<BR>".
+ "<BR>$pager". table(). <<END;
+ <TR>
+ <TH></TH>
+ <TH>Balance</TH>
+ <TH>Amount</TH>
+ <TH>Date</TH>
+ <TH>Contact name</TH>
+ <TH>Company</TH>
+ </TR>
+END
+
+ foreach my $cust_bill ( @cust_bill ) {
+ my($invnum, $owed, $charged, $date ) = (
+ $cust_bill->invnum,
+ sprintf("%.2f", $cust_bill->getfield('owed')),
+ sprintf("%.2f", $cust_bill->charged),
+ $cust_bill->_date,
+ );
+ my $pdate = time2str("%b %d %Y", $date);
+
+ my $rowspan = 1;
+
+ my $view = popurl(2). "view/cust_bill.cgi?$invnum";
+ print <<END;
+ <TR>
+ <TD ROWSPAN=$rowspan><A HREF="$view">$invnum</A></TD>
+ <TD ROWSPAN=$rowspan ALIGN="right"><A HREF="$view">\$$owed</A></TD>
+ <TD ROWSPAN=$rowspan ALIGN="right"><A HREF="$view">\$$charged</A></TD>
+ <TD ROWSPAN=$rowspan><A HREF="$view">$pdate</A></TD>
+END
+ my $custnum = $cust_bill->custnum;
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+ if ( $cust_main ) {
+ my $cview = popurl(2). "view/cust_main.cgi?". $cust_main->custnum;
+ my ( $name, $company ) = (
+ $cust_main->last. ', '. $cust_main->first,
+ $cust_main->company,
+ );
+ print <<END;
+ <TD ROWSPAN=$rowspan><A HREF="$cview">$name</A></TD>
+ <TD ROWSPAN=$rowspan><A HREF="$cview">$company</A></TD>
+END
+ } else {
+ print <<END
+ <TD ROWSPAN=$rowspan COLSPAN=2>WARNING: couldn't find cust_main.custnum $custnum (cust_bill.invnum $invnum)</TD>
+END
+ }
+
+ print "</TR>";
+ }
+ $tot_balance = sprintf("%.2f", $tot_balance);
+ $tot_amount = sprintf("%.2f", $tot_amount);
+ print "</TABLE>$pager<BR>". table(). <<END;
+ <TR><TD>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</TD><TH>Total<BR>Balance</TH><TH>Total<BR>Amount</TH></TR>
+ <TR><TD></TD><TD ALIGN="right">\$$tot_balance</TD><TD ALIGN="right">\$$tot_amount</TD></TD></TR>
+ </TABLE>
+ </BODY>
+</HTML>
+END
+
+}
+
+%>
diff --git a/httemplate/search/cust_bill.html b/httemplate/search/cust_bill.html
new file mode 100755
index 0000000..3ae624a
--- /dev/null
+++ b/httemplate/search/cust_bill.html
@@ -0,0 +1,101 @@
+<%
+ my( $count_query, $sql_query );
+ if ( $cgi->param('begin') || $cgi->param('end') || $cgi->keywords ) {
+
+ my $owed =
+ "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
+ WHERE cust_bill_pay.invnum = cust_bill.invnum )
+ - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
+ WHERE cust_credit_bill.invnum = cust_bill.invnum )";
+
+ my @where;
+ my $orderby = 'ORDER BY cust_bill._date';
+
+ if ( $cgi->param('begin') =~ /^(\d+)$/ ) {
+ push @where, "cust_bill._date >= $1",
+ }
+ if ( $cgi->param('end') =~ /^(\d+)$/ ) {
+ push @where, "cust_bill._date < $1",
+ }
+
+ my($query) = $cgi->keywords;
+ if ( $query =~ /^(OPEN(\d*)_)?(invnum|date|custnum)$/ ) {
+ my($open, $days, $field) = ($1, $2, $3);
+ $field = "_date" if $field eq 'date';
+ $orderby = "ORDER BY cust_bill.$field";
+ push @where, "0 != $owed" if $open;
+ push @where, "cust_bill._date < ". (time-86400*$days) if $days;
+ }
+
+ my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
+
+ $count_query = "SELECT COUNT(*), sum(charged), sum($owed)
+ FROM cust_bill $extra_sql";
+
+ $sql_query = {
+ 'table' => 'cust_bill',
+ 'hashref' => {},
+ 'select' => "cust_bill.*, $owed as owed",
+ 'extra_sql' => "$extra_sql $orderby"
+ };
+
+ } else {
+ $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/;
+ $count_query = "SELECT COUNT(*) FROM cust_bill WHERE invnum = $2";
+ $sql_query = {
+ 'table' => 'cust_bill',
+ 'hashref' => { 'invnum' => $2 },
+ #'select' => '*',
+ };
+ }
+
+ my $link = [ "${p}view/cust_bill.cgi?", 'invnum', ];
+ my $clink = sub {
+ my $cust_bill = shift;
+ my $cust_main = $cust_bill->cust_main;
+ $cust_main
+ ? [ "${p}view/cust_main.cgi?", 'custnum' ]
+ : '';
+ };
+
+%>
+<%= include( 'elements/search.html',
+ 'title' => 'Invoice Search Results',
+ 'name' => 'invoices',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'count_addl' => [ '$%.2f total invoiced',
+ '$%.2f total outstanding balance',
+ ],
+ 'redirect' => $link,
+ 'header' =>
+ [ 'Invoice #', qw(Balance Amount Date), 'Contact name',
+ 'Company' ],
+ 'fields' => [
+ 'invnum',
+ sub { sprintf('$%.2f', shift->get('owed') ) },
+ sub { sprintf('$%.2f', shift->charged ) },
+ sub { time2str('%b %d %Y', shift->_date ) },
+ sub { my $cust_bill = shift;
+ my $cust_main = $cust_bill->cust_main;
+ $cust_main
+ ? $cust_main->get('last'). ', '. $cust_main->first
+ : "WARNING: can't find cust_main.custnum ".
+ $cust_bill->custnum. ' (cust_bill.invnum '.
+ $cust_bill->invnum. ')';
+ },
+ sub { my $cust_main = shift->cust_main;
+ $cust_main ? $cust_main->company : '';
+ },
+ ],
+ 'links' => [
+ $link,
+ $link,
+ $link,
+ $link,
+ $clink,
+ $clink,
+ ],
+
+ )
+%>
diff --git a/httemplate/search/cust_bill_event.cgi b/httemplate/search/cust_bill_event.cgi
new file mode 100644
index 0000000..7c2b3a2
--- /dev/null
+++ b/httemplate/search/cust_bill_event.cgi
@@ -0,0 +1,62 @@
+<!-- mason kludge -->
+<%
+
+#false laziness with view/cust_bill.cgi
+
+$cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/;
+my $beginning = str2time($1) || 0;
+
+$cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/;
+my $ending = ( $1 ? str2time($1) : 4294880896 ) + 86399;
+
+my @cust_bill_event =
+ sort { $a->_date <=> $b->_date }
+ qsearch('cust_bill_event', {
+ _date => { op=> '>=', value=>$beginning },
+ statustext => { op=> '!=', value=>'' },
+# i wish...
+# _date => { op=> '<=', value=>$ending },
+ }, '', "AND _date <= $ending");
+
+%>
+
+<%= header('Failed billing events') %>
+
+<%= table() %>
+<TR>
+ <TH>Event</TH>
+ <TH>Date</TH>
+ <TH>Status</TH>
+ <TH>Invoice</TH>
+ <TH>(bill) name</TH>
+ <TH>company</TH>
+<% if ( defined dbdef->table('cust_main')->column('ship_last') ) { %>
+ <TH>(service) name</TH>
+ <TH>company</TH>
+<% } %>
+</TR>
+
+<% foreach my $cust_bill_event ( @cust_bill_event ) {
+ my $status = $cust_bill_event->status;
+ $status .= ': '.$cust_bill_event->statustext if $cust_bill_event->statustext;
+ my $cust_bill = $cust_bill_event->cust_bill;
+ my $cust_main = $cust_bill->cust_main;
+ my $invlink = "${p}view/cust_bill.cgi?". $cust_bill->invnum;
+ my $custlink = "${p}view/cust_main.cgi?". $cust_main->custnum;
+%>
+<TR>
+ <TD><%= $cust_bill_event->part_bill_event->event %></TD>
+ <TD><%= time2str("%a %b %e %T %Y", $cust_bill_event->_date) %></TD>
+ <TD><%= $status %></TD>
+ <TD><A HREF="<%=$invlink%>">Invoice #<%= $cust_bill->invnum %> (<%= time2str("%D", $cust_bill->_date ) %>)</A></TD>
+ <TD><A HREF="<%=$custlink%>"><%= $cust_main->last. ', '. $cust_main->first %></A></TD>
+ <TD><A HREF="<%=$custlink%>"><%= $cust_main->company %></A></TD>
+ <% if ( defined dbdef->table('cust_main')->column('ship_last') ) { %>
+ <TD><A HREF="<%=$custlink%>"><%= $cust_main->ship_last. ', '. $cust_main->ship_first %></A></TD>
+ <TD><A HREF="<%=$custlink%>"><%= $cust_main->ship_company %></A></TD>
+ <% } %>
+</TR>
+<% } %>
+</TABLE>
+
+</BODY></HTML>
diff --git a/httemplate/search/cust_bill_event.html b/httemplate/search/cust_bill_event.html
new file mode 100755
index 0000000..cd96ddf
--- /dev/null
+++ b/httemplate/search/cust_bill_event.html
@@ -0,0 +1,54 @@
+<HTML>
+ <HEAD>
+ <TITLE>Invoice event errors</TITLE>
+ <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <H1>Invoice event errors</H1>
+ <FORM ACTION="cust_bill_event.cgi" METHOD="post">
+ <TABLE>
+ <!--<TR>
+ <TD ALIGN="right">Customer type</TD>
+ <TD><SELECT MULTIPLE NAME="perhaps_payby">
+ <OPTION SELECTED VALUE="CARD">Credit card (automatic)
+ <OPTION SELECTED VALUE="CHEK">E-check (automatic)
+ <OPTION SELECTED VALUE="LECB">Phone bill billing
+ <OPTION SELECTED VALUE="BILL">Billing
+ <OPTION SELECTED VALUE="DCRD">Credit card (on-demand)
+ <OPTION SELECTED VALUE="DCHK">E-check (on-demand)
+ </TD>
+ </TR>
+ -->
+ <TR>
+ <TD ALIGN="right">From: </TD>
+ <TD><INPUT TYPE="text" NAME="beginning" ID="beginning_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="beginning_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "beginning_text",
+ ifFormat: "%m/%d/%Y",
+ button: "beginning_button",
+ align: "BR"
+ });
+</SCRIPT>
+ </TR>
+ <TR>
+ <TD ALIGN="right">To: </TD>
+ <TD><INPUT TYPE="text" NAME="ending" ID="ending_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="ending_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "ending_text",
+ ifFormat: "%m/%d/%Y",
+ button: "ending_button",
+ align: "BR"
+ });
+</SCRIPT>
+ </TR>
+ </TABLE>
+ <BR><INPUT TYPE="submit" VALUE="Get Report">
+ </FORM>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_credit.html b/httemplate/search/cust_credit.html
new file mode 100755
index 0000000..faaa7a8
--- /dev/null
+++ b/httemplate/search/cust_credit.html
@@ -0,0 +1,80 @@
+<%
+ #my( $count_query, $sql_query );
+
+ my @search = ();
+
+ if ( $cgi->param('otaker') && $cgi->param('otaker') =~ /^([\w\.\-]+)$/ ) {
+ push @search, "otaker = '$1'";
+ }
+
+ #false laziness with cust_pkg.cgi and cust_pay.cgi
+ if ( $cgi->param('beginning')
+ && $cgi->param('beginning') =~ /^([ 0-9\-\/]{1,10})$/ ) {
+ my $beginning = str2time($1);
+ push @search, "_date >= $beginning ";
+ }
+ if ( $cgi->param('ending')
+ && $cgi->param('ending') =~ /^([ 0-9\-\/]{1,10})$/ ) {
+ my $ending = str2time($1) + 86399;
+ push @search, " _date <= $ending ";
+ }
+
+ if ( $cgi->param('begin')
+ && $cgi->param('begin') =~ /^(\d+)$/ ) {
+ push @search, "_date >= $1 ";
+ }
+ if ( $cgi->param('end')
+ && $cgi->param('end') =~ /^(\d+)$/ ) {
+ push @search, " _date < $1 ";
+ }
+
+ my $where = scalar(@search)
+ ? 'WHERE '. join(' AND ', @search)
+ : '';
+
+ my $count_query = "SELECT COUNT(*), SUM(amount) FROM cust_credit $where";
+ my $sql_query = {
+ 'table' => 'cust_credit',
+ 'hashref' => {},
+ 'extra_sql' => $where,
+ };
+
+ my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
+
+%>
+<%= include( 'elements/search.html',
+ 'title' => 'Credit Search Results',
+ 'name' => 'credits',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'count_addl' => [ '$%.2f total credited', ],
+ #'redirect' => $link,
+ 'header' =>
+ [ qw(Amount Date), 'Cust #', 'Contact name',
+ qw(Company By Reason) ],
+ 'fields' => [
+ #'crednum',
+ sub { sprintf('$%.2f', shift->amount ) },
+ sub { time2str('%b %d %Y', shift->_date ) },
+ 'custnum',
+ sub { my $cust_main = shift->cust_main;
+ $cust_main->get('last'). ', '. $cust_main->first;
+ },
+ sub { my $cust_main = shift->cust_main;
+ $cust_main->company;
+ },
+ 'otaker',
+ 'reason',
+ ],
+ 'align' => 'rrrllll',
+ 'links' => [
+ '',
+ '',
+ $clink,
+ $clink,
+ $clink,
+ '',
+ '',
+ ],
+ )
+%>
diff --git a/httemplate/search/cust_main-otaker.cgi b/httemplate/search/cust_main-otaker.cgi
new file mode 100755
index 0000000..4421436
--- /dev/null
+++ b/httemplate/search/cust_main-otaker.cgi
@@ -0,0 +1,28 @@
+<HTML>
+ <HEAD>
+ <TITLE>Customer Search</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <FONT SIZE=7>
+ Customer Search
+ </FONT>
+ <BR>
+ <FORM ACTION="cust_main.cgi" METHOD="post">
+ Search for <B>Order taker</B>:
+ <INPUT TYPE="hidden" NAME="otaker_on" VALUE="TRUE">
+ <% my $sth = dbh->prepare("SELECT DISTINCT otaker FROM cust_main")
+ or die dbh->errstr;
+ $sth->execute() or die $sth->errstr;
+# my @otakers = map { $_->[0] } @{$sth->selectall_arrayref};
+ %>
+ <SELECT NAME="otaker">
+ <% my $otaker; while ( $otaker = $sth->fetchrow_arrayref ) { %>
+ <OPTION><%= $otaker->[0] %></OTAKER>
+ <% } %>
+ </SELECT>
+ <P><INPUT TYPE="submit" VALUE="Search">
+
+ </FORM>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_main-payinfo.html b/httemplate/search/cust_main-payinfo.html
new file mode 100755
index 0000000..671b5ef
--- /dev/null
+++ b/httemplate/search/cust_main-payinfo.html
@@ -0,0 +1,20 @@
+<HTML>
+ <HEAD>
+ <TITLE>Customer Search</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <FONT SIZE=7>
+ Customer Search
+ </FONT>
+ <BR>
+ <FORM ACTION="cust_main.cgi" METHOD="post">
+ Search for <B>Credit card #</B>:
+ <INPUT TYPE="hidden" NAME="card_on" VALUE="TRUE">
+ <INPUT TYPE="text" NAME="card">
+
+ <P><INPUT TYPE="submit" VALUE="Search">
+
+ </FORM>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_main-quickpay.html b/httemplate/search/cust_main-quickpay.html
new file mode 100755
index 0000000..d48f1d0
--- /dev/null
+++ b/httemplate/search/cust_main-quickpay.html
@@ -0,0 +1,44 @@
+<HTML>
+ <HEAD>
+ <TITLE>Quick payment entry</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <FONT SIZE=7>
+ Quick payment entry
+ </FONT>
+ <BR><BR>
+ <A HREF="../">Main Menu</A><BR><BR>
+ <FORM ACTION="cust_main.cgi" METHOD="post">
+ <INPUT TYPE="hidden" NAME="quickpay" VALUE="yes">
+ <INPUT TYPE="checkbox" NAME="last_on" CHECKED> Search for <B>last name</B>:
+ <INPUT TYPE="text" NAME="last_text">
+ using search method: <SELECT NAME="last_type">
+ <OPTION>All
+ <OPTION>Fuzzy
+ <OPTION>Substring
+ <OPTION SELECTED>Exact
+ </SELECT>
+
+ <P><INPUT TYPE="checkbox" NAME="company_on" CHECKED> Search for <B>company</B>:
+ <INPUT TYPE="text" NAME="company_text">
+ using search methods: <SELECT NAME="company_type">
+ <OPTION>All
+ <OPTION>Fuzzy
+ <OPTION>Substring
+ <OPTION SELECTED>Exact
+ </SELECT>
+
+ <P><INPUT TYPE="submit" VALUE="Search">
+
+ </FORM>
+
+ <HR>Explanation of search methods:
+ <UL>
+ <LI><B>All</B> - Try all search methods.
+ <LI><B>Fuzzy</B> - Searches for matches that are close to your text.
+ <LI><B>Substring</B> - Searches for matches that contain your text.
+ <LI><B>Exact</B> - Finds exact matches only, but much faster than the other search methods.
+ </UL>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_main.cgi b/httemplate/search/cust_main.cgi
new file mode 100755
index 0000000..27f23de
--- /dev/null
+++ b/httemplate/search/cust_main.cgi
@@ -0,0 +1,608 @@
+<%
+
+my $conf = new FS::Conf;
+my $maxrecords = $conf->config('maxsearchrecordsperpage');
+
+#my $cache;
+
+#my $monsterjoin = <<END;
+#cust_main left outer join (
+# ( cust_pkg left outer join part_pkg using(pkgpart)
+# ) left outer join (
+# (
+# (
+# ( cust_svc left outer join part_svc using (svcpart)
+# ) left outer join svc_acct using (svcnum)
+# ) left outer join svc_domain using(svcnum)
+# ) left outer join svc_forward using(svcnum)
+# ) using (pkgnum)
+#) using (custnum)
+#END
+
+#my $monsterjoin = <<END;
+#cust_main left outer join (
+# ( cust_pkg left outer join part_pkg using(pkgpart)
+# ) left outer join (
+# (
+# (
+# ( cust_svc left outer join part_svc using (svcpart)
+# ) left outer join (
+# svc_acct left outer join (
+# select svcnum, domain, catchall from svc_domain
+# ) as svc_acct_domsvc (
+# svc_acct_svcnum, svc_acct_domain, svc_acct_catchall
+# ) on svc_acct.domsvc = svc_acct_domsvc.svc_acct_svcnum
+# ) using (svcnum)
+# ) left outer join svc_domain using(svcnum)
+# ) left outer join svc_forward using(svcnum)
+# ) using (pkgnum)
+#) using (custnum)
+#END
+
+my $limit = '';
+$limit .= "LIMIT $maxrecords" if $maxrecords;
+
+my $offset = $cgi->param('offset') || 0;
+$limit .= " OFFSET $offset" if $offset;
+
+my $total = 0;
+
+my(@cust_main, $sortby, $orderby);
+if ( $cgi->param('browse')
+ || $cgi->param('otaker_on')
+ || $cgi->param('agentnum_on')
+) {
+
+ my %search = ();
+ if ( $cgi->param('browse') ) {
+ my $query = $cgi->param('browse');
+ if ( $query eq 'custnum' ) {
+ $sortby=\*custnum_sort;
+ $orderby = "ORDER BY custnum";
+ } elsif ( $query eq 'last' ) {
+ $sortby=\*last_sort;
+ $orderby = "ORDER BY LOWER(last || ' ' || first)";
+ } elsif ( $query eq 'company' ) {
+ $sortby=\*company_sort;
+ $orderby = "ORDER BY LOWER(company || ' ' || last || ' ' || first )";
+ } else {
+ die "unknown browse field $query";
+ }
+ } else {
+ $sortby = \*last_sort; #??
+ $orderby = "ORDER BY LOWER(last || ' ' || first)"; #??
+ if ( $cgi->param('otaker_on') ) {
+ $cgi->param('otaker') =~ /^(\w{1,32})$/ or eidiot "Illegal otaker\n";
+ $search{otaker} = $1;
+ } elsif ( $cgi->param('agentnum_on') ) {
+ $cgi->param('agentnum') =~ /^(\d+)$/ or eidiot "Illegal agentnum\n";
+ $search{agentnum} = $1;
+ } else {
+ die "unknown query...";
+ }
+ }
+
+ my @qual = ();
+
+ my $ncancelled = '';
+
+ if ( $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me
+ || ( $conf->exists('hidecancelledcustomers')
+ && ! $cgi->param('showcancelledcustomers') )
+ ) {
+ #grep { $_->ncancelled_pkgs || ! $_->all_pkgs }
+ push @qual, "
+ ( 0 < ( SELECT COUNT(*) FROM cust_pkg
+ WHERE cust_pkg.custnum = cust_main.custnum
+ AND ( cust_pkg.cancel IS NULL
+ OR cust_pkg.cancel = 0
+ )
+ )
+ OR 0 = ( SELECT COUNT(*) FROM cust_pkg
+ WHERE cust_pkg.custnum = cust_main.custnum
+ )
+ )
+ ";
+ }
+
+ push @qual, FS::cust_main->cancel_sql if $cgi->param('cancelled');
+ push @qual, FS::cust_main->prospect_sql if $cgi->param('prospect');
+ push @qual, FS::cust_main->active_sql if $cgi->param('active');
+ push @qual, FS::cust_main->susp_sql if $cgi->param('suspended');
+
+ #EWWWWWW
+ my $qual = join(' AND ',
+ map { "$_ = ". dbh->quote($search{$_}) } keys %search );
+
+ my $addl_qual = join(' AND ', @qual);
+
+ if ( $addl_qual ) {
+ $qual .= ' AND ' if $qual;
+ $qual .= $addl_qual;
+ }
+
+ $qual = " WHERE $qual" if $qual;
+ my $statement = "SELECT COUNT(*) FROM cust_main $qual";
+ my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
+ $sth->execute or die "Error executing \"$statement\": ". $sth->errstr;
+
+ $total = $sth->fetchrow_arrayref->[0];
+
+ if ( $addl_qual ) {
+ if ( %search ) {
+ $addl_qual = " AND $addl_qual";
+ } else {
+ $addl_qual = " WHERE $addl_qual";
+ }
+ }
+
+ @cust_main = qsearch('cust_main', \%search, '',
+ "$addl_qual $orderby $limit" );
+
+# foreach my $cust_main ( @just_cust_main ) {
+#
+# my @one_cust_main;
+# $FS::Record::DEBUG=1;
+# ( $cache, @one_cust_main ) = jsearch(
+# "$monsterjoin",
+# { 'custnum' => $cust_main->custnum },
+# '',
+# '',
+# 'cust_main',
+# 'custnum',
+# );
+# push @cust_main, @one_cust_main;
+# }
+
+} else {
+ @cust_main=();
+ $sortby = \*last_sort;
+
+ push @cust_main, @{&custnumsearch}
+ if $cgi->param('custnum_on') && $cgi->param('custnum_text');
+ push @cust_main, @{&cardsearch}
+ if $cgi->param('card_on') && $cgi->param('card');
+ push @cust_main, @{&lastsearch}
+ if $cgi->param('last_on') && $cgi->param('last_text');
+ push @cust_main, @{&companysearch}
+ if $cgi->param('company_on') && $cgi->param('company_text');
+ push @cust_main, @{&address2search}
+ if $cgi->param('address2_on') && $cgi->param('address2_text');
+ push @cust_main, @{&phonesearch}
+ if $cgi->param('phone_on') && $cgi->param('phone_text');
+ push @cust_main, @{&referralsearch}
+ if $cgi->param('referral_custnum');
+
+ if ( $cgi->param('company_on') && $cgi->param('company_text') ) {
+ $sortby = \*company_sort;
+ push @cust_main, @{&companysearch};
+ }
+
+ @cust_main = grep { $_->ncancelled_pkgs || ! $_->all_pkgs } @cust_main
+ if ! $cgi->param('cancelled')
+ && (
+ $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me
+ || ( $conf->exists('hidecancelledcustomers')
+ && ! $cgi->param('showcancelledcustomers') )
+ );
+
+ my %saw = ();
+ @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
+}
+
+my %all_pkgs;
+if ( $conf->exists('hidecancelledpackages' ) ) {
+ %all_pkgs = map { $_->custnum => [ $_->ncancelled_pkgs ] } @cust_main;
+} else {
+ %all_pkgs = map { $_->custnum => [ $_->all_pkgs ] } @cust_main;
+}
+#%all_pkgs = ();
+
+if ( scalar(@cust_main) == 1 && ! $cgi->param('referral_custnum') ) {
+ if ( $cgi->param('quickpay') eq 'yes' ) {
+ print $cgi->redirect(popurl(2). "edit/cust_pay.cgi?quickpay=yes;custnum=". $cust_main[0]->custnum);
+ } else {
+ print $cgi->redirect(popurl(2). "view/cust_main.cgi?". $cust_main[0]->custnum);
+ }
+ #exit;
+} elsif ( scalar(@cust_main) == 0 ) {
+%>
+<!-- mason kludge -->
+<%
+ eidiot "No matching customers found!\n";
+} else {
+%>
+<!-- mason kludge -->
+<%
+
+ $total ||= scalar(@cust_main);
+ print header("Customer Search Results",menubar(
+ 'Main Menu', popurl(2)
+ )), "$total matching customers found ";
+
+ #begin pager
+ my $pager = '';
+ if ( $total != scalar(@cust_main) && $maxrecords ) {
+ unless ( $offset == 0 ) {
+ $cgi->param('offset', $offset - $maxrecords);
+ $pager .= '<A HREF="'. $cgi->self_url.
+ '"><B><FONT SIZE="+1">Previous</FONT></B></A> ';
+ }
+ my $poff;
+ my $page;
+ for ( $poff = 0; $poff < $total; $poff += $maxrecords ) {
+ $page++;
+ if ( $offset == $poff ) {
+ $pager .= qq!<FONT SIZE="+2">$page</FONT> !;
+ } else {
+ $cgi->param('offset', $poff);
+ $pager .= qq!<A HREF="!. $cgi->self_url. qq!">$page</A> !;
+ }
+ }
+ unless ( $offset + $maxrecords > $total ) {
+ $cgi->param('offset', $offset + $maxrecords);
+ $pager .= '<A HREF="'. $cgi->self_url.
+ '"><B><FONT SIZE="+1">Next</FONT></B></A> ';
+ }
+ }
+ #end pager
+
+ unless ( $cgi->param('cancelled') ) {
+ if ( $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me
+ || ( $conf->exists('hidecancelledcustomers')
+ && ! $cgi->param('showcancelledcustomers')
+ )
+ ) {
+ $cgi->param('showcancelledcustomers', 1);
+ $cgi->param('offset', 0);
+ print qq!( <a href="!. $cgi->self_url. qq!">show!;
+ } else {
+ $cgi->param('showcancelledcustomers', 0);
+ $cgi->param('offset', 0);
+ print qq!( <a href="!. $cgi->self_url. qq!">hide!;
+ }
+ print ' cancelled customers</a> )';
+ }
+ if ( $cgi->param('referral_custnum') ) {
+ $cgi->param('referral_custnum') =~ /^(\d+)$/
+ or eidiot "Illegal referral_custnum\n";
+ my $referral_custnum = $1;
+ my $cust_main = qsearchs('cust_main', { custnum => $referral_custnum } );
+ print '<FORM METHOD=POST>'.
+ qq!<INPUT TYPE="hidden" NAME="referral_custnum" VALUE="$referral_custnum">!.
+ 'referrals of <A HREF="'. popurl(2).
+ "view/cust_main.cgi?$referral_custnum\">$referral_custnum: ".
+ ( $cust_main->company
+ || $cust_main->last. ', '. $cust_main->first ).
+ '</A>';
+ print "\n",<<END;
+ <SCRIPT>
+ function changed(what) {
+ what.form.submit();
+ }
+ </SCRIPT>
+END
+ print ' <SELECT NAME="referral_depth" SIZE="1" onChange="changed(this)">';
+ my $max = 8; #config file
+ $cgi->param('referral_depth') =~ /^(\d*)$/
+ or eidiot "Illegal referral_depth";
+ my $referral_depth = $1;
+
+ foreach my $depth ( 1 .. $max ) {
+ print '<OPTION',
+ ' SELECTED'x($depth == $referral_depth),
+ ">$depth";
+ }
+ print "</SELECT> levels deep".
+ '<NOSCRIPT> <INPUT TYPE="submit" VALUE="change"></NOSCRIPT>'.
+ '</FORM>';
+ }
+
+ print "<BR><BR>". $pager. &table(). <<END;
+ <TR>
+ <TH></TH>
+ <TH>(bill) name</TH>
+ <TH>company</TH>
+END
+
+if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+ print <<END;
+ <TH>(service) name</TH>
+ <TH>company</TH>
+END
+}
+
+print <<END;
+ <TH>Packages</TH>
+ <TH COLSPAN=2>Services</TH>
+ </TR>
+END
+
+ my(%saw,$cust_main);
+ my $p = popurl(2);
+ foreach $cust_main (
+ sort $sortby grep(!$saw{$_->custnum}++, @cust_main)
+ ) {
+ my($custnum,$last,$first,$company)=(
+ $cust_main->custnum,
+ $cust_main->getfield('last'),
+ $cust_main->getfield('first'),
+ $cust_main->company,
+ );
+
+ my(@lol_cust_svc);
+ my($rowspan)=0;#scalar( @{$all_pkgs{$custnum}} );
+ foreach ( @{$all_pkgs{$custnum}} ) {
+ #my(@cust_svc) = qsearch( 'cust_svc', { 'pkgnum' => $_->pkgnum } );
+ my @cust_svc = $_->cust_svc;
+ push @lol_cust_svc, \@cust_svc;
+ $rowspan += scalar(@cust_svc) || 1;
+ }
+
+ #my($rowspan) = scalar(@{$all_pkgs{$custnum}});
+ my $view;
+ if ( defined $cgi->param('quickpay') && $cgi->param('quickpay') eq 'yes' ) {
+ $view = $p. 'edit/cust_pay.cgi?quickpay=yes;custnum='. $custnum;
+ } else {
+ $view = $p. 'view/cust_main.cgi?'. $custnum;
+ }
+ my $pcompany = $company
+ ? qq!<A HREF="$view"><FONT SIZE=-1>$company</FONT></A>!
+ : '<FONT SIZE=-1>&nbsp;</FONT>';
+ print <<END;
+ <TR>
+ <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$custnum</FONT></A></TD>
+ <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$last, $first</FONT></A></TD>
+ <TD ROWSPAN=$rowspan>$pcompany</TD>
+END
+ if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+ my($ship_last,$ship_first,$ship_company)=(
+ $cust_main->ship_last || $cust_main->getfield('last'),
+ $cust_main->ship_last ? $cust_main->ship_first : $cust_main->first,
+ $cust_main->ship_last ? $cust_main->ship_company : $cust_main->company,
+ );
+ my $pship_company = $ship_company
+ ? qq!<A HREF="$view"><FONT SIZE=-1>$ship_company</FONT></A>!
+ : '<FONT SIZE=-1>&nbsp;</FONT>';
+ print <<END;
+ <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$ship_last, $ship_first</FONT></A></TD>
+ <TD ROWSPAN=$rowspan>$pship_company</A></TD>
+END
+ }
+
+ my($n1)='';
+ foreach ( @{$all_pkgs{$custnum}} ) {
+ my $pkgnum = $_->pkgnum;
+# my $part_pkg = qsearchs( 'part_pkg', { pkgpart => $_->pkgpart } );
+ my $part_pkg = $_->part_pkg;
+
+ my $pkg = $part_pkg->pkg;
+ my $comment = $part_pkg->comment;
+ my $pkgview = "${p}view/cust_main.cgi?$custnum#cust_pkg$pkgnum";
+ my @cust_svc = @{shift @lol_cust_svc};
+ #my(@cust_svc) = qsearch( 'cust_svc', { 'pkgnum' => $_->pkgnum } );
+ my $rowspan = scalar(@cust_svc) || 1;
+
+ print $n1, qq!<TD ROWSPAN=$rowspan><A HREF="$pkgview"><FONT SIZE=-1>$pkg - $comment</FONT></A></TD>!;
+ my($n2)='';
+ foreach my $cust_svc ( @cust_svc ) {
+ my($label, $value, $svcdb) = $cust_svc->label;
+ my($svcnum) = $cust_svc->svcnum;
+ my($sview) = $p.'view';
+ print $n2,qq!<TD><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$label</FONT></A></TD>!,
+ qq!<TD><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$value</FONT></A></TD>!;
+ $n2="</TR><TR>";
+ }
+ #print qq!</TR><TR>\n!;
+ $n1="</TR><TR>";
+ }
+ print "</TR>";
+ }
+
+ print "</TABLE>$pager</BODY></HTML>";
+
+}
+
+#undef $cache; #does this help?
+
+#
+
+sub last_sort {
+ lc($a->getfield('last')) cmp lc($b->getfield('last'))
+ || lc($a->first) cmp lc($b->first);
+}
+
+sub company_sort {
+ return -1 if $a->company && ! $b->company;
+ return 1 if ! $a->company && $b->company;
+ lc($a->company) cmp lc($b->company)
+ || lc($a->getfield('last')) cmp lc($b->getfield('last'))
+ || lc($a->first) cmp lc($b->first);;
+}
+
+sub custnum_sort {
+ $a->getfield('custnum') <=> $b->getfield('custnum');
+}
+
+sub custnumsearch {
+
+ my $custnum = $cgi->param('custnum_text');
+ $custnum =~ s/\D//g;
+ $custnum =~ /^(\d{1,23})$/ or eidiot "Illegal customer number\n";
+ $custnum = $1;
+
+ [ qsearchs('cust_main', { 'custnum' => $custnum } ) ];
+}
+
+sub cardsearch {
+
+ my($card)=$cgi->param('card');
+ $card =~ s/\D//g;
+ $card =~ /^(\d{13,16})$/ or eidiot "Illegal card number\n";
+ my($payinfo)=$1;
+
+ [ qsearch('cust_main',{'payinfo'=>$payinfo, 'payby'=>'CARD'}),
+ qsearch('cust_main',{'payinfo'=>$payinfo, 'payby'=>'DCRD'})
+ ];
+}
+
+sub referralsearch {
+ $cgi->param('referral_custnum') =~ /^(\d+)$/
+ or eidiot "Illegal referral_custnum";
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $1 } )
+ or eidiot "Customer $1 not found";
+ my $depth;
+ if ( $cgi->param('referral_depth') ) {
+ $cgi->param('referral_depth') =~ /^(\d+)$/
+ or eidiot "Illegal referral_depth";
+ $depth = $1;
+ } else {
+ $depth = 1;
+ }
+ [ $cust_main->referral_cust_main($depth) ];
+}
+
+sub lastsearch {
+ my(%last_type);
+ my @cust_main;
+ foreach ( $cgi->param('last_type') ) {
+ $last_type{$_}++;
+ }
+
+ $cgi->param('last_text') =~ /^([\w \,\.\-\']*)$/
+ or eidiot "Illegal last name";
+ my($last)=$1;
+
+ if ( $last_type{'Exact'} || $last_type{'Fuzzy'} ) {
+ push @cust_main, qsearch( 'cust_main',
+ { 'last' => { 'op' => 'ILIKE',
+ 'value' => $last } } );
+
+ push @cust_main, qsearch( 'cust_main',
+ { 'ship_last' => { 'op' => 'ILIKE',
+ 'value' => $last } } )
+ if defined dbdef->table('cust_main')->column('ship_last');
+ }
+
+ if ( $last_type{'Substring'} || $last_type{'All'} ) {
+
+ push @cust_main, qsearch( 'cust_main',
+ { 'last' => { 'op' => 'ILIKE',
+ 'value' => "%$last%" } } );
+
+ push @cust_main, qsearch( 'cust_main',
+ { 'ship_last' => { 'op' => 'ILIKE',
+ 'value' => "%$last%" } } )
+ if defined dbdef->table('cust_main')->column('ship_last');
+
+ }
+
+ if ( $last_type{'Fuzzy'} || $last_type{'All'} ) {
+ push @cust_main, FS::cust_main->fuzzy_search( { 'last' => $last } );
+ }
+
+ #if ($last_type{'Sound-alike'}) {
+ #}
+
+ \@cust_main;
+}
+
+sub companysearch {
+
+ my(%company_type);
+ my @cust_main;
+ foreach ( $cgi->param('company_type') ) {
+ $company_type{$_}++
+ };
+
+ $cgi->param('company_text') =~
+ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
+ or eidiot "Illegal company";
+ my $company = $1;
+
+ if ( $company_type{'Exact'} || $company_type{'Fuzzy'} ) {
+ push @cust_main, qsearch( 'cust_main',
+ { 'company' => { 'op' => 'ILIKE',
+ 'value' => $company } } );
+
+ push @cust_main, qsearch( 'cust_main',
+ { 'ship_company' => { 'op' => 'ILIKE',
+ 'value' => $company } } )
+ if defined dbdef->table('cust_main')->column('ship_last');
+ }
+
+ if ( $company_type{'Substring'} || $company_type{'All'} ) {
+
+ push @cust_main, qsearch( 'cust_main',
+ { 'company' => { 'op' => 'ILIKE',
+ 'value' => "%$company%" } } );
+
+ push @cust_main, qsearch( 'cust_main',
+ { 'ship_company' => { 'op' => 'ILIKE',
+ 'value' => "%$company%" } })
+ if defined dbdef->table('cust_main')->column('ship_last');
+
+ }
+
+ if ( $company_type{'Fuzzy'} || $company_type{'All'} ) {
+ push @cust_main, FS::cust_main->fuzzy_search( { 'company' => $company } );
+ }
+
+ if ($company_type{'Sound-alike'}) {
+ }
+
+ \@cust_main;
+}
+
+sub address2search {
+ my @cust_main;
+
+ $cgi->param('address2_text') =~
+ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
+ or eidiot "Illegal address2";
+ my $address2 = $1;
+
+ push @cust_main, qsearch( 'cust_main',
+ { 'address2' => { 'op' => 'ILIKE',
+ 'value' => $address2 } } );
+ push @cust_main, qsearch( 'cust_main',
+ { 'address2' => { 'op' => 'ILIKE',
+ 'value' => $address2 } } )
+ if defined dbdef->table('cust_main')->column('ship_last');
+
+ \@cust_main;
+}
+
+sub phonesearch {
+ my @cust_main;
+
+ my $phone = $cgi->param('phone_text');
+
+ #(no longer really) false laziness with Record::ut_phonen
+ #only works with US/CA numbers...
+ $phone =~ s/\D//g;
+ if ( $phone =~ /^(\d{3})(\d{3})(\d{4})(\d*)$/ ) {
+ $phone = "$1-$2-$3";
+ $phone .= " x$4" if $4;
+ } elsif ( $phone =~ /^(\d{3})(\d{4})$/ ) {
+ $phone = "$1-$2";
+ } elsif ( $phone =~ /^(\d{3,4})$/ ) {
+ $phone = $1;
+ } else {
+ eidiot gettext('illegal_phone'). ": $phone";
+ }
+
+ my @fields = qw(daytime night fax);
+ push @fields, qw(ship_daytime ship_night ship_fax)
+ if defined dbdef->table('cust_main')->column('ship_last');
+
+ for my $field ( @fields ) {
+ push @cust_main, qsearch ( 'cust_main',
+ { $field => { 'op' => 'LIKE',
+ 'value' => "%$phone%" } } );
+ }
+
+ \@cust_main;
+}
+
+%>
diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html
new file mode 100755
index 0000000..5a066e4
--- /dev/null
+++ b/httemplate/search/cust_main.html
@@ -0,0 +1,42 @@
+<HTML>
+ <HEAD>
+ <TITLE>Customer Search</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <FONT SIZE=7>
+ Customer Search
+ </FONT>
+ <BR><BR>
+ <FORM ACTION="cust_main.cgi" METHOD="post">
+ <INPUT TYPE="checkbox" NAME="last_on" CHECKED> Search for <B>last name</B>:
+ <INPUT TYPE="text" NAME="last_text">
+ using search method: <SELECT NAME="last_type">
+ <OPTION SELECTED>All
+ <OPTION>Fuzzy
+ <OPTION>Substring
+ <OPTION>Exact
+ </SELECT>
+
+ <P><INPUT TYPE="checkbox" NAME="company_on" CHECKED> Search for <B>company</B>:
+ <INPUT TYPE="text" NAME="company_text">
+ using search methods: <SELECT NAME="company_type">
+ <OPTION SELECTED>All
+ <OPTION>Fuzzy
+ <OPTION>Substring
+ <OPTION>Exact
+ </SELECT>
+
+ <P><INPUT TYPE="submit" VALUE="Search"> Note: Fuzzy searching can take a while. Please be patient.
+
+ </FORM>
+
+ <HR>Explanation of search methods:
+ <UL>
+ <LI><B>All</B> - Try all search methods.
+ <LI><B>Fuzzy</B> - Searches for matches that are close to your text.
+ <LI><B>Substring</B> - Searches for matches that contain your text.
+ <LI><B>Exact</B> - Finds exact matches only, but much faster than the other search methods.
+ </UL>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_pay.cgi b/httemplate/search/cust_pay.cgi
new file mode 100755
index 0000000..3f5b72a
--- /dev/null
+++ b/httemplate/search/cust_pay.cgi
@@ -0,0 +1,137 @@
+<%
+ my( $count_query, $sql_query );
+ if ( $cgi->param('magic') && $cgi->param('magic') eq '_date' ) {
+
+ my %search;
+ my @search;
+
+ if ( $cgi->param('payby') ) {
+ $cgi->param('payby') =~ /^(CARD|CHEK|BILL)(-(VisaMC|Amex|Discover))?$/
+ or die "illegal payby ". $cgi->param('payby');
+ $search{'payby'} = $1;
+ if ( $3 ) {
+ if ( $3 eq 'VisaMC' ) {
+ #avoid posix regexes for portability
+ push @search, " ( substring(payinfo from 1 for 1) = '4' ".
+ " OR substring(payinfo from 1 for 2) = '51' ".
+ " OR substring(payinfo from 1 for 2) = '52' ".
+ " OR substring(payinfo from 1 for 2) = '53' ".
+ " OR substring(payinfo from 1 for 2) = '54' ".
+ " OR substring(payinfo from 1 for 2) = '54' ".
+ " OR substring(payinfo from 1 for 2) = '55' ".
+ " ) ";
+ } elsif ( $3 eq 'Amex' ) {
+ push @search, " ( substring(payinfo from 1 for 2 ) = '34' ".
+ " OR substring(payinfo from 1 for 2 ) = '37' ".
+ " ) ";
+ } elsif ( $3 eq 'Discover' ) {
+ push @search, " substring(payinfo from 1 for 4 ) = '6011' ";
+ } else {
+ die "unknown card type $3";
+ }
+ }
+ }
+
+ #false laziness with cust_pkg.cgi
+ if ( $cgi->param('beginning')
+ && $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+ my $beginning = str2time($1);
+ push @search, "_date >= $beginning ";
+ }
+ if ( $cgi->param('ending')
+ && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+ my $ending = str2time($1) + 86399;
+ push @search, " _date <= $ending ";
+ }
+ if ( $cgi->param('begin')
+ && $cgi->param('begin') =~ /^(\d+)$/ ) {
+ push @search, "_date >= $1 ";
+ }
+ if ( $cgi->param('end')
+ && $cgi->param('end') =~ /^(\d+)$/ ) {
+ push @search, " _date < $1 ";
+ }
+
+ my $search;
+ if ( @search ) {
+ $search = ( scalar(keys %search) ? ' AND ' : ' WHERE ' ).
+ join(' AND ', @search);
+ }
+
+ my $hsearch = join(' AND ', map { "$_ = '$search{$_}'" } keys %search );
+ $count_query = "SELECT COUNT(*), SUM(paid) FROM cust_pay ".
+ ( $hsearch ? " WHERE $hsearch " : '' ).
+ $search;
+
+ $sql_query = {
+ 'table' => 'cust_pay',
+ 'hashref' => \%search,
+ 'extra_sql' => "$search ORDER BY _date",
+ };
+
+ } else {
+
+ $cgi->param('payinfo') =~ /^\s*(\d+)\s*$/ or die "illegal payinfo";
+ my $payinfo = $1;
+
+ $cgi->param('payby') =~ /^(\w+)$/ or die "illegal payby";
+ my $payby = $1;
+
+ $count_query = "SELECT COUNT(*), SUM(paid) FROM cust_pay ".
+ "WHERE payinfo = '$payinfo' AND payby = '$payby'";
+
+ $sql_query = {
+ 'table' => 'cust_pay',
+ 'hashref' => { 'payinfo' => $payinfo,
+ 'payby' => $payby },
+ 'extra_sql' => "ORDER BY _date",
+ };
+
+ }
+
+ my $link = [ "${p}view/cust_main.cgi?", 'custnum' ];
+
+%>
+<%= include( 'elements/search.html',
+ 'title' => 'Payment Search Results',
+ 'name' => 'payments',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'count_addl' => [ '$%.2f total paid', ],
+ 'header' =>
+ [ qw(Payment Amount Date), 'Cust #', 'Contact name',
+ 'Company', ],
+ 'fields' => [
+ sub {
+ my $cust_pay = shift;
+ if ( $cust_pay->payby eq 'CARD' ) {
+ 'Card #'. $cust_pay->payinfo_masked;
+ } elsif ( $cust_pay->payby eq 'CHEK' ) {
+ 'E-check acct#'. $cust_pay->payinfo;
+ } elsif ( $cust_pay->payby eq 'BILL' ) {
+ 'Check #'. $cust_pay->payinfo;
+ } else {
+ $cust_pay->payby. ' '. $cust_pay->payinfo;
+ }
+ },
+ sub { sprintf('$%.2f', shift->paid ) },
+ sub { time2str('%b %d %Y', shift->_date ) },
+ 'custnum',
+ sub { my $cust_main = shift->cust_main;
+ $cust_main->get('last'). ', '. $cust_main->first;
+ },
+ sub { my $cust_main = shift->cust_main;
+ $cust_main->company;
+ },
+ ],
+ 'align' => 'lrrrll',
+ 'links' => [
+ '',
+ '',
+ '',
+ $link,
+ $link,
+ $link,
+ ],
+ )
+%>
diff --git a/httemplate/search/cust_pay.html b/httemplate/search/cust_pay.html
new file mode 100755
index 0000000..3848d66
--- /dev/null
+++ b/httemplate/search/cust_pay.html
@@ -0,0 +1,18 @@
+<HTML>
+ <HEAD>
+ <TITLE>Check # Search</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <FONT SIZE=7>
+ Check # Search
+ </FONT>
+ <BR><BR>
+ <FORM ACTION="cust_pay.cgi" METHOD="post">
+ Search for <B>check #</B>:
+ <INPUT TYPE="text" NAME="payinfo">
+ <INPUT TYPE="hidden" NAME="payby" VALUE="BILL">
+ <BR><BR><INPUT TYPE="submit" VALUE="Search">
+ </FORM>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_pkg.cgi b/httemplate/search/cust_pkg.cgi
new file mode 100755
index 0000000..6d26317
--- /dev/null
+++ b/httemplate/search/cust_pkg.cgi
@@ -0,0 +1,363 @@
+<%
+
+my $conf = new FS::Conf;
+my $maxrecords = $conf->config('maxsearchrecordsperpage');
+
+my %part_pkg = map { $_->pkgpart => $_ } qsearch('part_pkg', {});
+
+my $limit = '';
+$limit .= "LIMIT $maxrecords" if $maxrecords;
+
+my $offset = $cgi->param('offset') || 0;
+$limit .= " OFFSET $offset" if $offset;
+
+my $total;
+
+my($query) = $cgi->keywords;
+my $sortby;
+my @cust_pkg;
+
+if ( $cgi->param('magic') && $cgi->param('magic') eq 'bill' ) {
+ $sortby=\*bill_sort;
+
+ #false laziness with cust_pay.cgi
+ my $range = '';
+ if ( $cgi->param('beginning')
+ && $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+ my $beginning = str2time($1);
+ $range = " WHERE bill >= $beginning ";
+ }
+ if ( $cgi->param('ending')
+ && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+ my $ending = str2time($1) + 86399;
+ $range .= ( $range ? ' AND ' : ' WHERE ' ). " bill <= $ending ";
+ }
+
+ $range .= ( $range ? 'AND ' : ' WHERE ' ). '( cancel IS NULL OR cancel = 0 )';
+
+ if ( $cgi->param('agentnum') =~ /^(\d+)$/ and $1 ) {
+ $range .= ( $range ? 'AND ' : ' WHERE ' ).
+ "$1 = ( SELECT agentnum FROM cust_main".
+ " WHERE cust_main.custnum = cust_pkg.custnum )";
+ }
+
+ #false laziness with below
+ my $statement = "SELECT COUNT(*) FROM cust_pkg $range";
+ warn $statement;
+ my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
+ $sth->execute or die "Error executing \"$statement\": ". $sth->errstr;
+
+ $total = $sth->fetchrow_arrayref->[0];
+
+ @cust_pkg = qsearch('cust_pkg',{}, '', " $range ORDER BY bill $limit" );
+
+} else {
+
+ my $qual = '';
+ if ( $cgi->param('magic') &&
+ $cgi->param('magic') =~ /^(active|suspended|canceled)$/
+ ) {
+
+ if ( $cgi->param('magic') eq 'active' ) {
+ $qual = 'WHERE ( susp IS NULL OR susp = 0 )'.
+ ' AND ( cancel IS NULL OR cancel = 0)';
+ } elsif ( $cgi->param('magic') eq 'suspended' ) {
+ $qual = 'WHERE susp IS NOT NULL AND susp != 0'.
+ ' AND ( cancel IS NULL OR cancel = 0)';
+ } elsif ( $cgi->param('magic') eq 'canceled' ) {
+ $qual = 'WHERE cancel IS NOT NULL AND cancel != 0';
+ } else {
+ die "guru meditation #420";
+ }
+
+ $sortby = \*pkgnum_sort;
+
+ if ( $cgi->param('pkgpart') =~ /^(\d+)$/ ) {
+ $qual .= " AND pkgpart = $1";
+ }
+
+ } elsif ( $query eq 'pkgnum' ) {
+
+ $sortby=\*pkgnum_sort;
+
+ } elsif ( $query eq 'APKG_pkgnum' ) {
+
+ $sortby=\*pkgnum_sort;
+
+ #@cust_pkg=();
+ ##perhaps this should go in cust_pkg as a qsearch-like constructor?
+ #my($cust_pkg);
+ #foreach $cust_pkg (
+ # qsearch('cust_pkg',{}, '', "ORDER BY pkgnum $limit" )
+ #) {
+ # my($flag)=0;
+ # my($pkg_svc);
+ # PKG_SVC:
+ # foreach $pkg_svc (qsearch('pkg_svc',{ 'pkgpart' => $cust_pkg->pkgpart })) {
+ # if ( $pkg_svc->quantity
+ # > scalar(qsearch('cust_svc',{
+ # 'pkgnum' => $cust_pkg->pkgnum,
+ # 'svcpart' => $pkg_svc->svcpart,
+ # }))
+ # )
+ # {
+ # $flag=1;
+ # last PKG_SVC;
+ # }
+ # }
+ # push @cust_pkg, $cust_pkg if $flag;
+ #}
+
+ if ( driver_name eq 'mysql' ) {
+ #$query = "DROP TABLE temp1_$$,temp2_$$;";
+ #my $sth = dbh->prepare($query);
+ #$sth->execute;
+
+ $query = "CREATE TEMPORARY TABLE temp1_$$ TYPE=MYISAM
+ SELECT cust_svc.pkgnum,cust_svc.svcpart,COUNT(*) as count
+ FROM cust_pkg,cust_svc,pkg_svc
+ WHERE cust_pkg.pkgnum = cust_svc.pkgnum
+ AND cust_svc.svcpart = pkg_svc.svcpart
+ AND cust_pkg.pkgpart = pkg_svc.pkgpart
+ GROUP BY cust_svc.pkgnum,cust_svc.svcpart";
+ my $sth = dbh->prepare($query) or die dbh->errstr. " preparing $query";
+
+ $sth->execute or die "Error executing \"$query\": ". $sth->errstr;
+
+ $query = "CREATE TEMPORARY TABLE temp2_$$ TYPE=MYISAM
+ SELECT cust_pkg.pkgnum FROM cust_pkg
+ LEFT JOIN pkg_svc ON (cust_pkg.pkgpart=pkg_svc.pkgpart)
+ LEFT JOIN temp1_$$ ON (cust_pkg.pkgnum = temp1_$$.pkgnum
+ AND pkg_svc.svcpart=temp1_$$.svcpart)
+ WHERE ( pkg_svc.quantity > temp1_$$.count
+ OR temp1_$$.pkgnum IS NULL )
+ AND pkg_svc.quantity != 0;";
+ $sth = dbh->prepare($query) or die dbh->errstr. " preparing $query";
+ $sth->execute or die "Error executing \"$query\": ". $sth->errstr;
+ $qual = " LEFT JOIN temp2_$$ ON cust_pkg.pkgnum = temp2_$$.pkgnum
+ WHERE temp2_$$.pkgnum IS NOT NULL";
+
+ } else {
+
+ $qual = "
+ WHERE 0 <
+ ( SELECT count(*) FROM pkg_svc
+ WHERE pkg_svc.pkgpart = cust_pkg.pkgpart
+ AND pkg_svc.quantity > ( SELECT count(*) FROM cust_svc
+ WHERE cust_svc.pkgnum = cust_pkg.pkgnum
+ AND cust_svc.svcpart = pkg_svc.svcpart
+ )
+ )
+ ";
+
+ }
+
+ } else {
+ die "Empty or unknown QUERY_STRING!";
+ }
+
+ my $statement = "SELECT COUNT(*) FROM cust_pkg $qual";
+ my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
+ $sth->execute or die "Error executing \"$statement\": ". $sth->errstr;
+
+ $total = $sth->fetchrow_arrayref->[0];
+
+ my $tblname = driver_name eq 'mysql' ? 'cust_pkg.' : '';
+ @cust_pkg =
+ qsearch('cust_pkg',{}, '', "$qual ORDER BY ${tblname}pkgnum $limit" );
+
+ if ( driver_name eq 'mysql' ) {
+ $query = "DROP TABLE temp1_$$,temp2_$$;";
+ my $sth = dbh->prepare($query) or die dbh->errstr. " doing $query";
+ $sth->execute; # or die "Error executing \"$query\": ". $sth->errstr;
+ }
+
+}
+
+if ( scalar(@cust_pkg) == 1 ) {
+ print $cgi->redirect("${p}view/cust_main.cgi?". $cust_pkg[0]->custnum.
+ "#cust_pkg". $cust_pkg[0]->pkgnum );
+ #exit;
+} elsif ( scalar(@cust_pkg) == 0 ) { #error
+%>
+<!-- mason kludge -->
+<%
+ eidiot("No packages found");
+} else {
+%>
+<!-- mason kludge -->
+<%
+ $total ||= scalar(@cust_pkg);
+
+ #begin pager
+ my $pager = '';
+ if ( $total != scalar(@cust_pkg) && $maxrecords ) {
+ unless ( $offset == 0 ) {
+ $cgi->param('offset', $offset - $maxrecords);
+ $pager .= '<A HREF="'. $cgi->self_url.
+ '"><B><FONT SIZE="+1">Previous</FONT></B></A> ';
+ }
+ my $poff;
+ my $page;
+ for ( $poff = 0; $poff < $total; $poff += $maxrecords ) {
+ $page++;
+ if ( $offset == $poff ) {
+ $pager .= qq!<FONT SIZE="+2">$page</FONT> !;
+ } else {
+ $cgi->param('offset', $poff);
+ $pager .= qq!<A HREF="!. $cgi->self_url. qq!">$page</A> !;
+ }
+ }
+ unless ( $offset + $maxrecords > $total ) {
+ $cgi->param('offset', $offset + $maxrecords);
+ $pager .= '<A HREF="'. $cgi->self_url.
+ '"><B><FONT SIZE="+1">Next</FONT></B></A> ';
+ }
+ }
+ #end pager
+
+ print header('Package Search Results',''),
+ "$total matching packages found<BR><BR>$pager", &table(), <<END;
+ <TR>
+ <TH>Package</TH>
+ <TH><FONT SIZE=-1>Setup</FONT></TH>
+END
+
+ print '<TH><FONT SIZE=-1>Last<BR>bill</FONT></TH>'
+ if defined dbdef->table('cust_pkg')->column('last_bill');
+
+ print <<END;
+ <TH><FONT SIZE=-1>Next<BR>bill</FONT></TH>
+ <TH><FONT SIZE=-1>Susp.</FONT></TH>
+ <TH><FONT SIZE=-1>Expire</FONT></TH>
+ <TH><FONT SIZE=-1>Cancel</FONT></TH>
+ <TH><FONT SIZE=-1>Cust#</FONT></TH>
+ <TH>(bill) name</TH>
+ <TH>company</TH>
+END
+
+ print '<TH>(service) name</TH><TH>company</TH>'
+ if defined dbdef->table('cust_main')->column('ship_last');
+
+ print '<TH COLSPAN=2>Services</TH></TR>';
+
+ my $n1 = '<TR>';
+ my(%saw,$cust_pkg);
+ foreach $cust_pkg (
+ sort $sortby grep(!$saw{$_->pkgnum}++, @cust_pkg)
+ ) {
+ my($cust_main)=qsearchs('cust_main',{'custnum'=>$cust_pkg->custnum});
+ my($pkgnum, $setup, $bill, $susp, $expire, $cancel,
+ $custnum, $last, $first, $company ) = (
+ $cust_pkg->pkgnum,
+ $cust_pkg->getfield('setup')
+ ? time2str("%D", $cust_pkg->getfield('setup') )
+ : '',
+ $cust_pkg->getfield('bill')
+ ? time2str("%D", $cust_pkg->getfield('bill') )
+ : '',
+ $cust_pkg->getfield('susp')
+ ? time2str("%D", $cust_pkg->getfield('susp') )
+ : '',
+ $cust_pkg->getfield('expire')
+ ? time2str("%D", $cust_pkg->getfield('expire') )
+ : '',
+ $cust_pkg->getfield('cancel')
+ ? time2str("%D", $cust_pkg->getfield('cancel') )
+ : '',
+ $cust_pkg->custnum,
+ $cust_main ? $cust_main->last : '',
+ $cust_main ? $cust_main->first : '',
+ $cust_main ? $cust_main->company : '',
+ );
+
+ my $last_bill = $cust_pkg->getfield('last_bill')
+ ? time2str("%D", $cust_pkg->getfield('last_bill') )
+ : ''
+ if defined dbdef->table('cust_pkg')->column('last_bill');
+
+ my($ship_last, $ship_first, $ship_company);
+ if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+ ($ship_last, $ship_first, $ship_company) = (
+ $cust_main
+ ? ( $cust_main->ship_last || $cust_main->getfield('last') )
+ : '',
+ $cust_main
+ ? ( $cust_main->ship_last
+ ? $cust_main->ship_first
+ : $cust_main->first )
+ : '',
+ $cust_main
+ ? ( $cust_main->ship_last
+ ? $cust_main->ship_company
+ : $cust_main->company )
+ : '',
+ );
+ }
+ my $pkg = $part_pkg{$cust_pkg->pkgpart}->pkg;
+ #$pkg .= ' - '. $part_pkg{$cust_pkg->pkgpart}->comment;
+ my @cust_svc = qsearch( 'cust_svc', { 'pkgnum' => $pkgnum } );
+ my $rowspan = scalar(@cust_svc) || 1;
+ my $p = popurl(2);
+ print $n1, <<END;
+ <TD ROWSPAN=$rowspan><A HREF="${p}view/cust_main.cgi?$custnum#cust_pkg$pkgnum"><FONT SIZE=-1>$pkgnum - $pkg</FONT></A></TD>
+ <TD ROWSPAN=$rowspan>$setup</TD>
+END
+
+ print "<TD ROWSPAN=$rowspan>$last_bill</TD>"
+ if defined dbdef->table('cust_pkg')->column('last_bill');
+
+ print <<END;
+ <TD ROWSPAN=$rowspan>$bill</TD>
+ <TD ROWSPAN=$rowspan>$susp</TD>
+ <TD ROWSPAN=$rowspan>$expire</TD>
+ <TD ROWSPAN=$rowspan>$cancel</TD>
+END
+ if ( $cust_main ) {
+ print <<END;
+ <TD ROWSPAN=$rowspan><FONT SIZE=-1><A HREF="${p}view/cust_main.cgi?$custnum">$custnum</A></FONT></TD>
+ <TD ROWSPAN=$rowspan><FONT SIZE=-1><A HREF="${p}view/cust_main.cgi?$custnum">$last, $first</A></FONT></TD>
+ <TD ROWSPAN=$rowspan><FONT SIZE=-1><A HREF="${p}view/cust_main.cgi?$custnum">$company</A></FONT></TD>
+END
+ if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+ print <<END;
+ <TD ROWSPAN=$rowspan><FONT SIZE=-1><A HREF="${p}view/cust_main.cgi?$custnum">$ship_last, $ship_first</A></FONT></TD>
+ <TD ROWSPAN=$rowspan><FONT SIZE=-1><A HREF="${p}view/cust_main.cgi?$custnum">$ship_company</A></FONT></TD>
+END
+ }
+ } else {
+ my $colspan = defined dbdef->table('cust_main')->column('ship_last')
+ ? 5 : 3;
+ print <<END;
+ <TD ROWSPAN=$rowspan COLSPAN=$colspan>WARNING: couldn't find cust_main.custnum $custnum (cust_pkg.pkgnum $pkgnum)</TD>
+END
+ }
+
+ my $n2 = '';
+ foreach my $cust_svc ( @cust_svc ) {
+ my($label, $value, $svcdb) = $cust_svc->label;
+ my $svcnum = $cust_svc->svcnum;
+ my $sview = $p. "view";
+ print $n2,qq!<TD><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$label</FONT></A></TD>!,
+ qq!<TD><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$value</FONT></A></TD>!;
+ $n2="</TR><TR>";
+ }
+
+ $n1 = "</TR><TR>";
+
+ }
+ print '</TR>';
+
+ print "</TABLE>$pager</BODY></HTML>";
+
+}
+
+sub pkgnum_sort {
+ $a->getfield('pkgnum') <=> $b->getfield('pkgnum');
+}
+
+sub bill_sort {
+ $a->getfield('bill') <=> $b->getfield('bill');
+}
+
+%>
diff --git a/httemplate/search/cust_pkg_report.cgi b/httemplate/search/cust_pkg_report.cgi
new file mode 100755
index 0000000..b316745
--- /dev/null
+++ b/httemplate/search/cust_pkg_report.cgi
@@ -0,0 +1,63 @@
+<HTML>
+ <HEAD>
+ <TITLE>Packages</TITLE>
+ <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <H1>Packages</H1>
+ <FORM ACTION="cust_pkg.cgi" METHOD="post">
+ <INPUT TYPE="hidden" NAME="magic" VALUE="bill">
+ Return packages with next bill date:<BR><BR>
+ <TABLE>
+ <TR>
+ <TD ALIGN="right">From: </TD>
+ <TD><INPUT TYPE="text" NAME="beginning" ID="beginning_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="beginning_button" STYLE="cursor: pointer" TITLE="Select date"><BR><I>m/d/y</I></TD>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "beginning_text",
+ ifFormat: "%m/%d/%Y",
+ button: "beginning_button",
+ align: "BR"
+ });
+</SCRIPT>
+ </TR>
+ <TR>
+ <TD ALIGN="right">To: </TD>
+ <TD><INPUT TYPE="text" NAME="ending" ID="ending_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="ending_button" STYLE="cursor: pointer" TITLE="Select date"><BR><I>m/d/y</I></TD>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "ending_text",
+ ifFormat: "%m/%d/%Y",
+ button: "ending_button",
+ align: "BR"
+ });
+</SCRIPT>
+ </TR>
+<% my %agent_search = dbdef->table('agent')->column('disabled')
+ ? ( 'disabled' => '' ) : ();
+ my @agents = qsearch( 'agent', \%agent_search );
+ if ( scalar(@agents) == 1 ) {
+%>
+ <INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agents[0]->agentnum %>">
+<% } else { %>
+
+ <TR>
+ <TD ALIGN="right">Agent: </TD>
+ <TD><SELECT NAME="agentnum"><OPTION VALUE="">(all)
+ <% foreach my $agent ( sort { $a->agent cmp $b->agent; } @agents) { %>
+ <OPTION VALUE="<%= $agent->agentnum %>"><%= $agent->agent %>
+ <% } %>
+ </TD>
+ </TR>
+<% } %>
+ </TABLE>
+ <BR><INPUT TYPE="submit" VALUE="Get Report">
+
+ </FORM>
+
+ </BODY>
+</HTML>
+
diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html
new file mode 100644
index 0000000..566ea83
--- /dev/null
+++ b/httemplate/search/elements/search.html
@@ -0,0 +1,139 @@
+<%
+
+ my(%opt) = @_;
+
+ my %align = (
+ 'l' => 'left',
+ 'r' => 'right',
+ 'c' => 'center',
+ ' ' => '',
+ '.' => '',
+ );
+ $opt{align} = [ map $align{$_}, split(//, $opt{align}) ],
+ unless !$opt{align} || ref($opt{align});
+
+ if ( ref($opt{'query'}) ) {
+
+ }
+
+ unless (exists($opt{'count_query'}) && length($opt{'count_query'})) {
+ ( $opt{'count_query'} = $opt{'query'} ) =~
+ s/^\s*SELECT\s*(.*?)\s+FROM\s/SELECT COUNT(*) FROM /i;
+ }
+
+ my $conf = new FS::Conf;
+ my $maxrecords = $conf->config('maxsearchrecordsperpage');
+
+ my $limit = $maxrecords ? "LIMIT $maxrecords" : '';
+
+ my $offset = $cgi->param('offset') || 0;
+ $limit .= " OFFSET $offset" if $offset;
+
+ my $count_sth = dbh->prepare($opt{'count_query'})
+ or die "Error preparing $opt{'count_query'}: ". dbh->errstr;
+ $count_sth->execute
+ or die "Error executing $opt{'count_query'}: ". $count_sth->errstr;
+ my $count_arrayref = $count_sth->fetchrow_arrayref;
+ my $total = $count_arrayref->[0];
+
+ #warn join(' / ', map { "$_ => $opt{$_}" } keys %opt ). "\n";
+
+ my $header = $opt{'header'};
+ my $rows;
+ if ( ref($opt{'query'}) ) {
+ #eval "use FS::$opt{'query'};";
+ $rows = [ qsearch(
+ $opt{'query'}->{'table'},
+ $opt{'query'}->{'hashref'} || {},
+ $opt{'query'}->{'select'},
+ $opt{'query'}->{'extra_sql'}. " $limit",
+ ) ];
+ } else {
+ my $sth = dbh->prepare("$opt{'query'} $limit")
+ or die "Error preparing $opt{'query'}: ". dbh->errstr;
+ $sth->execute
+ or die "Error executing $opt{'query'}: ". $sth->errstr;
+
+ #can get # of rows without fetching them all?
+ $rows = $sth->fetchall_arrayref;
+
+ $header ||= $sth->{NAME};
+ }
+
+ if ( exists($opt{'redirect'}) && scalar(@$rows) == 1 && $total == 1 ) {
+ my( $url, $method ) = @{$opt{'redirect'}};
+ redirect( $url. $rows->[0]->$method() );
+ } else {
+ $opt{'name'} =~ s/s$// if $total == 1;
+%>
+<%= include( '/elements/header.html', $opt{'title'},
+ include( '/elements/menubar.html', 'Main menu' => $p )
+ )
+%>
+<% my $pager = include ( '/elements/pager.html',
+ 'offset' => $offset,
+ 'num_rows' => scalar(@$rows),
+ 'total' => $total,
+ 'maxrecords' => $maxrecords,
+ );
+%>
+<% unless ( $total ) { %>
+ No matching <%= $opt{'name'} %> found.<BR>
+<% } else { %>
+ <%= $total %> total <%= $opt{'name'} %><BR>
+ <% if ( $opt{'count_addl'} ) { %>
+ <% my $n=0; foreach my $count ( @{$opt{'count_addl'}} ) { %>
+ <%= sprintf( $count, $count_arrayref->[++$n] ) %><BR>
+ <% } %>
+ <% } %>
+ <BR><%= $pager %>
+ <%= include( '/elements/table.html' ) %>
+ <TR>
+ <% foreach my $header ( @$header ) { %>
+ <TH><%= $header %></TH>
+ <% } %>
+ </TR>
+ <% foreach my $row ( @$rows ) { %>
+ <TR>
+ <% if ( $opt{'fields'} ) {
+ my $links = $opt{'links'} ? [ @{$opt{'links'}} ] : '';
+ my $aligns = $opt{'align'} ? [ @{$opt{'align'}} ] : '';
+ foreach my $field ( @{$opt{'fields'}} ) {
+ my $align = $aligns ? shift @$aligns : '';
+ $align = " ALIGN=$align" if $align;
+ my $a = '';
+ if ( $links ) {
+ my $link = shift @$links;
+ $link = &{$link}($row) if ref($link) eq 'CODE';
+ if ( $link ) {
+ my( $url, $method ) = @{$link};
+ if ( ref($method) eq 'CODE' ) {
+ $a = $url. &{$method}($row);
+ } else {
+ $a = $url. $row->$method();
+ }
+ $a = qq(<A HREF="$a">);
+ }
+ }
+ %>
+ <% if ( ref($field) eq 'CODE' ) { %>
+ <TD<%= $align %>><%= $a %><%= &{$field}($row) %><%= $a ? '</A>' : '' %></TD>
+ <% } else { %>
+ <TD<%= $align %>><%= $a %><%= $row->$field() %><%= $a ? '</A>' : '' %></TD>
+ <% } %>
+ <% } %>
+ <% } else { %>
+ <% foreach ( @$row ) { %>
+ <TD><%= $_ %></TD>
+ <% } %>
+ <% } %>
+ </TR>
+ <% } %>
+
+ </TABLE>
+ <%= $pager %>
+<% } %>
+</BODY>
+</HTML>
+<% } %>
+
diff --git a/httemplate/search/report_cust_credit.html b/httemplate/search/report_cust_credit.html
new file mode 100644
index 0000000..ceffca7
--- /dev/null
+++ b/httemplate/search/report_cust_credit.html
@@ -0,0 +1,58 @@
+<HTML>
+ <HEAD>
+ <TITLE>Credit report criteria</TITLE>
+ <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <H1>Credit report criteria</H1>
+ <FORM ACTION="cust_credit.html" METHOD="post">
+ <INPUT TYPE="hidden" NAME="magic" VALUE="_date">
+ <TABLE>
+ <TR>
+ <TD ALIGN="right">Credits by employee: </TD>
+<%
+ my $sth = dbh->prepare("SELECT DISTINCT otaker FROM cust_credit")
+ or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+ my @otakers = map { $_->[0] } @{$sth->fetchall_arrayref};
+%>
+ <TD><SELECT NAME="otaker">
+ <OPTION VALUE="">all</OPTION>
+ <% foreach my $otaker ( @otakers ) { %>
+ <OPTION VALUE="<%= $otaker %>"><%= $otaker %></OPTION>
+ <% } %>
+ </SELECT>
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">From: </TD>
+ <TD><INPUT TYPE="text" NAME="beginning" ID="beginning_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="beginning_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "beginning_text",
+ ifFormat: "%m/%d/%Y",
+ button: "beginning_button",
+ align: "BR"
+ });
+</SCRIPT>
+ </TR>
+ <TR>
+ <TD ALIGN="right">To: </TD>
+ <TD><INPUT TYPE="text" NAME="ending" ID="ending_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="ending_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "ending_text",
+ ifFormat: "%m/%d/%Y",
+ button: "ending_button",
+ align: "BR"
+ });
+</SCRIPT>
+ </TR>
+ </TABLE>
+ <BR><INPUT TYPE="submit" VALUE="Get Report">
+ </FORM>
+ </BODY>
+</HTML>
diff --git a/httemplate/search/report_cust_pay.html b/httemplate/search/report_cust_pay.html
new file mode 100644
index 0000000..95198c7
--- /dev/null
+++ b/httemplate/search/report_cust_pay.html
@@ -0,0 +1,55 @@
+<HTML>
+ <HEAD>
+ <TITLE>Payment report criteria</TITLE>
+ <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <H1>Payment report criteria</H1>
+ <FORM ACTION="cust_pay.cgi" METHOD="post">
+ <INPUT TYPE="hidden" NAME="magic" VALUE="_date">
+ <TABLE>
+ <TR>
+ <TD ALIGN="right">Payments of type: </TD>
+ <TD><SELECT NAME="payby">
+ <OPTION VALUE="">all</OPTION>
+ <OPTION VALUE="CARD">credit card (all)</OPTION>
+ <OPTION VALUE="CARD-VisaMC">credit card (Visa/MasterCard)</OPTION>
+ <OPTION VALUE="CARD-Amex">credit card (American Express)</OPTION>
+ <OPTION VALUE="CARD-Discover">credit card (Discover)</OPTION>
+ <OPTION VALUE="CHEK">electronic check / ACH</OPTION>
+ <OPTION VALUE="BILL">check / cash</OPTION>
+ </SELECT>
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">From: </TD>
+ <TD><INPUT TYPE="text" NAME="beginning" ID="beginning_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="beginning_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "beginning_text",
+ ifFormat: "%m/%d/%Y",
+ button: "beginning_button",
+ align: "BR"
+ });
+</SCRIPT>
+ </TR>
+ <TR>
+ <TD ALIGN="right">To: </TD>
+ <TD><INPUT TYPE="text" NAME="ending" ID="ending_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="ending_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "ending_text",
+ ifFormat: "%m/%d/%Y",
+ button: "ending_button",
+ align: "BR"
+ });
+</SCRIPT>
+ </TR>
+ </TABLE>
+ <BR><INPUT TYPE="submit" VALUE="Get Report">
+ </FORM>
+ </BODY>
+</HTML>
diff --git a/httemplate/search/report_prepaid_income.cgi b/httemplate/search/report_prepaid_income.cgi
new file mode 100644
index 0000000..1677591
--- /dev/null
+++ b/httemplate/search/report_prepaid_income.cgi
@@ -0,0 +1,86 @@
+<!-- mason kludge -->
+<%
+
+ #doesn't yet deal with daily/weekly packages
+
+ #needs to be re-written in sql for efficiency
+
+ my $time = time;
+
+ my $now = $cgi->param('date') && str2time($cgi->param('date')) || $time;
+ $now =~ /^(\d+)$/ or die "unparsable date?";
+ $now = $1;
+
+ my( $total, $total_legacy ) = ( 0, 0 );
+
+ my @cust_bill_pkg =
+ grep { $_->cust_pkg && $_->cust_pkg->part_pkg->freq !~ /^([01]|\d+[dw])$/ }
+ qsearch( 'cust_bill_pkg', {
+ 'recur' => { op=>'!=', value=>0 },
+ 'edate' => { op=>'>', value=>$now },
+ }, );
+
+ my @cust_pkg =
+ grep { $_->part_pkg->recur != 0
+ && $_->part_pkg->freq !~ /^([01]|\d+[dw])$/
+ }
+ qsearch ( 'cust_pkg', {
+ 'bill' => { op=>'>', value=>$now }
+ } );
+
+ foreach my $cust_bill_pkg ( @cust_bill_pkg) {
+ my $period = $cust_bill_pkg->edate - $cust_bill_pkg->sdate;
+
+ my $elapsed = $now - $cust_bill_pkg->sdate;
+ $elapsed = 0 if $elapsed < 0;
+
+ my $remaining = 1 - $elapsed/$period;
+
+ my $unearned = $remaining * $cust_bill_pkg->recur;
+ $total += $unearned;
+
+ }
+
+ foreach my $cust_pkg ( @cust_pkg ) {
+ my $period = $cust_pkg->bill - $cust_pkg->last_bill;
+
+ my $elapsed = $now - $cust_pkg->last_bill;
+ $elapsed = 0 if $elapsed < 0;
+
+ my $remaining = 1 - $elapsed/$period;
+
+ my $unearned = $remaining * $cust_pkg->part_pkg->recur; #!! only works for flat/legacy
+ $total_legacy += $unearned;
+
+ }
+
+ $total = sprintf('%.2f', $total);
+ $total_legacy = sprintf('%.2f', $total_legacy);
+
+%>
+
+<%= header( 'Prepaid Income (Unearned Revenue) Report',
+ menubar( 'Main Menu'=>$p, ) ) %>
+<%= table() %>
+ <TR>
+ <TH>Actual Unearned Revenue</TH>
+ <TH>Legacy Unearned Revenue</TH>
+ </TR>
+ <TR>
+ <TD ALIGN="right">$<%= $total %>
+ <TD ALIGN="right">
+ <%= $now == $time ? "\$$total_legacy" : '<i>N/A</i>'%>
+ </TD>
+ </TR>
+
+</TABLE>
+<BR>
+Actual unearned revenue is the amount of unearned revenue Freeside has
+actually invoiced for packages with longer-than monthly terms.
+<BR><BR>
+Legacy unearned revenue is the amount of unearned revenue represented by
+customer packages. This number may be larger than actual unearned
+revenue if you have imported longer-than monthly customer packages from
+a previous billing system.
+</BODY>
+</HTML>
diff --git a/httemplate/search/report_prepaid_income.html b/httemplate/search/report_prepaid_income.html
new file mode 100644
index 0000000..e8b6ac4
--- /dev/null
+++ b/httemplate/search/report_prepaid_income.html
@@ -0,0 +1,39 @@
+<HTML>
+ <HEAD>
+ <TITLE>Prepaid Income (Unearned Revenue) Report</TITLE>
+ <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <H1>Prepaid Income (Unearned Revenue) Report</H1>
+ <FORM ACTION="report_prepaid_income.cgi" METHOD="post">
+ <TABLE>
+ <TR>
+ <TD>Prepaid income (unearned revenue) as of </TD>
+ <TD>
+ <INPUT TYPE="text" NAME="date" ID="date_text" VALUE="now">
+ <IMG SRC="../images/calendar.png" ID="date_button" STYLE="cursor: pointer" TITLE="Select date">
+ </TD>
+ </TR>
+ <TR>
+ <TD>
+ </TD>
+ <TD><i>m/d/y</i></TD>
+ </TR>
+ </TABLE>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "date_text",
+ ifFormat: "%m/%d/%Y",
+ button: "date_button",
+ align: "BR"
+ });
+</SCRIPT>
+
+ <INPUT TYPE="submit" VALUE="Generate report">
+ </BODY>
+</HTML>
+ <TABLE>
+
diff --git a/httemplate/search/report_receivables.cgi b/httemplate/search/report_receivables.cgi
new file mode 100755
index 0000000..0e95ad7
--- /dev/null
+++ b/httemplate/search/report_receivables.cgi
@@ -0,0 +1,158 @@
+<!-- mason kludge -->
+<%
+
+ my $charged = <<END;
+ sum( charged
+ - coalesce(
+ ( select sum(amount) from cust_bill_pay
+ where cust_bill.invnum = cust_bill_pay.invnum )
+ ,0
+ )
+ - coalesce(
+ ( select sum(amount) from cust_credit_bill
+ where cust_bill.invnum = cust_credit_bill.invnum )
+ ,0
+ )
+
+ )
+END
+
+ my $owed_cols = <<END;
+ coalesce(
+ ( select $charged from cust_bill
+ where cust_bill._date > extract(epoch from now())-2592000
+ and cust_main.custnum = cust_bill.custnum
+ )
+ ,0
+ ) as owed_0_30,
+
+ coalesce(
+ ( select $charged from cust_bill
+ where cust_bill._date > extract(epoch from now())-5184000
+ and cust_bill._date <= extract(epoch from now())-2592000
+ and cust_main.custnum = cust_bill.custnum
+ )
+ ,0
+ ) as owed_30_60,
+
+ coalesce(
+ ( select $charged from cust_bill
+ where cust_bill._date > extract(epoch from now())-7776000
+ and cust_bill._date <= extract(epoch from now())-5184000
+ and cust_main.custnum = cust_bill.custnum
+ )
+ ,0
+ ) as owed_60_90,
+
+ coalesce(
+ ( select $charged from cust_bill
+ where cust_bill._date <= extract(epoch from now())-7776000
+ and cust_main.custnum = cust_bill.custnum
+ )
+ ,0
+ ) as owed_90_plus,
+
+ coalesce(
+ ( select $charged from cust_bill
+ where cust_main.custnum = cust_bill.custnum
+ )
+ ,0
+ ) as owed_total
+END
+
+ my $recurring = <<END;
+ 0 < ( select freq from part_pkg
+ where cust_pkg.pkgpart = part_pkg.pkgpart )
+END
+
+ my $packages_cols = <<END;
+
+ ( select count(*) from cust_pkg
+ where cust_main.custnum = cust_pkg.custnum
+ and $recurring
+ and ( cancel = 0 or cancel is null )
+ ) as uncancelled_pkgs,
+
+ ( select count(*) from cust_pkg
+ where cust_main.custnum = cust_pkg.custnum
+ and $recurring
+ and ( cancel = 0 or cancel is null )
+ and ( susp = 0 or susp is null )
+ ) as active_pkgs
+
+END
+
+ my $sql = <<END;
+
+select *, $owed_cols, $packages_cols from cust_main
+where 0 <
+ coalesce(
+ ( select $charged from cust_bill
+ where cust_main.custnum = cust_bill.custnum
+ )
+ ,0
+ )
+
+order by coalesce(lower(company), ''), lower(last)
+
+END
+
+ my $total_sql = "select $owed_cols";
+
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+
+ my $total_sth = dbh->prepare($total_sql) or die dbh->errstr;
+ $total_sth->execute or die $total_sth->errstr;
+
+%>
+<%= header('Accounts Receivable Aging Summary', menubar( 'Main Menu'=>$p, ) ) %>
+<%= table() %>
+ <TR>
+ <TH>Customer</TH>
+ <TH>Status</TH>
+ <TH>0-30</TH>
+ <TH>30-60</TH>
+ <TH>60-90</TH>
+ <TH>90+</TH>
+ <TH>Total</TH>
+ </TR>
+<% while ( my $row = $sth->fetchrow_hashref() ) {
+ my $status = 'Cancelled';
+ my $statuscol = 'FF0000';
+ if ( $row->{uncancelled_pkgs} ) {
+ $status = 'Suspended';
+ $statuscol = 'FF9900';
+ if ( $row->{active_pkgs} ) {
+ $status = 'Active';
+ $statuscol = '00CC00';
+ }
+ }
+%>
+ <TR>
+ <TD><A HREF="<%= $p %>view/cust_main.cgi?<%= $row->{'custnum'} %>"><%= $row->{'custnum'} %>:
+ <%= $row->{'company'} ? $row->{'company'}. ' (' : '' %><%= $row->{'last'}. ', '. $row->{'first'} %><%= $row->{'company'} ? ')' : '' %></A>
+ </TD>
+ <TD><B><FONT SIZE=-1 COLOR="#<%= $statuscol %>"><%= $status %></FONT></B></TD>
+ <TD ALIGN="right">$<%= sprintf("%.2f", $row->{'owed_0_30'} ) %></TD>
+ <TD ALIGN="right">$<%= sprintf("%.2f", $row->{'owed_30_60'} ) %></TD>
+ <TD ALIGN="right">$<%= sprintf("%.2f", $row->{'owed_60_90'} ) %></TD>
+ <TD ALIGN="right">$<%= sprintf("%.2f", $row->{'owed_90_plus'} ) %></TD>
+ <TD ALIGN="right"><B>$<%= sprintf("%.2f", $row->{'owed_total'} ) %></B></TD>
+ </TR>
+<% } %>
+<% my $row = $total_sth->fetchrow_hashref(); %>
+ <TR>
+ <TD COLSPAN=6>&nbsp;</TD>
+ </TR>
+ <TR>
+ <TD COLSPAN=2><I>Total</I></TD>
+ <TD ALIGN="right"><I>$<%= sprintf("%.2f", $row->{'owed_0_30'} ) %></TD>
+ <TD ALIGN="right"><I>$<%= sprintf("%.2f", $row->{'owed_30_60'} ) %></TD>
+ <TD ALIGN="right"><I>$<%= sprintf("%.2f", $row->{'owed_60_90'} ) %></TD>
+ <TD ALIGN="right"><I>$<%= sprintf("%.2f", $row->{'owed_90_plus'} ) %></TD>
+ <TD ALIGN="right"><I><B>$<%= sprintf("%.2f", $row->{'owed_total'} ) %></B></I></TD>
+ </TR>
+</TABLE>
+</BODY>
+</HTML>
diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi
new file mode 100755
index 0000000..f37e127
--- /dev/null
+++ b/httemplate/search/report_tax.cgi
@@ -0,0 +1,184 @@
+<!-- mason kludge -->
+<%
+
+my $user = getotaker;
+
+$cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/;
+my $pbeginning = $1;
+my $beginning = $1 ? str2time($1) : 0;
+
+$cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/;
+my $pending = $1;
+my $ending = ( $1 ? str2time($1) : 4294880896 ) + 86399;
+
+my($total, $exempt, $taxable, $tax) = ( 0, 0, 0, 0 );
+my $out = 'Out of taxable region(s)';
+my %regions;
+foreach my $r (
+ qsearch('cust_main_county', {}, '',
+ "WHERE 0 < ( SELECT COUNT(*) FROM cust_main
+ WHERE ( cust_main.county = cust_main_county.county
+ OR cust_main_county.county = ''
+ OR cust_main_county.county IS NULL )
+ AND ( cust_main.state = cust_main_county.state
+ OR cust_main_county.state = ''
+ OR cust_main_county.state IS NULL )
+ AND ( cust_main.country = cust_main_county.country )
+ LIMIT 1
+ )"
+ )
+) {
+ #warn $r->county. ' '. $r->state. ' '. $r->country. "\n";
+ my $label;
+ if ( $r->tax == 0 ) {
+ $label = $out;
+ } elsif ( $r->taxname ) {
+ $label = $r->taxname;
+ } else {
+ $label = $r->country;
+ $label = $r->state.", $label" if $r->state;
+ $label = $r->county." county, $label" if $r->county;
+ }
+
+ my $fromwhere = "
+ FROM cust_bill_pkg
+ JOIN cust_bill USING ( invnum )
+ JOIN cust_main USING ( custnum )
+ JOIN cust_pkg USING ( pkgnum )
+ JOIN part_pkg USING ( pkgpart )
+ WHERE _date >= $beginning AND _date <= $ending
+ AND ( county = ? OR ? = '' )
+ AND ( state = ? OR ? = '' )
+ AND ( country = ? )
+ AND payby != 'COMP'
+ ";
+ my @param = qw( county county state state country ); # taxclass);
+
+ my $num_others =
+ scalar_sql( $r, [qw( country state state county county taxname taxname )],
+ "SELECT COUNT(*) FROM cust_main_county
+ WHERE country = ?
+ AND ( state = ? OR ( state IS NULL AND ? = '' ) )
+ AND ( county = ? OR ( county IS NULL AND ? = '' ) )
+ AND ( taxname = ? OR ( taxname IS NULL AND ? = '' ) ) "
+ );
+
+ die "didn't even find self?" unless $num_others;
+
+ if ( $num_others > 1 ) {
+ $fromwhere .= " AND ( taxclass = ? ) ";
+ push @param, 'taxclass';
+ }
+
+ my $nottax = 'pkgnum != 0';
+
+ my $a = scalar_sql($r, \@param,
+ "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) $fromwhere AND $nottax"
+ );
+ $total += $a;
+ $regions{$label}->{'total'} += $a;
+
+ foreach my $e ( grep { $r->get($_.'tax') =~ /^Y/i }
+ qw( cust_bill_pkg.setup cust_bill_pkg.recur ) ) {
+ my $x = scalar_sql($r, \@param,
+ "SELECT SUM($e) $fromwhere AND $nottax"
+ );
+ $exempt += $x;
+ $regions{$label}->{'exempt'} += $x;
+ }
+
+ foreach my $e ( grep { $r->get($_.'tax') !~ /^Y/i }
+ qw( cust_bill_pkg.setup cust_bill_pkg.recur ) ) {
+ my $t = scalar_sql($r, \@param,
+ "SELECT SUM($e) $fromwhere AND $nottax AND ( tax != 'Y' OR tax IS NULL )"
+ );
+ $taxable += $t;
+ $regions{$label}->{'taxable'} += $t;
+
+ my $x = scalar_sql($r, \@param,
+ "SELECT SUM($e) $fromwhere AND $nottax AND tax = 'Y'"
+ );
+ $exempt += $x;
+ $regions{$label}->{'exempt'} += $x;
+ }
+
+ if ( defined($regions{$label}->{'rate'})
+ && $regions{$label}->{'rate'} != $r->tax.'%' ) {
+ $regions{$label}->{'rate'} = 'variable';
+ } else {
+ $regions{$label}->{'rate'} = $r->tax.'%';
+ }
+
+ #match itemdesc if necessary!
+ my $named_tax = $r->taxname ? 'AND itemdesc = '. dbh->quote($r->taxname) : '';
+ my $x = scalar_sql($r, \@param,
+ "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) $fromwhere ".
+ "AND pkgnum = 0 $named_tax",
+ );
+ $tax += $x;
+ $regions{$label}->{'tax'} += $x;
+
+ $regions{$label}->{'label'} = $label;
+
+}
+
+#ordering
+my @regions = map $regions{$_},
+ sort { ( ($a eq $out) cmp ($b eq $out) ) || ($b cmp $a) }
+ keys %regions;
+
+push @regions, {
+ 'label' => 'Total',
+ 'total' => $total,
+ 'exempt' => $exempt,
+ 'taxable' => $taxable,
+ 'rate' => '',
+ 'tax' => $tax,
+};
+
+#--
+
+#false laziness w/FS::Report::Table::Monthly (sub should probably be moved up
+#to FS::Report or FS::Record or who the fuck knows where)
+sub scalar_sql {
+ my( $r, $param, $sql ) = @_;
+ #warn "$sql\n";
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute( map $r->$_(), @$param )
+ or die "Unexpected error executing statement $sql: ". $sth->errstr;
+ $sth->fetchrow_arrayref->[0] || 0;
+}
+
+%>
+
+<%= header( "Sales Tax Report - $pbeginning through ".($pending||'now'),
+ menubar( 'Main Menu'=>$p, ) ) %>
+<%= table() %>
+ <TR>
+ <TH ROWSPAN=2></TH>
+ <TH COLSPAN=3>Sales</TH>
+ <TH ROWSPAN=2>Rate</TH>
+ <TH ROWSPAN=2>Tax</TH>
+ </TR>
+ <TR>
+ <TH>Total</TH>
+ <TH>Non-taxable</TH>
+ <TH>Taxable</TH>
+ </TR>
+ <% foreach my $region ( @regions ) { %>
+ <TR>
+ <TD><%= $region->{'label'} %></TD>
+ <TD ALIGN="right">$<%= sprintf('%.2f', $region->{'total'} ) %></TD>
+ <TD ALIGN="right">$<%= sprintf('%.2f', $region->{'exempt'} ) %></TD>
+ <TD ALIGN="right">$<%= sprintf('%.2f', $region->{'taxable'} ) %></TD>
+ <TD ALIGN="right"><%= $region->{'rate'} %></TD>
+ <TD ALIGN="right">$<%= sprintf('%.2f', $region->{'tax'} ) %></TD>
+ </TR>
+ <% } %>
+
+</TABLE>
+
+</BODY>
+</HTML>
+
+
diff --git a/httemplate/search/report_tax.html b/httemplate/search/report_tax.html
new file mode 100755
index 0000000..d217e56
--- /dev/null
+++ b/httemplate/search/report_tax.html
@@ -0,0 +1,44 @@
+<HTML>
+ <HEAD>
+ <TITLE>Tax Report Criteria</TITLE>
+ <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT> </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <H1>Tax Report Criteria</H1>
+ <FORM ACTION="report_tax.cgi" METHOD="post">
+ Return <B>tax report</B> for period:
+ <TABLE>
+ <TR>
+ <TD ALIGN="right">From: </TD>
+ <TD><INPUT TYPE="text" NAME="beginning" ID="beginning_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="beginning_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "beginning_text",
+ ifFormat: "%m/%d/%Y",
+ button: "beginning_button",
+ align: "BR"
+ });
+</SCRIPT>
+ </TR>
+ <TR>
+ <TD ALIGN="right">To: </TD>
+ <TD><INPUT TYPE="text" NAME="ending" ID="ending_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="ending_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "ending_text",
+ ifFormat: "%m/%d/%Y",
+ button: "ending_button",
+ align: "BR"
+ });
+</SCRIPT>
+ </TR>
+ </TABLE>
+
+ <BR><INPUT TYPE="submit" VALUE="Get Report">
+
+ </FORM>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/search/sql.html b/httemplate/search/sql.html
new file mode 100644
index 0000000..b28c045
--- /dev/null
+++ b/httemplate/search/sql.html
@@ -0,0 +1,7 @@
+<%= include( 'elements/search.html',
+ 'title' => 'Query Results',
+ 'name' => 'rows',
+ 'query' => 'SELECT '. ( $cgi->param('sql')
+ || eidiot('Empty query') ),
+ )
+%>
diff --git a/httemplate/search/sqlradius.cgi b/httemplate/search/sqlradius.cgi
new file mode 100644
index 0000000..b506ba1
--- /dev/null
+++ b/httemplate/search/sqlradius.cgi
@@ -0,0 +1,260 @@
+<%= include( '/elements/header.html', 'RADIUS Sessions',
+ include('/elements/menubar.html',
+ 'Main menu' => $p, # popurl(2),
+ ),
+
+ )
+%>
+
+<%
+ ###
+ # parse cgi params
+ ###
+
+ #sort of false laziness w/cust_pay.cgi
+ my $beginning = '';
+ my $ending = '';
+ if ( $cgi->param('beginning')
+ && $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+ $beginning = str2time($1);
+ }
+ if ( $cgi->param('ending')
+ && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+ $ending = str2time($1) + 86399;
+ }
+ if ( $cgi->param('begin') && $cgi->param('begin') =~ /^(\d+)$/ ) {
+ $beginning = $1;
+ }
+ if ( $cgi->param('end') && $cgi->param('end') =~ /^(\d+)$/ ) {
+ $ending = $1;
+ }
+
+ my $cgi_svc_acct = '';
+ if ( $cgi->param('svcnum') =~ /^(\d+)$/ ) {
+ $cgi_svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $1 } );
+ } elsif ( $cgi->param('username') =~ /^([^@]+)\@([^@]+)$/ ) {
+ my %search = { 'username' => $1 };
+ my $svc_domain = qsearchs('svc_domain', { 'domain' => $2 } );
+ if ( $svc_domain ) {
+ $search{'domsvc'} = $svc_domain->svcnum;
+ } else {
+ delete $search{'username'};
+ }
+ $cgi_svc_acct = qsearchs( 'svc_acct', \%search )
+ if keys %search;
+ } elsif ( $cgi->param('username') =~ /^(.+)$/ ) {
+ $cgi_svc_acct = qsearchs( 'svc_acct', { 'username' => $1 } );
+ }
+
+ my $ip = '';
+ if ( $cgi->param('ip') =~ /^((\d+\.){3}\d+)$/ ) {
+ $ip = $1;
+ }
+
+ ###
+ # field formatting subroutines
+ ###
+
+ my %user2svc_acct = ();
+ my $user_format = sub {
+ my ( $user, $session, $part_export ) = @_;
+
+ my $svc_acct = '';
+ if ( exists $user2svc_acct{$user} ) {
+ $svc_acct = $user2svc_acct{$user};
+ } else {
+ my %search = ();
+ if ( $part_export->exporttype eq 'sqlradius_withdomain' ) {
+ my $domain;
+ if ( $user =~ /^([^@]+)\@([^@]+)$/ ) {
+ $search{'username'} = $1;
+ $domain = $2;
+ } else {
+ $search{'username'} = $user;
+ $domain = $session->{'realm'};
+ }
+ my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } );
+ if ( $svc_domain ) {
+ $search{'domsvc'} = $svc_domain->svcnum;
+ } else {
+ delete $search{'username'};
+ }
+ } elsif ( $part_export->exporttype eq 'sqlradius' ) {
+ $search{'username'} = $user;
+ } else {
+ die 'unknown export type '. $part_export->exporttype.
+ " for $part_export\n";
+ }
+ if ( keys %search ) {
+ my @svc_acct =
+ grep { qsearchs( 'export_svc', {
+ 'exportnum' => $part_export->exportnum,
+ 'svcpart' => $_->cust_svc->svcpart,
+ } )
+ } qsearch( 'svc_acct', \%search );
+ if ( @svc_acct ) {
+ warn 'multiple svc_acct records for user $user found; '.
+ 'using first arbitrarily'
+ if scalar(@svc_acct) > 1;
+ $user2svc_acct{$user} = $svc_acct = shift @svc_acct;
+ }
+ }
+ }
+
+ if ( $svc_acct ) {
+ my $svcnum = $svc_acct->svcnum;
+ qq(<A HREF="${p}view/svc_acct.cgi?$svcnum"><B>$user</B></A>);
+ } else {
+ "<B>$user</B>";
+ }
+
+ };
+
+ my $customer_format = sub {
+ my( $unused, $session ) = @_;
+ return '&nbsp;' unless exists $user2svc_acct{$session->{'username'}};
+ my $svc_acct = $user2svc_acct{$session->{'username'}};
+ my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+ return '&nbsp;' unless $cust_pkg;
+ my $cust_main = $cust_pkg->cust_main;
+
+ qq!<A HREF="${p}view/cust_main.cgi?!. $cust_main->custnum. '">'.
+ $cust_pkg->cust_main->name. '</A>';
+ };
+
+ my $time_format = sub {
+ my $time = shift;
+ return '&nbsp;' if $time == 0;
+ my $pretty = time2str('%T%P %a&nbsp;%b&nbsp;%o&nbsp;%Y', $time );
+ $pretty =~ s/ (\d)(st|dn|rd|th)/$1$2/;
+ $pretty;
+ };
+
+ my $duration_format = sub {
+ my $seconds = shift;
+ my $hour = int($seconds/3600);
+ my $min = int( ($seconds%3600) / 60 );
+ my $sec = $seconds%60;
+ '<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=0>'.
+ '<TR><TD ALIGN="right">'.
+ ( $hour ? "<B>$hour</B>h" : '&nbsp;' ).
+ '</TD><TD ALIGN="right">'.
+ ( ( $hour || $min ) ? "<B>$min</B>m" : '&nbsp;' ).
+ '</TD><TD ALIGN="right">'.
+ "<B>$sec</B>s".
+ '</TD></TR></TABLE>';
+ };
+
+ my $octets_format = sub {
+ my $octets = shift;
+ my $megs = $octets / 1048576;
+ sprintf('<B>%.3f</B>&nbsp;megs', $megs);
+ #my $gigs = $octets / 1073741824
+ #sprintf('<B>%.3f</B> gigabytes', $gigs);
+ };
+
+ ###
+ # the fields
+ ###
+
+ tie my %fields, 'Tie::IxHash',
+ 'username' => {
+ name => 'User',
+ attrib => 'UserName',
+ fmt => $user_format,
+ align => 'left',
+ },
+ 'realm' => {
+ name => 'Realm',
+ attrib => 'Realm',
+ align => 'left',
+ },
+ 'dummy' => {
+ name => 'Customer',
+ attrib => '',
+ fmt => $customer_format,
+ align => 'left',
+ },
+ 'framedipaddress' => {
+ name => 'IP&nbsp;Address',
+ attrib => 'Framed-IP-Address',
+ fmt => sub { my $ip = shift;
+ length($ip) ? $ip : '&nbsp';
+ },
+ align => 'right',
+ },
+ 'acctstarttime' => {
+ name => 'Start&nbsp;time',
+ attrib => 'Acct-Start-Time',
+ fmt => $time_format,
+ align => 'left',
+ },
+ 'acctstoptime' => {
+ name => 'End&nbsp;time',
+ attrib => 'Acct-Stop-Time',
+ fmt => $time_format,
+ align => 'left',
+ },
+ 'acctsessiontime' => {
+ name => 'Duration',
+ attrib => 'Acct-Session-Time',
+ fmt => $duration_format,
+ align => 'right',
+ },
+ 'acctinputoctets' => {
+ name => 'Upload', # (from user)',
+ attrib => 'Acct-Input-Octets',
+ fmt => $octets_format,
+ align => 'right',
+ },
+ 'acctoutputoctets' => {
+ name => 'Download', # (to user)',
+ attrib => 'Acct-Output-Octets',
+ fmt => $octets_format,
+ align => 'right',
+ },
+ ;
+ $fields{$_}->{fmt} ||= sub { length($_[0]) ? shift : '&nbsp'; }
+ foreach keys %fields;
+
+ ###
+ # and finally, display the thing
+ ###
+
+ foreach my $part_export ( map $_->rebless,
+ qsearch( 'part_export', { 'exporttype' => 'sqlradius' } ),
+ qsearch( 'part_export', { 'exporttype' => 'sqlradius_withdomain' } )
+ ) {
+ %user2svc_acct = ();
+%>
+
+<%= $part_export->exporttype %> to <%= $part_export->machine %><BR>
+<%= include( '/elements/table.html' ) %>
+<TR>
+ <% foreach my $field ( keys %fields ) { %>
+ <TH>
+ <%= $fields{$field}->{name} %><BR>
+ <FONT SIZE=-2><%= $fields{$field}->{attrib} %></FONT>
+ </TH>
+ <% } %>
+</TR>
+<% foreach my $session (
+ @{ $part_export->usage_sessions( $beginning, $ending, $cgi_svc_acct, $ip ) }
+) { %>
+ <TR>
+ <% foreach my $field ( keys %fields ) { %>
+ <TD ALIGN="<%= $fields{$field}->{align} %>">
+ <%= &{ $fields{$field}->{fmt} }( $session->{$field},
+ $session,
+ $part_export,
+ )
+ %>
+ </TD>
+ <% } %>
+ </TR>
+<% } %>
+
+</TABLE>
+<BR><BR>
+
+<% } %>
diff --git a/httemplate/search/sqlradius.html b/httemplate/search/sqlradius.html
new file mode 100644
index 0000000..48a3d86
--- /dev/null
+++ b/httemplate/search/sqlradius.html
@@ -0,0 +1,70 @@
+<%= include( '/elements/header.html', 'Search RADIUS sessions', '', '', '
+<LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+<SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
+') %>
+<FORM NAME="OneTrueForm" ACTION="sqlradius.cgi" METHOD="POST">
+<% #include( '/elements/table.html' ) %>
+<%= ntable('#cccccc') %>
+<TR>
+ <TD ALIGN="right">Username: </TD>
+ <TD><INPUT TYPE="text" NAME="username"></TD>
+</TR>
+<TR>
+ <TD></TD>
+ <TD><FONT SIZE="-1"><I>(leave blank to show all users)</I></FONT></TD>
+</TR>
+<TR>
+ <TD ALIGN="right">IP address: </TD>
+ <TD><INPUT TYPE="text" NAME="ip"></TD>
+</TR>
+<TR>
+ <TD></TD>
+ <TD><FONT SIZE="-1"><I>(leave blank to show all IPs)</I></FONT></TD>
+</TR>
+<TR>
+ <TD ALIGN="right">From: </TD>
+ <TD>
+ <INPUT TYPE="text" NAME="beginning" ID="beginning_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="beginning_button" STYLE="cursor: pointer" TITLE="Select date">
+ </TD>
+ <SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "beginning_text",
+ ifFormat: "%m/%d/%Y",
+ button: "beginning_button",
+ align: "BR"
+ });
+ </SCRIPT>
+</TR>
+<TR>
+ <TD></TD>
+ <TD><i>m/d/y</i></TD>
+</TR>
+<TR>
+ <TD ALIGN="right">To: </TD>
+ <TD>
+ <INPUT TYPE="text" NAME="ending" ID="ending_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="ending_button" STYLE="cursor:pointer" TITLE="Select date">
+ </TD>
+ <SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "ending_text",
+ ifFormat: "%m/%d/%Y",
+ button: "ending_button",
+ align: "BR"
+ });
+ </SCRIPT>
+</TR>
+<TR>
+ <TD></TD>
+ <TD><i>m/d/y</i>
+ <BR><FONT SIZE="-1">(leave one or both dates blank for an open-ended search)</FONT>
+ </TD>
+</TR>
+</TABLE>
+<BR><INPUT TYPE="submit" VALUE="View sessions">
+</FORM>
+</BODY>
+</HTML>
+
+
diff --git a/httemplate/search/svc_acct.cgi b/httemplate/search/svc_acct.cgi
new file mode 100755
index 0000000..1e4a03d
--- /dev/null
+++ b/httemplate/search/svc_acct.cgi
@@ -0,0 +1,294 @@
+<%
+
+my $conf = new FS::Conf;
+my $maxrecords = $conf->config('maxsearchrecordsperpage');
+
+my $orderby = ''; #removeme
+
+my $limit = '';
+$limit .= "LIMIT $maxrecords" if $maxrecords;
+
+my $offset = $cgi->param('offset') || 0;
+$limit .= " OFFSET $offset" if $offset;
+
+my $total;
+
+my($query)=$cgi->keywords;
+$query ||= ''; #to avoid use of unitialized value errors
+
+my $unlinked = '';
+if ( $query =~ /^UN_(.*)$/ ) {
+ $query = $1;
+ my $empty = driver_name eq 'Pg' ? qq('') : qq("");
+ if ( driver_name eq 'mysql' ) {
+ $unlinked = "LEFT JOIN cust_svc ON cust_svc.svcnum = svc_acct.svcnum
+ WHERE cust_svc.pkgnum IS NULL
+ OR cust_svc.pkgnum = 0
+ OR cust_svc.pkgnum = $empty";
+ } else {
+ $unlinked = "
+ WHERE 0 <
+ ( SELECT count(*) FROM cust_svc
+ WHERE cust_svc.svcnum = svc_acct.svcnum
+ AND ( pkgnum IS NULL OR pkgnum = 0 )
+ )
+ ";
+ }
+}
+
+my $tblname = driver_name eq 'mysql' ? 'svc_acct.' : '';
+my(@svc_acct, $sortby);
+if ( $query eq 'svcnum' ) {
+ $sortby=\*svcnum_sort;
+ $orderby = "ORDER BY ${tblname}svcnum";
+} elsif ( $query eq 'username' ) {
+ $sortby=\*username_sort;
+ $orderby = "ORDER BY ${tblname}username";
+} elsif ( $query eq 'uid' ) {
+ $sortby=\*uid_sort;
+ $orderby = ( $unlinked ? ' AND' : ' WHERE' ).
+ " ${tblname}uid IS NOT NULL ORDER BY ${tblname}uid";
+} elsif ( $cgi->param('popnum') =~ /^(\d+)$/ ) {
+ $unlinked .= ( $unlinked ? 'AND' : 'WHERE' ).
+ " popnum = $1";
+ $sortby=\*username_sort;
+ $orderby = "ORDER BY ${tblname}username";
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+ $unlinked .= ( $unlinked ? ' AND' : ' WHERE' ).
+ " $1 = ( SELECT svcpart FROM cust_svc ".
+ " WHERE cust_svc.svcnum = svc_acct.svcnum ) ";
+ $sortby=\*uid_sort;
+ #$sortby=\*svcnum_sort;
+} else {
+ $sortby=\*uid_sort;
+ @svc_acct = @{&usernamesearch};
+}
+
+
+if ( $query eq 'svcnum'
+ || $query eq 'username'
+ || $query eq 'uid'
+ || $cgi->param('popnum') =~ /^(\d+)$/
+ || $cgi->param('svcpart') =~ /^(\d+)$/
+ ) {
+
+ my $statement = "SELECT COUNT(*) FROM svc_acct $unlinked";
+ my $sth = dbh->prepare($statement)
+ or die dbh->errstr. " doing $statement";
+ $sth->execute or die "Error executing \"$statement\": ". $sth->errstr;
+
+ $total = $sth->fetchrow_arrayref->[0];
+
+ @svc_acct = qsearch('svc_acct', {}, '', "$unlinked $orderby $limit");
+
+}
+
+if ( scalar(@svc_acct) == 1 ) {
+ my($svcnum)=$svc_acct[0]->svcnum;
+ print $cgi->redirect(popurl(2). "view/svc_acct.cgi?$svcnum"); #redirect
+ #exit;
+} elsif ( scalar(@svc_acct) == 0 ) { #error
+%>
+<!-- mason kludge -->
+<%
+ idiot("Account not found");
+} else {
+%>
+<!-- mason kludge -->
+<%
+ $total ||= scalar(@svc_acct);
+
+ #begin pager
+ my $pager = '';
+ if ( $total != scalar(@svc_acct) && $maxrecords ) {
+ unless ( $offset == 0 ) {
+ $cgi->param('offset', $offset - $maxrecords);
+ $pager .= '<A HREF="'. $cgi->self_url.
+ '"><B><FONT SIZE="+1">Previous</FONT></B></A> ';
+ }
+ my $poff;
+ my $page;
+ for ( $poff = 0; $poff < $total; $poff += $maxrecords ) {
+ $page++;
+ if ( $offset == $poff ) {
+ $pager .= qq!<FONT SIZE="+2">$page</FONT> !;
+ } else {
+ $cgi->param('offset', $poff);
+ $pager .= qq!<A HREF="!. $cgi->self_url. qq!">$page</A> !;
+ }
+ }
+ unless ( $offset + $maxrecords > $total ) {
+ $cgi->param('offset', $offset + $maxrecords);
+ $pager .= '<A HREF="'. $cgi->self_url.
+ '"><B><FONT SIZE="+1">Next</FONT></B></A> ';
+ }
+ }
+ #end pager
+
+ print header("Account Search Results",menubar('Main Menu'=>popurl(2))),
+ "$total matching accounts found<BR><BR>$pager",
+ &table(), <<END;
+ <TR>
+ <TH><FONT SIZE=-1>#</FONT></TH>
+ <TH><FONT SIZE=-1>Username</FONT></TH>
+ <TH><FONT SIZE=-1>Domain</FONT></TH>
+ <TH><FONT SIZE=-1>UID</FONT></TH>
+ <TH><FONT SIZE=-1>Service</FONT></TH>
+ <TH><FONT SIZE=-1>Cust#</FONT></TH>
+ <TH><FONT SIZE=-1>(bill) name</FONT></TH>
+ <TH><FONT SIZE=-1>company</FONT></TH>
+END
+ if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+ print <<END;
+ <TH><FONT SIZE=-1>(service) name</FONT></TH>
+ <TH><FONT SIZE=-1>company</FONT></TH>
+END
+ }
+ print "</TR>";
+
+ my(%saw,$svc_acct);
+ my $p = popurl(2);
+ foreach $svc_acct (
+ sort $sortby grep(!$saw{$_->svcnum}++, @svc_acct)
+ ) {
+ my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $svc_acct->svcnum })
+ or die "No cust_svc record for svcnum ". $svc_acct->svcnum;
+ my $part_svc = qsearchs('part_svc', { 'svcpart' => $cust_svc->svcpart })
+ or die "No part_svc record for svcpart ". $cust_svc->svcpart;
+
+ my $domain;
+ my $svc_domain = qsearchs('svc_domain', { 'svcnum' => $svc_acct->domsvc });
+ if ( $svc_domain ) {
+ $domain = "<A HREF=\"${p}view/svc_domain.cgi?". $svc_domain->svcnum.
+ "\">". $svc_domain->domain. "</A>";
+ } else {
+ die "No svc_domain.svcnum record for svc_acct.domsvc: ".
+ $svc_acct->domsvc;
+ }
+ my($cust_pkg,$cust_main);
+ if ( $cust_svc->pkgnum ) {
+ $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $cust_svc->pkgnum })
+ or die "No cust_pkg record for pkgnum ". $cust_svc->pkgnum;
+ $cust_main = qsearchs('cust_main', { 'custnum' => $cust_pkg->custnum })
+ or die "No cust_main record for custnum ". $cust_pkg->custnum;
+ }
+ my($svcnum, $username, $uid, $svc, $custnum, $last, $first, $company) = (
+ $svc_acct->svcnum,
+ $svc_acct->getfield('username'),
+ $svc_acct->getfield('uid'),
+ $part_svc->svc,
+ $cust_svc->pkgnum ? $cust_main->custnum : '',
+ $cust_svc->pkgnum ? $cust_main->getfield('last') : '',
+ $cust_svc->pkgnum ? $cust_main->getfield('first') : '',
+ $cust_svc->pkgnum ? $cust_main->company : '',
+ );
+ my($pcustnum) = $custnum
+ ? "<A HREF=\"${p}view/cust_main.cgi?$custnum\"><FONT SIZE=-1>$custnum</FONT></A>"
+ : "<I>(unlinked)</I>"
+ ;
+ my $pname = $custnum ? "<A HREF=\"${p}view/cust_main.cgi?$custnum\">$last, $first</A>" : '';
+ my $pcompany = $custnum ? "<A HREF=\"${p}view/cust_main.cgi?$custnum\">$company</A>" : '';
+ my($pship_name, $pship_company);
+ if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+ my($ship_last, $ship_first, $ship_company) = (
+ $cust_svc->pkgnum ? ( $cust_main->ship_last || $last ) : '',
+ $cust_svc->pkgnum ? ( $cust_main->ship_last
+ ? $cust_main->ship_first
+ : $first
+ ) : '',
+ $cust_svc->pkgnum ? ( $cust_main->ship_last
+ ? $cust_main->ship_company
+ : $company
+ ) : '',
+ );
+ $pship_name = $custnum ? "<A HREF=\"${p}view/cust_main.cgi?$custnum\">$ship_last, $ship_first</A>" : '';
+ $pship_company = $custnum ? "<A HREF=\"${p}view/cust_main.cgi?$custnum\">$ship_company</A>" : '';
+ }
+ print <<END;
+ <TR>
+ <TD><A HREF="${p}view/svc_acct.cgi?$svcnum"><FONT SIZE=-1>$svcnum</FONT></A></TD>
+ <TD><A HREF="${p}view/svc_acct.cgi?$svcnum"><FONT SIZE=-1>$username</FONT></A></TD>
+ <TD><FONT SIZE=-1>$domain</FONT></TD>
+ <TD><A HREF="${p}view/svc_acct.cgi?$svcnum"><FONT SIZE=-1>$uid</FONT></A></TD>
+ <TD><FONT SIZE=-1>$svc</FONT></TH>
+ <TD><FONT SIZE=-1>$pcustnum</FONT></TH>
+ <TD><FONT SIZE=-1>$pname<FONT></TH>
+ <TD><FONT SIZE=-1>$pcompany</FONT></TH>
+END
+ if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+ print <<END;
+ <TD><FONT SIZE=-1>$pship_name<FONT></TH>
+ <TD><FONT SIZE=-1>$pship_company</FONT></TH>
+END
+ }
+ print "</TR>";
+
+ }
+
+ print "</TABLE>$pager<BR>".
+ '</BODY></HTML>';
+
+}
+
+sub svcnum_sort {
+ $a->getfield('svcnum') <=> $b->getfield('svcnum');
+}
+
+sub username_sort {
+ $a->getfield('username') cmp $b->getfield('username');
+}
+
+sub uid_sort {
+ $a->getfield('uid') <=> $b->getfield('uid');
+}
+
+sub usernamesearch {
+
+ my @svc_acct;
+
+ my %username_type;
+ foreach ( $cgi->param('username_type') ) {
+ $username_type{$_}++;
+ }
+
+ $cgi->param('username') =~ /^([\w\-\.\&]+)$/; #untaint username_text
+ my $username = $1;
+
+ if ( $username_type{'Exact'} || $username_type{'Fuzzy'} ) {
+ push @svc_acct, qsearch( 'svc_acct',
+ { 'username' => { 'op' => 'ILIKE',
+ 'value' => $username } } );
+ }
+
+ if ( $username_type{'Substring'} || $username_type{'All'} ) {
+ push @svc_acct, qsearch( 'svc_acct',
+ { 'username' => { 'op' => 'ILIKE',
+ 'value' => "%$username%" } } );
+ }
+
+ if ( $username_type{'Fuzzy'} || $username_type{'All'} ) {
+ &FS::svc_acct::check_and_rebuild_fuzzyfiles;
+ my $all_username = &FS::svc_acct::all_username;
+
+ my %username;
+ if ( $username_type{'Fuzzy'} || $username_type{'All'} ) {
+ foreach ( amatch($username, [ qw(i) ], @$all_username) ) {
+ $username{$_}++;
+ }
+ }
+
+ #if ($username_type{'Sound-alike'}) {
+ #}
+
+ foreach ( keys %username ) {
+ push @svc_acct, qsearch('svc_acct',{'username'=>$_});
+ }
+
+ }
+
+ #[ qsearch('svc_acct',{'username'=>$username}) ];
+ \@svc_acct;
+
+}
+
+%>
diff --git a/httemplate/search/svc_acct.html b/httemplate/search/svc_acct.html
new file mode 100755
index 0000000..7423605
--- /dev/null
+++ b/httemplate/search/svc_acct.html
@@ -0,0 +1,19 @@
+<HTML>
+ <HEAD>
+ <TITLE>Account Search</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <FONT SIZE=7>
+ Account Search
+ </FONT>
+ <BR><BR>
+ <FORM ACTION="svc_acct.cgi" METHOD="post">
+ Search for <B>username</B>:
+ <INPUT TYPE="text" NAME="username">
+
+ <P><INPUT TYPE="submit" VALUE="Search">
+
+ </FORM>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/search/svc_domain.cgi b/httemplate/search/svc_domain.cgi
new file mode 100755
index 0000000..948b1d9
--- /dev/null
+++ b/httemplate/search/svc_domain.cgi
@@ -0,0 +1,161 @@
+<%
+
+my $conf = new FS::Conf;
+
+my($query)=$cgi->keywords;
+$query ||= ''; #to avoid use of unitialized value errors
+my(@svc_domain,$sortby);
+if ( $query eq 'svcnum' ) {
+ $sortby=\*svcnum_sort;
+ @svc_domain=qsearch('svc_domain',{});
+} elsif ( $query eq 'domain' ) {
+ $sortby=\*domain_sort;
+ @svc_domain=qsearch('svc_domain',{});
+} elsif ( $query eq 'UN_svcnum' ) {
+ $sortby=\*svcnum_sort;
+ @svc_domain = grep qsearchs('cust_svc',{
+ 'svcnum' => $_->svcnum,
+ 'pkgnum' => '',
+ }), qsearch('svc_domain',{});
+} elsif ( $query eq 'UN_domain' ) {
+ $sortby=\*domain_sort;
+ @svc_domain = grep qsearchs('cust_svc',{
+ 'svcnum' => $_->svcnum,
+ 'pkgnum' => '',
+ }), qsearch('svc_domain',{});
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+ @svc_domain =
+ qsearch( 'svc_domain', {}, '',
+ " WHERE $1 = ( SELECT svcpart FROM cust_svc ".
+ " WHERE cust_svc.svcnum = svc_domain.svcnum ) "
+ );
+ $sortby=\*svcnum_sort;
+} else {
+ $cgi->param('domain') =~ /^([\w\-\.]+)$/;
+ my($domain)=$1;
+ #push @svc_domain, qsearchs('svc_domain',{'domain'=>$domain});
+ @svc_domain = qsearchs('svc_domain',{'domain'=>$domain});
+}
+
+if ( scalar(@svc_domain) == 1 ) {
+ print $cgi->redirect(popurl(2). "view/svc_domain.cgi?". $svc_domain[0]->svcnum);
+ #exit;
+} elsif ( scalar(@svc_domain) == 0 ) {
+%>
+<!-- mason kludge -->
+<%
+ eidiot "No matching domains found!\n";
+} else {
+%>
+<!-- mason kludge -->
+<%
+ my($total)=scalar(@svc_domain);
+ print header("Domain Search Results",''), <<END;
+
+ $total matching domains found
+ <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
+ <TR>
+ <TH>Service #</TH>
+ <TH>Domain</TH>
+<!-- <TH>Mail to<BR><FONT SIZE=-1>(click to view account)</FONT></TH>
+ <TH>Forwards to<BR><FONT SIZE=-1>(click to view account)</FONT></TH>
+-->
+ </TR>
+END
+
+# my(%saw); # if we've multiple domains with the same
+ # svcnum, then we've a corrupt database
+
+ foreach my $svc_domain (
+# sort $sortby grep(!$saw{$_->svcnum}++, @svc_domain)
+ sort $sortby (@svc_domain)
+ ) {
+ my($svcnum,$domain)=(
+ $svc_domain->svcnum,
+ $svc_domain->domain,
+ );
+
+ #don't display all accounts here
+ my $rowspan = 1;
+
+ #my @svc_acct=qsearch('svc_acct',{'domsvc' => $svcnum});
+ #my $rowspan = 0;
+ #
+ #my $n1 = '';
+ #my($svc_acct, @rows);
+ #foreach $svc_acct (
+ # sort {$b->getfield('username') cmp $a->getfield('username')} (@svc_acct)
+ #) {
+ #
+ # my (@forwards) = ();
+ #
+ # my($svcnum,$username)=(
+ # $svc_acct->svcnum,
+ # $svc_acct->username,
+ # );
+ #
+ # my @svc_forward = qsearch( 'svc_forward', { 'srcsvc' => $svcnum } );
+ # my $svc_forward;
+ # foreach $svc_forward (@svc_forward) {
+ # my($dstsvc,$dst) = (
+ # $svc_forward->dstsvc,
+ # $svc_forward->dst,
+ # );
+ # if ($dstsvc) {
+ # my $dst_svc_acct=qsearchs( 'svc_acct', { 'svcnum' => $dstsvc } );
+ # my $destination=$dst_svc_acct->email;
+ # push @forwards, qq!<TD><A HREF="!, popurl(2),
+ # qq!view/svc_acct.cgi?$dstsvc">$destination</A>!,
+ # qq!</TD></TR>!
+ # ;
+ # }else{
+ # push @forwards, qq!<TD>$dst</TD></TR>!
+ # ;
+ # }
+ # }
+ #
+ # push @rows, qq!$n1<TD ROWSPAN=!, (scalar(@svc_forward) || 1),
+ # qq!><A HREF="!. popurl(2). qq!view/svc_acct.cgi?$svcnum">!,
+ # #print '', ( ($domuser eq '*') ? "<I>(anything)</I>" : $domuser );
+ # ( ($username eq '*') ? "<I>(anything)</I>" : $username ),
+ # qq!\@$domain</A> </TD>!,
+ # ;
+ #
+ # push @rows, @forwards;
+ #
+ # $rowspan += (scalar(@svc_forward) || 1);
+ # $n1 = "</TR><TR>";
+ #}
+ ##end of false laziness
+ #
+ #
+
+ print <<END;
+ <TR>
+ <TD ROWSPAN=$rowspan><A HREF="${p}view/svc_domain.cgi?$svcnum">$svcnum</A></TD>
+ <TD ROWSPAN=$rowspan><A HREF="${p}view/svc_domain.cgi?$svcnum">$domain</A></TD>
+END
+
+ #print @rows;
+ print "</TR>";
+
+ }
+
+ print <<END;
+ </TABLE>
+ </BODY>
+</HTML>
+END
+
+}
+
+sub svcnum_sort {
+ $a->getfield('svcnum') <=> $b->getfield('svcnum');
+}
+
+sub domain_sort {
+ $a->getfield('domain') cmp $b->getfield('domain');
+}
+
+
+%>
diff --git a/httemplate/search/svc_domain.html b/httemplate/search/svc_domain.html
new file mode 100755
index 0000000..94bb9a6
--- /dev/null
+++ b/httemplate/search/svc_domain.html
@@ -0,0 +1,19 @@
+<HTML>
+ <HEAD>
+ <TITLE>Domain Search</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <FONT SIZE=7>
+ Domain Search
+ </FONT>
+ <BR><BR>
+ <FORM ACTION="svc_domain.cgi" METHOD="post">
+ Search for <B>domain</B>:
+ <INPUT TYPE="text" NAME="domain">
+
+ <P><INPUT TYPE="submit" VALUE="Search">
+
+ </FORM>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/search/svc_external.cgi b/httemplate/search/svc_external.cgi
new file mode 100755
index 0000000..c5ac134
--- /dev/null
+++ b/httemplate/search/svc_external.cgi
@@ -0,0 +1,101 @@
+<%
+
+my $conf = new FS::Conf;
+
+my($query)=$cgi->keywords;
+$query ||= ''; #to avoid use of unitialized value errors
+my(@svc_external,$sortby);
+if ( $query eq 'svcnum' ) {
+ $sortby=\*svcnum_sort;
+ @svc_external=qsearch('svc_external',{});
+} elsif ( $query eq 'id' ) {
+ $sortby=\*id_sort;
+ @svc_external=qsearch('svc_external',{});
+} elsif ( $query eq 'UN_svcnum' ) {
+ $sortby=\*svcnum_sort;
+ @svc_external = grep qsearchs('cust_svc',{
+ 'svcnum' => $_->svcnum,
+ 'pkgnum' => '',
+ }), qsearch('svc_external',{});
+} elsif ( $query eq 'UN_id' ) {
+ $sortby=\*id_sort;
+ @svc_external = grep qsearchs('cust_svc',{
+ 'svcnum' => $_->svcnum,
+ 'pkgnum' => '',
+ }), qsearch('svc_external',{});
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+ @svc_external =
+ qsearch( 'svc_external', {}, '',
+ " WHERE $1 = ( SELECT svcpart FROM cust_svc ".
+ " WHERE cust_svc.svcnum = svc_external.svcnum ) "
+ );
+ $sortby=\*svcnum_sort;
+} else {
+ $cgi->param('id') =~ /^([\w\-\.]+)$/;
+ my($id)=$1;
+ #push @svc_domain, qsearchs('svc_domain',{'domain'=>$domain});
+ @svc_external = qsearchs('svc_external',{'id'=>$id});
+}
+
+if ( scalar(@svc_external) == 1 ) {
+ print $cgi->redirect(popurl(2). "view/svc_external.cgi?". $svc_external[0]->svcnum);
+ #exit;
+} elsif ( scalar(@svc_external) == 0 ) {
+%>
+<!-- mason kludge -->
+<%
+ eidiot "No matching external services found!\n";
+} else {
+%>
+<!-- mason kludge -->
+<%= header("External Search Results",'') %>
+
+ <%= scalar(@svc_external) %> matching external services found
+ <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
+ <TR>
+ <TH>Service #</TH>
+ <TH><%= FS::Msgcat::_gettext('svc_external-id') || 'External&nbsp;ID' %></TH>
+ <TH><%= FS::Msgcat::_gettext('svc_external-title') || 'Title' %></TH>
+ </TR>
+
+<%
+ foreach my $svc_external (
+ sort $sortby (@svc_external)
+ ) {
+ my($svcnum, $id, $title)=(
+ $svc_external->svcnum,
+ $svc_external->id,
+ $svc_external->title,
+ );
+
+ my $rowspan = 1;
+
+ print <<END;
+ <TR>
+ <TD ROWSPAN=$rowspan><A HREF="${p}view/svc_external.cgi?$svcnum">$svcnum</A></TD>
+ <TD ROWSPAN=$rowspan><A HREF="${p}view/svc_external.cgi?$svcnum">$id</A></TD>
+ <TD ROWSPAN=$rowspan><A HREF="${p}view/svc_external.cgi?$svcnum">$title</A></TD>
+END
+
+ #print @rows;
+ print "</TR>";
+
+ }
+
+ print <<END;
+ </TABLE>
+ </BODY>
+</HTML>
+END
+
+}
+
+sub svcnum_sort {
+ $a->getfield('svcnum') <=> $b->getfield('svcnum');
+}
+
+sub id_sort {
+ $a->getfield('id') <=> $b->getfield('id');
+}
+
+%>
diff --git a/httemplate/search/svc_forward.cgi b/httemplate/search/svc_forward.cgi
new file mode 100755
index 0000000..10094bc
--- /dev/null
+++ b/httemplate/search/svc_forward.cgi
@@ -0,0 +1,79 @@
+<%
+
+my $conf = new FS::Conf;
+
+my($query)=$cgi->keywords;
+$query ||= ''; #to avoid use of unitialized value errors
+my(@svc_forward,$sortby);
+if ( $query eq 'svcnum' ) {
+ $sortby=\*svcnum_sort;
+ @svc_forward=qsearch('svc_forward',{});
+} else {
+ eidiot('unimplemented');
+}
+
+if ( scalar(@svc_forward) == 1 ) {
+ print $cgi->redirect(popurl(2). "view/svc_forward.cgi?". $svc_forward[0]->svcnum);
+ #exit;
+} elsif ( scalar(@svc_forward) == 0 ) {
+%>
+<!-- mason kludge -->
+<%
+ eidiot "No matching forwards found!\n";
+} else {
+%>
+<!-- mason kludge -->
+<%
+ my $total = scalar(@svc_forward);
+ print header("Mail forward Search Results",''), <<END;
+
+ $total matching mail forwards found
+ <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
+ <TR>
+ <TH>Service #<BR><FONT SIZE=-1>(click to view forward)</FONT></TH>
+ <TH>Mail to<BR><FONT SIZE=-1>(click to view account)</FONT></TH>
+ <TH>Forwards to<BR><FONT SIZE=-1>(click to view account)</FONT></TH>
+ </TR>
+END
+
+ foreach my $svc_forward (
+ sort $sortby (@svc_forward)
+ ) {
+ my $svcnum = $svc_forward->svcnum;
+
+ my $src = $svc_forward->src;
+ $src = "<I>(anything)</I>$src" if $src =~ /^@/;
+ if ( $svc_forward->srcsvc_acct ) {
+ $src = qq!<A HREF="${p}view/svc_acct.cgi?!. $svc_forward->srcsvc. '">'.
+ $svc_forward->srcsvc_acct->email. '</A>';
+ }
+
+ my $dst = $svc_forward->dst;
+ if ( $svc_forward->dstsvc_acct ) {
+ $dst = qq!<A HREF="${p}view/svc_acct.cgi?!. $svc_forward->dstsvc. '">'.
+ $svc_forward->dstsvc_acct->email. '</A>';
+ }
+
+ print <<END;
+ <TR>
+ <TD><A HREF="${p}view/svc_forward.cgi?$svcnum">$svcnum</A></TD>
+ <TD>$src</TD>
+ <TD>$dst</TD>
+ </TR>
+END
+
+ }
+
+ print <<END;
+ </TABLE>
+ </BODY>
+</HTML>
+END
+
+}
+
+sub svcnum_sort {
+ $a->getfield('svcnum') <=> $b->getfield('svcnum');
+}
+
+%>
diff --git a/httemplate/search/svc_www.cgi b/httemplate/search/svc_www.cgi
new file mode 100755
index 0000000..1f05c23
--- /dev/null
+++ b/httemplate/search/svc_www.cgi
@@ -0,0 +1,42 @@
+<%
+
+#my $conf = new FS::Conf;
+
+my($query)=$cgi->keywords;
+$query ||= ''; #to avoid use of unitialized value errors
+my(@svc_www, $orderby);
+if ( $query eq 'svcnum' ) {
+ $orderby = 'ORDER BY svcnum';
+} else {
+ eidiot('unimplemented');
+}
+
+my $count_query = 'SELECT COUNT(*) FROM svc_www';
+my $sql_query = {
+ 'table' => 'svc_www',
+ 'hashref' => {},
+ 'extra_sql' => $orderby,
+};
+
+my $link = [ "${p}view/svc_www.cgi?", 'svcnum', ];
+#my $dlink = [ "${p}view/svc_www.cgi?", 'svcnum', ];
+my $ulink = [ "${p}view/svc_acct.cgi?", 'usersvc', ];
+
+
+%>
+<%= include( 'elements/search.html',
+ 'title' => 'Virtual Host Search Results',
+ 'name' => 'virtual hosts',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'header' => [ '#', 'Zone', 'User', ],
+ 'fields' => [ 'svcnum',
+ sub { $_[0]->domain_record->zone },
+ sub { $_[0]->svc_acct->email },
+ ],
+ 'links' => [ $link,
+ '',
+ $ulink,
+ ],
+ )
+%>
diff --git a/httemplate/view/cust_bill-pdf.cgi b/httemplate/view/cust_bill-pdf.cgi
new file mode 100755
index 0000000..a72a605
--- /dev/null
+++ b/httemplate/view/cust_bill-pdf.cgi
@@ -0,0 +1,18 @@
+<%
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^((.+)-)?(\d+)(.pdf)?$/;
+my $templatename = $2;
+my $invnum = $3;
+
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+die "Invoice #$invnum not found!" unless $cust_bill;
+
+my $pdf = $cust_bill->print_pdf( '', $templatename);
+
+http_header('Content-Type' => 'application/pdf' );
+http_header('Content-Length' => length($pdf) );
+http_header('Cache-control' => 'max-age=60' );
+%>
+<%= $pdf %>
diff --git a/httemplate/view/cust_bill-ps.cgi b/httemplate/view/cust_bill-ps.cgi
new file mode 100755
index 0000000..8485a15
--- /dev/null
+++ b/httemplate/view/cust_bill-ps.cgi
@@ -0,0 +1,14 @@
+<%
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^((.+)-)?(\d+)$/;
+my $templatename = $2;
+my $invnum = $3;
+
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+die "Invoice #$invnum not found!" unless $cust_bill;
+
+http_header('Content-Type' => 'application/postscript' );
+%>
+<%= $cust_bill->print_ps( '', $templatename) %>
diff --git a/httemplate/view/cust_bill.cgi b/httemplate/view/cust_bill.cgi
new file mode 100755
index 0000000..34f5331
--- /dev/null
+++ b/httemplate/view/cust_bill.cgi
@@ -0,0 +1,82 @@
+<!-- mason kludge -->
+<%
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^((.+)-)?(\d+)$/;
+my $templatename = $2;
+my $invnum = $3;
+
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+die "Invoice #$invnum not found!" unless $cust_bill;
+my $custnum = $cust_bill->getfield('custnum');
+
+#my $printed = $cust_bill->printed;
+
+print header('Invoice View', menubar(
+ "Main Menu" => $p,
+ "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+));
+
+print qq!<A HREF="${p}edit/cust_pay.cgi?$invnum">Enter payments (check/cash) against this invoice</A> | !
+ if $cust_bill->owed > 0;
+
+print qq!<A HREF="${p}misc/print-invoice.cgi?$invnum">Reprint this invoice</A>!;
+if ( grep { $_ ne 'POST' } $cust_bill->cust_main->invoicing_list ) {
+ print qq! | <A HREF="${p}misc/email-invoice.cgi?$invnum">!.
+ qq!Re-email this invoice</A>!;
+}
+
+print '<BR><BR>';
+
+my $conf = new FS::Conf;
+if ( $conf->exists('invoice_latex') ) {
+ my $link = "${p}view/cust_bill-pdf.cgi?";
+ $link .= "$templatename-" if $templatename;
+ $link .= "$invnum.pdf";
+ print menubar(
+ 'View typeset invoice' => $link,
+ ), '<BR><BR>';
+}
+
+#false laziness with search/cust_bill_event.cgi
+
+unless ( $templatename ) {
+ print table(). '<TR><TH>Event</TH><TH>Date</TH><TH>Status</TH></TR>';
+ foreach my $cust_bill_event (
+ sort { $a->_date <=> $b->_date } $cust_bill->cust_bill_event
+ ) {
+ my $status = $cust_bill_event->status;
+ $status .= ': '. $cust_bill_event->statustext
+ if $cust_bill_event->statustext;
+ my $part_bill_event = $cust_bill_event->part_bill_event;
+ print '<TR><TD>'. $part_bill_event->event;
+
+ if (
+ $part_bill_event->plan eq 'send_alternate'
+ && $part_bill_event->plandata =~ /^templatename (.*)$/m
+ ) {
+ my $templatename = $1;
+ print qq! ( <A HREF="${p}view/cust_bill.cgi?$templatename-$invnum">!.
+ 'view text</A> | '.
+ qq!<A HREF="${p}view/cust_bill-pdf.cgi?$templatename-$invnum.pdf">!.
+ 'view typeset</A> )';
+ }
+
+ print '</TD><TD>'.
+ time2str("%a %b %e %T %Y", $cust_bill_event->_date). '</TD><TD>'.
+ $status. '</TD></TR>';
+ }
+ print '</TABLE><BR>';
+}
+
+print '<PRE>', $cust_bill->print_text('', $templatename);
+
+ #formatting
+ print <<END;
+ </PRE></FONT>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/view/cust_main.cgi b/httemplate/view/cust_main.cgi
new file mode 100755
index 0000000..0b51a87
--- /dev/null
+++ b/httemplate/view/cust_main.cgi
@@ -0,0 +1,1064 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my %uiview = ();
+my %uiadd = ();
+foreach my $part_svc ( qsearch('part_svc',{}) ) {
+ $uiview{$part_svc->svcpart} = popurl(2). "view/". $part_svc->svcdb . ".cgi";
+ $uiadd{$part_svc->svcpart}= popurl(2). "edit/". $part_svc->svcdb . ".cgi";
+}
+
+print header("Customer View", menubar(
+ 'Main Menu' => popurl(2)
+));
+
+%>
+
+<STYLE TYPE="text/css">
+.package TH { font-size: medium }
+.package TR { font-size: smaller }
+.package .provision { font-weight: bold }
+</STYLE>
+
+<%
+
+die "No customer specified (bad URL)!" unless $cgi->keywords;
+my($query) = $cgi->keywords; # needs parens with my, ->keywords returns array
+$query =~ /^(\d+)$/;
+my $custnum = $1;
+my $cust_main = qsearchs('cust_main',{'custnum'=>$custnum});
+die "Customer not found!" unless $cust_main;
+
+print qq!<A HREF="${p}edit/cust_main.cgi?$custnum">Edit this customer</A>!;
+
+%>
+
+<SCRIPT>
+function areyousure(href, message) {
+ if (confirm(message) == true)
+ window.location.href = href;
+}
+</SCRIPT>
+
+<%
+
+print qq! | <A HREF="javascript:areyousure('${p}misc/cust_main-cancel.cgi?$custnum', 'Perminantly delete all services and cancel this customer?')">!.
+ 'Cancel this customer</A>'
+ if $cust_main->ncancelled_pkgs;
+
+print qq! | <A HREF="${p}misc/delete-customer.cgi?$custnum">!.
+ 'Delete this customer</A>'
+ if $conf->exists('deletecustomers');
+
+unless ( $conf->exists('disable_customer_referrals') ) {
+ print qq! | <A HREF="!, popurl(2),
+ qq!edit/cust_main.cgi?referral_custnum=$custnum">!,
+ qq!Refer a new customer</A>!;
+
+ print qq! | <A HREF="!, popurl(2),
+ qq!search/cust_main.cgi?referral_custnum=$custnum">!,
+ qq!View this customer's referrals</A>!;
+}
+
+print '<BR><BR>';
+
+my $signupurl = $conf->config('signupurl');
+if ( $signupurl ) {
+print "This customer's signup URL: ".
+ "<a href=\"$signupurl?ref=$custnum\">$signupurl?ref=$custnum</a><BR><BR>";
+}
+
+print '<A NAME="cust_main"></A>';
+
+print &itable(), '<TR>';
+
+print '<TD VALIGN="top">';
+
+ print "Billing address", &ntable("#cccccc"), "<TR><TD>",
+ &ntable("#cccccc",2),
+ '<TR><TD ALIGN="right">Contact&nbsp;name</TD>',
+ '<TD COLSPAN=3 BGCOLOR="#ffffff">',
+ $cust_main->last, ', ', $cust_main->first,
+ '</TD>';
+print '<TD ALIGN="right">SS#</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->ss || '&nbsp', '</TD>'
+ if $conf->exists('show_ss');
+
+print '</TR>',
+ '<TR><TD ALIGN="right">Company</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->company,
+ '</TD></TR>',
+ '<TR><TD ALIGN="right">Address</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->address1,
+ '</TD></TR>',
+ ;
+ print '<TR><TD ALIGN="right">&nbsp;</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->address2, '</TD></TR>'
+ if $cust_main->address2;
+ print '<TR><TD ALIGN="right">City</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->city,
+ '</TD><TD ALIGN="right">State</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->state,
+ '</TD><TD ALIGN="right">Zip</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->zip, '</TD></TR>',
+ '<TR><TD ALIGN="right">Country</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->country,
+ '</TD></TR>',
+ ;
+ my $daytime_label = FS::Msgcat::_gettext('daytime') || 'Day&nbsp;Phone';
+ my $night_label = FS::Msgcat::_gettext('night') || 'Night&nbsp;Phone';
+ print '<TR><TD ALIGN="right">'. $daytime_label.
+ '</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->daytime || '&nbsp', '</TD></TR>',
+ '<TR><TD ALIGN="right">'. $night_label.
+ '</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->night || '&nbsp', '</TD></TR>',
+ '<TR><TD ALIGN="right">Fax</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->fax || '&nbsp', '</TD></TR>',
+ '</TABLE>', "</TD></TR></TABLE>"
+ ;
+
+ if ( defined $cust_main->dbdef_table->column('ship_last') ) {
+
+ my $pre = $cust_main->ship_last ? 'ship_' : '';
+
+ print "<BR>Service address", &ntable("#cccccc"), "<TR><TD>",
+ &ntable("#cccccc",2),
+ '<TR><TD ALIGN="right">Contact name</TD>',
+ '<TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->get("${pre}last"), ', ', $cust_main->get("${pre}first"),
+ '</TD></TR>',
+ '<TR><TD ALIGN="right">Company</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->get("${pre}company"),
+ '</TD></TR>',
+ '<TR><TD ALIGN="right">Address</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->get("${pre}address1"),
+ '</TD></TR>',
+ ;
+ print '<TR><TD ALIGN="right">&nbsp;</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->get("${pre}address2"), '</TD></TR>'
+ if $cust_main->get("${pre}address2");
+ print '<TR><TD ALIGN="right">City</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->get("${pre}city"),
+ '</TD><TD ALIGN="right">State</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->get("${pre}state"),
+ '</TD><TD ALIGN="right">Zip</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->get("${pre}zip"), '</TD></TR>',
+ '<TR><TD ALIGN="right">Country</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->get("${pre}country"),
+ '</TD></TR>',
+ ;
+ print '<TR><TD ALIGN="right">'. $daytime_label. '</TD>',
+ '<TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->get("${pre}daytime") || '&nbsp', '</TD></TR>',
+ '<TR><TD ALIGN="right">'. $night_label. '</TD>'.
+ '<TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->get("${pre}night") || '&nbsp', '</TD></TR>',
+ '<TR><TD ALIGN="right">Fax</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+ $cust_main->get("${pre}fax") || '&nbsp', '</TD></TR>',
+ '</TABLE>', "</TD></TR></TABLE>"
+ ;
+
+ }
+
+print '</TD>';
+
+print '<TD VALIGN="top">';
+
+ print &ntable("#cccccc"), "<TR><TD>", &ntable("#cccccc",2),
+ '<TR><TD ALIGN="right">Customer&nbsp;number</TD><TD BGCOLOR="#ffffff">',
+ $custnum, '</TD></TR>',
+ ;
+
+ my @agents = qsearch( 'agent', {} );
+ my $agent;
+ unless ( scalar(@agents) == 1 ) {
+ $agent = qsearchs('agent',{ 'agentnum' => $cust_main->agentnum } );
+ print '<TR><TD ALIGN="right">Agent</TD><TD BGCOLOR="#ffffff">',
+ $agent->agentnum, ": ", $agent->agent, '</TD></TR>';
+ } else {
+ $agent = $agents[0];
+ }
+ my @referrals = qsearch( 'part_referral', {} );
+ unless ( scalar(@referrals) == 1 ) {
+ my $referral = qsearchs('part_referral', {
+ 'refnum' => $cust_main->refnum
+ } );
+ print '<TR><TD ALIGN="right">Advertising&nbsp;source</TD><TD BGCOLOR="#ffffff">',
+ $referral->refnum, ": ", $referral->referral, '</TD></TR>';
+ }
+ print '<TR><TD ALIGN="right">Order taker</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->otaker, '</TD></TR>';
+
+ print '<TR><TD ALIGN="right">Referring&nbsp;Customer</TD><TD BGCOLOR="#ffffff">';
+ my $referring_cust_main = '';
+ if ( $cust_main->referral_custnum
+ && ( $referring_cust_main =
+ qsearchs('cust_main', { custnum => $cust_main->referral_custnum } )
+ )
+ ) {
+ print '<A HREF="'. popurl(1). 'cust_main.cgi?'.
+ $cust_main->referral_custnum. '">'.
+ $cust_main->referral_custnum. ': '.
+ ( $referring_cust_main->company
+ ? $referring_cust_main->company. ' ('.
+ $referring_cust_main->last. ', '. $referring_cust_main->first.
+ ')'
+ : $referring_cust_main->last. ', '. $referring_cust_main->first
+ ).
+ '</A>';
+ }
+ print '</TD></TR>';
+
+ print '</TABLE></TD></TR></TABLE>';
+
+print '<BR>';
+
+if ( $conf->config('payby-default') ne 'HIDE' ) {
+
+ my @invoicing_list = $cust_main->invoicing_list;
+ print "Billing information (",
+ qq!<A HREF="!, popurl(2), qq!misc/bill.cgi?$custnum">!, "Bill now</A>)",
+ &ntable("#cccccc"), "<TR><TD>", &ntable("#cccccc",2),
+ '<TR><TD ALIGN="right">Tax&nbsp;exempt</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->tax ? 'yes' : 'no',
+ '</TD></TR>',
+ '<TR><TD ALIGN="right">Postal&nbsp;invoices</TD><TD BGCOLOR="#ffffff">',
+ ( grep { $_ eq 'POST' } @invoicing_list ) ? 'yes' : 'no',
+ '</TD></TR>',
+ '<TR><TD ALIGN="right">Email&nbsp;invoices</TD><TD BGCOLOR="#ffffff">',
+ join(', ', grep { $_ ne 'POST' } @invoicing_list ) || 'no',
+ '</TD></TR>',
+ '<TR><TD ALIGN="right">Billing&nbsp;type</TD><TD BGCOLOR="#ffffff">',
+ ;
+
+ if ( $cust_main->payby eq 'CARD' || $cust_main->payby eq 'DCRD' ) {
+ my $payinfo = $cust_main->payinfo_masked;
+ print 'Credit&nbsp;card&nbsp;',
+ ( $cust_main->payby eq 'CARD' ? '(automatic)' : '(on-demand)' ),
+ '</TD></TR>',
+ '<TR><TD ALIGN="right">Card number</TD><TD BGCOLOR="#ffffff">',
+ $payinfo, '</TD></TR>',
+ '<TR><TD ALIGN="right">Expiration</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->paydate, '</TD></TR>',
+ '<TR><TD ALIGN="right">Name on card</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->payname, '</TD></TR>'
+ ;
+ } elsif ( $cust_main->payby eq 'CHEK' || $cust_main->payby eq 'DCHK') {
+ my( $account, $aba ) = split('@', $cust_main->payinfo );
+ print 'Electronic&nbsp;check&nbsp;',
+ ( $cust_main->payby eq 'CHEK' ? '(automatic)' : '(on-demand)' ),
+ '</TD></TR>',
+ '<TR><TD ALIGN="right">Account number</TD><TD BGCOLOR="#ffffff">',
+ $account, '</TD></TR>',
+ '<TR><TD ALIGN="right">ABA/Routing code</TD><TD BGCOLOR="#ffffff">',
+ $aba, '</TD></TR>',
+ '<TR><TD ALIGN="right">Bank name</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->payname, '</TD></TR>'
+ ;
+ } elsif ( $cust_main->payby eq 'LECB' ) {
+ $cust_main->payinfo =~ /^(\d{3})(\d{3})(\d{4})$/;
+ my $payinfo = "$1-$2-$3";
+ print 'Phone&nbsp;bill&nbsp;billing</TD></TR>',
+ '<TR><TD ALIGN="right">Phone number</TD><TD BGCOLOR="#ffffff">',
+ $payinfo, '</TD></TR>',
+ ;
+ } elsif ( $cust_main->payby eq 'BILL' ) {
+ print 'Billing</TD></TR>';
+ print '<TR><TD ALIGN="right">P.O. </TD><TD BGCOLOR="#ffffff">',
+ $cust_main->payinfo, '</TD></TR>',
+ if $cust_main->payinfo;
+ print '<TR><TD ALIGN="right">Expiration</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->paydate, '</TD></TR>',
+ '<TR><TD ALIGN="right">Attention</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->payname, '</TD></TR>',
+ ;
+ } elsif ( $cust_main->payby eq 'COMP' ) {
+ print 'Complimentary</TD></TR>',
+ '<TR><TD ALIGN="right">Authorized&nbsp;by</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->payinfo, '</TD></TR>',
+ '<TR><TD ALIGN="right">Expiration</TD><TD BGCOLOR="#ffffff">',
+ $cust_main->paydate, '</TD></TR>',
+ ;
+ }
+
+ print "</TABLE></TD></TR></TABLE>";
+
+}
+
+print '</TD></TR></TABLE>';
+
+if ( defined $cust_main->dbdef_table->column('comments')
+ && $cust_main->comments =~ /[^\s\n\r]/ )
+{
+ print "<BR>Comments". &ntable("#cccccc"). "<TR><TD>".
+ &ntable("#cccccc",2).
+ '<TR><TD BGCOLOR="#ffffff"><PRE>'.
+ encode_entities($cust_main->comments).
+ '</PRE></TD></TR></TABLE></TABLE>';
+}
+
+%>
+
+</TD></TR></TABLE>
+
+<BR>
+<SCRIPT TYPE="text/javascript">
+function enable_order_pkg () {
+ if ( document.OrderPkgForm.pkgpart.selectedIndex > 0 ) {
+ document.OrderPkgForm.submit.disabled = false;
+ } else {
+ document.OrderPkgForm.submit.disabled = true;
+ }
+}
+</SCRIPT>
+<FORM NAME="OrderPkgForm" ACTION="<%= $p %>edit/process/quick-cust_pkg.cgi" METHOD="POST">
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<%= $custnum %>">
+<SELECT NAME="pkgpart" onChange="enable_order_pkg()"><OPTION>Order additional package
+
+<%
+foreach my $part_pkg (
+ qsearch( 'part_pkg', { 'disabled' => '' }, '',
+ ' AND 0 < ( SELECT COUNT(*) FROM type_pkgs '.
+ ' WHERE typenum = '. $agent->typenum.
+ ' AND type_pkgs.pkgpart = part_pkg.pkgpart )'
+ )
+) {
+%>
+<OPTION VALUE="<%= $part_pkg->pkgpart %>"><%= $part_pkg->pkg %> - <%= $part_pkg->comment %>
+<% } %>
+
+</SELECT><INPUT NAME="submit" TYPE="submit" VALUE="Order Package" disabled></FORM><BR>
+
+<%
+
+if ( $conf->config('payby-default') ne 'HIDE' ) {
+
+ print
+ qq!<FORM ACTION="${p}edit/process/quick-charge.cgi" METHOD="POST">!.
+ qq!<INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">!.
+ qq!Description:<INPUT TYPE="text" NAME="pkg">!.
+ qq!&nbsp;Amount:<INPUT TYPE="text" NAME="amount" SIZE=6>!.
+ qq!&nbsp;!;
+
+ #false laziness w/ edit/part_pkg.cgi
+ if ( $conf->exists('enable_taxclasses') ) {
+ print '<SELECT NAME="taxclass">';
+ my $sth = dbh->prepare('SELECT DISTINCT taxclass FROM cust_main_county')
+ or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+ foreach my $taxclass ( map $_->[0], @{$sth->fetchall_arrayref} ) {
+ print qq!<OPTION VALUE="$taxclass"!;
+ #print ' SELECTED' if $taxclass eq $hashref->{taxclass};
+ print qq!>$taxclass</OPTION>!;
+ }
+ print '</SELECT>';
+ } else {
+ print '<INPUT TYPE="hidden" NAME="taxclass" VALUE="">';
+ }
+
+ print qq!<INPUT TYPE="submit" VALUE="One-time charge"></FORM><BR>!;
+
+}
+
+print qq!<A NAME="cust_pkg">Packages</A> !,
+ qq!( <A HREF="!, popurl(2), qq!edit/cust_pkg.cgi?$custnum">Order and cancel packages</A> (preserves services) )!,
+;
+
+#begin display packages
+
+#get package info
+
+my $packages = get_packages($cust_main, $conf);
+
+if ( @$packages ) {
+%>
+<TABLE CLASS="package" BORDER=1 CELLSPACING=0 CELLPADDING=2 BORDERCOLOR="#999999">
+<TR>
+ <TH>Package</TH>
+ <TH>Status</TH>
+ <TH COLSPAN=2>Services</TH>
+</TR>
+<%
+foreach my $pkg (sort pkgsort_pkgnum_cancel @$packages) {
+ my $rowspan = 0;
+
+ if ($pkg->{cancel}) {
+ $rowspan = 0;
+ } else {
+ foreach my $svcpart (@{$pkg->{svcparts}}) {
+ $rowspan += $svcpart->{count};
+ $rowspan++ if ($svcpart->{count} < $svcpart->{quantity});
+ }
+ }
+
+%>
+<!--pkgnum: <%=$pkg->{pkgnum}%>-->
+<TR>
+ <TD ROWSPAN=<%=$rowspan%>>
+ <A NAME="cust_pkg<%=$pkg->{pkgnum}%>"><%=$pkg->{pkgnum}%></A>:
+ <%=$pkg->{pkg}%> - <%=$pkg->{comment}%><BR>
+<% unless ($pkg->{cancel}) { %>
+ (&nbsp;<%=pkg_change_link($pkg)%>&nbsp;)
+ (&nbsp;<%=pkg_dates_link($pkg)%>&nbsp;|&nbsp;<%=pkg_customize_link($pkg,$custnum)%>&nbsp;)
+<% } %>
+ </TD>
+<%
+ #foreach (qw(setup last_bill next_bill susp expire cancel)) {
+ # print qq! <TD ROWSPAN=$rowspan>! . pkg_datestr($pkg,$_,$conf) . qq!</TD>\n!;
+ #}
+ print "<TD ROWSPAN=$rowspan>". &itable('');
+
+ sub freq {
+
+ #false laziness w/edit/part_pkg.cgi
+ my %freq = ( #move this
+ '1d' => 'daily',
+ '1w' => 'weekly',
+ '2w' => 'biweekly (every 2 weeks)',
+ '1' => 'monthly',
+ '2' => 'bimonthly (every 2 months)',
+ '3' => 'quarterly (every 3 months)',
+ '6' => 'semiannually (every 6 months)',
+ '12' => 'annually',
+ '24' => 'biannually (every 2 years)',
+ );
+
+ my $freq = shift;
+ exists $freq{$freq} ? $freq{$freq} : "every&nbsp;$freq&nbsp;months";
+ }
+
+ #eomove
+
+ if ( $pkg->{cancel} ) { #status: cancelled
+
+ print '<TR><TD><FONT COLOR="#ff0000"><B>Cancelled&nbsp;</B></FONT></TD>'.
+ '<TD>'. pkg_datestr($pkg,'cancel',$conf). '</TD></TR>';
+ unless ( $pkg->{setup} ) {
+ print '<TR><TD COLSPAN=2>Never billed</TD></TR>';
+ } else {
+ print "<TR><TD>Setup&nbsp;</TD><TD>".
+ pkg_datestr($pkg, 'setup',$conf). '</TD></TR>';
+ print "<TR><TD>Last&nbsp;bill&nbsp;</TD><TD>".
+ pkg_datestr($pkg, 'last_bill',$conf). '</TD></TR>'
+ if $pkg->{'last_bill'};
+ print "<TR><TD>Suspended&nbsp;</TD><TD>".
+ pkg_datestr($pkg, 'susp',$conf). '</TD></TR>'
+ if $pkg->{'susp'};
+ }
+
+ } else {
+
+ if ( $pkg->{susp} ) { #status: suspended
+ print '<TR><TD><FONT COLOR="#FF9900"><B>Suspended</B>&nbsp;</FONT></TD>'.
+ '<TD>'. pkg_datestr($pkg,'susp',$conf). '</TD></TR>';
+ unless ( $pkg->{setup} ) {
+ print '<TR><TD COLSPAN=2>Never billed</TD></TR>';
+ } else {
+ print "<TR><TD>Setup&nbsp;</TD><TD>".
+ pkg_datestr($pkg, 'setup',$conf). '</TD></TR>';
+ }
+ print "<TR><TD>Last&nbsp;bill&nbsp;</TD><TD>".
+ pkg_datestr($pkg, 'last_bill',$conf). '</TD></TR>'
+ if $pkg->{'last_bill'};
+ # next bill ??
+ print "<TR><TD>Expires&nbsp;</TD><TD>".
+ pkg_datestr($pkg, 'expire',$conf). '</TD></TR>'
+ if $pkg->{'expire'};
+ print '<TR><TD COLSPAN=2>(&nbsp;'. pkg_unsuspend_link($pkg).
+ '&nbsp;|&nbsp;'. pkg_cancel_link($pkg). '&nbsp;)</TD></TR>';
+
+ } else { #status: active
+
+ unless ( $pkg->{setup} ) { #not setup
+
+ print '<TR><TD COLSPAN=2>Not&nbsp;yet&nbsp;billed&nbsp;(';
+ unless ( $pkg->{freq} ) {
+ print 'one-time&nbsp;charge)</TD></TR>';
+ print '<TR><TD COLSPAN=2>(&nbsp;'. pkg_cancel_link($pkg).
+ '&nbsp;)</TD</TR>';
+ } else {
+ print 'billed&nbsp;'. freq($pkg->{freq}). ')</TD></TR>';
+ }
+
+ } else { #setup
+
+ unless ( $pkg->{freq} ) {
+ print "<TR><TD COLSPAN=2>One-time&nbsp;charge</TD></TR>".
+ '<TR><TD>Billed&nbsp;</TD><TD>'.
+ pkg_datestr($pkg,'setup',$conf). '</TD></TR>';
+ } else {
+ print '<TR><TD COLSPAN=2><FONT COLOR="#00CC00"><B>Active</B></FONT>'.
+ ',&nbsp;billed&nbsp;'. freq($pkg->{freq}). '</TD></TR>'.
+ '<TR><TD>Setup&nbsp;</TD><TD>'.
+ pkg_datestr($pkg, 'setup',$conf). '</TD></TR>';
+ }
+
+ }
+
+ print "<TR><TD>Last&nbsp;bill&nbsp;</TD><TD>".
+ pkg_datestr($pkg, 'last_bill',$conf). '</TD></TR>'
+ if $pkg->{'last_bill'};
+ print "<TR><TD>Next&nbsp;bill&nbsp;</TD><TD>".
+ pkg_datestr($pkg, 'next_bill',$conf). '</TD></TR>'
+ if $pkg->{'next_bill'};
+ print "<TR><TD>Expires&nbsp;</TD><TD>".
+ pkg_datestr($pkg, 'expire',$conf). '</TD></TR>'
+ if $pkg->{'expire'};
+ if ( $pkg->{freq} ) {
+ print '<TR><TD COLSPAN=2>(&nbsp;'. pkg_suspend_link($pkg).
+ '&nbsp;|&nbsp;'. pkg_cancel_link($pkg). '&nbsp;)</TD></TR>';
+ }
+
+ }
+
+ }
+
+ print "</TABLE></TD>\n";
+
+ if ($rowspan == 0) { print qq!</TR>\n!; next; }
+
+ my $cnt = 0;
+ foreach my $svcpart (sort {$a->{svcpart} <=> $b->{svcpart}} @{$pkg->{svcparts}}) {
+ foreach my $service (@{$svcpart->{services}}) {
+ print '<TR>' if ($cnt > 0);
+%>
+ <TD><%=svc_link($svcpart,$service)%></TD>
+ <TD><%=svc_label_link($svcpart,$service)%><BR>(&nbsp;<%=svc_unprovision_link($service)%>&nbsp;)</TD>
+</TR>
+<%
+ $cnt++;
+ }
+ if ($svcpart->{count} < $svcpart->{quantity}) {
+ print qq!<TR>\n! if ($cnt > 0);
+ print qq! <TD COLSPAN=2>!.svc_provision_link($pkg, $svcpart, $conf).qq!</TD>\n</TR>\n!;
+ }
+ }
+}
+print '</TABLE>';
+}
+
+#end display packages
+%>
+
+<% if ( $conf->config('payby-default') ne 'HIDE' ) { %>
+
+ <BR><BR><A NAME="history"><FONT SIZE="+2">Payment History</FONT></A><BR>
+ <A HREF="<%= $p %>edit/cust_pay.cgi?custnum=<%= $custnum %>">Post cash/check payment</A>
+ | <A HREF="<%= $p %>misc/payment.cgi?payby=CARD;custnum=<%= $custnum %>">Process credit card payment</A>
+ | <A HREF="<%= $p %>misc/payment.cgi?payby=CHEK;custnum=<%= $custnum %>">Process electronic check (ACH) payment</A>
+ <BR><A HREF="<%= $p %>edit/cust_credit.cgi?<%= $custnum %>">Post credit</A>
+ <BR>
+
+ <%
+ #get payment history
+ my @history = ();
+
+ #invoices
+ foreach my $cust_bill ($cust_main->cust_bill) {
+ my $pre = ( $cust_bill->owed > 0 )
+ ? '<B><FONT SIZE="+1" COLOR="#FF0000">Open '
+ : '';
+ my $post = ( $cust_bill->owed > 0 ) ? '</FONT></B>' : '';
+ my $invnum = $cust_bill->invnum;
+ push @history, {
+ 'date' => $cust_bill->_date,
+ 'desc' => qq!<A HREF="${p}view/cust_bill.cgi?$invnum">!. $pre.
+ "Invoice #$invnum (Balance \$". $cust_bill->owed. ')'.
+ $post. '</A>',
+ 'charge' => $cust_bill->charged,
+ };
+ }
+
+ #payments (some false laziness w/credits)
+ foreach my $cust_pay ($cust_main->cust_pay) {
+
+ my $payby = $cust_pay->payby;
+ my $payinfo = $payby eq 'CARD'
+ ? $cust_pay->payinfo_masked
+ : $cust_pay->payinfo;
+ my @cust_bill_pay = $cust_pay->cust_bill_pay;
+ my @cust_pay_refund = $cust_pay->cust_pay_refund;
+
+ my $target = "$payby$payinfo";
+ $payby =~ s/^BILL$/Check #/ if $payinfo;
+ $payby =~ s/^CHEK$/Electronic check /;
+ $payby =~ s/^BILL$//;
+ $payby =~ s/^(CARD|COMP)$/$1 /;
+ my $info = $payby ? " ($payby$payinfo)" : '';
+
+ my( $pre, $post, $desc, $apply, $ext ) = ( '', '', '', '', '' );
+ if ( scalar(@cust_bill_pay) == 0
+ && scalar(@cust_pay_refund) == 0 ) {
+ #completely unapplied
+ $pre = '<B><FONT COLOR="#FF0000">Unapplied ';
+ $post = '</FONT></B>';
+ $apply = qq! (<A HREF="${p}edit/cust_bill_pay.cgi?!.
+ $cust_pay->paynum. '">apply</A>)';
+ } elsif ( scalar(@cust_bill_pay) == 1
+ && scalar(@cust_pay_refund) == 0
+ && $cust_pay->unapplied == 0 ) {
+ #applied to one invoice, the usual situation
+ $desc = ' applied to Invoice #'. $cust_bill_pay[0]->invnum;
+ } elsif ( scalar(@cust_bill_pay) == 0
+ && scalar(@cust_pay_refund) == 1
+ && $cust_pay->unapplied == 0 ) {
+ #applied to one refund
+ $desc = ' refunded on '. time2str("%D", $cust_pay_refund[0]->_date);
+ } else {
+ #complicated
+ $desc = '<BR>';
+ foreach my $app ( sort { $a->_date <=> $b->_date }
+ ( @cust_bill_pay, @cust_pay_refund ) ) {
+ if ( $app->isa('FS::cust_bill_pay') ) {
+ $desc .= '&nbsp;&nbsp;'.
+ '$'. $app->amount.
+ ' applied to Invoice #'. $app->invnum.
+ '<BR>';
+ #' on '. time2str("%D", $cust_bill_pay->_date).
+ } elsif ( $app->isa('FS::cust_pay_refund') ) {
+ $desc .= '&nbsp;&nbsp;'.
+ '$'. $app->amount.
+ ' refunded on'. time2str("%D", $app->_date).
+ '<BR>';
+ } else {
+ die "$app is not a FS::cust_bill_pay or FS::cust_pay_refund";
+ }
+ }
+ if ( $cust_pay->unapplied > 0 ) {
+ $desc .= '&nbsp;&nbsp;'.
+ '<B><FONT COLOR="#FF0000">$'.
+ $cust_pay->unapplied. ' unapplied</FONT></B>'.
+ qq! (<A HREF="${p}edit/cust_bill_pay.cgi?!.
+ $cust_pay->paynum. '">apply</A>)'.
+ '<BR>';
+ }
+ }
+
+ my $refund = '';
+ my $refund_days = $conf->config('card_refund-days') || 120;
+ if ( $cust_pay->closed !~ /^Y/i
+ && $cust_pay->payby =~ /^(CARD|CHEK)$/
+ && time-$cust_pay->_date < $refund_days*86400
+ && $cust_pay->unrefunded > 0
+ ) {
+ $refund = qq! (<A HREF="!. qq!${p}edit/cust_refund.cgi?payby=$1;!.
+ qq!paynum=!. $cust_pay->paynum. qq!">refund</A>)!;
+ }
+
+ my $void = '';
+ if ( $cust_pay->closed !~ /^Y/i
+ && $cust_pay->payby !~ /^(CARD|CHEK)$/
+ ) {
+ $void = qq! (<A HREF="javascript:areyousure('!.
+ qq!${p}misc/void-cust_pay.cgi?!. $cust_pay->paynum.
+ qq!', 'Are you sure you want to void this payment?')">!.
+ qq!void</A>)!;
+ }
+
+ my $delete = '';
+ if ( $cust_pay->closed !~ /^Y/i && $conf->exists('deletepayments') ) {
+ $delete = qq! (<A HREF="javascript:areyousure('!.
+ qq!${p}misc/delete-cust_pay.cgi?!. $cust_pay->paynum.
+ qq!', 'Are you sure you want to delete this payment?')">!.
+ qq!delete</A>)!;
+ }
+
+ my $unapply = '';
+ if ( $cust_pay->closed !~ /^Y/i
+ && $conf->exists('unapplypayments')
+ && scalar(@cust_bill_pay) ) {
+ $unapply = qq! (<A HREF="javascript:areyousure('!.
+ qq!${p}misc/unapply-cust_pay.cgi?!. $cust_pay->paynum.
+ qq!', 'Are you sure you want to unapply this payment?')">!.
+ qq!unapply</A>)!;
+ }
+
+ push @history, {
+ 'date' => $cust_pay->_date,
+ 'desc' => $pre. "Payment$post$info$desc".
+ "$apply$refund$void$delete$unapply",
+ 'payment' => $cust_pay->paid,
+ 'target' => $target,
+ };
+ }
+
+ #voided payments
+ foreach my $cust_pay_void ($cust_main->cust_pay_void) {
+
+ my $payby = $cust_pay_void->payby;
+ my $payinfo = $payby eq 'CARD'
+ ? $cust_pay_void->payinfo_masked
+ : $cust_pay_void->payinfo;
+
+ $payby =~ s/^BILL$/Check #/ if $payinfo;
+ $payby =~ s/^CHEK$/Electronic check /;
+ $payby =~ s/^BILL$//;
+ $payby =~ s/^(CARD|COMP)$/$1 /;
+ my $info = $payby ? " ($payby$payinfo)" : '';
+
+ push @history, {
+ 'date' => $cust_pay_void->_date,
+ 'desc' => "<DEL>Payment $info</DEL> <I>voided ".
+ time2str("%D", $cust_pay_void->void_date).
+ " by ". $cust_pay_void->otaker. '</i>',
+ 'void_payment' => $cust_pay_void->paid,
+ };
+
+ }
+
+ #credits (some false laziness w/payments)
+ foreach my $cust_credit ($cust_main->cust_credit) {
+
+ my @cust_credit_bill = $cust_credit->cust_credit_bill;
+ my @cust_credit_refund = $cust_credit->cust_credit_refund;
+
+ my( $pre, $post, $desc, $apply, $ext ) = ( '', '', '', '', '' );
+ if ( scalar(@cust_credit_bill) == 0
+ && scalar(@cust_credit_refund) == 0 ) {
+ #completely unapplied
+ $pre = '<B><FONT COLOR="#FF0000">Unapplied ';
+ $post = '</FONT></B>';
+ $apply = qq! (<A HREF="${p}edit/cust_credit_bill.cgi?!.
+ $cust_credit->crednum. '">apply</A>)';
+ } elsif ( scalar(@cust_credit_bill) == 1
+ && scalar(@cust_credit_refund) == 0
+ && $cust_credit->credited == 0 ) {
+ #applied to one invoice, the usual situation
+ $desc = ' applied to Invoice #'. $cust_credit_bill[0]->invnum;
+ } elsif ( scalar(@cust_credit_bill) == 0
+ && scalar(@cust_credit_refund) == 1
+ && $cust_credit->credited == 0 ) {
+ #applied to one refund
+ $desc = ' refunded on '. time2str("%D", $cust_credit_refund[0]->_date);
+ } else {
+ #complicated
+ $desc = '<BR>';
+ foreach my $app ( sort { $a->_date <=> $b->_date }
+ ( @cust_credit_bill, @cust_credit_refund ) ) {
+ if ( $app->isa('FS::cust_credit_bill') ) {
+ $desc .= '&nbsp;&nbsp;'.
+ '$'. $app->amount.
+ ' applied to Invoice #'. $app->invnum.
+ '<BR>';
+ #' on '. time2str("%D", $app->_date).
+ } elsif ( $app->isa('FS::cust_credit_refund') ) {
+ $desc .= '&nbsp;&nbsp;'.
+ '$'. $app->amount.
+ ' refunded on'. time2str("%D", $app->_date).
+ '<BR>';
+ } else {
+ die "$app is not a FS::cust_credit_bill or a FS::cust_credit_refund";
+ }
+ }
+ if ( $cust_credit->credited > 0 ) {
+ $desc .= '&nbsp;&nbsp;<B><FONT COLOR="#FF0000">$'.
+ $cust_credit->credited. ' unapplied</FONT></B>'.
+ qq! (<A HREF="${p}edit/cust_credit_bill.cgi?!.
+ $cust_credit->crednum. '">apply</A>)'.
+ '<BR>';
+ }
+ }
+#
+ my $delete = '';
+ if ( $cust_credit->closed !~ /^Y/i && $conf->exists('deletecredits') ) {
+ $delete = qq! (<A HREF="javascript:areyousure('!.
+ qq!${p}misc/delete-cust_credit.cgi?!. $cust_credit->crednum.
+ qq!', 'Are you sure you want to delete this credit?')">!.
+ qq!delete</A>)!;
+ }
+
+ my $unapply = '';
+ if ( $cust_credit->closed !~ /^Y/i
+ && $conf->exists('unapplycredits')
+ && scalar(@cust_credit_bill) ) {
+ $unapply = qq! (<A HREF="javascript:areyousure('!.
+ qq!${p}misc/unapply-cust_credit.cgi?!. $cust_credit->crednum.
+ qq!', 'Are you sure you want to unapply this credit?')">!.
+ qq!unapply</A>)!;
+ }
+
+ push @history, {
+ 'date' => $cust_credit->_date,
+ 'desc' => $pre. "Credit$post by ". $cust_credit->otaker.
+ ' ('. $cust_credit->reason. ')'.
+ "$desc$apply$delete$unapply",
+ 'credit' => $cust_credit->amount,
+ };
+
+ }
+
+ #refunds
+ foreach my $cust_refund ($cust_main->cust_refund) {
+
+ my $payby = $cust_refund->payby;
+ my $payinfo = $payby eq 'CARD'
+ ? $cust_refund->payinfo_masked
+ : $cust_refund->payinfo;
+
+ $payby =~ s/^BILL$/Check #/ if $payinfo;
+ $payby =~ s/^CHEK$/Electronic check /;
+ $payby =~ s/^(CARD|COMP)$/$1 /;
+
+ push @history, {
+ 'date' => $cust_refund->_date,
+ 'desc' => "Refund ($payby$payinfo) by ". $cust_refund->otaker,
+ 'refund' => $cust_refund->refund,
+ };
+
+ }
+
+ %>
+
+ <%= table() %>
+ <TR>
+ <TH>Date</TH>
+ <TH>Description</TH>
+ <TH><FONT SIZE=-1>Charge</FONT></TH>
+ <TH><FONT SIZE=-1>Payment</FONT></TH>
+ <TH><FONT SIZE=-1>In-house<BR>Credit</FONT></TH>
+ <TH><FONT SIZE=-1>Refund</FONT></TH>
+ <TH><FONT SIZE=-1>Balance</FONT></TH>
+ </TR>
+
+ <%
+ #display payment history
+
+ my %target;
+ my $balance = 0;
+ foreach my $item ( sort { $a->{'date'} <=> $b->{'date'} } @history ) {
+
+ my $charge = exists($item->{'charge'})
+ ? sprintf('$%.2f', $item->{'charge'})
+ : '';
+ my $payment = exists($item->{'payment'})
+ ? sprintf('-&nbsp;$%.2f', $item->{'payment'})
+ : '';
+ $payment ||= sprintf('<DEL>-&nbsp;$%.2f</DEL>', $item->{'void_payment'})
+ if exists($item->{'void_payment'});
+ my $credit = exists($item->{'credit'})
+ ? sprintf('-&nbsp;$%.2f', $item->{'credit'})
+ : '';
+ my $refund = exists($item->{'refund'})
+ ? sprintf('$%.2f', $item->{'refund'})
+ : '';
+
+ my $target = exists($item->{'target'}) ? $item->{'target'} : '';
+
+ $balance += $item->{'charge'} if exists $item->{'charge'};
+ $balance -= $item->{'payment'} if exists $item->{'payment'};
+ $balance -= $item->{'credit'} if exists $item->{'credit'};
+ $balance += $item->{'refund'} if exists $item->{'refund'};
+ $balance = sprintf("%.2f", $balance);
+ $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
+ ( my $showbalance = '$'. $balance ) =~ s/^\$\-/-&nbsp;\$/;
+
+ %>
+
+ <TR>
+ <TD>
+ <% unless ( !$target || $target{$target}++ ) { %>
+ <A NAME="<%= $target %>">
+ <% } %>
+ <%= time2str("%D",$item->{'date'}) %>
+ <% if ( $target && $target{$target} == 1 ) { %>
+ </A>
+ <% } %>
+ </FONT>
+ </TD>
+ <TD><%= $item->{'desc'} %></TD>
+ <TD ALIGN="right"><%= $charge %></TD>
+ <TD ALIGN="right"><%= $payment %></TD>
+ <TD ALIGN="right"><%= $credit %></TD>
+ <TD ALIGN="right"><%= $refund %></TD>
+ <TD ALIGN="right"><%= $showbalance %></TD>
+ </TR>
+
+ <% } %>
+
+ </TABLE>
+
+<% } %>
+
+</BODY></HTML>
+
+<%
+#subroutines
+
+sub get_packages {
+ my $cust_main = shift or return undef;
+ my $conf = shift;
+
+ my @packages = ();
+
+ foreach my $cust_pkg (
+ $conf->exists('hidecancelledpackages')
+ ? $cust_main->ncancelled_pkgs
+ : $cust_main->all_pkgs
+ ) {
+
+ my $part_pkg = $cust_pkg->part_pkg;
+
+ my %pkg = ();
+ $pkg{pkgnum} = $cust_pkg->pkgnum;
+ $pkg{pkg} = $part_pkg->pkg;
+ $pkg{pkgpart} = $part_pkg->pkgpart;
+ $pkg{comment} = $part_pkg->getfield('comment');
+ $pkg{freq} = $part_pkg->freq;
+ $pkg{setup} = $cust_pkg->getfield('setup');
+ $pkg{last_bill} = $cust_pkg->getfield('last_bill');
+ $pkg{next_bill} = $cust_pkg->getfield('bill');
+ $pkg{susp} = $cust_pkg->getfield('susp');
+ $pkg{expire} = $cust_pkg->getfield('expire');
+ $pkg{cancel} = $cust_pkg->getfield('cancel');
+
+ my %svcparts = map {
+ $_->svcpart => {
+ $_->part_svc->hash,
+ 'quantity' => $_->quantity,
+ 'count' => $cust_pkg->num_cust_svc($_->svcpart),
+ #'services' => [],
+ };
+ } $part_pkg->pkg_svc;
+
+ foreach my $cust_svc ( $cust_pkg->cust_svc ) {
+ #warn "svcnum ". $cust_svc->svcnum. " / svcpart ". $cust_svc->svcpart. "\n";
+ my $svc = {
+ 'svcnum' => $cust_svc->svcnum,
+ 'label' => ($cust_svc->label)[1],
+ };
+
+ #false laziness with above, to catch extraneous services. whole
+ #damn thing should be OO...
+ my $svcpart = ( $svcparts{$cust_svc->svcpart} ||= {
+ $cust_svc->part_svc->hash,
+ 'quantity' => 0,
+ 'count' => $cust_pkg->num_cust_svc($cust_svc->svcpart),
+ #'services' => [],
+ } );
+
+ push @{$svcpart->{services}}, $svc;
+
+ }
+
+ $pkg{svcparts} = [ values %svcparts ];
+
+ push @packages, \%pkg;
+
+ }
+
+ return \@packages;
+
+}
+
+sub svc_link {
+
+ my ($svcpart, $svc) = (shift,shift) or return '';
+ return qq!<A HREF="${p}view/$svcpart->{svcdb}.cgi?$svc->{svcnum}">$svcpart->{svc}</A>!;
+
+}
+
+sub svc_label_link {
+
+ my ($svcpart, $svc) = (shift,shift) or return '';
+ return qq!<A HREF="${p}view/$svcpart->{svcdb}.cgi?$svc->{svcnum}">$svc->{label}</A>!;
+
+}
+
+sub svc_provision_link {
+ my ($pkg, $svcpart, $conf) = @_;
+ ( my $svc_nbsp = $svcpart->{svc} ) =~ s/\s+/&nbsp;/g;
+ my $num_left = $svcpart->{quantity} - $svcpart->{count};
+ my $pkgnum_svcpart = "pkgnum$pkg->{pkgnum}-svcpart$svcpart->{svcpart}";
+
+ my $url;
+ if ( $svcpart->{svcdb} eq 'svc_external'
+ && $conf->exists('svc_external-skip_manual')
+ ) {
+ $url = "${p}edit/process/$svcpart->{svcdb}.cgi?".
+ "pkgnum=$pkg->{pkgnum}&".
+ "svcpart=$svcpart->{svcpart}";
+ } else {
+ $url = "${p}edit/$svcpart->{svcdb}.cgi?$pkgnum_svcpart";
+ }
+
+ my $link = qq!<A CLASS="provision" HREF="$url">!.
+ "Provision&nbsp;$svc_nbsp&nbsp;($num_left)</A>";
+ if ( $conf->exists('legacy_link') ) {
+ $link .= '<BR>'.
+ qq!<A CLASS="provision" HREF="${p}misc/link.cgi?!.
+ qq!$pkgnum_svcpart">!.
+ "Link&nbsp;to&nbsp;legacy&nbsp;$svc_nbsp&nbsp;($num_left)</A>";
+ }
+ $link;
+}
+
+sub svc_unprovision_link {
+ my $svc = shift or return '';
+ qq!<A HREF="javascript:areyousure('${p}misc/unprovision.cgi?$svc->{svcnum}',!.
+ qq!'Permanently unprovision and delete this service?')">Unprovision</A>!;
+}
+
+# This should be generalized to use config options to determine order.
+sub pkgsort_pkgnum_cancel {
+ if ($a->{cancel} and $b->{cancel}) {
+ return ($a->{pkgnum} <=> $b->{pkgnum});
+ } elsif ($a->{cancel} or $b->{cancel}) {
+ return (-1) if ($b->{cancel});
+ return (1) if ($a->{cancel});
+ return (0);
+ } else {
+ return($a->{pkgnum} <=> $b->{pkgnum});
+ }
+}
+
+sub pkg_datestr {
+ my($pkg, $field, $conf) = @_ or return '';
+ return '&nbsp;' unless $pkg->{$field};
+ my $format = $conf->exists('pkg_showtimes')
+ ? '<B>%D</B>&nbsp;<FONT SIZE=-3>%l:%M:%S%P&nbsp;%z</FONT>'
+ : '<B>%b&nbsp;%o,&nbsp;%Y</B>';
+ ( my $strip = time2str($format, $pkg->{$field}) ) =~ s/ (\d)/$1/g;
+ $strip;
+}
+
+sub pkg_change_link {
+ my $pkg = shift or return '';
+ return qq!<a href="${p}misc/change_pkg.cgi?$pkg->{pkgnum}">!.
+ qq!Change&nbsp;package</a>!;
+}
+
+sub pkg_suspend_link {
+ my $pkg = shift or return '';
+ return qq!<a href="${p}misc/susp_pkg.cgi?$pkg->{pkgnum}">Suspend</a>!;
+}
+
+sub pkg_unsuspend_link {
+ my $pkg = shift or return '';
+ return qq!<a href="${p}misc/unsusp_pkg.cgi?$pkg->{pkgnum}">Unsuspend</a>!;
+}
+
+sub pkg_cancel_link {
+ my $pkg = shift or return '';
+ qq!<A HREF="javascript:areyousure('${p}misc/cancel_pkg.cgi?$pkg->{pkgnum}', !.
+ qq!'Permanently delete included services and cancel this package?')">!.
+ qq!Cancel now</A> | !.
+ qq!<A HREF="${p}misc/expire_pkg.cgi?$pkg->{pkgnum}">Cancel later</A>!;
+}
+
+sub pkg_dates_link {
+ my $pkg = shift or return '';
+ qq!<A HREF="${p}edit/REAL_cust_pkg.cgi?$pkg->{pkgnum}">Edit&nbsp;dates</A>!;
+}
+
+sub pkg_customize_link {
+ my $pkg = shift or return '';
+ my $custnum = shift;
+ qq!<A HREF="${p}edit/part_pkg.cgi?keywords=$custnum;clone=$pkg->{pkgpart};!.
+ qq!pkgnum=$pkg->{pkgnum}">Customize</A>!;
+}
+
+%>
+
diff --git a/httemplate/view/cust_pkg.cgi b/httemplate/view/cust_pkg.cgi
new file mode 100755
index 0000000..5f0e6bf
--- /dev/null
+++ b/httemplate/view/cust_pkg.cgi
@@ -0,0 +1,164 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my %uiview = ();
+my %uiadd = ();
+foreach my $part_svc ( qsearch('part_svc',{}) ) {
+ $uiview{$part_svc->svcpart} = popurl(2). "view/". $part_svc->svcdb . ".cgi";
+ $uiadd{$part_svc->svcpart}= popurl(2). "edit/". $part_svc->svcdb . ".cgi";
+}
+
+my ($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $pkgnum = $1;
+
+#get package record
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+die "No package!" unless $cust_pkg;
+my $part_pkg = qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->getfield('pkgpart')});
+
+my $custnum = $cust_pkg->getfield('custnum');
+print header('Package View', menubar(
+ "View this customer (#$custnum)" => popurl(2). "view/cust_main.cgi?$custnum",
+ 'Main Menu' => popurl(2)
+));
+
+#print info
+my ($susp,$cancel,$expire)=(
+ $cust_pkg->getfield('susp'),
+ $cust_pkg->getfield('cancel'),
+ $cust_pkg->getfield('expire'),
+);
+my($pkg,$comment)=($part_pkg->getfield('pkg'),$part_pkg->getfield('comment'));
+my($setup,$bill)=($cust_pkg->getfield('setup'),$cust_pkg->getfield('bill'));
+my $otaker = $cust_pkg->getfield('otaker');
+
+print <<END;
+<SCRIPT>
+function areyousure(href) {
+ if (confirm("Permanently delete included services and cancel this package?") == true)
+ window.location.href = href;
+}
+</SCRIPT>
+END
+
+print "Package information";
+print ' (<A HREF="'. popurl(2). 'misc/unsusp_pkg.cgi?'. $pkgnum.
+ '">unsuspend</A>)'
+ if ( $susp && ! $cancel );
+
+print ' (<A HREF="'. popurl(2). 'misc/susp_pkg.cgi?'. $pkgnum.
+ '">suspend</A>)'
+ unless ( $susp || $cancel );
+
+print ' (<A HREF="javascript:areyousure(\''. popurl(2). 'misc/cancel_pkg.cgi?'.
+ $pkgnum. '\')">cancel</A>)'
+ unless $cancel;
+
+print ' (<A HREF="'. popurl(2). 'edit/REAL_cust_pkg.cgi?'. $pkgnum.
+ '">edit dates</A>)';
+
+print &ntable("#cccccc"), '<TR><TD>', &ntable("#cccccc",2),
+ '<TR><TD ALIGN="right">Package number</TD><TD BGCOLOR="#ffffff">',
+ $pkgnum, '</TD></TR>',
+ '<TR><TD ALIGN="right">Package</TD><TD BGCOLOR="#ffffff">',
+ $pkg, '</TD></TR>',
+ '<TR><TD ALIGN="right">Comment</TD><TD BGCOLOR="#ffffff">',
+ $comment, '</TD></TR>',
+ '<TR><TD ALIGN="right">Setup date</TD><TD BGCOLOR="#ffffff">',
+ ( $setup ? time2str("%D",$setup) : "(Not setup)" ), '</TD></TR>';
+
+print '<TR><TD ALIGN="right">Last bill date</TD><TD BGCOLOR="#ffffff">',
+ ( $cust_pkg->get('last_bill') ? time2str("%D",$cust_pkg->get('last_bill')) : "&nbsp;" ),
+ '</TD></TR>'
+ if $cust_pkg->dbdef_table->column('last_bill');
+
+print '<TR><TD ALIGN="right">Next bill date</TD><TD BGCOLOR="#ffffff">',
+ ( $bill ? time2str("%D",$bill) : "&nbsp;" ), '</TD></TR>';
+
+print '<TR><TD ALIGN="right">Suspension date</TD><TD BGCOLOR="#ffffff">',
+ time2str("%D",$susp), '</TD></TR>' if $susp;
+print '<TR><TD ALIGN="right">Expiration date</TD><TD BGCOLOR="#ffffff">',
+ time2str("%D",$expire), '</TD></TR>' if $expire;
+print '<TR><TD ALIGN="right">Cancellation date</TD><TD BGCOLOR="#ffffff">',
+ time2str("%D",$cancel), '</TD></TR>' if $cancel;
+print '<TR><TD ALIGN="right">Order taker</TD><TD BGCOLOR="#ffffff">',
+ $otaker, '</TD></TR>',
+ '</TABLE></TD></TR></TABLE>';
+
+unless ($expire) {
+ print <<END;
+<FORM ACTION="../misc/expire_pkg.cgi" METHOD="post">
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">
+Expire (date): <INPUT TYPE="text" NAME="date" VALUE="" >
+<INPUT TYPE="submit" VALUE="Cancel later">
+END
+}
+
+unless ($cancel) {
+
+ #services
+ print '<BR>Service Information', &table();
+
+ #list of services this pkgpart includes
+ my $pkg_svc;
+ my %pkg_svc = ();
+ foreach $pkg_svc ( qsearch('pkg_svc',{'pkgpart'=> $cust_pkg->pkgpart }) ) {
+ $pkg_svc{$pkg_svc->svcpart} = $pkg_svc->quantity if $pkg_svc->quantity;
+ }
+
+ #list of records from cust_svc
+ my $svcpart;
+ foreach $svcpart (sort {$a <=> $b} keys %pkg_svc) {
+
+ my($svc)=qsearchs('part_svc',{'svcpart'=>$svcpart})->getfield('svc');
+
+ my(@cust_svc)=qsearch('cust_svc',{'pkgnum'=>$pkgnum,
+ 'svcpart'=>$svcpart,
+ });
+
+ my($enum);
+ for $enum ( 1 .. $pkg_svc{$svcpart} ) {
+
+ my($cust_svc);
+ if ( $cust_svc=shift @cust_svc ) {
+ my($svcnum)=$cust_svc->svcnum;
+ my($label, $value, $svcdb) = $cust_svc->label;
+ print <<END;
+<TR><TD><A HREF="$uiview{$svcpart}?$svcnum">(View/Edit) $svc: $value<A></TD></TR>
+END
+ } else {
+ print qq!<TR><TD>!.
+ qq!<A HREF="$uiadd{$svcpart}?pkgnum$pkgnum-svcpart$svcpart">!.
+ qq!(Provision) $svc</A>!;
+
+ print qq! or <A HREF="../misc/link.cgi?pkgnum$pkgnum-svcpart$svcpart">!.
+ qq!(Link to legacy) $svc</A>!
+ if $conf->exists('legacy_link');
+
+ print '</TD></TR>';
+ }
+
+ }
+ warn "WARNING: Leftover services pkgnum $pkgnum!" if @cust_svc;;
+ }
+
+ print "</TABLE><FONT SIZE=-1>",
+ "Choose (View/Edit) to view or edit an existing service<BR>",
+ "Choose (Provision) to setup a new service<BR>";
+
+ print "Choose (Link to legacy) to link to a legacy (pre-Freeside) service"
+ if $conf->exists('legacy_link');
+
+ print "</FONT>";
+}
+
+#formatting
+print <<END;
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/view/svc_acct.cgi b/httemplate/view/svc_acct.cgi
new file mode 100755
index 0000000..6ca9bf0
--- /dev/null
+++ b/httemplate/view/svc_acct.cgi
@@ -0,0 +1,272 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$svcnum});
+die "Unknown svcnum" unless $svc_acct;
+
+#false laziness w/all svc_*.cgi
+my $cust_svc = qsearchs( 'cust_svc' , { 'svcnum' => $svcnum } );
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+ $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $pkgnum } );
+ $custnum = $cust_pkg->custnum;
+} else {
+ $cust_pkg = '';
+ $custnum = '';
+}
+#eofalse
+
+my $part_svc = qsearchs('part_svc',{'svcpart'=> $cust_svc->svcpart } );
+die "Unknown svcpart" unless $part_svc;
+
+my $domain;
+if ( $svc_acct->domsvc ) {
+ my $svc_domain = qsearchs('svc_domain', { 'svcnum' => $svc_acct->domsvc } );
+ die "Unknown domain" unless $svc_domain;
+ $domain = $svc_domain->domain;
+} else {
+ die "No svc_domain.svcnum record for svc_acct.domsvc: ". $cust_svc->domsvc;
+}
+
+%>
+
+<SCRIPT>
+function areyousure(href) {
+ if (confirm("Permanently delete this account?") == true)
+ window.location.href = href;
+}
+</SCRIPT>
+
+<%= header('Account View', menubar(
+ ( ( $pkgnum || $custnum )
+ ? ( "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+ )
+ : ( "Cancel this (unaudited) account" =>
+ "javascript:areyousure(\'${p}misc/cancel-unaudited.cgi?$svcnum\')" )
+ ),
+ "Main menu" => $p,
+)) %>
+
+<%
+
+#if ( $cust_pkg && $cust_pkg->part_pkg->plan eq 'sqlradacct_hour' ) {
+if ( $part_svc->part_export('sqlradius')
+ || $part_svc->part_export('sqlradius_withdomain')
+) {
+
+ my $last_bill;
+ my %plandata;
+ if ( $cust_pkg ) {
+ #false laziness w/httemplate/edit/part_pkg... this stuff doesn't really
+ #belong in plan data
+ %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
+ split("\n", $cust_pkg->part_pkg->plandata );
+
+ $last_bill = $cust_pkg->last_bill;
+ } else {
+ $last_bill = 0;
+ %plandata = ();
+ }
+
+ my $seconds = $svc_acct->seconds_since_sqlradacct( $last_bill, time );
+ my $hour = int($seconds/3600);
+ my $min = int( ($seconds%3600) / 60 );
+ my $sec = $seconds%60;
+
+ my $input = $svc_acct->attribute_since_sqlradacct(
+ $last_bill, time, 'AcctInputOctets'
+ ) / 1048576;
+ my $output = $svc_acct->attribute_since_sqlradacct(
+ $last_bill, time, 'AcctOutputOctets'
+ ) / 1048576;
+
+%>
+
+ RADIUS session information<BR>
+ <%= ntable('#cccccc',2) %>
+ <TR><TD BGCOLOR="#ffffff">
+
+ <% if ( $seconds ) { %>
+ Online <B><%= $hour %></B>h <B><%= $min %></B>m <B><%= $sec %></B>s
+ <% } else { %>
+ Has not logged on
+ <% } %>
+
+ <% if ( $cust_pkg ) { %>
+ since last bill (<%= time2str('%a %b %o %Y', $last_bill) %>)
+ <% if ( length($plandata{recur_included_hours}) ) { %>
+ - <%= $plandata{recur_included_hours} %> total hours in plan
+ <% } %>
+ <BR>
+ <% } else { %>
+ (no billing cycle available for unaudited account)<BR>
+ <% } %>
+
+ Upload: <B><%= sprintf("%.3f", $input) %></B> megabytes<BR>
+ Download: <B><%= sprintf("%.3f", $output) %></B> megabytes<BR>
+
+ <% my $href = qq!<A HREF="${p}search/sqlradius.cgi?svcnum=$svcnum!; %>
+ View session detail:
+ <%= $href %>;begin=<%= $last_bill %>">this billing cycle</A>
+ | <%= $href %>;begin=<%= time-15552000 %>">past six months</A>
+ | <%= $href %>">all sessions</A>
+
+ </TD></TR></TABLE><BR>
+
+<% } %>
+
+<SCRIPT TYPE="text/javascript">
+function enable_change () {
+ if ( document.OneTrueForm.svcpart.selectedIndex > 1 ) {
+ document.OneTrueForm.submit.disabled = false;
+ } else {
+ document.OneTrueForm.submit.disabled = true;
+ }
+}
+</SCRIPT>
+<FORM NAME="OneTrueForm" ACTION="<%=$p%>edit/process/cust_svc.cgi">
+<INPUT TYPE="hidden" NAME="svcnum" VALUE="<%= $svcnum %>">
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<%= $pkgnum %>">
+
+<% #print qq!<BR><A HREF="../misc/sendconfig.cgi?$svcnum">Send account information</A>!; %>
+
+<%
+ my @part_svc = ();
+ if ( $pkgnum ) {
+ @part_svc = grep { $_->svcdb eq 'svc_acct'
+ && $_->svcpart != $part_svc->svcpart }
+ $cust_pkg->available_part_svc;
+ } else {
+ @part_svc = qsearch('part_svc', {
+ svcdb => 'svc_acct',
+ disabled => '',
+ svcpart => { op=>'!=', value=>$part_svc->svcpart },
+ } );
+ }
+%>
+
+Service Information
+| <A HREF="<%=$p%>edit/svc_acct.cgi?<%=$svcnum%>">Edit this information</A>
+
+<% if ( @part_svc ) { %>
+| <SELECT NAME="svcpart" onChange="enable_change()">
+ <OPTION VALUE="">Change service</OPTION>
+ <OPTION VALUE="">--------------</OPTION>
+ <% foreach my $part_svc ( @part_svc ) { %>
+ <OPTION VALUE="<%= $part_svc->svcpart %>"><%= $part_svc->svc %></OPTION>
+ <% } %>
+ </SELECT>
+ <INPUT NAME="submit" TYPE="submit" VALUE="Change" disabled>
+<% } %>
+
+<%= &ntable("#cccccc") %><TR><TD><%= &ntable("#cccccc",2) %>
+<TR><TD ALIGN="right">Service number</TD>
+ <TD BGCOLOR="#ffffff"><%= $svcnum %></TD></TR>
+<TR><TD ALIGN="right">Service</TD>
+ <TD BGCOLOR="#ffffff"><%= $part_svc->svc %></TD></TR>
+<TR><TD ALIGN="right">Username</TD>
+ <TD BGCOLOR="#ffffff"><%= $svc_acct->username %></TD></TR>
+<TR><TD ALIGN="right">Domain</TD>
+ <TD BGCOLOR="#ffffff"><%= $domain %></TD></TR>
+
+<TR><TD ALIGN="right">Password</TD>
+ <TD BGCOLOR="#ffffff"><%
+
+my $password = $svc_acct->_password;
+if ( $password =~ /^\*\w+\* (.*)$/ ) {
+ $password = $1;
+ print "<I>(login disabled)</I> ";
+}
+if ( $conf->exists('showpasswords') ) {
+ print '<PRE>'. encode_entities($password). '</PRE>';
+} else {
+ print "<I>(hidden)</I>";
+}
+print "</TR></TD>";
+$password = '';
+
+if ( $conf->exists('security_phrase') ) {
+ my $sec_phrase = $svc_acct->sec_phrase;
+ print '<TR><TD ALIGN="right">Security phrase</TD><TD BGCOLOR="#ffffff">'.
+ $svc_acct->sec_phrase. '</TD></TR>';
+}
+
+my $svc_acct_pop = $svc_acct->popnum
+ ? qsearchs('svc_acct_pop',{'popnum'=>$svc_acct->popnum})
+ : '';
+print "<TR><TD ALIGN=\"right\">Access number</TD>".
+ "<TD BGCOLOR=\"#ffffff\">". $svc_acct_pop->text. '</TD></TR>'
+ if $svc_acct_pop;
+
+if ($svc_acct->uid ne '') {
+ print "<TR><TD ALIGN=\"right\">Uid</TD>".
+ "<TD BGCOLOR=\"#ffffff\">". $svc_acct->uid. "</TD></TR>",
+ "<TR><TD ALIGN=\"right\">Gid</TD>".
+ "<TD BGCOLOR=\"#ffffff\">". $svc_acct->gid. "</TD></TR>",
+ "<TR><TD ALIGN=\"right\">GECOS</TD>".
+ "<TD BGCOLOR=\"#ffffff\">". $svc_acct->finger. "</TD></TR>",
+ "<TR><TD ALIGN=\"right\">Home directory</TD>".
+ "<TD BGCOLOR=\"#ffffff\">". $svc_acct->dir. "</TD></TR>",
+ "<TR><TD ALIGN=\"right\">Shell</TD>".
+ "<TD BGCOLOR=\"#ffffff\">". $svc_acct->shell. "</TD></TR>",
+ "<TR><TD ALIGN=\"right\">Quota</TD>".
+ "<TD BGCOLOR=\"#ffffff\">". $svc_acct->quota. "</TD></TR>"
+ ;
+} else {
+ print "<TR><TH COLSPAN=2>(No shell account)</TH></TR>";
+}
+
+if ($svc_acct->slipip) {
+ print "<TR><TD ALIGN=\"right\">IP address</TD><TD BGCOLOR=\"#ffffff\">".
+ ( ( $svc_acct->slipip eq "0.0.0.0" || $svc_acct->slipip eq '0e0' )
+ ? "<I>(Dynamic)</I>"
+ : $svc_acct->slipip
+ ). "</TD>";
+ my($attribute);
+ foreach $attribute ( grep /^radius_/, $svc_acct->fields ) {
+ #warn $attribute;
+ $attribute =~ /^radius_(.*)$/;
+ my $pattribute = $FS::raddb::attrib{$1};
+ print "<TR><TD ALIGN=\"right\">Radius (reply) $pattribute</TD>".
+ "<TD BGCOLOR=\"#ffffff\">". $svc_acct->getfield($attribute).
+ "</TD></TR>";
+ }
+ foreach $attribute ( grep /^rc_/, $svc_acct->fields ) {
+ #warn $attribute;
+ $attribute =~ /^rc_(.*)$/;
+ my $pattribute = $FS::raddb::attrib{$1};
+ print "<TR><TD ALIGN=\"right\">Radius (check) $pattribute: </TD>".
+ "<TD BGCOLOR=\"#ffffff\">". $svc_acct->getfield($attribute).
+ "</TD></TR>";
+ }
+} else {
+ print "<TR><TH COLSPAN=2>(No SLIP/PPP account)</TH></TR>";
+}
+
+print '<TR><TD ALIGN="right">RADIUS groups</TD><TD BGCOLOR="#ffffff">'.
+ join('<BR>', $svc_acct->radius_groups). '</TD></TR>';
+
+# Can this be abstracted further? Maybe a library function like
+# widget('HTML', 'view', $svc_acct) ? It would definitely make UI
+# style management easier.
+
+foreach (sort { $a cmp $b } $svc_acct->virtual_fields) {
+ print $svc_acct->pvf($_)->widget('HTML', 'view', $svc_acct->getfield($_)),
+ "\n";
+}
+%>
+</TABLE></TD></TR></TABLE></FORM>
+<%
+
+print '<BR><BR>';
+
+print join("\n", $conf->config('svc_acct-notes') ). '<BR><BR>'.
+ joblisting({'svcnum'=>$svcnum}, 1). '</BODY></HTML>';
+
+%>
diff --git a/httemplate/view/svc_broadband.cgi b/httemplate/view/svc_broadband.cgi
new file mode 100644
index 0000000..ae23386
--- /dev/null
+++ b/httemplate/view/svc_broadband.cgi
@@ -0,0 +1,142 @@
+<!-- mason kludge -->
+<%
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_broadband = qsearchs( 'svc_broadband', { 'svcnum' => $svcnum } )
+ or die "svc_broadband: Unknown svcnum $svcnum";
+
+#false laziness w/all svc_*.cgi
+my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $svcnum } );
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+ $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $pkgnum } );
+ $custnum = $cust_pkg->custnum;
+} else {
+ $cust_pkg = '';
+ $custnum = '';
+}
+#eofalse
+
+my $router = $svc_broadband->addr_block->router;
+
+if (not $router) { die "Could not lookup router for svc_broadband (svcnum $svcnum)" };
+
+my (
+ $routername,
+ $routernum,
+ $speed_down,
+ $speed_up,
+ $ip_addr
+ ) = (
+ $router->getfield('routername'),
+ $router->getfield('routernum'),
+ $svc_broadband->getfield('speed_down'),
+ $svc_broadband->getfield('speed_up'),
+ $svc_broadband->getfield('ip_addr')
+ );
+%>
+
+<%=header('Broadband Service View', menubar(
+ ( ( $custnum )
+ ? ( "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+ )
+ : ( "Cancel this (unaudited) website" =>
+ "${p}misc/cancel-unaudited.cgi?$svcnum" )
+ ),
+ "Main menu" => $p,
+))
+%>
+
+<A HREF="<%=${p}%>edit/svc_broadband.cgi?<%=$svcnum%>">Edit this information</A>
+<BR>
+<%=ntable("#cccccc")%>
+ <TR>
+ <TD>
+ <%=ntable("#cccccc",2)%>
+ <TR>
+ <TD ALIGN="right">Service number</TD>
+ <TD BGCOLOR="#ffffff"><%=$svcnum%></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Router</TD>
+ <TD BGCOLOR="#ffffff"><%=$routernum%>: <%=$routername%></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Download Speed</TD>
+ <TD BGCOLOR="#ffffff"><%=$speed_down%></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Upload Speed</TD>
+ <TD BGCOLOR="#ffffff"><%=$speed_up%></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">IP Address</TD>
+ <TD BGCOLOR="#ffffff"><%=$ip_addr%></TD>
+ </TR>
+ <TR COLSPAN="2"><TD></TD></TR>
+
+<%
+foreach (sort { $a cmp $b } $svc_broadband->virtual_fields) {
+ print $svc_broadband->pvf($_)->widget('HTML', 'view',
+ $svc_broadband->getfield($_)), "\n";
+}
+
+%>
+ </TABLE>
+ </TD>
+ </TR>
+</TABLE>
+
+<BR>
+<%=ntable("#cccccc", 2)%>
+<%
+ my $sb_router = qsearchs('router', { svcnum => $svcnum });
+ if ($sb_router) {
+ %>
+ <B>Router associated: <%=$sb_router->routername%> </B>
+ <A HREF="<%=popurl(2)%>edit/router.cgi?<%=$sb_router->routernum%>">
+ (details)
+ </A>
+ <BR>
+ <% my @addr_block;
+ if (@addr_block = $sb_router->addr_block) {
+ %>
+ <B>Address space </B>
+ <A HREF="<%=popurl(2)%>browse/addr_block.cgi">
+ (edit)
+ </A>
+ <BR>
+ <% print ntable("#cccccc", 1);
+ foreach (@addr_block) { %>
+ <TR>
+ <TD><%=$_->ip_gateway%>/<%=$_->ip_netmask%></TD>
+ </TR>
+ <% } %>
+ </TABLE>
+ <% } else { %>
+ <B>No address space allocated.</B>
+ <% } %>
+ <BR>
+ <%
+ } else {
+%>
+
+<FORM METHOD="GET" ACTION="<%=popurl(2)%>edit/router.cgi">
+ <INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$svcnum%>">
+Add router named
+ <INPUT TYPE="text" NAME="routername" SIZE="32" VALUE="Broadband router (<%=$svcnum%>)">
+ <INPUT TYPE="submit" VALUE="Add router">
+</FORM>
+
+<%
+}
+%>
+
+<BR>
+<%=joblisting({'svcnum'=>$svcnum}, 1)%>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/view/svc_domain.cgi b/httemplate/view/svc_domain.cgi
new file mode 100755
index 0000000..cd9f79d
--- /dev/null
+++ b/httemplate/view/svc_domain.cgi
@@ -0,0 +1,108 @@
+<!-- mason kludge -->
+<%
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_domain = qsearchs('svc_domain',{'svcnum'=>$svcnum});
+die "Unknown svcnum" unless $svc_domain;
+
+my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$svcnum});
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+ $cust_pkg=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+ $custnum=$cust_pkg->getfield('custnum');
+} else {
+ $cust_pkg = '';
+ $custnum = '';
+}
+
+my $part_svc = qsearchs('part_svc',{'svcpart'=> $cust_svc->svcpart } );
+die "Unknown svcpart" unless $part_svc;
+
+my $email = '';
+if ($svc_domain->catchall) {
+ my $svc_acct = qsearchs('svc_acct',{'svcnum'=> $svc_domain->catchall } );
+ die "Unknown svcpart" unless $svc_acct;
+ $email = $svc_acct->email;
+}
+
+my $domain = $svc_domain->domain;
+
+%>
+
+<%= header('Domain View', menubar(
+ ( ( $pkgnum || $custnum )
+ ? ( "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+ )
+ : ( "Cancel this (unaudited) domain" =>
+ "${p}misc/cancel-unaudited.cgi?$svcnum" )
+ ),
+ "Main menu" => $p,
+)) %>
+
+Service #<%= $svcnum %>
+<BR>Service: <B><%= $part_svc->svc %></B>
+<BR>Domain name: <B><%= $domain %></B>
+<BR>Catch all email <A HREF="<%= ${p} %>misc/catchall.cgi?<%= $svcnum %>">(change)</A>:
+<%= $email ? "<B>$email</B>" : "<I>(none)<I>" %>
+<BR><BR><A HREF="<%= ${p} %>misc/whois.cgi?custnum=<%=$custnum%>;svcnum=<%=$svcnum%>;domain=<%=$domain%>">View whois information.</A>
+<BR><BR>
+<SCRIPT>
+ function areyousure(href) {
+ if ( confirm("Remove this record?") == true )
+ window.location.href = href;
+ }
+ function slave_areyousure() {
+ return confirm("Remove all records and slave from " + document.SlaveForm.recdata.value + "?");
+ }
+</SCRIPT>
+
+<% my @records; if ( @records = $svc_domain->domain_record ) { %>
+ <%= ntable("",2) %>
+ <tr><th>Zone</th><th>Type</th><th>Data</th></tr>
+
+ <% foreach my $domain_record ( @records ) {
+ my $type = $domain_record->rectype eq '_mstr'
+ ? "(slave)"
+ : $domain_record->recaf. ' '. $domain_record->rectype;
+ %>
+
+ <tr><td><%= $domain_record->reczone %></td>
+ <td><%= $type %></td>
+ <td><%= $domain_record->recdata %>
+
+ <% unless ( $domain_record->rectype eq 'SOA' ) { %>
+ (<A HREF="javascript:areyousure('<%=$p%>misc/delete-domain_record.cgi?<%=$domain_record->recnum%>')">delete</A>)
+ <% } %>
+ </td></tr>
+ <% } %>
+ </table>
+<% } %>
+
+<BR>
+<FORM METHOD="POST" ACTION="<%=$p%>edit/process/domain_record.cgi">
+<INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$svcnum%>">
+<INPUT TYPE="text" NAME="reczone">
+<INPUT TYPE="hidden" NAME="recaf" VALUE="IN"> IN
+ <SELECT NAME="rectype">
+<% foreach (qw( A NS CNAME MX PTR) ) { %>
+ <OPTION VALUE="<%=$_%>"><%=$_%></OPTION>
+<% } %>
+ </SELECT>
+<INPUT TYPE="text" NAME="recdata"> <INPUT TYPE="submit" VALUE="Add record">
+</FORM><BR><BR>or<BR><BR>
+<FORM NAME="SlaveForm" METHOD="POST" ACTION="<%=$p%>edit/process/domain_record.cgi">
+<INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$svcnum%>">
+
+<% if ( @records ) { %> Delete all records and <% } %>
+Slave from nameserver IP
+<INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$svcnum%>">
+<INPUT TYPE="hidden" NAME="reczone" VALUE="@">
+<INPUT TYPE="hidden" NAME="recaf" VALUE="IN">
+<INPUT TYPE="hidden" NAME="rectype" VALUE="_mstr">
+<INPUT TYPE="text" NAME="recdata"> <INPUT TYPE="submit" VALUE="Slave domain" onClick="return slave_areyousure()">
+</FORM>
+<BR><BR><%= joblisting({'svcnum'=>$svcnum}, 1) %>
+</BODY></HTML>
diff --git a/httemplate/view/svc_external.cgi b/httemplate/view/svc_external.cgi
new file mode 100644
index 0000000..49183cd
--- /dev/null
+++ b/httemplate/view/svc_external.cgi
@@ -0,0 +1,54 @@
+<!-- mason kludge -->
+<%
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_external = qsearchs( 'svc_external', { 'svcnum' => $svcnum } )
+ or die "svc_external: Unknown svcnum $svcnum";
+
+my $conf = new FS::Conf;
+
+#false laziness w/all svc_*.cgi
+my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $svcnum } );
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+ $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $pkgnum } );
+ $custnum = $cust_pkg->custnum;
+} else {
+ $cust_pkg = '';
+ $custnum = '';
+}
+#eofalse
+
+
+%>
+
+<%= header('External Service View', menubar(
+ ( ( $custnum )
+ ? ( "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+ )
+ : ( "Cancel this (unaudited) external service" =>
+ "${p}misc/cancel-unaudited.cgi?$svcnum" )
+ ),
+ "Main menu" => $p,
+)) %>
+
+<A HREF="<%=$p%>edit/svc_external.cgi?<%=$svcnum%>">Edit this information</A><BR>
+<%= ntable("#cccccc") %><TR><TD><%= ntable("#cccccc",2) %>
+
+<TR><TD ALIGN="right">Service number</TD>
+ <TD BGCOLOR="#ffffff"><%= $svcnum %></TD></TR>
+<TR><TD ALIGN="right"><%= FS::Msgcat::_gettext('svc_external-id') || 'External&nbsp;ID' %></TD>
+ <TD BGCOLOR="#ffffff"><%= $conf->config('svc_external-display_type') eq 'artera_turbo' ? sprintf('%010d', $svc_external->id) : $svc_external->id %></TD></TR>
+<TR><TD ALIGN="right"><%= FS::Msgcat::_gettext('svc_external-title') || 'Title' %></TD>
+ <TD BGCOLOR="#ffffff"><%= $svc_external->title %></TD></TR>
+
+<% foreach (sort { $a cmp $b } $svc_external->virtual_fields) { %>
+ <%= $svc_external->pvf($_)->widget('HTML', 'view', $svc_external->getfield($_)) %>
+<% } %>
+
+</TABLE></TD></TR></TABLE>
+<BR><%= joblisting({'svcnum'=>$svcnum}, 1) %>
+</BODY></HTML>
diff --git a/httemplate/view/svc_forward.cgi b/httemplate/view/svc_forward.cgi
new file mode 100755
index 0000000..52360bc
--- /dev/null
+++ b/httemplate/view/svc_forward.cgi
@@ -0,0 +1,84 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_forward = qsearchs('svc_forward',{'svcnum'=>$svcnum});
+die "Unknown svcnum" unless $svc_forward;
+
+my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$svcnum});
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+ $cust_pkg=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+ $custnum=$cust_pkg->getfield('custnum');
+} else {
+ $cust_pkg = '';
+ $custnum = '';
+}
+
+my $part_svc = qsearchs('part_svc',{'svcpart'=> $cust_svc->svcpart } )
+ or die "Unkonwn svcpart";
+
+print header('Mail Forward View', menubar(
+ ( ( $pkgnum || $custnum )
+ ? ( "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+ )
+ : ( "Cancel this (unaudited) mail forward" =>
+ "${p}misc/cancel-unaudited.cgi?$svcnum" )
+ ),
+ "Main menu" => $p,
+));
+
+my($srcsvc,$dstsvc,$dst) = (
+ $svc_forward->srcsvc,
+ $svc_forward->dstsvc,
+ $svc_forward->dst,
+);
+my $src = $svc_forward->dbdef_table->column('src') ? $svc_forward->src : '';
+
+my $svc = $part_svc->svc;
+
+my $source;
+if ($srcsvc) {
+ my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$srcsvc})
+ or die "Corrupted database: no svc_acct.svcnum matching srcsvc $srcsvc";
+ $source = $svc_acct->email;
+} else {
+ $source = $src;
+}
+
+my $destination;
+if ($dstsvc) {
+ my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$dstsvc})
+ or die "Corrupted database: no svc_acct.svcnum matching dstsvc $dstsvc";
+ $destination = $svc_acct->email;
+} else {
+ $destination = $dst;
+}
+
+print qq!<A HREF="${p}edit/svc_forward.cgi?$svcnum">Edit this information</A>!.
+ ntable("#cccccc",2).
+ '<TR><TD ALIGN="right">Service number</TD>'.
+ qq!<TD BGCOLOR="#ffffff">$svcnum</TD></TR>!.
+ '<TR><TD ALIGN="right">Service</TD>'.
+ qq!<TD BGCOLOR="#ffffff">$svc</TD></TR>!.
+ qq!<TR><TD ALIGN="right">Email to</TD>!.
+ qq!<TD BGCOLOR="#ffffff">$source</TD></TR>!.
+ qq!<TR><TD ALIGN="right">Forwards to </TD>!.
+ qq!<TD BGCOLOR="#ffffff">$destination</TD></TR>!;
+
+foreach (sort { $a cmp $b } $svc_forward->virtual_fields) {
+ print $svc_forward->pvf($_)->widget('HTML', 'view', $svc_forward->getfield($_)),
+ "\n";
+}
+
+print qq! </TABLE>!.
+ '<BR>'. joblisting({'svcnum'=>$svcnum}, 1).
+ '</BODY></HTML>'
+;
+
+%>
diff --git a/httemplate/view/svc_www.cgi b/httemplate/view/svc_www.cgi
new file mode 100644
index 0000000..2980f84
--- /dev/null
+++ b/httemplate/view/svc_www.cgi
@@ -0,0 +1,61 @@
+<!-- mason kludge -->
+<%
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_www = qsearchs( 'svc_www', { 'svcnum' => $svcnum } )
+ or die "svc_www: Unknown svcnum $svcnum";
+
+#false laziness w/all svc_*.cgi
+my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $svcnum } );
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+ $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $pkgnum } );
+ $custnum = $cust_pkg->custnum;
+} else {
+ $cust_pkg = '';
+ $custnum = '';
+}
+#eofalse
+
+my $usersvc = $svc_www->usersvc;
+my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $usersvc } )
+ or die "svc_www: Unknown usersvc $usersvc";
+my $email = $svc_acct->email;
+
+my $domain_record = qsearchs('domain_record', { 'recnum' => $svc_www->recnum } )
+ or die "svc_www: Unknown recnum ". $svc_www->recnum;
+
+my $www = $domain_record->zone;
+
+print header('Website View', menubar(
+ ( ( $custnum )
+ ? ( "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+ )
+ : ( "Cancel this (unaudited) website" =>
+ "${p}misc/cancel-unaudited.cgi?$svcnum" )
+ ),
+ "Main menu" => $p,
+)).
+ qq!<A HREF="${p}edit/svc_www.cgi?$svcnum">Edit this information</A><BR>!.
+ ntable("#cccccc"). '<TR><TD>'. ntable("#cccccc",2).
+ qq!<TR><TD ALIGN="right">Service number</TD>!.
+ qq!<TD BGCOLOR="#ffffff">$svcnum</TD></TR>!.
+ qq!<TR><TD ALIGN="right">Website name</TD>!.
+ qq!<TD BGCOLOR="#ffffff"><A HREF="http://$www">$www<A></TD></TR>!.
+ qq!<TR><TD ALIGN="right">Account</TD>!.
+ qq!<TD BGCOLOR="#ffffff"><A HREF="${p}view/svc_acct.cgi?$usersvc">$email</A></TD></TR>!;
+
+foreach (sort { $a cmp $b } $svc_www->virtual_fields) {
+ print $svc_www->pvf($_)->widget('HTML', 'view', $svc_www->getfield($_)),
+ "\n";
+}
+
+
+print '</TABLE></TD></TR></TABLE>'.
+ '<BR>'. joblisting({'svcnum'=>$svcnum}, 1).
+ '</BODY></HTML>'
+;
+%>