summaryrefslogtreecommitdiff
path: root/httemplate
diff options
context:
space:
mode:
Diffstat (limited to 'httemplate')
-rwxr-xr-xhttemplate/.htaccess3
-rw-r--r--httemplate/autohandler35
-rw-r--r--httemplate/browse/access_group.html82
-rw-r--r--httemplate/browse/access_user.html102
-rw-r--r--httemplate/browse/addr_block.cgi86
-rwxr-xr-xhttemplate/browse/agent.cgi374
-rwxr-xr-xhttemplate/browse/agent_type.cgi63
-rwxr-xr-xhttemplate/browse/cust_main_county.cgi170
-rw-r--r--httemplate/browse/elements/browse.html6
-rw-r--r--httemplate/browse/inventory_class.html93
-rwxr-xr-xhttemplate/browse/msgcat.cgi44
-rwxr-xr-xhttemplate/browse/nas.cgi81
-rwxr-xr-xhttemplate/browse/part_bill_event.cgi123
-rwxr-xr-xhttemplate/browse/part_export.cgi44
-rwxr-xr-xhttemplate/browse/part_pkg.cgi261
-rwxr-xr-xhttemplate/browse/part_referral.html147
-rwxr-xr-xhttemplate/browse/part_svc.cgi212
-rw-r--r--httemplate/browse/part_virtual_field.cgi46
-rw-r--r--httemplate/browse/payment_gateway.html79
-rw-r--r--httemplate/browse/pkg_class.html30
-rw-r--r--httemplate/browse/rate.cgi37
-rw-r--r--httemplate/browse/reason.html68
-rw-r--r--httemplate/browse/reason_type.html72
-rw-r--r--httemplate/browse/router.cgi64
-rwxr-xr-xhttemplate/browse/svc_acct_pop.cgi73
-rw-r--r--httemplate/config/config-process.cgi62
-rw-r--r--httemplate/config/config-view.cgi95
-rw-r--r--httemplate/config/config.cgi263
-rw-r--r--httemplate/docs/ach.html10
-rwxr-xr-xhttemplate/docs/admin.html41
-rw-r--r--httemplate/docs/cvv2.html24
-rw-r--r--httemplate/docs/ieak.html75
-rw-r--r--httemplate/docs/index.html32
-rwxr-xr-xhttemplate/docs/legacy.html39
-rw-r--r--httemplate/docs/overview-new.diabin0 -> 2422 bytes
-rw-r--r--httemplate/docs/overview-new.pngbin0 -> 29062 bytes
-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 -> 16364 bytes
-rw-r--r--httemplate/docs/schema.html533
-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/edit/REAL_cust_pkg.cgi185
-rw-r--r--httemplate/edit/access_group.html46
-rw-r--r--httemplate/edit/access_user.html44
-rwxr-xr-xhttemplate/edit/agent.cgi115
-rw-r--r--httemplate/edit/agent_payment_gateway.html70
-rwxr-xr-xhttemplate/edit/agent_type.cgi57
-rw-r--r--httemplate/edit/bulk-cust_svc.html99
-rwxr-xr-xhttemplate/edit/cust_bill_pay.cgi85
-rwxr-xr-xhttemplate/edit/cust_credit.cgi80
-rwxr-xr-xhttemplate/edit/cust_credit_bill.cgi92
-rwxr-xr-xhttemplate/edit/cust_main.cgi510
-rw-r--r--httemplate/edit/cust_main/billing.html442
-rw-r--r--httemplate/edit/cust_main/contact.html134
-rw-r--r--httemplate/edit/cust_main/select-country.html76
-rw-r--r--httemplate/edit/cust_main/select-county.html113
-rw-r--r--httemplate/edit/cust_main/select-domain.html66
-rw-r--r--httemplate/edit/cust_main/select-state.html20
-rwxr-xr-xhttemplate/edit/cust_main_county-expand.cgi59
-rwxr-xr-xhttemplate/edit/cust_main_county.cgi99
-rwxr-xr-xhttemplate/edit/cust_main_note.cgi51
-rwxr-xr-xhttemplate/edit/cust_pay.cgi145
-rwxr-xr-xhttemplate/edit/cust_pkg.cgi152
-rwxr-xr-xhttemplate/edit/cust_refund.cgi126
-rw-r--r--httemplate/edit/elements/edit.html234
-rw-r--r--httemplate/edit/elements/svc_Common.html101
-rw-r--r--httemplate/edit/inventory_class.html10
-rwxr-xr-xhttemplate/edit/msgcat.cgi57
-rwxr-xr-xhttemplate/edit/part_bill_event.cgi531
-rw-r--r--httemplate/edit/part_export.cgi130
-rwxr-xr-xhttemplate/edit/part_pkg.cgi400
-rwxr-xr-xhttemplate/edit/part_referral.html9
-rwxr-xr-xhttemplate/edit/part_svc.cgi354
-rw-r--r--httemplate/edit/part_virtual_field.cgi103
-rw-r--r--httemplate/edit/payment_gateway.html134
-rw-r--r--httemplate/edit/pkg_class.html16
-rw-r--r--httemplate/edit/prepay_credit.cgi111
-rwxr-xr-xhttemplate/edit/process/REAL_cust_pkg.cgi35
-rw-r--r--httemplate/edit/process/access_group.html15
-rw-r--r--httemplate/edit/process/access_user.html15
-rwxr-xr-xhttemplate/edit/process/addr_block/add.cgi21
-rwxr-xr-xhttemplate/edit/process/addr_block/allocate.cgi26
-rwxr-xr-xhttemplate/edit/process/addr_block/deallocate.cgi25
-rwxr-xr-xhttemplate/edit/process/addr_block/split.cgi20
-rwxr-xr-xhttemplate/edit/process/agent.cgi29
-rw-r--r--httemplate/edit/process/agent_payment_gateway.html26
-rwxr-xr-xhttemplate/edit/process/agent_type.cgi37
-rw-r--r--httemplate/edit/process/bulk-cust_svc.cgi4
-rwxr-xr-xhttemplate/edit/process/cust_bill_pay.cgi54
-rwxr-xr-xhttemplate/edit/process/cust_credit.cgi38
-rwxr-xr-xhttemplate/edit/process/cust_credit_bill.cgi55
-rwxr-xr-xhttemplate/edit/process/cust_main.cgi176
-rwxr-xr-xhttemplate/edit/process/cust_main_county-collapse.cgi36
-rwxr-xr-xhttemplate/edit/process/cust_main_county-expand.cgi59
-rwxr-xr-xhttemplate/edit/process/cust_main_county.cgi31
-rwxr-xr-xhttemplate/edit/process/cust_main_note.cgi52
-rwxr-xr-xhttemplate/edit/process/cust_pay.cgi56
-rwxr-xr-xhttemplate/edit/process/cust_pkg.cgi44
-rwxr-xr-xhttemplate/edit/process/cust_refund.cgi34
-rw-r--r--httemplate/edit/process/cust_svc.cgi30
-rwxr-xr-xhttemplate/edit/process/domain_record.cgi36
-rw-r--r--httemplate/edit/process/elements/process.html111
-rw-r--r--httemplate/edit/process/elements/svc_Common.html15
-rw-r--r--httemplate/edit/process/generic.cgi73
-rw-r--r--httemplate/edit/process/inventory_class.html5
-rw-r--r--httemplate/edit/process/msgcat.cgi21
-rwxr-xr-xhttemplate/edit/process/part_bill_event.cgi89
-rw-r--r--httemplate/edit/process/part_export.cgi40
-rwxr-xr-xhttemplate/edit/process/part_pkg.cgi75
-rwxr-xr-xhttemplate/edit/process/part_referral.html5
-rwxr-xr-xhttemplate/edit/process/part_svc.cgi4
-rw-r--r--httemplate/edit/process/payment_gateway.html34
-rw-r--r--httemplate/edit/process/pkg_class.html5
-rw-r--r--httemplate/edit/process/prepay_credit.cgi63
-rw-r--r--httemplate/edit/process/quick-charge.cgi47
-rw-r--r--httemplate/edit/process/quick-cust_pkg.cgi27
-rwxr-xr-xhttemplate/edit/process/rate.cgi4
-rwxr-xr-xhttemplate/edit/process/rate_region.cgi52
-rw-r--r--httemplate/edit/process/reason.html6
-rw-r--r--httemplate/edit/process/reason_type.html6
-rw-r--r--httemplate/edit/process/reg_code.cgi50
-rw-r--r--httemplate/edit/process/router.cgi68
-rw-r--r--httemplate/edit/process/svc_Common.html13
-rwxr-xr-xhttemplate/edit/process/svc_acct.cgi50
-rwxr-xr-xhttemplate/edit/process/svc_acct_pop.cgi29
-rw-r--r--httemplate/edit/process/svc_broadband.cgi37
-rwxr-xr-xhttemplate/edit/process/svc_domain.cgi32
-rwxr-xr-xhttemplate/edit/process/svc_external.cgi30
-rwxr-xr-xhttemplate/edit/process/svc_forward.cgi30
-rw-r--r--httemplate/edit/process/svc_phone.html4
-rw-r--r--httemplate/edit/process/svc_www.cgi37
-rw-r--r--httemplate/edit/quick-charge.html166
-rw-r--r--httemplate/edit/rate.cgi116
-rw-r--r--httemplate/edit/rate_region.cgi119
-rw-r--r--httemplate/edit/reason.html45
-rw-r--r--httemplate/edit/reason_type.html28
-rw-r--r--httemplate/edit/reg_code.cgi39
-rwxr-xr-xhttemplate/edit/router.cgi78
-rw-r--r--httemplate/edit/svc_Common.html30
-rwxr-xr-xhttemplate/edit/svc_acct.cgi456
-rwxr-xr-xhttemplate/edit/svc_acct_pop.cgi57
-rw-r--r--httemplate/edit/svc_broadband.cgi254
-rwxr-xr-xhttemplate/edit/svc_domain.cgi90
-rw-r--r--httemplate/edit/svc_external.cgi99
-rwxr-xr-xhttemplate/edit/svc_forward.cgi180
-rw-r--r--httemplate/edit/svc_phone.cgi11
-rw-r--r--httemplate/edit/svc_www.cgi220
-rw-r--r--httemplate/elements/calendar-en.js127
-rw-r--r--httemplate/elements/calendar-setup.js200
-rw-r--r--httemplate/elements/calendar-win2k-2.css271
-rw-r--r--httemplate/elements/calendar.js1806
-rw-r--r--httemplate/elements/calendar_stripped.js14
-rw-r--r--httemplate/elements/checkboxes-table-name.html85
-rw-r--r--httemplate/elements/checkboxes-table.html123
-rw-r--r--httemplate/elements/cssexpr.js66
-rw-r--r--httemplate/elements/dashboard-toplist.html109
-rw-r--r--httemplate/elements/error.html4
-rw-r--r--httemplate/elements/footer.html5
-rw-r--r--httemplate/elements/freeside.css15
-rw-r--r--httemplate/elements/header-popup.html23
-rw-r--r--httemplate/elements/header.html244
-rw-r--r--httemplate/elements/iframecontentmws.js20
-rw-r--r--httemplate/elements/jsrsClient.js356
-rw-r--r--httemplate/elements/jsrsServer.html4
-rw-r--r--httemplate/elements/menu.html353
-rw-r--r--httemplate/elements/menubar.html10
-rw-r--r--httemplate/elements/overlibmws.js697
-rw-r--r--httemplate/elements/overlibmws_crossframe.js44
-rw-r--r--httemplate/elements/overlibmws_draggable.js78
-rw-r--r--httemplate/elements/overlibmws_iframe.js93
-rw-r--r--httemplate/elements/pager.html55
-rw-r--r--httemplate/elements/phonenumber.html22
-rw-r--r--httemplate/elements/progress-init.html85
-rw-r--r--httemplate/elements/progress-popup.html105
-rw-r--r--httemplate/elements/qlib/box.js29
-rw-r--r--httemplate/elements/qlib/boxctrl.js48
-rw-r--r--httemplate/elements/qlib/boxres.js42
-rw-r--r--httemplate/elements/qlib/button.js74
-rw-r--r--httemplate/elements/qlib/buttonres.js23
-rw-r--r--httemplate/elements/qlib/control.js51
-rw-r--r--httemplate/elements/qlib/counter.js81
-rw-r--r--httemplate/elements/qlib/imagelist.js25
-rw-r--r--httemplate/elements/qlib/label.js72
-rw-r--r--httemplate/elements/qlib/messagebox.js57
-rw-r--r--httemplate/elements/qlib/progress.js73
-rw-r--r--httemplate/elements/qlib/sound.js47
-rw-r--r--httemplate/elements/qlib/sprite.js125
-rw-r--r--httemplate/elements/qlib/window.js25
-rw-r--r--httemplate/elements/qlib/wndctrl.js322
-rw-r--r--httemplate/elements/search-cust_main.html164
-rw-r--r--httemplate/elements/select-access_group.html16
-rw-r--r--httemplate/elements/select-agent.html19
-rw-r--r--httemplate/elements/select-cust-fields.html24
-rw-r--r--httemplate/elements/select-cust_pkg-status.html21
-rw-r--r--httemplate/elements/select-month_year.html62
-rw-r--r--httemplate/elements/select-part_referral.html18
-rw-r--r--httemplate/elements/select-pkg_class.html16
-rw-r--r--httemplate/elements/select-table.html85
-rw-r--r--httemplate/elements/select-taxclass.html38
-rw-r--r--httemplate/elements/small_custview.html3
-rw-r--r--httemplate/elements/table-grid.html21
-rw-r--r--httemplate/elements/table.html11
-rw-r--r--httemplate/elements/tr-input-beginning_ending.html63
-rw-r--r--httemplate/elements/tr-input-date-field.html40
-rw-r--r--httemplate/elements/tr-input-lessthan_greaterthan.html13
-rw-r--r--httemplate/elements/tr-select-access_group.html22
-rw-r--r--httemplate/elements/tr-select-agent.html34
-rw-r--r--httemplate/elements/tr-select-cust-fields.html15
-rw-r--r--httemplate/elements/tr-select-cust_pkg-status.html14
-rw-r--r--httemplate/elements/tr-select-from_to.html52
-rw-r--r--httemplate/elements/tr-select-part_referral.html30
-rw-r--r--httemplate/elements/tr-select-pkg_class.html20
-rwxr-xr-xhttemplate/elements/tr-select-reason.html101
-rw-r--r--httemplate/elements/tr-select-taxclass.html32
-rw-r--r--httemplate/elements/tr-selectmultiple-part_pkg.html19
-rw-r--r--httemplate/elements/xmenu.css196
-rw-r--r--httemplate/elements/xmenu.js668
-rw-r--r--httemplate/elements/xmenu.top.css211
-rw-r--r--httemplate/elements/xmenu.top.js671
-rw-r--r--httemplate/elements/xmlhttp.html111
-rw-r--r--httemplate/graph/cust_bill_pkg.cgi121
-rw-r--r--httemplate/graph/elements/monthly.html207
-rw-r--r--httemplate/graph/money_time.cgi85
-rw-r--r--httemplate/graph/report_cust_bill_pkg.html35
-rw-r--r--httemplate/graph/report_money_time.html39
-rw-r--r--httemplate/images/32clear.gifbin0 -> 815 bytes
-rw-r--r--httemplate/images/ach.pngbin0 -> 29759 bytes
-rw-r--r--httemplate/images/arrow.down.pngbin0 -> 155 bytes
-rw-r--r--httemplate/images/arrow.right.black.pngbin0 -> 160 bytes
-rw-r--r--httemplate/images/arrow.right.pngbin0 -> 160 bytes
-rw-r--r--httemplate/images/background-cheat.pngbin0 -> 338 bytes
-rw-r--r--httemplate/images/black-gradient.pngbin0 -> 397 bytes
-rw-r--r--httemplate/images/black-gray-corner.pngbin0 -> 460 bytes
-rw-r--r--httemplate/images/black-gray-gradient.pngbin0 -> 384 bytes
-rw-r--r--httemplate/images/black-gray-side.pngbin0 -> 198 bytes
-rw-r--r--httemplate/images/black-gray-top.pngbin0 -> 203 bytes
-rw-r--r--httemplate/images/calendar-disabled.pngbin0 -> 209 bytes
-rw-r--r--httemplate/images/calendar.pngbin0 -> 426 bytes
-rw-r--r--httemplate/images/cvv2.pngbin0 -> 7791 bytes
-rw-r--r--httemplate/images/cvv2_amex.pngbin0 -> 9539 bytes
-rw-r--r--httemplate/images/menu-left-example.pngbin0 -> 24709 bytes
-rw-r--r--httemplate/images/menu-top-example.pngbin0 -> 22816 bytes
-rw-r--r--httemplate/images/progressbar-empty.pngbin0 -> 90 bytes
-rw-r--r--httemplate/images/progressbar-full.pngbin0 -> 79 bytes
-rw-r--r--httemplate/images/red_telephone_mimooh_01.pngbin0 -> 921 bytes
-rw-r--r--httemplate/images/small-logo.pngbin0 -> 4887 bytes
-rw-r--r--httemplate/index.html54
-rw-r--r--httemplate/misc/batch-cust_pay.html395
-rwxr-xr-xhttemplate/misc/bill.cgi41
-rwxr-xr-xhttemplate/misc/cancel-unaudited.cgi36
-rwxr-xr-xhttemplate/misc/cancel_pkg.html95
-rwxr-xr-xhttemplate/misc/catchall.cgi134
-rw-r--r--httemplate/misc/cdr-import.html16
-rwxr-xr-xhttemplate/misc/change_pkg.cgi69
-rw-r--r--httemplate/misc/counties.cgi7
-rwxr-xr-xhttemplate/misc/cust_main-cancel.cgi23
-rw-r--r--httemplate/misc/cust_main-import.cgi76
-rw-r--r--httemplate/misc/cust_main-import_charges.cgi14
-rw-r--r--httemplate/misc/delete-agent_payment_gateway.cgi15
-rwxr-xr-xhttemplate/misc/delete-cust_credit.cgi17
-rwxr-xr-xhttemplate/misc/delete-cust_pay.cgi17
-rwxr-xr-xhttemplate/misc/delete-cust_refund.cgi17
-rwxr-xr-xhttemplate/misc/delete-customer.cgi61
-rwxr-xr-xhttemplate/misc/delete-domain_record.cgi16
-rwxr-xr-xhttemplate/misc/delete-part_export.cgi16
-rw-r--r--httemplate/misc/download-batch.cgi171
-rw-r--r--httemplate/misc/dump.cgi20
-rwxr-xr-xhttemplate/misc/email-invoice.cgi18
-rw-r--r--httemplate/misc/email_invoice_events.cgi4
-rw-r--r--httemplate/misc/email_invoices.cgi4
-rwxr-xr-xhttemplate/misc/fax-invoice.cgi18
-rw-r--r--httemplate/misc/fax_invoice_events.cgi4
-rw-r--r--httemplate/misc/fax_invoices.cgi4
-rw-r--r--httemplate/misc/inventory_item-import.html21
-rwxr-xr-xhttemplate/misc/link.cgi77
-rw-r--r--httemplate/misc/meta-import.cgi73
-rw-r--r--httemplate/misc/payment.cgi207
-rwxr-xr-xhttemplate/misc/print-invoice.cgi18
-rw-r--r--httemplate/misc/print_invoice_events.cgi4
-rw-r--r--httemplate/misc/print_invoices.cgi4
-rw-r--r--httemplate/misc/process/batch-cust_pay.cgi45
-rwxr-xr-xhttemplate/misc/process/cancel_pkg.html78
-rwxr-xr-xhttemplate/misc/process/catchall.cgi34
-rw-r--r--httemplate/misc/process/cdr-import.html30
-rw-r--r--httemplate/misc/process/cust_main-import.cgi35
-rw-r--r--httemplate/misc/process/cust_main-import_charges.cgi30
-rwxr-xr-xhttemplate/misc/process/delete-customer.cgi30
-rw-r--r--httemplate/misc/process/inventory_item-import.html31
-rwxr-xr-xhttemplate/misc/process/link.cgi78
-rw-r--r--httemplate/misc/process/meta-import.cgi185
-rw-r--r--httemplate/misc/process/payment.cgi148
-rwxr-xr-xhttemplate/misc/process/recharge_svc.html46
-rw-r--r--httemplate/misc/queue.cgi48
-rwxr-xr-xhttemplate/misc/recharge_svc.html48
-rw-r--r--httemplate/misc/states.cgi7
-rw-r--r--httemplate/misc/svc_acct-domains.cgi31
-rwxr-xr-xhttemplate/misc/unapply-cust_credit.cgi19
-rwxr-xr-xhttemplate/misc/unapply-cust_pay.cgi19
-rwxr-xr-xhttemplate/misc/unprovision.cgi31
-rwxr-xr-xhttemplate/misc/unsusp_pkg.cgi16
-rwxr-xr-xhttemplate/misc/unvoid-cust_pay_void.cgi17
-rw-r--r--httemplate/misc/upload-batch.cgi39
-rwxr-xr-xhttemplate/misc/void-cust_pay.cgi17
-rw-r--r--httemplate/misc/whois.cgi27
-rw-r--r--httemplate/misc/xmlhttp-cust_main-search.cgi22
-rw-r--r--httemplate/misc/xmlrpc.cgi18
-rw-r--r--httemplate/pref/pref-process.html44
-rw-r--r--httemplate/pref/pref.html61
-rw-r--r--httemplate/search/cdr.html41
-rwxr-xr-xhttemplate/search/cust_bill.html216
-rw-r--r--httemplate/search/cust_bill_event.cgi161
-rwxr-xr-xhttemplate/search/cust_bill_event.html64
-rw-r--r--httemplate/search/cust_bill_pkg.cgi191
-rwxr-xr-xhttemplate/search/cust_credit.html104
-rwxr-xr-xhttemplate/search/cust_main-otaker.cgi31
-rw-r--r--httemplate/search/cust_main-zip.html99
-rwxr-xr-xhttemplate/search/cust_main.cgi728
-rwxr-xr-xhttemplate/search/cust_main.html47
-rwxr-xr-xhttemplate/search/cust_pay.cgi228
-rwxr-xr-xhttemplate/search/cust_pay_batch.cgi190
-rwxr-xr-xhttemplate/search/cust_pkg.cgi339
-rw-r--r--httemplate/search/cust_svc.html138
-rw-r--r--httemplate/search/cust_tax_exempt_pkg.cgi182
-rw-r--r--httemplate/search/elements/search.html658
-rw-r--r--httemplate/search/inventory_item.html125
-rwxr-xr-xhttemplate/search/pay_batch.cgi130
-rw-r--r--httemplate/search/pay_batch.html33
-rw-r--r--httemplate/search/prepay_credit.html68
-rw-r--r--httemplate/search/queue.html139
-rw-r--r--httemplate/search/reg_code.html40
-rw-r--r--httemplate/search/report_cdr.html17
-rw-r--r--httemplate/search/report_cust_bill.html34
-rw-r--r--httemplate/search/report_cust_credit.html53
-rw-r--r--httemplate/search/report_cust_main-zip.html52
-rw-r--r--httemplate/search/report_cust_pay.html78
-rw-r--r--httemplate/search/report_cust_pay_batch.html43
-rwxr-xr-xhttemplate/search/report_cust_pkg.html144
-rw-r--r--httemplate/search/report_prepaid_income.cgi87
-rw-r--r--httemplate/search/report_prepaid_income.html43
-rwxr-xr-xhttemplate/search/report_receivables.cgi229
-rwxr-xr-xhttemplate/search/report_receivables.html26
-rwxr-xr-xhttemplate/search/report_tax.cgi539
-rwxr-xr-xhttemplate/search/report_tax.html42
-rw-r--r--httemplate/search/sql.html13
-rw-r--r--httemplate/search/sqlradius.cgi311
-rw-r--r--httemplate/search/sqlradius.html59
-rwxr-xr-xhttemplate/search/svc_acct.cgi174
-rwxr-xr-xhttemplate/search/svc_broadband.cgi121
-rwxr-xr-xhttemplate/search/svc_domain.cgi110
-rwxr-xr-xhttemplate/search/svc_external.cgi153
-rwxr-xr-xhttemplate/search/svc_forward.cgi145
-rw-r--r--httemplate/search/svc_phone.cgi115
-rwxr-xr-xhttemplate/search/svc_www.cgi111
-rwxr-xr-xhttemplate/view/cust_bill-logo.cgi20
-rwxr-xr-xhttemplate/view/cust_bill-pdf.cgi28
-rwxr-xr-xhttemplate/view/cust_bill-ps.cgi24
-rwxr-xr-xhttemplate/view/cust_bill.cgi165
-rwxr-xr-xhttemplate/view/cust_main.cgi171
-rw-r--r--httemplate/view/cust_main/billing.html192
-rw-r--r--httemplate/view/cust_main/contacts.html100
-rw-r--r--httemplate/view/cust_main/misc.html112
-rwxr-xr-xhttemplate/view/cust_main/notes.html93
-rw-r--r--httemplate/view/cust_main/order_pkg.html41
-rwxr-xr-xhttemplate/view/cust_main/packages.html545
-rw-r--r--httemplate/view/cust_main/payment_history.html605
-rw-r--r--httemplate/view/cust_main/quick-charge.html73
-rw-r--r--httemplate/view/cust_main/tickets.html78
-rw-r--r--httemplate/view/elements/svc_Common.html137
-rw-r--r--httemplate/view/svc_Common.html29
-rwxr-xr-xhttemplate/view/svc_acct.cgi364
-rw-r--r--httemplate/view/svc_broadband.cgi213
-rwxr-xr-xhttemplate/view/svc_domain.cgi145
-rw-r--r--httemplate/view/svc_external.cgi64
-rwxr-xr-xhttemplate/view/svc_forward.cgi94
-rw-r--r--httemplate/view/svc_phone.cgi10
-rw-r--r--httemplate/view/svc_www.cgi88
380 files changed, 35228 insertions, 0 deletions
diff --git a/httemplate/.htaccess b/httemplate/.htaccess
new file mode 100755
index 000000000..f8c6b9c0c
--- /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 000000000..bdea50534
--- /dev/null
+++ b/httemplate/autohandler
@@ -0,0 +1,35 @@
+% $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' ) {
+
+ ## barely worth it, just in case someone tries to use profiling on a
+ ## non-RT install
+ #eval "use Text::Wrapper;";
+ #die $@ if $@;
+
+ my $wrapper = new Text::Wrapper( columns => 80 );
+ my $text = dbh->sprintProfile();
+ #my $text = $wrapper->wrap( dbh->sprintProfile() );
+ $text =~ s/^/ /mg;
+
+ $profile = '<PRE>'.
+ encode_entities( $text ).
+ #"\n\n". &sprintAutoProfile(). '</PRE>';
+ "\n\n". '</PRE>';
+ }
+
+ dbh->{'private_profile'} = {};
+}
+
+s/(<\/BODY>[\s\n]*<\/HTML>[\s\n]*)$/$profile$1/i;
+</%filter>
+<%cleanup>
+ dbh->commit();
+</%cleanup>
diff --git a/httemplate/browse/access_group.html b/httemplate/browse/access_group.html
new file mode 100644
index 000000000..ca162a094
--- /dev/null
+++ b/httemplate/browse/access_group.html
@@ -0,0 +1,82 @@
+<% include( 'elements/browse.html',
+ 'title' => 'Internal Access Groups',
+ 'menubar' => [ # 'Main menu' => $p,
+ 'Internal users' => $p.'browse/access_user.html',
+ ],
+ 'html_init' => $html_init,
+ 'name' => 'internal access groups',
+ 'query' => { 'table' => 'access_group',
+ 'hashref' => {},
+ 'extra_sql' => 'ORDER BY groupname', #??
+ },
+ 'count_query' => $count_query,
+ 'header' => [ '#',
+ 'Group name',
+ 'Agents',
+ 'Rights',
+ ],
+ 'fields' => [ 'groupnum',
+ 'groupname',
+ $agents_sub,
+ $rights_sub,
+ ],
+ 'links' => [ $link,
+ $link,
+ '',
+ '',
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $html_init =
+ "Internal access groups control access to the back-office interface.<BR><BR>".
+ qq!<A HREF="${p}edit/access_group.html"><I>Add an internal access group</I></A><BR><BR>!;
+
+#false laziness w/access_user.html & agent_type.cgi
+my $agents_sub = sub {
+ my $access_group = shift;
+
+ [ map {
+ my $access_groupagent = $_;
+ my $agent = $access_groupagent->agent;
+ [
+ {
+ 'data' => $agent->agent,
+ 'align' => 'left',
+ 'link' => $p. 'edit/agent.cgi?'. $agent->agentnum,
+ },
+ ];
+ }
+ grep { $_->agent } #?
+ $access_group->access_groupagent,
+
+ ];
+
+};
+
+my $rights_sub = sub {
+ my $access_group = shift;
+
+ [ map { my $access_right = $_;
+ [
+ {
+ 'data' => $access_right->rightname,
+ 'align' => 'left',
+ },
+ ];
+ }
+ $access_group->access_rights,
+
+ ];
+
+};
+
+my $count_query = 'SELECT COUNT(*) FROM access_group';
+
+my $link = [ $p.'edit/access_group.html?', 'groupnum' ];
+
+</%init>
diff --git a/httemplate/browse/access_user.html b/httemplate/browse/access_user.html
new file mode 100644
index 000000000..8eb3e330a
--- /dev/null
+++ b/httemplate/browse/access_user.html
@@ -0,0 +1,102 @@
+<% include( 'elements/browse.html',
+ 'title' => 'Internal Users',
+ 'menubar' => [ #'Main menu' => $p,
+ 'Internal access groups' => $p.'browse/access_group.html',
+ ],
+ 'html_init' => $html_init,
+ 'html_posttotal' => $posttotal,
+ 'name' => 'internal users',
+ 'query' => { 'table' => 'access_user',
+ 'hashref' => \%search,
+ 'extra_sql' => 'ORDER BY last, first',
+ },
+ 'count_query' => $count_query,
+ 'header' => \@header,
+ 'fields' => \@fields,
+ 'links' => \@links,
+ 'style' => \@style,
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $html_init =
+ "Internal users have access to the back-office interface. Typically, this is your employees and contractors, but in a VISP setup, you can also add accounts for your reseller's employees. It is <B>highly recommended</B> to add a <B>separate account for each person</B> rather than using role accounts.<BR><BR>".
+ qq!<A HREF="${p}edit/access_user.html"><I>Add an internal user</I></A><BR><BR>!;
+
+#false laziness w/part_pkg.cgi
+my %search = ();
+my $search = '';
+unless ( $cgi->param('showdisabled') ) {
+ %search = ( 'disabled' => '' );
+ $search = "( disabled = '' OR disabled IS NULL )";
+}
+
+#false laziness w/access_group.html & agent_type.cgi
+my $groups_sub = sub {
+ my $access_user = shift;
+
+ [ map {
+ my $access_usergroup = $_;
+ my $access_group = $access_usergroup->access_group;
+ [
+ {
+ 'data' => $access_group->groupname,
+ 'align' => 'left',
+ 'link' =>
+ $p. 'edit/access_group.html?'. $access_usergroup->groupnum,
+ },
+ ];
+ }
+ grep { $_->access_group # and ! $_->access_group->disabled
+ }
+ $access_user->access_usergroup,
+
+ ];
+
+};
+
+my $posttotal;
+if ( $cgi->param('showdisabled') ) {
+ $cgi->param('showdisabled', 0);
+ $posttotal = '( <a href="'. $cgi->self_url. '">hide disabled users</a> )';
+ $cgi->param('showdisabled', 1);
+} else {
+ $cgi->param('showdisabled', 1);
+ $posttotal = '( <a href="'. $cgi->self_url. '">show disabled users</a> )';
+ $cgi->param('showdisabled', 0);
+}
+
+my $count_query = 'SELECT COUNT(*) FROM access_user';
+$count_query .= " WHERE $search"
+ if $search;
+
+my $link = [ $p.'edit/access_user.html?', 'usernum' ];
+
+my @header = ( '#', 'Username' );
+my @fields = ( 'usernum', 'username' );
+my $align = 'rl';
+my @links = ( $link, $link );
+my @style = ( '', '' );
+
+#false laziness w/part_pkg.cgi
+#unless ( $cgi->param('showdisabled') ) { #its been reversed already
+if ( $cgi->param('showdisabled') ) { #its been reversed already
+ push @header, 'Status';
+ push @fields, sub { shift->disabled
+ ? '<FONT COLOR="#FF0000">DISABLED</FONT>'
+ : '<FONT COLOR="#00CC00">Active</FONT>'
+ };
+ push @links, '';
+ $align .= 'c';
+ push @style, 'b';
+}
+
+push @header, 'Full name', 'Groups';
+push @fields, 'name', $groups_sub;
+push @links, $link, '';
+$align .= 'll';
+
+</%init>
diff --git a/httemplate/browse/addr_block.cgi b/httemplate/browse/addr_block.cgi
new file mode 100644
index 000000000..408d57298
--- /dev/null
+++ b/httemplate/browse/addr_block.cgi
@@ -0,0 +1,86 @@
+<% include("/elements/header.html",'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>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+</%init>
diff --git a/httemplate/browse/agent.cgi b/httemplate/browse/agent.cgi
new file mode 100755
index 000000000..063f259de
--- /dev/null
+++ b/httemplate/browse/agent.cgi
@@ -0,0 +1,374 @@
+<% include("/elements/header.html",'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> )'; }
+ %>
+% }
+
+
+<% include('/elements/table-grid.html') %>
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor = '';
+%
+
+
+<TR>
+ <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=<% ( $cgi->param('showdisabled') || !dbdef->table('agent')->column('disabled') ) ? 2 : 3 %>>Agent</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Type</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Customers</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>Customer<BR>packages</FONT></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Reports</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Registration codes</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Prepaid cards</TH>
+% if ( $conf->config('ticket_system') ) {
+
+ <TH CLASS="grid" BGCOLOR="#cccccc">Ticketing</TH>
+% }
+
+ <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>Payment Gateway Overrides</FONT></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>Freq.</FONT></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"><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;
+%
+% my $cust_pkg_link = $p. 'search/cust_pkg.cgi?agentnum='. $agent->agentnum;
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+%
+
+
+ <TR>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="<%$p%>edit/agent.cgi?<% $agent->agentnum %>">
+ <% $agent->agentnum %></A></TD>
+% if ( dbdef->table('agent')->column('disabled')
+% && !$cgi->param('showdisabled') ) {
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $agent->disabled ? 'DISABLED' : '' %></TD>
+% }
+
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="<%$p%>edit/agent.cgi?<% $agent->agentnum %>">
+ <% $agent->agent %></A></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="<%$p%>edit/agent_type.cgi?<% $agent->typenum %>"><% $agent->agent_type->atype %></A></TD>
+
+ <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+ <TABLE CLASS="inv" CELLSPACING=0 CELLPADDING=0>
+
+ <TR>
+ <TH ALIGN="right" WIDTH="40%">
+ <FONT COLOR="#7e0079">
+ <% my $num_prospect = $agent->num_prospect_cust_main %>&nbsp;
+ </FONT>
+ </TH>
+
+ <TD>
+% if ( $num_prospect ) {
+
+ <A HREF="<% $cust_main_link %>&prospect=1">
+% }
+prospects
+% if ($num_prospect ) {
+</A>
+% }
+
+ <TD>
+ </TR>
+
+ <TR>
+ <TH ALIGN="right" WIDTH="40%">
+ <FONT COLOR="#0000CC">
+ <% my $num_inactive = $agent->num_inactive_cust_main %>&nbsp;
+ </FONT>
+ </TH>
+
+ <TD>
+% if ( $num_inactive ) {
+
+ <A HREF="<% $cust_main_link %>&inactive=1">
+% }
+inactive
+% if ( $num_inactive ) {
+</A>
+% }
+
+ </TD>
+ </TR>
+
+ <TR>
+ <TH ALIGN="right" WIDTH="40%">
+ <FONT COLOR="#00CC00">
+ <% my $num_active = $agent->num_active_cust_main %>&nbsp;
+ </FONT>
+ </TH>
+
+ <TD>
+% if ( $num_active ) {
+
+ <A HREF="<% $cust_main_link %>&active=1">
+% }
+active
+% if ( $num_active ) {
+</A>
+% }
+
+ </TD>
+ </TR>
+
+ <TR>
+ <TH ALIGN="right" WIDTH="40%">
+ <FONT COLOR="#FF9900">
+ <% my $num_susp = $agent->num_susp_cust_main %>&nbsp;
+ </FONT>
+ </TH>
+
+ <TD>
+% if ( $num_susp ) {
+
+ <A HREF="<% $cust_main_link %>&suspended=1">
+% }
+suspended
+% if ( $num_susp ) {
+</A>
+% }
+
+ </TD>
+ </TR>
+
+ <TR>
+ <TH ALIGN="right" WIDTH="40%">
+ <FONT COLOR="#FF0000">
+ <% my $num_cancel = $agent->num_cancel_cust_main %>&nbsp;
+ </FONT>
+ </TH>
+
+ <TD>
+% if ( $num_cancel ) {
+
+ <A HREF="<% $cust_main_link %>&showcancelledcustomers=1&cancelled=1">
+% }
+cancelled
+% if ( $num_cancel ) {
+</A>
+% }
+
+ </TD>
+ </TR>
+
+ </TABLE>
+ </TD>
+
+ <TD CLASS="inv" BGCOLOR="<% $bgcolor %>" VALIGN="bottom">
+ <TABLE CLASS="inv" CELLSPACING=0 CELLPADDING=0>
+
+ <TR>
+ <TH ALIGN="right" WIDTH="40%">
+ <FONT COLOR="#0000CC">
+ <% my $num_inactive_pkg = $agent->num_inactive_cust_pkg %>&nbsp;
+ </FONT>
+ </TH>
+
+ <TD>
+% if ( $num_inactive_pkg ) {
+
+ <A HREF="<% $cust_pkg_link %>&magic=inactive">
+% }
+inactive
+% if ( $num_inactive_pkg ) {
+</A>
+% }
+
+ </TD>
+ </TR>
+
+ <TR>
+ <TH ALIGN="right" WIDTH="40%">
+ <FONT COLOR="#00CC00">
+ <% my $num_active_pkg = $agent->num_active_cust_pkg %>&nbsp;
+ </FONT>
+ </TH>
+
+ <TD>
+% if ( $num_active_pkg ) {
+
+ <A HREF="<% $cust_pkg_link %>&magic=active">
+% }
+active
+% if ( $num_active_pkg ) {
+</A>
+% }
+
+ </TD>
+ </TR>
+
+ <TR>
+ <TH ALIGN="right" WIDTH="40%">
+ <FONT COLOR="#FF9900">
+ <% my $num_susp_pkg = $agent->num_susp_cust_pkg %>&nbsp;
+ </FONT>
+
+ </TH>
+ <TD>
+% if ( $num_susp_pkg ) {
+
+ <A HREF="<% $cust_pkg_link %>&magic=suspended">
+% }
+suspended
+% if ( $num_susp_pkg ) {
+</A>
+% }
+
+ </TD>
+ </TR>
+
+ <TR>
+ <TH ALIGN="right" WIDTH="40%">
+ <FONT COLOR="#FF0000">
+ <% my $num_cancel_pkg = $agent->num_cancel_cust_pkg %>&nbsp;
+ </FONT>
+ </TH>
+
+ <TD>
+% if ( $num_cancel_pkg ) {
+
+ <A HREF="<% $cust_pkg_link %>&magic=cancelled">
+% }
+cancelled
+% if ( $num_cancel_pkg ) {
+</A>
+% }
+
+ </TD>
+ </TR>
+
+ </TABLE>
+ </TD>
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <A HREF="<% $p %>search/report_cust_pay.html?agentnum=<% $agent->agentnum %>">Payments</A>
+ <BR><A HREF="<% $p %>search/report_cust_credit.html?agentnum=<% $agent->agentnum %>">Credits</A>
+ <BR><A HREF="<% $p %>search/report_receivables.cgi?agentnum=<% $agent->agentnum %>">A/R Aging</A>
+ <!--<BR><A HREF="<% $p %>search/money_time.cgi?agentnum=<% $agent->agentnum %>">Sales/Credits/Receipts</A>-->
+
+ </TD>
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% my $num_reg_code = $agent->num_reg_code %>
+% if ( $num_reg_code ) {
+
+ <A HREF="<%$p%>search/reg_code.html?agentnum=<% $agent->agentnum %>">
+% }
+Unused
+% if ( $num_reg_code ) {
+</A>
+% }
+
+ <BR><A HREF="<%$p%>edit/reg_code.cgi?agentnum=<% $agent->agentnum %>">Generate codes</A>
+ </TD>
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% my $num_prepay_credit = $agent->num_prepay_credit %>
+% if ( $num_prepay_credit ) {
+
+ <A HREF="<%$p%>search/prepay_credit.html?agentnum=<% $agent->agentnum %>">
+% }
+Unused
+% if ( $num_prepay_credit ) {
+</A>
+% }
+
+ <BR><A HREF="<%$p%>edit/prepay_credit.cgi?agentnum=<% $agent->agentnum %>">Generate cards</A>
+ </TD>
+% if ( $conf->config('ticket_system') ) {
+
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+% if ( $agent->ticketing_queueid ) {
+
+ Queue: <% $agent->ticketing_queueid %>: <% $agent->ticketing_queue %><BR>
+% }
+
+ </TD>
+% }
+
+
+ <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+ <TABLE CLASS="inv" CELLSPACING=0 CELLPADDING=0>
+% foreach my $override (
+% # sort { } want taxclass-full stuff first? and default cards (empty cardtype)
+% qsearch('agent_payment_gateway', { 'agentnum' => $agent->agentnum } )
+% ) {
+%
+
+ <TR>
+ <TD>
+ <% $override->cardtype || 'Default' %> to <% $override->payment_gateway->gateway_module %> (<% $override->payment_gateway->gateway_username %>)
+ <% $override->taxclass
+ ? ' for '. $override->taxclass. ' only'
+ : ''
+ %>
+ <FONT SIZE=-1><A HREF="<%$p%>misc/delete-agent_payment_gateway.cgi?<% $override->agentgatewaynum %>">(delete)</A></FONT>
+ </TD>
+ </TR>
+% }
+
+ <TR>
+ <TD><FONT SIZE=-1><A HREF="<%$p%>edit/agent_payment_gateway.html?agentnum=<% $agent->agentnum %>">(add override)</A></FONT></TD>
+ </TR>
+ </TABLE>
+ </TD>
+
+<!--
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $agent->freq %></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $agent->prog %></TD>
+-->
+
+ </TR>
+% }
+
+
+ </TABLE>
+ </BODY>
+</HTML>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my %search;
+if ( $cgi->param('showdisabled')
+ || !dbdef->table('agent')->column('disabled') ) {
+ %search = ();
+} else {
+ %search = ( 'disabled' => '' );
+}
+
+my $conf = new FS::Conf;
+
+</%init>
diff --git a/httemplate/browse/agent_type.cgi b/httemplate/browse/agent_type.cgi
new file mode 100755
index 000000000..b4e4fcf99
--- /dev/null
+++ b/httemplate/browse/agent_type.cgi
@@ -0,0 +1,63 @@
+<% include( 'elements/browse.html',
+ 'title' => 'Agent Types',
+ 'menubar' => [ #'Main menu' => $p,
+ 'Agents' =>"${p}browse/agent.cgi",
+ ],
+ 'html_init' => $html_init,
+ 'name' => 'agent types',
+ 'query' => { 'table' => 'agent_type',
+ 'hashref' => {},
+ 'extra_sql' => 'ORDER BY typenum', # 'ORDER BY atype',
+ },
+ 'count_query' => $count_query,
+ 'header' => [ '#',
+ 'Agent Type',
+ 'Packages',
+ ],
+ 'fields' => [ 'typenum',
+ 'atype',
+ $packages_sub,
+ ],
+ 'links' => [ $link,
+ $link,
+ '',
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $html_init =
+'Agent types define groups of packages that you can then assign to'.
+' particular agents.<BR><BR>'.
+qq!<A HREF="${p}edit/agent_type.cgi"><I>Add a new agent type</I></A><BR><BR>!;
+
+my $count_query = 'SELECT COUNT(*) FROM agent_type';
+
+#false laziness w/access_user.html
+my $packages_sub = sub {
+my $agent_type = shift;
+
+[ map {
+ my $type_pkgs = $_;
+ #my $part_pkg = $type_pkgs->part_pkg;
+ [
+ {
+ #'data' => $part_pkg->pkg. ' - '. $part_pkg->comment,
+ 'data' => $type_pkgs->pkg. ' - '. $type_pkgs->comment,
+ 'align' => 'left',
+ 'link' => $p. 'edit/part_pkg.cgi?'. $type_pkgs->pkgpart,
+ },
+ ];
+ }
+
+ $agent_type->type_pkgs_enabled
+];
+
+};
+
+my $link = [ $p.'edit/agent_type.cgi?', 'typenum' ];
+
+</%init>
diff --git a/httemplate/browse/cust_main_county.cgi b/httemplate/browse/cust_main_county.cgi
new file mode 100755
index 000000000..3bbbb4b47
--- /dev/null
+++ b/httemplate/browse/cust_main_county.cgi
@@ -0,0 +1,170 @@
+<% include('/elements/header.html', "Tax Rate Listing", menubar(
+ 'Edit tax rates' => $p. "edit/cust_main_county.cgi",
+)) %>
+
+ 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.
+%
+%my $conf = new FS::Conf;
+%my $enable_taxclasses = $conf->exists('enable_taxclasses');
+%
+%if ( $enable_taxclasses ) {
+
+
+ <BR>Click on <u>expand taxclasses</u> to specify tax classes
+% }
+
+
+<BR><BR>
+<% table() %>
+
+ <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>
+%
+%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;
+%
+%
+
+ <TR>
+ <TD BGCOLOR="#ffffff"><% $hashref->{country} %></TD>
+%
+%
+% 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;
+% }
+%
+%
+
+
+ <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>!
+ %>
+% if ( $j>1 ) {
+
+ <FONT SIZE=-1><A HREF="<% $p %>edit/process/cust_main_county-collapse.cgi?<% $hashref->{taxnum} %>">collapse state</A></FONT>
+% }
+
+
+ </TD>
+% }
+% # $sup=$newsup;
+
+
+ <TD
+% if ( $hashref->{county} ) {
+%
+ BGCOLOR="#ffffff"><% $hashref->{county} %>
+% } else {
+%
+ BGCOLOR="#cccccc">(ALL)
+% if ( $hashref->{state} ) {
+
+ <FONT SIZE=-1><A HREF="<% $p %>edit/cust_main_county-expand.cgi?<% $hashref->{taxnum} %>">expand state</A></FONT>
+% }
+% }
+
+ </TD>
+
+ <TD
+% if ( $hashref->{taxclass} ) {
+%
+ BGCOLOR="#ffffff"><% $hashref->{taxclass} %>
+% } else {
+%
+ BGCOLOR="#cccccc">(ALL)
+% if ( $enable_taxclasses ) {
+
+ <FONT SIZE=-1><A HREF="<% $p %>edit/cust_main_county-expand.cgi?taxclass<% $hashref->{taxnum} %>">expand taxclasses</A></FONT>
+% }
+% }
+
+ </TD>
+
+ <TD
+% if ( $hashref->{taxname} ) {
+%
+ BGCOLOR="#ffffff"><% $hashref->{taxname} %>
+% } else {
+%
+ BGCOLOR="#cccccc">Tax
+% }
+
+ </TD>
+
+ <TD BGCOLOR="#ffffff"><% $hashref->{tax} %>%</TD>
+
+ <TD BGCOLOR="#ffffff">
+% if ( $hashref->{exempt_amount} > 0 ) {
+
+ $<% sprintf("%.2f", $hashref->{exempt_amount} ) %>&nbsp;per&nbsp;month<BR>
+% }
+% if ( $hashref->{setuptax} =~ /^Y$/i ) {
+
+ Setup&nbsp;fee<BR>
+% }
+% if ( $hashref->{recurtax} =~ /^Y$/i ) {
+
+ Recurring&nbsp;fee<BR>
+% }
+
+
+ </TD>
+
+ </TR>
+% }
+
+
+</TABLE>
+
+<% include('/elements/footer.html') %>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+</%init>
diff --git a/httemplate/browse/elements/browse.html b/httemplate/browse/elements/browse.html
new file mode 100644
index 000000000..2cc5a9660
--- /dev/null
+++ b/httemplate/browse/elements/browse.html
@@ -0,0 +1,6 @@
+<% include( '/search/elements/search.html',
+ @_,
+ 'disable_download' => 1,
+ 'disable_nonefound' => 1,
+ )
+%>
diff --git a/httemplate/browse/inventory_class.html b/httemplate/browse/inventory_class.html
new file mode 100644
index 000000000..8ce131ac2
--- /dev/null
+++ b/httemplate/browse/inventory_class.html
@@ -0,0 +1,93 @@
+<% include( 'elements/browse.html',
+ 'title' => 'Inventory Classes',
+ 'name' => 'inventory classes',
+ 'menubar' => [ 'Add a new inventory class' =>
+ $p.'edit/inventory_class.html',
+ ],
+ 'query' => { 'table' => 'inventory_class', },
+ 'count_query' => 'SELECT COUNT(*) FROM inventory_class',
+ 'header' => [ '#', 'Inventory class', 'Inventory' ],
+ 'fields' => [ 'classnum',
+ 'classname',
+ sub {
+ #my $inventory_class = shift;
+ my $i_c = shift;
+
+ my $link =
+ $p. 'search/inventory_item.html?'.
+ 'classnum='. $i_c->classnum;
+
+ my %actioncol = ();
+ foreach ( keys %inv_action_link ) {
+ my($label, $baseurl, $method) =
+ @{ $inv_action_link{$_} };
+ my $url = $baseurl. $i_c->$method();
+ $actioncol{$_} =
+ '<FONT SIZE="-1">'.
+ '('.
+ '<A HREF="'.$url.'">'.
+ $label.
+ '</A>'.
+ ')'.
+ '</FONT>';
+ }
+
+ my %num = map {
+ $_ => $i_c->$_();
+ } keys %labels;
+
+ [ map {
+ [
+ {
+ 'data' => '<B>'. $num{$_}. '</B>',
+ 'align' => 'right',
+ },
+ {
+ 'data' => $labels{$_},
+ 'align' => 'left',
+ 'link' => ( $num{$_}
+ ? $link.$link{$_}
+ : ''
+ ),
+ },
+ { 'data' => $actioncol{$_},
+ 'align' => 'left',
+ },
+ ]
+ } keys %labels
+ ];
+ },
+ ],
+ 'links' => [ $link,
+ $link,
+ '',
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+tie my %labels, 'Tie::IxHash',
+ 'num_avail' => 'Available', # <FONT SIZE="-1"><A HREF="eventually">(upload batch)</A></FONT>',
+ 'num_used' => 'In use', #'Used', #'Allocated',
+ 'num_total' => 'Total',
+;
+
+my %link = (
+ 'num_avail' => ';avail=1',
+ 'num_used' => ';used=1',
+ 'num_total' => '',
+);
+
+my %inv_action_link = (
+ 'num_avail' => [ 'upload batch',
+ $p.'misc/inventory_item-import.html?classnum=',
+ 'classnum'
+ ],
+);
+
+my $link = [ "${p}edit/inventory_class.html?", 'classnum' ];
+
+</%init>
diff --git a/httemplate/browse/msgcat.cgi b/httemplate/browse/msgcat.cgi
new file mode 100755
index 000000000..2c916dc9f
--- /dev/null
+++ b/httemplate/browse/msgcat.cgi
@@ -0,0 +1,44 @@
+<% include('/elements/header.html', "View Message catalog", menubar(
+ 'Edit message catalog' => $p. "edit/msgcat.cgi",
+)) %>
+<% $widget->html %>
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+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;
+ },
+
+);
+
+</%init>
diff --git a/httemplate/browse/nas.cgi b/httemplate/browse/nas.cgi
new file mode 100755
index 000000000..022c65ea7
--- /dev/null
+++ b/httemplate/browse/nas.cgi
@@ -0,0 +1,81 @@
+<!-- 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 000000000..682058b1d
--- /dev/null
+++ b/httemplate/browse/part_bill_event.cgi
@@ -0,0 +1,123 @@
+<% include("/elements/header.html",'Invoice Event Listing', menubar( 'Main Menu' => $p) ) %>
+
+ Invoice events are actions taken on open 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> )'; }
+%>
+<BR><BR>
+% tie my %payby, 'Tie::IxHash', FS::payby->cust_payby2longname;
+% tie my %freq, 'Tie::IxHash', '1d' => 'daily', '1m' => 'monthly';
+% foreach my $payby ( keys %payby ) {
+% my $oldfreq = '';
+%
+% my @payby_part_bill_event =
+% grep { $payby eq $_->payby }
+% sort { ( $a->freq || '1d') cmp ( $b->freq || '1d' ) # for now
+% || $a->seconds <=> $b->seconds
+% || $a->weight <=> $b->weight
+% || $a->eventpart <=> $b->eventpart
+% }
+% @part_bill_event;
+%
+%
+% if ( @payby_part_bill_event ) {
+
+
+ <% include('/elements/table-grid.html') %>
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor;
+%
+%
+% foreach my $part_bill_event ( @payby_part_bill_event ) {
+% my $url = "${p}edit/part_bill_event.cgi?". $part_bill_event->eventpart;
+% my $delay = duration_exact($part_bill_event->seconds);
+% ( my $plandata = $part_bill_event->plandata ) =~ s/\n/<BR>/go;
+% my $freq = $part_bill_event->freq || '1d';
+% my $reason = $part_bill_event->reasontext ;
+%
+% if ( $oldfreq ne $freq ) {
+
+
+ <TR>
+ <TH CLASS="grid" BGCOLOR="#999999" COLSPAN=<% $cgi->param('showdisabled') ? 7 : 8 %>><% ucfirst($freq{$freq}) %> event tests for <FONT SIZE="+1"><I><% $payby{$payby} %> customers</I></FONT></TH>
+ </TR>
+
+ <TR>
+ <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=<% $cgi->param('showdisabled') ? 2 : 3 %>>Event</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">After</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Action</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Reason</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Options</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Code</TH>
+ </TR>
+%
+% $oldfreq = $freq;
+% $bgcolor = '';
+%
+% }
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+
+
+ <TR>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="<% $url %>">
+ <% $part_bill_event->eventpart %></A></TD>
+% unless ( $cgi->param('showdisabled') ) {
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $part_bill_event->disabled ? 'DISABLED' : '' %></TD>
+% }
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="<% $url %>">
+ <% $part_bill_event->event %></A></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $delay %></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $part_bill_event->plan %></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $reason %></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $plandata %></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><FONT SIZE="-1">
+ <% $part_bill_event->eventcode %></FONT></TD>
+ </TR>
+% }
+
+ </TABLE>
+ <BR><BR>
+% }
+% }
+
+
+</BODY>
+</HTML>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+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);
+
+</%init>
diff --git a/httemplate/browse/part_export.cgi b/httemplate/browse/part_export.cgi
new file mode 100755
index 000000000..7b8ac8c20
--- /dev/null
+++ b/httemplate/browse/part_export.cgi
@@ -0,0 +1,44 @@
+<% include("/elements/header.html","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>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+</%init>
diff --git a/httemplate/browse/part_pkg.cgi b/httemplate/browse/part_pkg.cgi
new file mode 100755
index 000000000..6b62ec67b
--- /dev/null
+++ b/httemplate/browse/part_pkg.cgi
@@ -0,0 +1,261 @@
+<% include( 'elements/browse.html',
+ 'title' => 'Package Definitions',
+ 'menubar' => [ 'Main Menu' => $p ],
+ 'html_init' => $html_init,
+ 'html_posttotal' => $posttotal,
+ 'name' => 'package definitions',
+ 'query' => { 'select' => $select,
+ 'table' => 'part_pkg',
+ 'hashref' => \%search,
+ 'extra_sql' => "ORDER BY $orderby",
+ },
+ 'count_query' => $count_query,
+ 'header' => \@header,
+ 'fields' => \@fields,
+ 'links' => \@links,
+ 'align' => $align,
+ 'style' => \@style,
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+#false laziness w/access_user.html
+my %search = ();
+my $search = '';
+unless ( $cgi->param('showdisabled') ) {
+ %search = ( 'disabled' => '' );
+ $search = "( disabled = '' OR disabled IS NULL )";
+}
+
+my $select = '*';
+my $orderby = 'pkgpart';
+if ( $cgi->param('active') ) {
+
+ $orderby = 'num_active DESC';
+}
+ $select = "
+
+ *,
+
+ ( SELECT COUNT(*) FROM cust_pkg WHERE cust_pkg.pkgpart = part_pkg.pkgpart
+ AND ( cancel IS NULL OR cancel = 0 )
+ AND ( susp IS NULL OR susp = 0 )
+ ) AS num_active,
+
+ ( SELECT COUNT(*) FROM cust_pkg WHERE cust_pkg.pkgpart = part_pkg.pkgpart
+ AND ( cancel IS NULL OR cancel = 0 )
+ AND susp IS NOT NULL AND susp != 0
+ ) AS num_suspended,
+
+ ( SELECT COUNT(*) FROM cust_pkg WHERE cust_pkg.pkgpart = part_pkg.pkgpart
+ AND cancel IS NOT NULL AND cancel != 0
+ ) AS num_cancelled
+
+ ";
+
+#}
+
+my $conf = new FS::Conf;
+my $taxclasses = $conf->exists('enable_taxclasses');
+
+my $html_init;
+#unless ( $cgi->param('active') ) {
+ $html_init = qq!
+ 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>
+ !;
+#}
+
+my $posttotal;
+if ( $cgi->param('showdisabled') ) {
+ $cgi->param('showdisabled', 0);
+ $posttotal = '( <a href="'. $cgi->self_url. '">hide disabled packages</a> )';
+ $cgi->param('showdisabled', 1);
+} else {
+ $cgi->param('showdisabled', 1);
+ $posttotal = '( <a href="'. $cgi->self_url. '">show disabled packages</a> )';
+ $cgi->param('showdisabled', 0);
+}
+
+
+# ------
+
+my $link = [ $p.'edit/part_pkg.cgi?', 'pkgpart' ];
+
+my @header = ( '#', 'Package', 'Comment' );
+my @fields = ( 'pkgpart', 'pkg', 'comment' );
+my $align = 'rll';
+my @links = ( $link, $link, '' );
+my @style = ( '', '', '' );
+
+#false laziness w/access_user.html
+#unless ( $cgi->param('showdisabled') ) { #its been reversed already
+if ( $cgi->param('showdisabled') ) { #its been reversed already
+ push @header, 'Status';
+ push @fields, sub { shift->disabled
+ ? '<FONT COLOR="#FF0000">DISABLED</FONT>'
+ : '<FONT COLOR="#00CC00">Active</FONT>'
+ };
+ push @links, '';
+ $align .= 'c';
+ push @style, 'b';
+}
+
+unless ( 0 ) { #already showing only one class or something?
+ push @header, 'Class';
+ push @fields, sub { shift->classname || '(none)'; };
+ $align .= 'l';
+}
+
+#if ( $cgi->param('active') ) {
+ push @header, 'Customer<BR>packages';
+ my %col = (
+ 'active' => '00CC00',
+ 'suspended' => 'FF9900',
+ 'cancelled' => 'FF0000',
+ #'one-time charge' => '000000',
+ 'charge' => '000000',
+ );
+ my $cust_pkg_link = $p. 'search/cust_pkg.cgi?pkgpart=';
+ push @fields, sub { my $part_pkg = shift;
+ [
+ map {
+ my $magic = $_;
+ my $label = $_;
+ if ( $magic eq 'active' && $part_pkg->freq == 0 ) {
+ $magic = 'inactive';
+ #$label = 'one-time charge',
+ $label = 'charge',
+ }
+
+ [
+ {
+ 'data' => '<B><FONT COLOR="#'. $col{$label}. '">'.
+ $part_pkg->get("num_$_").
+ '</FONT></B>',
+ 'align' => 'right',
+ },
+ {
+ 'data' => $label.
+ ( $part_pkg->get("num_$_") != 1
+ && $label =~ /charge$/
+ ? 's'
+ : ''
+ ),
+ 'align' => 'left',
+ 'link' => ( $part_pkg->get("num_$_")
+ ? $cust_pkg_link.
+ $part_pkg->pkgpart.
+ ";magic=$magic"
+ : ''
+ ),
+ },
+ ],
+ } (qw( active suspended cancelled ))
+ ]; };
+ $align .= 'r';
+#}
+
+push @header, 'Frequency';
+push @fields, sub { shift->freq_pretty; };
+$align .= 'l';
+
+if ( $taxclasses ) {
+ push @header, 'Taxclass';
+ push @fields, sub { shift->taxclass() || '&nbsp;'; };
+ $align .= 'l';
+}
+
+push @header, 'Plan',
+ 'Data',
+ 'Services';
+ #'Service', 'Quan', 'Primary';
+
+push @fields, sub { shift->plan || '(legacy)' },
+
+ sub {
+ my $part_pkg = shift;
+ if ( $part_pkg->plan ) {
+
+ [ map {
+ /^(\w+)=(.*)$/; #or something;
+ [
+ { 'data' => $1,
+ 'align' => 'right',
+ },
+ { 'data' => $2,
+ 'align' => 'left',
+ },
+ ];
+ }
+ split(/\n/, $part_pkg->plandata)
+ ];
+
+ } else {
+
+ [ map { [
+ { 'data' => uc($_),
+ 'align' => 'right',
+ },
+ {
+ 'data' => $part_pkg->$_(),
+ 'align' => 'left',
+ },
+ ];
+ }
+ (qw(setup recur))
+ ];
+
+ }
+
+ },
+
+ sub {
+ my $part_pkg = shift;
+
+ [ map {
+ my $pkg_svc = $_;
+ my $part_svc = $pkg_svc->part_svc;
+ my $svc = $part_svc->svc;
+ if ( $pkg_svc->primary_svc =~ /^Y/i ) {
+ $svc = "<B>$svc (PRIMARY)</B>";
+ }
+ $svc =~ s/ +/&nbsp;/g;
+
+ [
+ {
+ 'data' => '<B>'. $pkg_svc->quantity. '</B>',
+ 'align' => 'right'
+ },
+ {
+ 'data' => $svc,
+ 'align' => 'left',
+ 'link' => $p. 'edit/part_svc.cgi?'.
+ $part_svc->svcpart,
+ },
+ ];
+ }
+ sort { $b->primary_svc =~ /^Y/i
+ <=> $a->primary_svc =~ /^Y/i
+ }
+ $part_pkg->pkg_svc
+
+ ];
+
+ };
+
+$align .= 'lrl'; #rr';
+
+# --------
+
+my $count_query = 'SELECT COUNT(*) FROM part_pkg';
+$count_query .= " WHERE $search"
+ if $search;
+
+</%init>
diff --git a/httemplate/browse/part_referral.html b/httemplate/browse/part_referral.html
new file mode 100755
index 000000000..065d8c1c1
--- /dev/null
+++ b/httemplate/browse/part_referral.html
@@ -0,0 +1,147 @@
+<% include("/elements/header.html","Advertising source Listing" ) %>
+
+Where a customer heard about your service. Tracked for informational purposes.
+<BR><BR>
+
+<A HREF="<% $p %>edit/part_referral.html"><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 $curuser = $FS::CurrentUser::CurrentUser;
+%
+% my $statement = "SELECT COUNT(*) FROM h_cust_main
+% WHERE history_action = 'insert'
+% AND refnum = ?
+% AND history_date >= ?
+% AND history_date < ?
+% AND ". $curuser->agentnums_sql;
+% my $sth = dbh->prepare($statement)
+% or die dbh->errstr;
+%
+% my $show_agentnums = scalar($curuser->agentnums);
+%
+%
+
+
+<% include('/elements/table-grid.html') %>
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor = '';
+%
+
+
+<TR>
+ <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=2 ROWSPAN=2>Advertising source</TH>
+% if ( $show_agentnums ) {
+
+ <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Agent</TH>
+% }
+
+ <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=<% scalar(keys %after) %>>Customers</TH>
+</TR>
+% for my $period ( keys %after ) {
+
+ <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1><% $period %></FONT></TH>
+% }
+
+</TR>
+%
+%foreach my $part_referral ( FS::part_referral->all_part_referral(1) ) {
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+% $a = 0;
+%
+%
+
+ <TR>
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+% if ( $part_referral->agentnum || $curuser->access_right('Edit global advertising sources') ) {
+% $a++;
+%
+
+ <A HREF="<% $p %>edit/part_referral.html?<% $part_referral->refnum %>">
+% }
+
+ <% $part_referral->refnum %><% $a ? '</A>' : '' %></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+% if ( $a ) {
+
+ <A HREF="<% $p %>edit/part_referral.html?<% $part_referral->refnum %>">
+% }
+
+ <% $part_referral->referral %><% $a ? '</A>' : '' %></TD>
+% if ( $show_agentnums ) {
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $part_referral->agentnum ? $part_referral->agent->agent : '(global)' %></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 CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right"><% $number %></TD>
+% }
+
+ </TR>
+% }
+%
+% $statement =~ s/AND refnum = \?//;
+% $sth = dbh->prepare($statement)
+% or die dbh->errstr;
+%
+
+ <TR>
+ <TD BGCOLOR="#dddddd" ALIGN="center" COLSPAN=3><B>Total</B></TD>
+% for my $period ( keys %after ) {
+% $sth->execute( $today-$after{$period},
+% $today+$before{$period},
+% ) or die $sth->errstr;
+% my $number = $sth->fetchrow_arrayref->[0];
+%
+
+ <TD BGCOLOR="#dddddd" ALIGN="right"><B><% $number %><B></TD>
+% }
+
+ </TR>
+ </TABLE>
+ </BODY>
+</HTML>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration')
+ || $FS::CurrentUser::CurrentUser->access_right('Edit advertising sources')
+ || $FS::CurrentUser::CurrentUser->access_right('Edit global advertising sources');
+</%init>
diff --git a/httemplate/browse/part_svc.cgi b/httemplate/browse/part_svc.cgi
new file mode 100755
index 000000000..369495571
--- /dev/null
+++ b/httemplate/browse/part_svc.cgi
@@ -0,0 +1,212 @@
+<% include("/elements/header.html",'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> )'; }
+%>
+% $cgi->param('showdisabled', ( 1 ^ $cgi->param('showdisabled') ) );
+
+<% include('/elements/table-grid.html') %>
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor = '';
+
+ <TR>
+
+ <TH CLASS="grid" BGCOLOR="#cccccc"><A HREF="<% do { $cgi->param('orderby', 'svcpart'); $cgi->self_url } %>">#</A></TH>
+
+% if ( $cgi->param('showdisabled') ) {
+ <TH CLASS="grid" BGCOLOR="#cccccc">Status</TH>
+% }
+
+ <TH CLASS="grid" BGCOLOR="#cccccc"><A HREF="<% do { $cgi->param('orderby', 'svc'); $cgi->self_url; } %>">Service</A></TH>
+
+ <TH CLASS="grid" BGCOLOR="#cccccc">Table</TH>
+
+ <TH CLASS="grid" BGCOLOR="#cccccc"><A HREF="<% do { $cgi->param('orderby', 'active'); $cgi->self_url; } %>"><FONT SIZE=-1>Customer<BR>Services</FONT></A></TH>
+
+ <TH CLASS="grid" BGCOLOR="#cccccc">Export</TH>
+
+ <TH CLASS="grid" BGCOLOR="#cccccc">Field</TH>
+
+ <TH COLSPAN=2 CLASS="grid" BGCOLOR="#cccccc">Modifier</TH>
+
+ </TR>
+
+% foreach my $part_svc ( @part_svc ) {
+% my $svcdb = $part_svc->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?". $part_svc->svcpart;
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+
+
+ <TR>
+
+ <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <A HREF="<% $url %>"><% $part_svc->svcpart %></A>
+ </TD>
+
+% if ( $cgi->param('showdisabled') ) {
+ <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $part_svc->disabled
+ ? '<FONT COLOR="#FF0000"><B>Disabled</B></FONT>'
+ : '<FONT COLOR="#00CC00"><B>Enabled</B></FONT>'
+ %>
+ </TD>
+% }
+
+ <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="<% $url %>">
+ <% $part_svc->svc %></A></TD>
+
+ <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $svcdb %></TD>
+
+ <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <FONT COLOR="#00CC00"><B><% $num_active_cust_svc{$part_svc->svcpart} %></B></FONT>&nbsp;<% $num_active_cust_svc{$part_svc->svcpart} ? FS::UI::Web::svc_url( 'ahref' => 1, 'm' => $m, 'action' => 'search', 'part_svc' => $part_svc, 'query' => "svcpart=". $part_svc->svcpart ) : '<A NAME="zero">' %>active</A>
+
+% if ( $num_active_cust_svc{$part_svc->svcpart} ) {
+ <BR><FONT SIZE="-1">[ <A HREF="<%$p%>edit/bulk-cust_svc.html?svcpart=<% $part_svc->svcpart %>">change</A> ]</FONT>
+% }
+
+ </TD>
+
+ <TD ROWSPAN=<% $rowspan %> CLASS="inv" BGCOLOR="<% $bgcolor %>">
+ <TABLE CLASS="inv">
+%
+%# 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>
+
+% unless ( @fields ) {
+% for ( 1..3 ) {
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"</TD>
+% }
+% }
+%
+% my($n1)='';
+% foreach my $field ( @fields ) {
+% my $flag = $part_svc->part_svc_column($field)->columnflag;
+%
+
+ <% $n1 %>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $field %></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $flag{$flag} %></TD>
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+% my $value = $part_svc->part_svc_column($field)->columnvalue;
+% if ( $flag =~ /^[MA]$/ ) {
+% $inventory_class{$value}
+% ||= qsearchs('inventory_class', { 'classnum' => $value } );
+%
+
+ <% $inventory_class{$value}
+ ? $inventory_class{$value}->classname
+ : "WARNING: inventory_class.classnum $value not found" %>
+% } else {
+
+ <% $value %>
+% }
+
+ </TD>
+% $n1="</TR><TR>";
+% }
+%
+
+ </TR>
+% }
+
+</TABLE>
+</BODY>
+</HTML>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+#code duplication w/ edit/part_svc.cgi, should move this hash to part_svc.pm
+my %flag = (
+ '' => '',
+ 'D' => 'Default',
+ 'F' => 'Fixed (unchangeable)',
+ 'S' => 'Selectable choice',
+ #'M' => 'Manual selection from inventory',
+ 'M' => 'Manual selected from inventory',
+ #'A' => 'Automatically fill in from inventory',
+ 'A' => 'Automatically filled in from inventory',
+ 'X' => 'Excluded',
+);
+
+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 = map { $_->svcpart => $_->num_cust_svc } @part_svc;
+
+if ( $cgi->param('orderby') eq 'active' ) {
+ @part_svc = sort { $num_active_cust_svc{$b->svcpart} <=>
+ $num_active_cust_svc{$a->svcpart} } @part_svc;
+} elsif ( $cgi->param('orderby') eq 'svc' ) {
+ @part_svc = sort { lc($a->svc) cmp lc($b->svc) } @part_svc;
+}
+
+my %inventory_class = ();
+
+</%init>
diff --git a/httemplate/browse/part_virtual_field.cgi b/httemplate/browse/part_virtual_field.cgi
new file mode 100644
index 000000000..2e12603a0
--- /dev/null
+++ b/httemplate/browse/part_virtual_field.cgi
@@ -0,0 +1,46 @@
+<% include("/elements/header.html",'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>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+</%init>
diff --git a/httemplate/browse/payment_gateway.html b/httemplate/browse/payment_gateway.html
new file mode 100644
index 000000000..720858e9b
--- /dev/null
+++ b/httemplate/browse/payment_gateway.html
@@ -0,0 +1,79 @@
+<% include("/elements/header.html",'Payment gateways', menubar(
+ 'Main Menu' => $p,
+ 'Agents' => $p. 'browse/agent.cgi',
+)) %>
+
+<A HREF="<% $p %>edit/payment_gateway.html"><I>Add a new payment gateway</I></A><BR><BR>
+
+<% $cgi->param('showdisabled')
+ ? do { $cgi->param('showdisabled', 0);
+ '( <a href="'. $cgi->self_url. '">hide disabled gateways</a> )'; }
+ : do { $cgi->param('showdisabled', 1);
+ '( <a href="'. $cgi->self_url. '">show disabled gateways</a> )'; }
+%>
+
+<% table() %>
+<TR>
+ <TH COLSPAN=<% $cgi->param('showdisabled') ? 1 : 2 %>>#</TH>
+ <TH>Gateway</TH>
+ <TH>Username</TH>
+ <TH>Password</TH>
+ <TH>Action</TH>
+ <TH>Options</TH>
+</TR>
+% foreach my $payment_gateway ( qsearch( 'payment_gateway', \%search ) ) {
+
+
+ <TR>
+ <TD><% $payment_gateway->gatewaynum %></TD>
+% if ( !$cgi->param('showdisabled') ) {
+
+ <TD><% $payment_gateway->disabled ? 'DISABLED' : '' %></TD>
+% }
+
+ <TD><% $payment_gateway->gateway_module %>
+ <FONT SIZE="-1">
+ <A HREF="<%$p%>edit/payment_gateway.html?<% $payment_gateway->gatewaynum %>">(edit)</A>
+ <% !$payment_gateway->disabled
+ ? '<A HREF="'. $p. 'misc/disable-payment_gateway.cgi?'. $payment_gateway->gatewaynum.'">(disable)</A>'
+ : ''
+ %>
+ </FONT>
+ </TD>
+ <TD><% $payment_gateway->gateway_username %></TD>
+ <TD> - </TD>
+ <TD><% $payment_gateway->gateway_action %></TD>
+ <TD>
+ <TABLE CELLSPACING=0 CELLPADDING=0>
+% my %options = $payment_gateway->options;
+% foreach my $option ( keys %options ) {
+%
+
+ <TR>
+ <TH><% $option %>:</TH>
+ <TD><% $options{$option} %></TD>
+ </TR>
+% }
+
+ </TABLE>
+ </TD>
+ </TR>
+% }
+
+
+</TABLE>
+</BODY>
+</HTML>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my %search;
+if ( $cgi->param('showdisabled') ) {
+ %search = ();
+} else {
+ %search = ( 'disabled' => '' );
+}
+
+</%init>
diff --git a/httemplate/browse/pkg_class.html b/httemplate/browse/pkg_class.html
new file mode 100644
index 000000000..886029df5
--- /dev/null
+++ b/httemplate/browse/pkg_class.html
@@ -0,0 +1,30 @@
+<% include( 'elements/browse.html',
+ 'title' => 'Package classes',
+ 'html_init' => $html_init,
+ 'name' => 'package classes',
+ 'disableable' => 1,
+ 'query' => { 'table' => 'pkg_class',
+ 'hashref' => {},
+ 'extra_sql' => 'ORDER BY classnum',
+ },
+ 'count_query' => $count_query,
+ 'header' => [ '#', 'Class', ],
+ 'fields' => [ 'classnum', 'classname' ],
+ 'links' => [ $link, $link ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $html_init =
+ 'Package classes define groups of packages, for reporting and '.
+ 'convenience purposes.<BR><BR>'.
+ qq!<A HREF="${p}edit/pkg_class.html"><I>Add a package class</I></A><BR><BR>!;
+
+my $count_query = 'SELECT COUNT(*) FROM pkg_class';
+
+my $link = [ $p.'edit/pkg_class.html?', 'classnum' ];
+
+</%init>
diff --git a/httemplate/browse/rate.cgi b/httemplate/browse/rate.cgi
new file mode 100644
index 000000000..584891aea
--- /dev/null
+++ b/httemplate/browse/rate.cgi
@@ -0,0 +1,37 @@
+<% include( 'elements/browse.html',
+ 'title' => 'Rate plans',
+ 'menubar' => [ 'Main menu' => $p, ],
+ 'html_init' => $html_init,
+ 'name' => 'rate plans',
+ 'query' => { 'table' => 'rate',
+ 'hashref' => {},
+ 'extra_sql' => 'ORDER BY ratenum',
+ },
+ 'count_query' => $count_query,
+ 'header' => [ '#', 'Rate plan', ],
+ 'fields' => [ 'ratenum', 'ratename' ],
+ 'links' => [ $link, $link ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $html_init =
+'Rate plans, regions and prefixes for VoIP and call billing.<BR><BR>'.
+qq!<A HREF="${p}edit/rate.cgi"><I>Add a rate plan</I></A>!.
+qq! | <A HREF="${p}edit/rate_region.cgi"><I>Add a region</I></A>!.
+'<BR><BR>
+ <SCRIPT>
+ function rate_areyousure(href) {
+ if (confirm("Are you sure you want to delete this rate plan?") == true)
+ window.location.href = href;
+ }
+ </SCRIPT>';
+
+my $count_query = 'SELECT COUNT(*) FROM rate';
+
+my $link = [ $p.'edit/rate.cgi?', 'ratenum' ];
+
+</%init>
diff --git a/httemplate/browse/reason.html b/httemplate/browse/reason.html
new file mode 100644
index 000000000..b017f8f58
--- /dev/null
+++ b/httemplate/browse/reason.html
@@ -0,0 +1,68 @@
+<% include( 'elements/browse.html',
+ 'title' => ucfirst($classname) . ' Reasons',
+ 'menubar' => [ # 'Main menu' => $p,
+ ucfirst($classname) . ' Reason Types' =>
+ $p.'browse/reason_type.html?class='.
+ $class,
+ ],
+ 'html_init' => $html_init,
+ 'name' => $classname . ' reasons',
+ 'query' => { 'table' => 'reason',
+ 'hashref' => {},
+ 'extra_sql' => $where_clause .
+ 'ORDER BY reason_type',
+ 'addl_from' => 'LEFT JOIN reason_type ON reason_type.typenum = reason.reason_type',
+ },
+ 'count_query' => $count_query,
+ 'header' => [ '#',
+ ucfirst($classname) . ' Reason Type',
+ ucfirst($classname) . ' Reason',
+ ],
+ 'fields' => [ 'reasonnum',
+ sub { shift->reasontype->type },
+ 'reason',
+ ],
+ 'links' => [ $link,
+ $link,
+ '',
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+$cgi->param('class') =~ /^(\w)$/ or die "illegal class";
+my $class = $1;
+
+my %classmap = ( 'C' => 'cancel',
+ 'S' => 'suspend',
+ );
+
+my $classname = $classmap{$class};
+
+my $html_init = ucfirst($classname) .
+" reasons explain why we $classname a package.<BR><BR>".
+qq!<A HREF="${p}edit/reason.html?class=$class">!.
+"<I>Add a $classname reason</I></A><BR><BR>";
+
+my $where_clause = "WHERE class='$class'";
+$where_clause .= " AND (disabled = '' OR disabled IS NULL)"
+unless $cgi->param('showdisabled');
+
+my $disabledurl = $cgi->param('showdisabled')
+ ? do { $cgi->param('showdisabled', 0);
+ '( <a href="'. $cgi->self_url. '">hide disabled reasons</a> )'; }
+ : do { $cgi->param('showdisabled', 1);
+ '( <a href="'. $cgi->self_url. '">show disabled reasons</a> )'; }
+ ;
+
+$html_init .= $disabledurl;
+
+my $count_query = 'SELECT COUNT(*) FROM reason LEFT JOIN reason_type on ' .
+ 'reason_type.typenum = reason.reason_type ' . $where_clause;
+
+my $link = [ $p."edit/reason.html?class=$class&reasonnum=", 'reasonnum' ];
+
+</%init>
diff --git a/httemplate/browse/reason_type.html b/httemplate/browse/reason_type.html
new file mode 100644
index 000000000..09f451c9f
--- /dev/null
+++ b/httemplate/browse/reason_type.html
@@ -0,0 +1,72 @@
+<% include( 'elements/browse.html',
+ 'title' => ucfirst($classname) . " Reason Types",
+ 'menubar' => [ ucfirst($classname) . " reasons" =>
+ $p.'browse/reason.html?class=' . $class,
+ ],
+ 'html_init' => $html_init,
+ 'name' => $classname . " reason types",
+ 'query' => { 'table' => 'reason_type',
+ 'hashref' => {},
+ 'extra_sql' => $where_clause .
+ 'ORDER BY typenum',
+ },
+ 'count_query' => $count_query,
+ 'header' => [ '#',
+ ucfirst($classname) . ' Reason Type',
+ ucfirst($classname) . ' Reasons',
+ ],
+ 'fields' => [ 'typenum',
+ 'type',
+ $reasons_sub,
+ ],
+ 'links' => [ $link,
+ $link,
+ '',
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+$cgi->param('class') =~ /^(\w)$/ or die "illegal class";
+my $class=$1;
+
+my %classmap = ( 'C' => 'cancel',
+ 'S' => 'suspend',
+ );
+
+my $classname = $classmap{$class};
+
+my $html_init = ucfirst($classname) .
+ " reason types allow groups of $classname reasons for reporting purposes." .
+ qq!<BR><BR><A HREF="${p}edit/reason_type.html?class=$class"><I>Add a ! .
+ $classname . " reason type</I></A><BR><BR>";
+
+my $reasons_sub = sub {
+ my $reason_type = shift;
+
+ [ map {
+ [
+ {
+ 'data' => $_->reason,
+ 'align' => 'left',
+ 'link' => $p. "edit/reason.html?class=$class&reasonnum=".
+ $_->reasonnum,
+ },
+ ];
+ }
+ $reason_type->enabled_reasons,
+
+ ];
+
+};
+
+my $where_clause = "WHERE class='$class'";
+my $count_query = 'SELECT COUNT(*) FROM reason_type ';
+$count_query .= $where_clause;
+
+my $link = [ $p.'edit/reason_type.html?class='.$class.'&typenum=', 'typenum' ];
+
+</%init>
diff --git a/httemplate/browse/router.cgi b/httemplate/browse/router.cgi
new file mode 100644
index 000000000..6dcd93a71
--- /dev/null
+++ b/httemplate/browse/router.cgi
@@ -0,0 +1,64 @@
+<% include("/elements/header.html",'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>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+</%init>
diff --git a/httemplate/browse/svc_acct_pop.cgi b/httemplate/browse/svc_acct_pop.cgi
new file mode 100755
index 000000000..306d02afb
--- /dev/null
+++ b/httemplate/browse/svc_acct_pop.cgi
@@ -0,0 +1,73 @@
+<% include("/elements/header.html",'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>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $accounts_sth = dbh->prepare("SELECT COUNT(*) FROM svc_acct
+ WHERE popnum = ? ")
+ or die dbh->errstr;
+
+</%init>
diff --git a/httemplate/config/config-process.cgi b/httemplate/config/config-process.cgi
new file mode 100644
index 000000000..d8f0d8e93
--- /dev/null
+++ b/httemplate/config/config-process.cgi
@@ -0,0 +1,62 @@
+<%init>
+
+die "access denied\n"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+# errant GET/POST protection
+my $Vars = scalar($cgi->Vars);
+my $num_Vars = scalar(keys %$Vars);
+die "only received $num_Vars params; errant or truncated GET/POST?".
+ " aborting - not updating config\n"
+ unless $num_Vars > 100;
+
+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' || $type eq 'select-sub' ) {
+ 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;
+}
+
+</%init>
+<% $cgi->redirect("config-view.cgi") %>
diff --git a/httemplate/config/config-view.cgi b/httemplate/config/config-view.cgi
new file mode 100644
index 000000000..91ba33769
--- /dev/null
+++ b/httemplate/config/config-view.cgi
@@ -0,0 +1,95 @@
+<% include("/elements/header.html",'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>
+% } elsif ( $type eq 'select-sub' ) {
+
+ <tr>
+ <td bgcolor="#ffffff">
+ <% $conf->config($i->key) %>:
+ <% &{ $i->option_sub }( $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>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+</%init>
diff --git a/httemplate/config/config.cgi b/httemplate/config/config.cgi
new file mode 100644
index 000000000..6c3a51aca
--- /dev/null
+++ b/httemplate/config/config.cgi
@@ -0,0 +1,263 @@
+<% include("/elements/header.html",'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 %hash = ();
+% if ( $i->select_enum ) {
+% tie %hash, 'Tie::IxHash',
+% '' => '', map { $_ => $_ } @{ $i->select_enum };
+% } elsif ( $i->select_hash ) {
+% if ( ref($i->select_hash) eq 'ARRAY' ) {
+% tie %hash, 'Tie::IxHash',
+% '' => '', @{ $i->select_hash };
+% } else {
+% tie %hash, 'Tie::IxHash',
+% '' => '', %{ $i->select_hash };
+% }
+% } else {
+% %hash = ( '' => 'WARNING: neither select_enum nor select_hash specified in Conf.pm for configuration option "'. $i->key. '"' );
+% }
+%
+% my %saw = ();
+% foreach my $value ( keys %hash ) {
+% local($^W)=0; next if $saw{$value}++;
+% my $label = $hash{$value};
+%
+
+
+ <option value="<% $value %>"<% $value eq $conf->config($i->key) || ( $type eq 'selectmultiple' && grep { $_ eq $value } $conf->config($i->key) ) ? ' SELECTED' : '' %>><% $label %>
+% }
+% my $curvalue = $conf->config($i->key);
+% if ( $conf->exists($i->key) && $curvalue
+% && ! $hash{$curvalue}
+% ) {
+%
+
+
+ <option value="<% $conf->config($i->key) %>" SELECTED><% exists( $hash{ $conf->config($i->key) } ) ? $hash{ $conf->config($i->key) } : $conf->config($i->key) %>
+% }
+
+
+ </select>
+% } elsif ( $type eq 'select-sub' ) {
+
+
+ <select name="<% $i->key. $n %>">
+ <option value="">
+% my %options = &{$i->options_sub};
+% my @options = sort { $a <=> $b } keys %options;
+% my %saw;
+% foreach my $value ( @options ) {
+% local($^W)=0; next if $saw{$value}++;
+%
+
+ <option value="<% $value %>"<% $value eq $conf->config($i->key) ? ' SELECTED' : '' %>><% $value %>: <% $options{$value} %>
+% }
+% if ( $conf->exists($i->key) && $conf->config($i->key) && ! exists $options{$conf->config($i->key)} ) {
+
+ <option value=<% $conf->config($i->key) %> SELECTED><% $conf->config($i->key) %>: <% &{ $i->option_sub }( $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>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+</%init>
diff --git a/httemplate/docs/ach.html b/httemplate/docs/ach.html
new file mode 100644
index 000000000..b8a17c87d
--- /dev/null
+++ b/httemplate/docs/ach.html
@@ -0,0 +1,10 @@
+<HTML>
+ <HEAD>
+ <TITLE>
+ Electronic check (ACH) information
+ </TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#ffffff">
+ <IMG BORDER=0 SRC="../images/ach.png">
+ </BODY>
+</HTML>
diff --git a/httemplate/docs/admin.html b/httemplate/docs/admin.html
new file mode 100755
index 000000000..2aa934812
--- /dev/null
+++ b/httemplate/docs/admin.html
@@ -0,0 +1,41 @@
+<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>Go 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</u> 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/cvv2.html b/httemplate/docs/cvv2.html
new file mode 100644
index 000000000..767098537
--- /dev/null
+++ b/httemplate/docs/cvv2.html
@@ -0,0 +1,24 @@
+<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>
+ </BODY>
+</HTML>
diff --git a/httemplate/docs/ieak.html b/httemplate/docs/ieak.html
new file mode 100644
index 000000000..00c53423c
--- /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 000000000..3b419de00
--- /dev/null
+++ b/httemplate/docs/index.html
@@ -0,0 +1,32 @@
+<head>
+ <title>Freeside Documentation</title>
+</head>
+<body bgcolor="#ffffff">
+ <h1>Freeside Documentation</h1>
+<img src="overview-new.png">
+<h3>Installation and upgrades</h3>
+<ul>
+ <li><a href="http://www.sisd.com/mediawiki/index.php/Freeside:1.7:Documentation:Installation">New Installation</a>
+ <li><a href="http://www.sisd.com/mediawiki/index.php/Freeside:1.7:Documentation:RT_Installation">Installing integrated RT ticketing</a>
+ <li><a href="http://www.sisd.com/mediawiki/index.php/Freeside:1.7:Documentation:Self-Service_Installation">Signup/Self-service installation</a>
+ <li><a href="http://www.sisd.com/mediawiki/index.php/Freeside:1.7:Documentation:Upgrading">Upgrading from 1.5.8 or 1.6.X</a>
+</ul>
+<h3>Configuration and setup</h3>
+<ul>
+<!--
+ <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="http://www.sisd.com/mediawiki/index.php/Freeside:1.7:Documentation:Administration#Exports_.28provisioning.29">Exports</a>
+ <li><a href="http://www.sisd.com/mediawiki/index.php/Freeside:1.7:Documentation:Administration#Billing">Billing</a>
+</ul>
+<h3>Developer</h3>
+<ul>
+ <li><a href="schema.html">Schema reference</a>
+ <li><a href="man/FS.html">Perl API</a>
+ <li><a href="legacy.html">Importing legacy data</a>
+</ul>
+</body>
diff --git a/httemplate/docs/legacy.html b/httemplate/docs/legacy.html
new file mode 100755
index 000000000..94efe53af
--- /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/overview-new.dia b/httemplate/docs/overview-new.dia
new file mode 100644
index 000000000..d9989a359
--- /dev/null
+++ b/httemplate/docs/overview-new.dia
Binary files differ
diff --git a/httemplate/docs/overview-new.png b/httemplate/docs/overview-new.png
new file mode 100644
index 000000000..bf815463b
--- /dev/null
+++ b/httemplate/docs/overview-new.png
Binary files differ
diff --git a/httemplate/docs/overview.dia b/httemplate/docs/overview.dia
new file mode 100644
index 000000000..a0e34c30e
--- /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 000000000..bf2dbc26c
--- /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 000000000..fc1dde956
--- /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 000000000..e00f59ce1
--- /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 000000000..cd4914a6c
--- /dev/null
+++ b/httemplate/docs/schema.html
@@ -0,0 +1,533 @@
+
+ <title>Schema reference</title>
+</head>
+<body>
+ <h1>Schema reference</h1>
+ Schema diagram (1.4.1): <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>billpkgnum - primary_key
+ <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="pay_batch" href="man/FS/pay_batch.html">pay_batch</a> - Pending batch
+ <ul>
+ <li>batchnum
+ <li>status
+ <li>download
+ <li>upload
+ </ul>
+ <li><a name="cust_pay_batch" href="man/FS/cust_pay_batch.html">cust_pay_batch</a> - Pending batch members
+ <ul>
+ <li>paybatchnum
+ <li>batchnum
+ <li>payby - CARD, CHEK, LECB, BILL, or COMP
+ <li>payinfo - account number
+ <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
+ <li>status
+ </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>promo_code - promotional code
+ <li><i>deprecated</i> setup - setup fee expression
+ <li>freq - recurring frequency (months)
+ <li><i>deprecated</i> 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><i>deprecated</i> plandata - additional price plan data
+ <li>disabled - Disabled flag, empty or `Y'
+ </ul>
+ <li><a name="part_pkg_option" href="man/FS/part_pkg_option.html">part_pkg_option</a> - Package definition options
+ <ul>
+ <li>optionnum - primary key
+ <li>pkgpart - <a href="#part_pkg">Package definition</a>
+ <li>optionname - option name
+ <li>optionvalue - option value
+ </ul>
+ <li><a name="reg_code" href="man/FS/reg_code.html">reg_code</A> - One-time registration codes
+ <ul>
+ <li>codenum - primary key
+ <li>code
+ <li>agentnum - <a href="#agent">Agent</a>
+ </ul>
+ <li><a name="reg_code_pkg" href="man/FS/reg_code_pkg.html">reg_code_pkg</A> - Registration code link to package definitions
+ <ul>
+ <li>codepkgnum - primary key
+ <li>codenum - <a href="#reg_code">Registration code</a>
+ <li>pkgpart - <a href="#part_pkg">Package definition</a>
+ </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>pkgsvcnum - primary key
+ <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> - prepaid cards
+ <ul>
+ <li>prepaynum - primary key
+ <li>identifier - text or numeric string of prepaid card
+ <li>amount - amount of prepayment
+ <li>seconds - prepaid time instead of (or in addition to) monetary value
+ <li>agentnum - optional agent assignment for prepaid cards
+ </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>typepkgnum - primary key
+ <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="rate" href="man/FS/rate.html">rate</a> - Call rate plans
+ <ul>
+ <li>ratenum - primary key
+ <li>ratename
+ </ul>
+ <li><a name="rate_detail" href="man/FS/rate_detail.html">rate_detail</a> - Call rate detail
+ <ul>
+ <li>ratedetailnum - primary key
+ <li>ratenum - <a href="#rate">rate plan</a>
+ <li>orig_regionnum - call origination <a href="#rate_region">region</a>
+ <li>dest_regionnum - call destination <a href="#rate_region">region</a>
+ <li>min_included - included minutes
+ <li>min_charge - charge per minute
+ <li>sec_granularity - granularity in seconds, i.e. 6 or 60
+ </ul>
+ <li><a name="rate_region" href="man/FS/rate_region.html">rate_region</a> - Call rate region
+ <ul>
+ <li>regionnum - primary key
+ <li>regionname
+ </ul>
+ <li><a name="rate_prefix" href="man/FS/rate_prefix.html">rate_prefix</a> - Call rate prefix
+ <ul>
+ <li>prefixnum - primary key
+ <li>regionnum - <a href="#rate_region">rate region</a>
+ <li>countrycode
+ <li>npa
+ <li>nxx
+ </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>
+ <li><a name="clientapi_session" href="man/FS/clientapi_session.html">clientapi_session</a> - ClientAPI session store
+ <ul>
+ <li>sessionnum - primary key
+ <li>sessionid - session ID
+ <li>namespace - session namespace
+ </ul>
+ <li><a name="clientapi_session_field" href="man/FS/clientapi_session_field.html">clientapi_session_field</a> - Client API session store data
+ <ul>
+ <li>fieldnum - primary key
+ <li>sessionnum - <a href="#session">session</a>
+ <li>fieldname
+ <li>fieldvalue
+ </ul>
+ </ul>
+</body>
diff --git a/httemplate/docs/schema.png b/httemplate/docs/schema.png
new file mode 100644
index 000000000..d0392e76f
--- /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 000000000..72e16424e
--- /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 000000000..97d7aa794
--- /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 000000000..d2c501e35
--- /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/edit/REAL_cust_pkg.cgi b/httemplate/edit/REAL_cust_pkg.cgi
new file mode 100755
index 000000000..69bbb9b22
--- /dev/null
+++ b/httemplate/edit/REAL_cust_pkg.cgi
@@ -0,0 +1,185 @@
+%
+%
+%my $error ='';
+%my $pkgnum = '';
+%if ( $cgi->param('error') ) {
+% $error = $cgi->param('error');
+% $pkgnum = $cgi->param('pkgnum');
+% if ( $error eq '_bill_areyousure' ) {
+% my $bill = $cgi->param('bill');
+% $error = "You are attempting to set the next bill date to $bill, which is
+% in the past. This will charge the customer for the interval
+% from $bill until now. Are you sure you want to do this? ".
+% '<INPUT TYPE="checkbox" NAME="bill_areyousure" VALUE="1">';
+% }
+%} 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')));
+% $cust_pkg->last_bill(str2time($cgi->param('last_bill')));
+%}
+%
+%#my $custnum = $cust_pkg->getfield('custnum');
+%
+
+
+<% include("/elements/header.html",'Customer package - Edit dates') %>
+%
+%#, 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');
+%
+%
+
+
+<FORM NAME="formname" ACTION="process/REAL_cust_pkg.cgi" METHOD="POST">
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
+% if ( $error ) {
+
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $error %></FONT>
+% }
+%
+%
+%#my $format = "%c %z (%Z)";
+%my $format = "%m/%d/%Y %T %z (%Z)";
+%
+%#false laziness w/view/cust_main/packages.html
+%#my( $billed_or_prepaid,
+%my( $last_bill_or_renewed, $next_bill_or_prepaid_until );
+%unless ( $part_pkg->is_prepaid ) {
+% #$billed_or_prepaid = 'billed';
+% $last_bill_or_renewed = 'Last bill';
+% $next_bill_or_prepaid_until = 'Next bill';
+%} else {
+% #$billed_or_prepaid = 'prepaid';
+% $last_bill_or_renewed = 'Renewed';
+% $next_bill_or_prepaid_until = 'Prepaid until';
+%}
+%
+%
+
+
+<% 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>
+
+ <TR>
+ <TD ALIGN="right"><% $last_bill_or_renewed %> 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>
+
+ <TR>
+ <TD ALIGN="right"><% $next_bill_or_prepaid_until %> 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>
+% if ( $susp ) {
+
+ <TR>
+ <TD ALIGN="right">Suspension date</TD>
+ <TD BGCOLOR="#ffffff"><% time2str($format, $susp) %></TD>
+ </TR>
+% }
+
+
+ <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>
+% if ( $cancel ) {
+
+ <TR>
+ <TD ALIGN="right">Cancellation date</TD>
+ <TD BGCOLOR="#ffffff"><% time2str($format, $cancel) %></TD>
+ </TR>
+% }
+
+
+</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/access_group.html b/httemplate/edit/access_group.html
new file mode 100644
index 000000000..d447512c2
--- /dev/null
+++ b/httemplate/edit/access_group.html
@@ -0,0 +1,46 @@
+<% include( 'elements/edit.html',
+ 'name' => 'Internal Access Group',
+ 'table' => 'access_group',
+ 'labels' => {
+ 'groupnum' => 'Group number',
+ 'groupname' => 'Group name',
+ },
+
+ 'viewall_dir' => 'browse',
+
+ 'html_bottom' =>
+ sub {
+ my $access_group = shift;
+
+ "<BR>Group virtualized to customers of agents:<BR>".
+ ntable("#cccccc",2).
+ '<TR><TD>'.
+ include( '/elements/checkboxes-table.html',
+ 'source_obj' => $access_group,
+ 'link_table' => 'access_groupagent',
+ 'target_table' => 'agent',
+ 'name_col' => 'agent',
+ 'target_link' => $p.'edit/agent.cgi?',
+ 'disable-able' => 1,
+ ).
+ '</TR></TD></TABLE>'.
+
+ "<BR>Group rights:<BR>".
+ ntable("#cccccc",2).
+ '<TR><TD>'.
+ include( '/elements/checkboxes-table-name.html',
+ 'source_obj' => $access_group,
+ 'link_table' => 'access_right',
+ 'link_static' => { 'righttype' =>
+ 'FS::access_group',
+ },
+ 'num_col' => 'rightobjnum',
+ 'name_col' => 'rightname',
+ 'names_list' => [ FS::AccessRight->rights() ],
+ ).
+ '</TR></TD></TABLE>'
+
+ ;
+ },
+ )
+%>
diff --git a/httemplate/edit/access_user.html b/httemplate/edit/access_user.html
new file mode 100644
index 000000000..065e60c4b
--- /dev/null
+++ b/httemplate/edit/access_user.html
@@ -0,0 +1,44 @@
+<% include( 'elements/edit.html',
+ 'name' => 'Internal User',
+ 'table' => 'access_user',
+ 'fields' => [
+ 'username',
+ { field=>'_password', type=>'password' },
+ { field=>'_password2', type=>'password' },
+ 'last',
+ 'first',
+ { field=>'disabled', type=>'checkbox', value=>'Y' },
+ ],
+ 'labels' => {
+ 'usernum' => 'User number',
+ 'username' => 'Username',
+ '_password' => 'Password',
+ '_password2'=> 'Re-enter Password',
+ 'last' => 'Last name',
+ 'first' => 'First name',
+ 'disabled' => 'Disable employee',
+ },
+ 'edit_callback' => sub { my( $c, $o ) = @_;
+ $o->set('_password', '');
+ },
+ 'viewall_dir' => 'browse',
+ 'html_bottom' =>
+ sub {
+ my $access_user = shift;
+
+ '<BR>Internal Access Groups<BR>'.
+ ntable("#cccccc",2).
+ '<TR><TD>'.
+ include( '/elements/checkboxes-table.html',
+ 'source_obj' => $access_user,
+ 'link_table' => 'access_usergroup',
+ 'target_table' => 'access_group',
+ 'name_col' => 'groupname',
+ 'target_link' => $p.'edit/access_group.html?',
+ #'disable-able' => 1,
+ ).
+ '</TR></TD></TABLE>'
+ ;
+ },
+ )
+%>
diff --git a/httemplate/edit/agent.cgi b/httemplate/edit/agent.cgi
new file mode 100755
index 000000000..ce514a680
--- /dev/null
+++ b/httemplate/edit/agent.cgi
@@ -0,0 +1,115 @@
+%
+%
+%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;
+%
+%my $conf = new FS::Conf;
+%
+%
+
+
+<% include("/elements/header.html","$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>
+
+ <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 ( $conf->config('ticket_system') ) {
+% my $default_queueid = $conf->config('ticket_system-default_queueid');
+% my $default_queue = FS::TicketSystem->queue($default_queueid);
+% $default_queue = "(default) $default_queueid: $default_queue"
+% if $default_queueid;
+% my %queues = FS::TicketSystem->queues();
+% my @queueids = sort { $a <=> $b } keys %queues;
+%
+
+ <TR>
+ <TD ALIGN="right">Ticketing queue</TD>
+ <TD>
+ <SELECT NAME="ticketing_queueid">
+ <OPTION VALUE=""><% $default_queue %>
+% foreach my $queueid ( @queueids ) {
+
+ <OPTION VALUE="<% $queueid %>" <% $agent->ticketing_queueid == $queueid ? ' SELECTED' : '' %>><% $queueid %>: <% $queues{$queueid} %>
+% }
+
+ </SELECT>
+ </TD>
+ </TR>
+% }
+
+
+ <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_payment_gateway.html b/httemplate/edit/agent_payment_gateway.html
new file mode 100644
index 000000000..08a2fa6bf
--- /dev/null
+++ b/httemplate/edit/agent_payment_gateway.html
@@ -0,0 +1,70 @@
+%
+%
+%$cgi->param('agentnum') =~ /(\d+)$/ or die "illegal agentnum";
+%my $agent = qsearchs('agent', { 'agentnum' => $1 } );
+%die "agentnum $1 not found" unless $agent;
+%
+%#my @agent_payment_gateway;
+%if ( $cgi->param('error') ) {
+%}
+%
+%my $action = 'Add';
+%
+%
+
+
+<% include("/elements/header.html","$action payment gateway override for ". $agent->agent, menubar(
+ 'Main Menu' => $p,
+ #'View all payment gateways' => $p. 'browse/payment_gateway.html',
+ 'View all agents' => $p. 'browse/agent.html',
+)) %>
+% if ( $cgi->param('error') ) {
+
+<FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+% }
+
+
+<FORM ACTION="<%popurl(1)%>process/agent_payment_gateway.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="agentnum" VALUE="<% $agent->agentnum %>">
+
+Use gateway <SELECT NAME="gatewaynum">
+% foreach my $payment_gateway (
+% qsearch('payment_gateway', { 'disabled' => '' } )
+% ) {
+%
+
+ <OPTION VALUE="<% $payment_gateway->gatewaynum %>"><% $payment_gateway->gateway_module %> (<% $payment_gateway->gateway_username %>)
+% }
+
+</SELECT>
+<BR><BR>
+
+for <SELECT NAME="cardtype" MULTIPLE>
+% foreach my $cardtype (
+% "",
+% "VISA card",
+% "MasterCard",
+% "Discover card",
+% "American Express card",
+% "Diner's Club/Carte Blanche",
+% "enRoute",
+% "JCB",
+% "BankCard",
+% "Switch",
+% "Solo",
+% 'ACH',
+%) {
+
+ <OPTION VALUE="<% $cardtype %>"><% $cardtype || '(Default fallback)' %>
+% }
+
+</SELECT>
+<BR><BR>
+
+(optional) when invoice contains only items of taxclass <INPUT TYPE="text" NAME="taxclass">
+<BR><BR>
+
+<INPUT TYPE="submit" VALUE="Add gateway override">
+</FORM>
+</BODY>
+</HTML>
diff --git a/httemplate/edit/agent_type.cgi b/httemplate/edit/agent_type.cgi
new file mode 100755
index 000000000..5438e5c3b
--- /dev/null
+++ b/httemplate/edit/agent_type.cgi
@@ -0,0 +1,57 @@
+%
+%
+%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';
+%
+%
+<% include("/elements/header.html","$action Agent Type", menubar(
+ 'Main Menu' => "$p",
+ 'View all agent types' => "${p}browse/agent_type.cgi",
+))
+%>
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+% }
+
+
+<FORM ACTION="<% popurl(1) %>process/agent_type.cgi" METHOD=POST>
+<INPUT TYPE="hidden" NAME="typenum" VALUE="<% $agent_type->typenum %>">
+Agent Type #<% $agent_type->typenum || "(NEW)" %>
+<BR>
+
+Agent Type
+<INPUT TYPE="text" NAME="atype" SIZE=32 VALUE="<% $agent_type->atype %>">
+<BR><BR>
+
+Select which packages agents of this type may sell to customers<BR>
+<% ntable("#cccccc", 2) %><TR><TD>
+<% include('/elements/checkboxes-table.html',
+ 'source_obj' => $agent_type,
+ 'link_table' => 'type_pkgs',
+ 'target_table' => 'part_pkg',
+ 'name_callback' => sub { $_[0]->pkg. ' - '. $_[0]->comment; },
+ 'target_link' => $p.'edit/part_pkg.cgi?',
+ 'disable-able' => 1,
+
+ )
+%>
+</TD></TR></TABLE>
+<BR>
+
+<INPUT TYPE="submit" VALUE="<% $agent_type->typenum ? "Apply changes" : "Add agent type" %>">
+
+ </FORM>
+
+<% include('/elements/footer.html') %>
diff --git a/httemplate/edit/bulk-cust_svc.html b/httemplate/edit/bulk-cust_svc.html
new file mode 100644
index 000000000..f2efc3ff9
--- /dev/null
+++ b/httemplate/edit/bulk-cust_svc.html
@@ -0,0 +1,99 @@
+<% include("/elements/header.html", 'Bulk customer service change',
+ menubar(
+ 'Main Menu' => $p,
+ ),
+ )
+%>
+
+<SCRIPT TYPE="text/javascript" SRC="../elements/overlibmws.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="../elements/overlibmws_iframe.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="../elements/overlibmws_draggable.js"></SCRIPT>
+
+<% include('/elements/progress-init.html',
+ 'OneTrueForm',
+ [qw( old_svcpart new_svcpart pkgpart )],
+ 'process/bulk-cust_svc.cgi',
+ $p.'browse/part_svc.cgi',
+ )
+%>
+
+<FORM NAME="OneTrueForm">
+%
+% $cgi->param('svcpart') =~ /^(\d+)$/
+% or die "illegal svcpart: ". $cgi->param('svcpart');
+%
+% my $old_svcpart = $1;
+% my $src_part_svc = qsearchs('part_svc', { 'svcpart' => $old_svcpart } )
+% or die "unknown svcpart: $old_svcpart";
+%
+
+
+<INPUT NAME="old_svcpart" TYPE="hidden" VALUE="<% $old_svcpart %>">
+Change <!-- customer
+<B><% $src_part_svc->svcpart %>: <% $src_part_svc->svc %></B> services
+<BR>
+-->
+
+<SELECT NAME="pkgpart">
+% my $num_cust_svc = $src_part_svc->num_cust_svc;
+% if ( $num_cust_svc > 1 ) {
+
+ <OPTION VALUE="">all <% $num_cust_svc %> <% $src_part_svc->svc %> services
+% } else {
+
+ <OPTION VALUE="">the <% $num_cust_svc %> <% $src_part_svc->svc %> service
+% }
+%
+% my $num_unlinked = $src_part_svc->num_cust_svc(0);
+% if ( $num_unlinked ) {
+%
+
+ <OPTION VALUE="0">the <% $num_unlinked %> unlinked <% $src_part_svc->svc %> services
+% }
+% foreach my $schwartz (
+% grep { $_->[1] }
+% map { [ $_, $src_part_svc->num_cust_svc($_->pkgpart) ] }
+% qsearch('part_pkg', {} )
+% ) {
+% my( $part_pkg, $num_cust_svc ) = @$schwartz;
+%
+
+ <OPTION VALUE="<% $part_pkg->pkgpart %>">the <% $num_cust_svc %>
+ <% $src_part_svc->svc %> service<% $num_cust_svc > 1 ? 's in' : ' in a' %>
+ <% $part_pkg->pkg %> package<% $num_cust_svc > 1 ? 's' : '' %>
+% }
+
+</SELECT>
+<BR>
+
+to new service definition
+<SELECT NAME="new_svcpart">
+% foreach my $dest_part_svc (
+% grep { $_->svcpart != $old_svcpart
+% && $_->svcdb eq $src_part_svc->svcdb
+% }
+% qsearch('part_svc', { 'disabled' => '' } )
+% ) {
+%
+
+ <OPTION VALUE="<% $dest_part_svc->svcpart %>"><% $dest_part_svc->svcpart %>: <% $dest_part_svc->svc %>
+% }
+
+</SELECT>
+<BR>
+
+<BR>
+
+<SCRIPT TYPE="text/javascript">
+var confirm_change = '<P ALIGN="center"><B>Bulk customer service change - Are you sure?</B><BR><P ALIGN="CENTER" <INPUT TYPE="button" VALUE="Yes, make changes" onClick="process();">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<INPUT TYPE="BUTTON" VALUE="Cancel" onClick="cClick()">';
+</SCRIPT>
+
+<INPUT TYPE="button" VALUE="Bulk change customer services" onClick="overlib(confirm_change, CAPTION, 'Confirm bulk customer service change', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH, 576, HEIGHT, 128, TEXTSIZE, 3, BGCOLOR, '#ff0000', CGCOLOR, '#ff0000' );">
+
+</FORM>
+
+</BODY>
+</HTML>
+
+
+
diff --git a/httemplate/edit/cust_bill_pay.cgi b/httemplate/edit/cust_bill_pay.cgi
new file mode 100755
index 000000000..498d477cd
--- /dev/null
+++ b/httemplate/edit/cust_bill_pay.cgi
@@ -0,0 +1,85 @@
+<% header("Apply Payment", '') %>
+
+% if ( $cgi->param('error') ) {
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+
+<FORM ACTION="<% $p1 %>process/cust_bill_pay.cgi" METHOD=POST>
+
+Payment #<B><% $paynum %></B>
+<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>
+
+<SCRIPT TYPE="text/javascript">
+function changed(what) {
+ cust_bill = what.options[what.selectedIndex].value;
+
+% foreach my $cust_bill ( @cust_bill ) {
+
+ if ( cust_bill == <% $cust_bill->invnum %> ) {
+ what.form.amount.value = "<% min($cust_bill->owed, $unapplied) %>";
+ }
+
+% }
+
+ if ( cust_bill == "Refund" ) {
+ what.form.amount.value = "<% $unapplied %>";
+ }
+}
+</SCRIPT>
+
+<BR>Invoice #<SELECT NAME="invnum" SIZE=1 onChange="changed(this)">
+<OPTION VALUE="">
+
+% foreach my $cust_bill ( @cust_bill ) {
+ <OPTION<% $cust_bill->invnum eq $invnum ? ' SELECTED' : '' %> VALUE="<% $cust_bill->invnum %>"><% $cust_bill->invnum %> - <% time2str("%D", $cust_bill->_date) %> - $<% $cust_bill->owed %>
+% }
+
+<OPTION VALUE="Refund">Refund
+</SELECT>
+
+<BR>Amount $<INPUT TYPE="text" NAME="amount" VALUE="<% $amount %>" SIZE=8 MAXLENGTH=8>
+
+<BR>
+<CENTER><INPUT TYPE="submit" VALUE="Apply"></CENTER>
+
+</FORM>
+</BODY>
+</HTML>
+
+<%init>
+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);
+
+my $cust_pay = qsearchs('cust_pay', { 'paynum' => $paynum } );
+die "payment $paynum not found!" unless $cust_pay;
+
+my $unapplied = $cust_pay->unapplied;
+
+my @cust_bill = sort { $a->_date <=> $b->_date
+ or $a->invnum <=> $b->invnum
+ }
+ grep { $_->owed != 0 }
+ qsearch('cust_bill', { 'custnum' => $cust_pay->custnum } );
+</%init>
+
diff --git a/httemplate/edit/cust_credit.cgi b/httemplate/edit/cust_credit.cgi
new file mode 100755
index 000000000..13d062c74
--- /dev/null
+++ b/httemplate/edit/cust_credit.cgi
@@ -0,0 +1,80 @@
+<% include('/elements/header-popup.html', 'Enter Credit') %>
+
+% if ( $cgi->param('error') ) {
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+
+<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 %>">
+
+Credit
+<% ntable("#cccccc", 2) %>
+
+ <TR>
+ <TD ALIGN="right">Date</TD>
+ <TD BGCOLOR="#ffffff"><% time2str("%D",$_date) %></TD>
+ </TR>
+
+ <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!;
+%
+
+ <TR>
+ <TD ALIGN="right">Reason</TD>
+ <TD BGCOLOR="#ffffff"><INPUT TYPE="text" NAME="reason" VALUE="<% $reason %>" SIZE=32></TD>
+ </TR>
+
+ <TR>
+ <TD ALIGN="right">Auto-apply<BR>to invoices</TD>
+ <TD><SELECT NAME="apply"><OPTION VALUE="yes" SELECTED>yes<OPTION>no</SELECT></TD>
+ </TR>
+
+</TABLE>
+
+<BR>
+
+<CENTER><INPUT TYPE="submit" VALUE="Enter credit"></CENTER>
+
+</FORM>
+</BODY>
+</HTML>
+
+<%once>
+my $conf = new FS::Conf;
+</%once>
+
+<%init>
+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);
+</%init>
diff --git a/httemplate/edit/cust_credit_bill.cgi b/httemplate/edit/cust_credit_bill.cgi
new file mode 100755
index 000000000..249ba31d0
--- /dev/null
+++ b/httemplate/edit/cust_credit_bill.cgi
@@ -0,0 +1,92 @@
+<% header("Apply Credit", '') %>
+
+% if ( $cgi->param('error') ) {
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+
+<FORM ACTION="<% $p1 %>process/cust_credit_bill.cgi" METHOD=POST>
+
+Credit #<B><% $crednum %></B>
+<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>
+
+<SCRIPT>
+function changed(what) {
+ cust_bill = what.options[what.selectedIndex].value;
+
+% foreach my $cust_bill ( @cust_bill ) {
+
+ if ( cust_bill == <% $cust_bill->invnum %> ) {
+ what.form.amount.value = "<% min($cust_bill->owed, $credited) %>";
+ }
+
+% }
+
+ if ( cust_bill == "Refund" ) {
+ what.form.amount.value = "<% $credited %>";
+ }
+}
+</SCRIPT>
+
+<BR>Invoice #<SELECT NAME="invnum" SIZE=1 onChange="changed(this)">
+<OPTION VALUE="">
+
+% foreach my $cust_bill ( @cust_bill ) {
+ <OPTION<% $cust_bill->invnum eq $invnum ? ' SELECTED' : '' %> VALUE="<% $cust_bill->invnum %>"><% $cust_bill->invnum %> - <% time2str("%D",$cust_bill->_date) %> - $<% $cust_bill->owed %>
+% }
+
+<OPTION VALUE="Refund">Refund
+</SELECT>
+
+<BR>Amount $<INPUT TYPE="text" NAME="amount" VALUE="<% $amount %>" SIZE=8 MAXLENGTH=8>
+
+<BR>
+<CENTER><INPUT TYPE="submit" VALUE="Apply"></CENTER>
+
+</FORM>
+</BODY>
+</HTML>
+
+<%init>
+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);
+
+my $cust_credit = qsearchs('cust_credit', { 'crednum' => $crednum } );
+die "credit $crednum not found!" unless $cust_credit;
+
+my $credited = $cust_credit->credited;
+
+my @cust_bill = sort { $a->_date <=> $b->_date
+ or $a->invnum <=> $b->invnum
+ }
+ grep { $_->owed != 0 }
+ qsearch('cust_bill', { 'custnum' => $cust_credit->custnum } );
+</%init>
+
diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi
new file mode 100755
index 000000000..a843772d2
--- /dev/null
+++ b/httemplate/edit/cust_main.cgi
@@ -0,0 +1,510 @@
+%
+%
+% #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, $saved_domsvc);
+%my(@invoicing_list);
+%my $payinfo;
+%my $same = '';
+%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_domsvc = $cgi->param('domsvc') || '';
+% if ( $saved_domsvc =~ /^(\d+)$/ ) {
+% $saved_domsvc = $1;
+% } else {
+% $saved_domsvc = '';
+% }
+% $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') );
+% $same = $cgi->param('same');
+% $cust_main->setfield('paid' => $cgi->param('paid')) if $cgi->param('paid');
+% $payinfo = $cust_main->payinfo; # don't mask an entered value on errors
+%} 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;
+% $saved_domsvc = 0;
+% $username = '';
+% $password = '';
+% $popnum = 0;
+% @invoicing_list = $cust_main->invoicing_list;
+% $payinfo = $cust_main->paymask;
+%} else {
+% $custnum='';
+% $cust_main = new FS::cust_main ( {} );
+% $cust_main->otaker( &getotaker );
+% $cust_main->referral_custnum( $cgi->param('referral_custnum') );
+% $saved_pkgpart = 0;
+% $saved_domsvc = 0;
+% $username = '';
+% $password = '';
+% $popnum = 0;
+% @invoicing_list = ();
+% push @invoicing_list, 'POST'
+% unless $conf->exists('disablepostalinvoicedefault');
+% $payinfo = '';
+%}
+%$cgi->delete_all();
+%
+%my $action = $custnum ? 'Edit' : 'Add';
+%$action .= ": ". $cust_main->name if $custnum;
+%
+%my $r = qq!<font color="#ff0000">*</font>&nbsp;!;
+%
+%
+
+
+<!-- top -->
+
+<% include('/elements/header.html',
+ "Customer $action",
+ '',
+ ' onUnload="myclose()"'
+) %>
+% if ( $error ) {
+
+<FONT SIZE="+1" COLOR="#ff0000">Error: <% $error %></FONT><BR><BR>
+% }
+
+
+<FORM NAME="topform" STYLE="margin-bottom: 0">
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
+% if ( $custnum ) {
+
+ Customer #<B><% $custnum %></B> -
+ <B><FONT COLOR="<% $cust_main->statuscolor %>">
+ <% ucfirst($cust_main->status) %>
+ </FONT></B>
+ <BR><BR>
+% }
+
+
+<% &ntable("#cccccc") %>
+
+<!-- agent -->
+
+<% include('/elements/tr-select-agent.html', $cust_main->agentnum,
+ 'label' => "<B>${r}Agent</B>",
+ 'empty_label' => 'Select agent',
+ )
+%>
+
+<!-- referral (advertising source) -->
+%
+%my $refnum = $cust_main->refnum || $conf->config('referraldefault') || 0;
+%if ( $custnum && ! $conf->exists('editreferrals') ) {
+%
+
+
+ <INPUT TYPE="hidden" NAME="refnum" VALUE="<% $refnum %>">
+% } else {
+
+
+ <% include('/elements/tr-select-part_referral.html', $refnum ) %>
+% }
+
+
+<!-- referring customer -->
+%
+%my $referring_cust_main = '';
+%if ( $cust_main->referral_custnum
+% and $referring_cust_main =
+% qsearchs('cust_main', { custnum => $cust_main->referral_custnum } )
+%) {
+%
+
+
+ <TR>
+ <TD ALIGN="right">Referring customer</TD>
+ <TD>
+ <A HREF="<% popurl(1) %>/cust_main.cgi?<% $cust_main->referral_custnum %>"><% $cust_main->referral_custnum %>: <% $referring_cust_main->name %></A>
+ </TD>
+ </TR>
+ <INPUT TYPE="hidden" NAME="referral_custnum" VALUE="<% $cust_main->referral_custnum %>">
+% } elsif ( ! $conf->exists('disable_customer_referrals') ) {
+
+
+ <TR>
+ <TD ALIGN="right">Referring customer</TD>
+ <TD>
+ <!-- <INPUT TYPE="text" NAME="referral_custnum" VALUE=""> -->
+ <% include('/elements/search-cust_main.html',
+ 'field_name' => 'referral_custnum',
+ )
+ %>
+ </TD>
+ </TR>
+% } else {
+
+
+ <INPUT TYPE="hidden" NAME="referral_custnum" VALUE="">
+% }
+
+
+</TABLE>
+
+<!-- birthdate -->
+
+% if ( $conf->exists('cust_main-enable_birthdate') ) {
+
+ <BR>
+ <% ntable("#cccccc", 2) %>
+ <% include ('/elements/tr-input-date-field.html',
+ 'birthdate',
+ $cust_main->birthdate,
+ 'Date of Birth',
+ $conf->config('date_format') || "%m/%d/%Y",
+ 1)
+ %>
+
+ </TABLE>
+
+% }
+
+<!-- contact info -->
+
+<BR><BR>
+Billing address
+<% include('cust_main/contact.html', $cust_main, '', 'bill_changed(this)', '' ) %>
+
+<!-- service address -->
+% if ( defined $cust_main->dbdef_table->column('ship_last') ) {
+
+
+<SCRIPT>
+function bill_changed(what) {
+ if ( what.form.same.checked ) {
+% for (qw( last first company address1 address2 city zip daytime night fax )) {
+
+ what.form.ship_<%$_%>.value = what.form.<%$_%>.value;
+% }
+
+ what.form.ship_country.selectedIndex = what.form.country.selectedIndex;
+
+ function fix_ship_county() {
+ what.form.ship_county.selectedIndex = what.form.county.selectedIndex;
+ }
+
+ function fix_ship_state() {
+ what.form.ship_state.selectedIndex = what.form.state.selectedIndex;
+ ship_state_changed(what.form.ship_state, fix_ship_county );
+ }
+
+ ship_country_changed(what.form.ship_country, fix_ship_state );
+
+ }
+}
+function samechanged(what) {
+ if ( what.checked ) {
+ bill_changed(what);
+% for (qw( last first company address1 address2 city county state zip country daytime night fax )) {
+
+ what.form.ship_<%$_%>.disabled = true;
+ what.form.ship_<%$_%>.style.backgroundColor = '#dddddd';
+% }
+
+ } else {
+% for (qw( last first company address1 address2 city county state zip country daytime night fax )) {
+
+ what.form.ship_<%$_%>.disabled = false;
+ what.form.ship_<%$_%>.style.backgroundColor = '#ffffff';
+% }
+
+ }
+}
+</SCRIPT>
+%
+% my $checked = '';
+% my $disabled = '';
+% my $disabledselect = '';
+% unless ( $cust_main->ship_last && $same ne 'Y' ) {
+% $checked = 'CHECKED';
+% $disabled = 'DISABLED STYLE="background-color: #dddddd"';
+% foreach (
+% qw( last first company address1 address2 city county state zip country
+% daytime night fax )
+% ) {
+% $cust_main->set("ship_$_", $cust_main->get($_) );
+% }
+% }
+%
+
+
+<BR>
+Service address
+(<INPUT TYPE="checkbox" NAME="same" VALUE="Y" onClick="samechanged(this)" <%$checked%>>same as billing address)
+<% include('cust_main/contact.html', $cust_main, 'ship_', '', $disabled ) %>
+% }
+
+
+<!-- billing info -->
+
+<% include( 'cust_main/billing.html', $cust_main,
+ 'payinfo' => $payinfo,
+ 'invoicing_list' => \@invoicing_list,
+ )
+%>
+
+<SCRIPT>
+function bottomfixup(what) {
+
+ var topvars = new Array(
+ 'birthdate',
+
+ 'custnum', 'agentnum', 'refnum', 'referral_custnum',
+
+ 'last', 'first', 'ss', 'company',
+ 'address1', 'address2', 'city',
+ 'county', 'state', 'zip', 'country',
+ 'daytime', 'night', 'fax',
+
+ 'same',
+
+ 'ship_last', 'ship_first', 'ship_company',
+ 'ship_address1', 'ship_address2', 'ship_city',
+ 'ship_county', 'ship_state', 'ship_zip', 'ship_country',
+ 'ship_daytime','ship_night', 'ship_fax',
+
+ 'select' // XXX key
+ );
+
+ var layervars = new Array(
+ 'payauto',
+ 'payinfo', 'payinfo1', 'payinfo2',
+ 'payname', 'exp_month', 'exp_year', 'paycvv',
+ 'paystart_month', 'paystart_year', 'payissue',
+ 'payip',
+ 'paid'
+ );
+
+ var billing_bottomvars = new Array(
+ 'tax',
+ 'invoicing_list', 'invoicing_list_POST', 'invoicing_list_FAX',
+ 'spool_cdr'
+ );
+
+ for ( f=0; f < topvars.length; f++ ) {
+ var field = topvars[f];
+ copyelement( document.topform.elements[field],
+ document.bottomform.elements[field]
+ );
+ }
+
+ var layerform = document.topform.select.options[document.topform.select.selectedIndex].value;
+ for ( f=0; f < layervars.length; f++ ) {
+ var field = layervars[f];
+ copyelement( document.forms[layerform].elements[field],
+ document.bottomform.elements[field]
+ );
+ }
+
+ for ( f=0; f < billing_bottomvars.length; f++ ) {
+ var field = billing_bottomvars[f];
+ copyelement( document.billing_bottomform.elements[field],
+ document.bottomform.elements[field]
+ );
+ }
+
+}
+
+function copyelement(from, to) {
+ if ( from == undefined ) {
+ to.value = '';
+ } else if ( from.type == 'select-one' ) {
+ to.value = from.options[from.selectedIndex].value;
+ //alert(from + " (" + from.type + "): " + to.name + " => (" + from.selectedIndex + ") " + to.value);
+ } else if ( from.type == 'checkbox' ) {
+ if ( from.checked ) {
+ to.value = from.value;
+ } else {
+ to.value = '';
+ }
+ } else {
+ if ( from.value == undefined ) {
+ to.value = '';
+ } else {
+ to.value = from.value;
+ }
+ }
+ //alert(from + " (" + from.type + "): " + to.name + " => " + to.value);
+}
+
+</SCRIPT>
+
+<FORM ACTION="<% popurl(1) %>process/cust_main.cgi" METHOD=POST NAME="bottomform" onSubmit="document.bottomform.submit.disabled=true; bottomfixup(this.form);" STYLE="margin-top: 0; margin-bottom: 0">
+% foreach my $hidden (
+% 'birthdate',
+%
+% 'custnum', 'agentnum', 'refnum', 'referral_custnum',
+% 'last', 'first', 'ss', 'company',
+% 'address1', 'address2', 'city',
+% 'county', 'state', 'zip', 'country',
+% 'daytime', 'night', 'fax',
+%
+% 'same',
+%
+% 'ship_last', 'ship_first', 'ship_company',
+% 'ship_address1', 'ship_address2', 'ship_city',
+% 'ship_county', 'ship_state', 'ship_zip', 'ship_country',
+% 'ship_daytime','ship_night', 'ship_fax',
+%
+% 'select', #XXX key
+%
+% 'payauto',
+% 'payinfo', 'payinfo1', 'payinfo2',
+% 'payname', 'exp_month', 'exp_year', 'paycvv',
+% 'paystart_month', 'paystart_year', 'payissue',
+% 'payip',
+% 'paid',
+%
+% 'tax',
+% 'invoicing_list', 'invoicing_list_POST', 'invoicing_list_FAX',
+% 'spool_cdr'
+% ) {
+%
+
+ <INPUT TYPE="hidden" NAME="<% $hidden %>" VALUE="">
+% }
+%
+% my $ro_comments = $conf->exists('cust_main-use_comments')?'':'readonly';
+% if (!$ro_comments || $cust_main->comments) {
+
+<BR>Comments
+<% &ntable("#cccccc") %>
+ <TR>
+ <TD>
+ <TEXTAREA COLS=80 ROWS=5 WRAP="HARD" NAME="comments" <%$ro_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;
+% my @agents = $FS::CurrentUser::CurrentUser->agents;
+% if ( scalar(@agents) == 1 ) {
+% # $pkgpart->{PKGPART} is true iff $custnum may purchase PKGPART
+% $pkgpart = $agents[0]->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' => '' }, '', 'ORDER BY pkg' ); # case?
+%
+% if ( @part_pkg ) {
+%
+% # print "<BR><BR>First package", &itable("#cccccc", "0 ALIGN=LEFT"),
+% #apiabuse & undesirable wrapping
+%
+%
+
+ <BR>First package
+ <% ntable("#cccccc") %>
+
+ <TR>
+ <TD COLSPAN=2>
+ <% include('cust_main/select-domain.html',
+ 'pkgparts' => \@part_pkg,
+ 'saved_pkgpart' => $saved_pkgpart,
+ 'saved_domsvc' => $saved_domsvc,
+ )
+ %>
+ </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;
+%
+
+
+ <TR>
+ <TD ALIGN="right">Username</TD>
+ <TD>
+ <INPUT TYPE="text" NAME="username" VALUE="<% $username %>" SIZE=<% $ulen2 %> MAXLENGTH=<% $ulen %>>
+ </TD>
+ </TR>
+
+ <TR>
+ <TD ALIGN="right">Domain</TD>
+ <TD>
+ <SELECT NAME="domsvc">
+ <OPTION>(none)</OPTION>
+ </SELECT>
+ </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>
+
+ <TR>
+ <TD ALIGN="right">Access number</TD>
+ <TD><% FS::svc_acct_pop::popselector($popnum) %></TD>
+ </TR>
+ </TABLE>
+% }
+% }
+
+
+<INPUT TYPE="hidden" NAME="otaker" VALUE="<% $cust_main->otaker %>">
+<BR>
+<INPUT TYPE="submit" NAME="submit" VALUE="<% $custnum ? "Apply Changes" : "Add Customer" %>">
+<BR>
+</FORM>
+
+<% include('/elements/footer.html') %>
+
diff --git a/httemplate/edit/cust_main/billing.html b/httemplate/edit/cust_main/billing.html
new file mode 100644
index 000000000..ba10929ed
--- /dev/null
+++ b/httemplate/edit/cust_main/billing.html
@@ -0,0 +1,442 @@
+%if ( $payby_default eq 'HIDE' ) {
+%
+% $cust_main->payby('BILL') unless $cust_main->payby;
+
+ <INPUT TYPE="hidden" NAME="select" VALUE="<% $cust_main->payby %>">
+
+ </FORM>
+
+ <FORM NAME="<% $cust_main->payby %>" STYLE="margin-top: 0; margin-bottom: 0">
+
+ <INPUT TYPE="hidden" NAME="payinfo" VALUE="<% $cust_main->paymask %>">
+
+% foreach my $field (qw( payname paycvv paystart_month paystart_year payissue payip )) {
+
+ <INPUT TYPE="hidden" NAME="<% $field %>" VALUE="<% $cust_main->getfield($field) %>">
+
+% }
+
+% #false laziness w/elements/select-month_year.html & view/cust_main/billing.html
+% my( $mon, $year );
+% my $date = $cust_main->paydate || '12-2037';
+% if ( $date =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #PostgreSQL date format
+% ( $mon, $year ) = ( $2, $1 );
+% } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
+% ( $mon, $year ) = ( $1, $3 );
+% } else {
+% die "unrecognized expiration date format: $date";
+% }
+
+ <INPUT TYPE="hidden" NAME="exp_month" VALUE="<% $mon %>">
+ <INPUT TYPE="hidden" NAME="exp_year" VALUE="<% $year %>">
+
+ </FORM>
+
+ <FORM NAME="billing_bottomform" STYLE="margin-top: 0; margin-bottom: 0">
+
+ <INPUT TYPE="hidden" NAME="tax" VALUE="<% $cust_main->tax %>">
+
+ <INPUT TYPE="hidden" NAME="invoicing_list" VALUE="<% join(', ', @invoicing_list) %>">
+
+ </FORM>
+
+% } else {
+%
+% my $r = qq!<font color="#ff0000">*</font>&nbsp;!;
+
+ <BR>Billing information
+ <% &ntable("#cccccc") %>
+
+ <TR>
+ <TD ALIGN="right" WIDTH="200"><%$r%>Billing type</TD>
+
+ <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;
+ }
+
+ function card_changed(what) {
+ if (
+ what.form.payinfo.value.substring(0, 4) == '4093'
+ || what.form.payinfo.value.substring(0, 4) == '4911'
+ || what.form.payinfo.value.substring(0, 4) == '4936'
+ || what.form.payinfo.value.substring(0, 6) == '564132'
+ || what.form.payinfo.value.substring(0, 2) == '63'
+ || what.form.payinfo.value.substring(0, 2) == '67'
+ )
+ {
+ what.form.paystart_month.disabled = false;
+ what.form.paystart_year.disabled = false;
+ what.form.payissue.disabled = false;
+ what.form.paystart_month.style.backgroundColor = '#ffffff';
+ what.form.paystart_year.style.backgroundColor = '#ffffff';
+ what.form.payissue.style.backgroundColor = '#ffffff';
+ document.getElementById('paystart_label').style.color = '#000000';
+ document.getElementById('payissue_label').style.color = '#000000';
+ } else {
+ what.form.paystart_month.disabled = true;
+ what.form.paystart_year.disabled = true;
+ what.form.payissue.disabled = true;
+ what.form.paystart_month.style.backgroundColor = '#dddddd';
+ what.form.paystart_year.style.backgroundColor = '#dddddd';
+ what.form.payissue.style.backgroundColor = '#dddddd';
+ document.getElementById('paystart_label').style.color = '#999999';
+ document.getElementById('payissue_label').style.color = '#999999';
+ }
+ return true;
+ }
+
+ </SCRIPT>
+
+ <SCRIPT TYPE="text/javascript" SRC="../elements/overlibmws.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/overlibmws_iframe.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript" SRC="../elements/overlibmws_draggable.js"></SCRIPT>
+ <SCRIPT TYPE="text/javascript">
+ function OLiframeContent(src, width, height, name) {
+ return ('<iframe src="'+src+'" width="'+width+'" height="'+height+'"'
+ +(name?' name="'+name+'" id="'+name+'"':'')+' scrolling="auto">'
+ +'<div>[iframe not supported]</div></iframe>');
+ }
+ </SCRIPT>
+
+% my $payby = $cust_main->payby;
+% my( $account, $aba ) = split('@', $payinfo);
+%
+% my $disabled = 'DISABLED style="background-color: #dddddd"';
+% my $text_disabled = 'style="color: #999999"';
+%
+% if ( $payby =~ /^(CARD|DCRD)$/ && cardtype($payinfo) =~ /^(Switch|Solo)$/ ) {
+% $disabled = 'style="background-color: #ffffff"';
+% $text_disabled = 'style="color: #000000";'
+% }
+%
+% my %payby = (
+%
+% 'CARD' =>
+%
+% '<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 HEIGHT=192>'.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">${r}Card number </TD>!.
+% qq!<TD WIDTH="408"><INPUT TYPE="text" NAME="payinfo" VALUE="!. ( $payby =~ /^(CARD|DCRD)$/ ? $payinfo : '' ). qq!" MAXLENGTH=19 onChange="card_changed(this)" onKeyUp="card_changed(this)"></TD></TR>!.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">${r}Expiration </TD>!.
+% '<TD WIDTH="408">'.
+%
+% include('/elements/select-month_year.html',
+% 'prefix' => 'exp',
+% 'selected_date' =>
+% ( $payby =~ /^(CARD|DCRD)$/ ? $cust_main->paydate : '' ),
+% ).
+%
+% '</TD></TR>'.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">CVV2&nbsp;!.
+%
+% qq!(<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/cvv2.html', 480, 352, 'cvv2_popup' ), CAPTION, 'CVV2 Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;">help</A>)!.
+% qq!</TD>!.
+% '<TD WIDTH="408"><INPUT TYPE="text" NAME="paycvv" VALUE="'. ( $payby =~ /^(CARD|DCRD)$/ && !$cust_main->is_encrypted($cust_main->paycvv) ? $cust_main->paycvv : '' ). '" SIZE=4 MAXLENGTH=4>'.
+%
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200"><SPAN ID="paystart_label" $text_disabled>Start date </SPAN></TD>!.
+% '<TD WIDTH="408">'.
+%
+% include('/elements/select-month_year.html',
+% 'prefix' => 'paystart',
+% 'disabled' => $disabled,
+% 'empty_option' => 1,
+% 'start_year' => 2000,
+% 'end_year' => (localtime())[5] + 1900,
+% 'selected_date' => (
+% ( $payby =~ /^(CARD|DCRD)$/
+% && cardtype($payinfo) =~ /^(Switch|Solo)$/ )
+% ? $cust_main->paystart_month. '-'.
+% $cust_main->paystart_year
+% : ''
+% )
+% ).
+%
+% qq!<SPAN ID="payissue_label" $text_disabled> or Issue number </SPAN>!.
+% '<INPUT TYPE="text" NAME="payissue" VALUE="'. ( $payby =~ /^(CARD|DCRD)$/ ? $cust_main->payissue : '' ). qq!" SIZE=3 MAXLENGTH=2 $disabled></TD></TR>!.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">${r}Exact name on card </TD>!.
+% qq!<TD WIDTH="408"><INPUT TYPE="text" NAME="payname" VALUE="!. ( $payby =~ /^(CARD|DCRD)$/ ? $cust_main->payname : '' ). qq!"></TD></TR>!.
+%
+% qq!<TR><TD COLSPAN=2 WIDTH="608"><INPUT TYPE="checkbox" NAME="payauto" !. ( $payby eq 'DCRD' ? '' : 'CHECKED' ). '> Charge future payments to this card automatically</TD></TR>'.
+%
+% '</TABLE>',
+%
+% 'CHEK' =>
+%
+% '<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 HEIGHT=192>'.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">${r}Account number </TD>!.
+% qq!<TD WIDTH="408"><INPUT TYPE="text" SIZE=12 NAME="payinfo1" VALUE="!. ( $payby =~ /^(CHEK|DCHK)$/ ? $account : '' ). '"></TD></TR>'.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">${r}ABA/Routing number </TD>!.
+% qq!<TD WIDTH="408"><INPUT TYPE="text" SIZE=10 MAXLENGTH=9 NAME="payinfo2" VALUE="!. ( $payby =~ /^(CHEK|DCHK)$/ ? $aba : '' ). qq!" SIZE=10 MAXLENGTH=9> !.
+% qq!(<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/ach.html', 380, 240, 'ach_popup' ), CAPTION, 'ACH Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;">help</A>)!.
+% qq!</TD></TR>!.
+%
+% qq!<INPUT TYPE="hidden" NAME="exp_month" VALUE="12">!.
+% qq!<INPUT TYPE="hidden" NAME="exp_year" VALUE="2037">!.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">${r}Bank name </TD>!.
+% qq!<TD WIDTH="408"><INPUT TYPE="text" NAME="payname" VALUE="!. ( $payby =~ /^(CHEK|DCHK)$/ ? $cust_main->payname : '' ). qq!"></TD></TR>!.
+%
+% qq!<TR><TD COLSPAN=2 WIDTH="608"><INPUT TYPE="checkbox" NAME="payauto" !. ( $payby eq 'DCHK' ? '' : 'CHECKED' ). '> Charge future payments to this electronic check automatically</TD></TR>'.
+%
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+%
+% '</TABLE>',
+%
+% 'LECB' =>
+%
+% '<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 HEIGHT=192>'.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">${r}Phone number </TD>!.
+% qq!<TD WIDTH="408"><INPUT TYPE="text" NAME="payinfo" VALUE="!. ( $payby eq 'LECB' ? $cust_main->payinfo : '' ). qq!" MAXLENGTH=15 SIZE=16></TD></TR>!.
+%
+% qq!<INPUT TYPE="hidden" NAME="exp_month" VALUE="12">!.
+% qq!<INPUT TYPE="hidden" NAME="exp_year" VALUE="2037">!.
+% qq!<INPUT TYPE="hidden" NAME="payname" VALUE="">!.
+%
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+%
+% '</TABLE>',
+%
+% 'BILL' =>
+%
+% '<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 HEIGHT=192>'.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">P.O. </TD>!.
+% qq!<TD WIDTH="408"><INPUT TYPE="text" NAME="payinfo" VALUE="!. ( $payby eq 'BILL' ? $cust_main->payinfo : '' ). qq!"></TD></TR>!.
+%
+% qq!<INPUT TYPE="hidden" NAME="exp_month" VALUE="12">!.
+% qq!<INPUT TYPE="hidden" NAME="exp_year" VALUE="2037">!.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">Attention </TD>!.
+% qq!<TD WIDTH="408"><INPUT TYPE="text" NAME="payname" VALUE="!. ( $payby eq 'BILL' ? $cust_main->payname : '' ). qq!"></TD></TR>!.
+%
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+%
+% '</TABLE>',
+%
+% 'COMP' =>
+%
+% '<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 HEIGHT=192>'.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">${r}Approved by </TD>!.
+% qq!<TD WIDTH="408"><INPUT TYPE="text" NAME="payinfo" VALUE=""></TD></TR>!.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">${r}Expiration </TD>!.
+% '<TD WIDTH="408">'.
+%
+% include('/elements/select-month_year.html',
+% 'prefix' => 'exp',
+% 'selected_date' =>
+% ( $payby eq 'COMP' ? $cust_main->paydate : '' ),
+% ).
+%
+% '</TD></TR>'.
+%
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+%
+% '</TABLE>',
+%
+% 'CASH' =>
+%
+% '<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 HEIGHT=192>'.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">${r}Amount </TD>!.
+% qq!<TD WIDTH="408"><INPUT TYPE="text" NAME="paid" VALUE="!. ( $payby eq 'CASH' ? $cust_main->paid : '' ). qq!"></TD></TR>!.
+%
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+%
+% '</TABLE>',
+%
+% 'WEST' =>
+%
+% '<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 HEIGHT=192>'.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">${r}Amount </TD>!.
+% qq!<TD WIDTH="408"><INPUT TYPE="text" NAME="paid" VALUE="!. ( $payby eq 'WEST' ? $cust_main->paid : '' ). qq!"></TD></TR>!.
+%
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+%
+% '</TABLE>',
+%
+% 'MCRD' =>
+%
+% '<TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 HEIGHT=192>'.
+%
+% qq!<TR><TD ALIGN="right" WIDTH="200">${r}Amount </TD>!.
+% qq!<TD WIDTH="408"><INPUT TYPE="text" NAME="paid" VALUE="!. ( $payby eq 'MCRD' ? $cust_main->paid : '' ). qq!"></TD></TR>!.
+%
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+% '<TR><TD>&nbsp;</TD></TR>'.
+%
+% '</TABLE>',
+%
+% );
+%
+% #this should use FS::payby
+% my %allopt = (
+% 'CARD' => 'Credit card',
+% 'CHEK' => 'Electronic check',
+% 'LECB' => 'Phone bill billing',
+% 'BILL' => 'Billing',
+% 'CASH' => 'Cash', # initial payment, then billing',
+% 'WEST' => 'Western Union', # initial payment, then billing',
+% 'MCRD' => 'Manual credit card', # initial payment, then billing',
+% 'COMP' => 'Complimentary',
+% );
+% if ( $cust_main->custnum ) { #don't offer CASH/WEST/MCRD initial payment types
+% # when editing customer
+% delete $allopt{$_} for qw(CASH WEST MCRD);
+% }
+%
+% tie my %options, 'Tie::IxHash',
+% map { $_ => $allopt{$_} }
+% grep { exists $allopt{$_} }
+% @payby;
+%
+% my %payby2option = (
+% ( map { $_ => $_ } keys %options ),
+% 'DCRD' => 'CARD',
+% 'DCHK' => 'CHEK',
+% );
+%
+% my $widget = new HTML::Widgets::SelectLayers(
+% 'options' => \%options,
+% #'form_name' => 'dummy',
+% #'form_action' => 'nothingyet',
+% #chops bottom of page in IE# 'under_position' => 'absolute',
+% 'html_between' => '</TD></TR></TABLE>',
+% 'selected_layer' => $payby2option{$payby || $payby_default || $payby[0] },
+% 'layer_callback' => sub { my $layer = shift; $payby{$layer}; },
+% );
+%
+%
+
+
+ <TD WIDTH="408"><% $widget->html %>
+
+ <FORM NAME="billing_bottomform" STYLE="margin-top: 0; margin-bottom: 0">
+
+ <% &ntable("#cccccc") %>
+
+ <TR><TD>&nbsp;</TD></TR>
+
+ <TR>
+ <TD WIDTH="608" COLSPAN="2"><INPUT TYPE="checkbox" NAME="tax" VALUE="Y" <% $cust_main->tax eq "Y" ? 'CHECKED' : '' %>> Tax Exempt</TD>
+ </TR>
+
+ <TR>
+ <TD WIDTH="608" COLSPAN="2"><INPUT TYPE="checkbox" NAME="invoicing_list_POST" VALUE="POST" <%
+
+ ( grep { $_ eq 'POST' } @invoicing_list )
+
+ ? 'CHECKED'
+ : ''
+
+ %>> Postal mail invoice
+
+ </TD>
+ </TR>
+
+ <TR>
+ <TD WIDTH="608" COLSPAN="2"><INPUT TYPE="checkbox" NAME="invoicing_list_FAX" VALUE="FAX" <%
+
+ ( grep { $_ eq 'FAX' } @invoicing_list )
+ ? 'CHECKED'
+ : ''
+
+ %>> Fax invoice
+
+ </TD>
+ </TR>
+
+ <TR>
+ <TD ALIGN="right" WIDTH="200">Email invoice </TD>
+ <TD WIDTH="408"><INPUT TYPE="text" NAME="invoicing_list" VALUE="<% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) %>"></TD>
+ </TR>
+% if ( $conf->exists('voip-cust_cdr_spools') ) {
+
+ <TR>
+ <TD COLSPAN="2"><INPUT TYPE="checkbox" NAME="spool_cdr" VALUE="Y" <% $cust_main->spool_cdr eq "Y" ? 'CHECKED' : '' %>> Spool CDRs</TD>
+ </TR>
+% } else {
+
+ <INPUT TYPE="hidden" NAME="spool_cdr" VALUE="<% $cust_main->spool_cdr %>">
+% }
+
+
+ </TABLE>
+
+ </FORM>
+
+ <% $r %> required fields
+% }
+
+<%init>
+
+my( $cust_main, %options ) = @_;
+my @invoicing_list = @{ $options{'invoicing_list'} };
+my $payinfo = $options{'payinfo'};
+my $conf = new FS::Conf;
+my $payby_default = $conf->config('payby-default');
+
+my @payby = grep /\w/, $conf->config('payby');
+#@payby = (qw( CARD DCRD CHEK DCHK LECB BILL CASH WEST COMP ))
+@payby = (qw( CARD DCRD CHEK DCHK LECB BILL CASH COMP ))
+ unless @payby;
+
+</%init>
diff --git a/httemplate/edit/cust_main/contact.html b/httemplate/edit/cust_main/contact.html
new file mode 100644
index 000000000..e813986cd
--- /dev/null
+++ b/httemplate/edit/cust_main/contact.html
@@ -0,0 +1,134 @@
+<% &ntable("#cccccc") %>
+
+<TR>
+ <TH ALIGN="right"><%$r%>Contact&nbsp;name<BR>(last,&nbsp;first)</TH>
+ <TD COLSPAN=5>
+ <INPUT TYPE="text" NAME="<%$pre%>last" VALUE="<% $cust_main->get($pre.'last') %>" onChange="<% $onchange %>" <%$disabled%>> ,
+ <INPUT TYPE="text" NAME="<%$pre%>first" VALUE="<% $cust_main->get($pre.'first') %>" onChange="<% $onchange %>" <%$disabled%>>
+ </TD>
+% if ( $conf->exists('show_ss') && !$pre ) {
+
+ <TD ALIGN="right">SS#</TD>
+ <TD><INPUT TYPE="text" NAME="ss" VALUE="<% $cust_main->ss %>" SIZE=11></TD>
+% } elsif ( !$pre ) {
+
+ <TD><INPUT TYPE="hidden" NAME="ss" VALUE="<% $cust_main->ss %>"></TD>
+% }
+
+
+</TR>
+
+<TR>
+ <TD ALIGN="right">Company</TD>
+ <TD COLSPAN=7>
+ <INPUT TYPE="text" NAME="<%$pre%>company" VALUE="<% $cust_main->get($pre.'company') %>" SIZE=70 onChange="<% $onchange %>" <%$disabled%>>
+ </TD>
+</TR>
+
+<TR>
+ <TH ALIGN="right"><%$r%>Address</TH>
+ <TD COLSPAN=7>
+ <INPUT TYPE="text" NAME="<%$pre%>address1" VALUE="<% $cust_main->get($pre.'address1') %>" SIZE=70 onChange="<% $onchange %>" <%$disabled%>>
+ </TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right">&nbsp;</TD>
+ <TD COLSPAN=7>
+ <INPUT TYPE="text" NAME="<%$pre%>address2" VALUE="<% $cust_main->get($pre.'address2') %>" SIZE=70 onChange="<% $onchange %>" <%$disabled%>>
+ </TD>
+</TR>
+
+<TR>
+ <TH ALIGN="right"><%$r%>City</TH>
+ <TD>
+ <INPUT TYPE="text" NAME="<%$pre%>city" VALUE="<% $cust_main->get($pre.'city') %>" onChange="<% $onchange %>" <%$disabled%>>
+ </TD>
+ <TH ALIGN="right" ID="<%$pre%>countylabel" <%$county_style%>><%$r%>County</TH>
+ <TD>
+ <% include('select-county.html', %select_hash ) %>
+ </TD>
+ <TH ALIGN="right"><%$r%>State</TH>
+ <TD>
+ <% include('select-state.html', %select_hash ) %>
+ </TD>
+ <TH><%$r%>Zip</TH>
+ <TD>
+ <INPUT TYPE="text" NAME="<%$pre%>zip" VALUE="<% $cust_main->get($pre.'zip') %>" SIZE=10 onChange="<% $onchange %>" <%$disabled%>>
+ </TD>
+</TR>
+
+<TR>
+ <TH ALIGN="right"><%$r%>Country</TH>
+ <TD COLSPAN=5><% include('select-country.html', %select_hash ) %></TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right"><% $daytime_label %></TD>
+ <TD COLSPAN=5>
+ <INPUT TYPE="text" NAME="<%$pre%>daytime" VALUE="<% $cust_main->get($pre.'daytime') %>" SIZE=18 onChange="<% $onchange %>" <%$disabled%>>
+ </TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right"><% $night_label %></TD>
+ <TD COLSPAN=5>
+ <INPUT TYPE="text" NAME="<%$pre%>night" VALUE="<% $cust_main->get($pre.'night') %>" SIZE=18 onChange="<% $onchange %>" <%$disabled%>>
+ </TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right">Fax</TD>
+ <TD COLSPAN=5>
+ <INPUT TYPE="text" NAME="<%$pre%>fax" VALUE="<% $cust_main->get($pre.'fax') %>" SIZE=12 onChange="<% $onchange %>" <%$disabled%>>
+ </TD>
+</TR>
+
+</TABLE>
+<%$r%>required fields<BR>
+
+<%init>
+
+my( $cust_main, $pre, $onchange, $disabled ) = @_;
+my $conf = new FS::Conf;
+
+#false laziness with ship state
+my $countrydefault = $conf->config('countrydefault') || 'US';
+$cust_main->set($pre.'country', $countrydefault )
+ unless $cust_main->get($pre.'country');
+
+my $statedefault = $conf->config('statedefault')
+ || ($countrydefault eq 'US' ? 'CA' : '');
+$cust_main->set($pre.'state', $statedefault )
+ unless $cust_main->get($pre.'state')
+ || $cust_main->get($pre.'country') ne $countrydefault;
+
+#my($county_html, $state_html, $country_html) =
+# FS::cust_main_county::regionselector( $cust_main->get($pre.'county'),
+# $cust_main->get($pre.'state'),
+# $cust_main->get($pre.'country'),
+# $pre,
+# $onchange,
+# $disabled,
+# );
+
+my %select_hash = (
+ 'county' => $cust_main->get($pre.'county'),
+ 'state' => $cust_main->get($pre.'state'),
+ 'country' => $cust_main->get($pre.'country'),
+ 'prefix' => $pre,
+ 'onchange' => $onchange,
+ 'disabled' => $disabled,
+);
+
+my @counties = counties( $cust_main->get($pre.'state'),
+ $cust_main->get($pre.'country'),
+ );
+my $county_style = scalar(@counties) > 1 ? '' : 'STYLE="visibility:hidden"';
+
+my $daytime_label = FS::Msgcat::_gettext('daytime') || 'Day Phone';
+my $night_label = FS::Msgcat::_gettext('night') || 'Night Phone';
+
+my $r = qq!<font color="#ff0000">*</font>&nbsp;!;
+
+</%init>
diff --git a/httemplate/edit/cust_main/select-country.html b/httemplate/edit/cust_main/select-country.html
new file mode 100644
index 000000000..137f61975
--- /dev/null
+++ b/httemplate/edit/cust_main/select-country.html
@@ -0,0 +1,76 @@
+
+<% include('/elements/xmlhttp.html',
+ 'url' => $p.'misc/states.cgi',
+ 'subs' => [ $opt{'prefix'}. 'get_states' ],
+ )
+%>
+
+<SCRIPT TYPE="text/javascript">
+
+ function opt(what,value,text) {
+ var optionName = new Option(text, value, false, false);
+ var length = what.length;
+ what.options[length] = optionName;
+ }
+
+ function <% $opt{'prefix'} %>country_changed(what, callback) {
+
+ country = what.options[what.selectedIndex].value;
+
+ function <% $opt{'prefix'} %>update_states(states) {
+
+ // blank the current state list
+ for ( var i = what.form.<% $opt{'prefix'} %>state.length; i >= 0; i-- )
+ what.form.<% $opt{'prefix'} %>state.options[i] = null;
+
+ // add the new states
+ var statesArray = eval('(' + states + ')' );
+ for ( var s = 0; s < statesArray.length; s=s+2 ) {
+ var stateLabel = statesArray[s+1];
+ if ( stateLabel == "" )
+ stateLabel = '(n/a)';
+ opt(what.form.<% $opt{'prefix'} %>state, statesArray[s], stateLabel);
+ }
+
+ //run the callback
+ if ( callback != null )
+ callback();
+ }
+
+ // go get the new states
+ <% $opt{'prefix'} %>get_states( country, <% $opt{'prefix'} %>update_states );
+
+ }
+
+</SCRIPT>
+
+<SELECT NAME="<% $opt{'prefix'} %>country" onChange="<% $opt{'prefix'} %>country_changed(this); <% $opt{'onchange'} %>" <% $opt{'disabled'} %>>
+
+% foreach my $country (
+% sort { ($b eq $countrydefault) <=> ($a eq $countrydefault)
+% or code2country($a) cmp code2country($b) }
+% map { $_->country }
+% qsearch({
+% 'select' => 'country',
+% 'table' => 'cust_main_county',
+% 'hashref' => {},
+% 'extra_sql' => 'GROUP BY country',
+% })
+% ) {
+
+ <OPTION VALUE="<% $country %>"<% $country eq $opt{'country'} ? ' SELECTED' : '' %>><% code2country($country). " ($country)" %>
+
+% }
+
+</SELECT>
+
+<%init>
+my %opt = @_;
+foreach my $opt (qw( county state country prefix onchange disabled )) {
+ $opt{$_} = '' unless exists($opt{$_}) && defined($opt{$_});
+}
+
+my $conf = new FS::Conf;
+my $countrydefault = $conf->config('countrydefault') || 'US';
+</%init>
+
diff --git a/httemplate/edit/cust_main/select-county.html b/httemplate/edit/cust_main/select-county.html
new file mode 100644
index 000000000..0dc826896
--- /dev/null
+++ b/httemplate/edit/cust_main/select-county.html
@@ -0,0 +1,113 @@
+% if ( $countyflag ) {
+
+ <% include('/elements/xmlhttp.html',
+ 'url' => $p.'misc/counties.cgi',
+ 'subs' => [ $opt{'prefix'}. 'get_counties' ],
+ )
+ %>
+
+ <SCRIPT TYPE="text/javascript">
+
+ function opt(what,value,text) {
+ var optionName = new Option(text, value, false, false);
+ var length = what.length;
+ what.options[length] = optionName;
+ }
+
+ function <% $opt{'prefix'} %>state_changed(what, callback) {
+
+ state = what.options[what.selectedIndex].value;
+ country = what.form.<% $opt{'prefix'} %>country.options[what.form.<% $opt{'prefix'} %>country.selectedIndex].value;
+
+ function <% $opt{'prefix'} %>update_counties(counties) {
+
+ // blank the current county list
+ for ( var i = what.form.<% $opt{'prefix'} %>county.length; i >= 0; i-- )
+ what.form.<% $opt{'prefix'} %>county.options[i] = null;
+
+ // add the new counties
+ var countiesArray = eval('(' + counties + ')' );
+ for ( var s = 0; s < countiesArray.length; s++ ) {
+ var countyLabel = countiesArray[s];
+ if ( countyLabel == "" )
+ countyLabel = '(n/a)';
+ opt(what.form.<% $opt{'prefix'} %>county, countiesArray[s], countyLabel);
+ }
+
+ var countyFormLabel = document.getElementById('<% $opt{'prefix'} %>countylabel');
+
+ if ( countiesArray.length > 1 ) {
+ what.form.<% $opt{'prefix'} %>county.style.display = '';
+ countyFormLabel.style.visibility = 'visible';
+ } else {
+ what.form.<% $opt{'prefix'} %>county.style.display = 'none';
+ countyFormLabel.style.visibility = 'hidden';
+ }
+
+ //run the callback
+ if ( callback != null )
+ callback();
+ }
+
+ // go get the new counties
+ <% $opt{'prefix'} %>get_counties( state, country, <% $opt{'prefix'} %>update_counties );
+
+ }
+
+ </SCRIPT>
+
+ <SELECT NAME="<% $opt{'prefix'} %>county" onChange="<% $opt{'onchange'} %>" <% $opt{'disabled'} %>>
+
+% foreach my $county ( @counties ) {
+
+ <OPTION VALUE="<% $county %>"<% $county eq $opt{'county'} ? ' SELECTED' : '' %>><% $county %>
+
+% }
+
+ </SELECT>
+
+% } else {
+
+
+ <SCRIPT TYPE="text/javascript">
+ function <% $opt{'prefix'} %>state_changed(what) {
+ }
+ </SCRIPT>
+
+ <INPUT TYPE="hidden" NAME="<% $opt{'prefix'} %>county" VALUE="<% $opt{'county'} %>">
+
+% }
+
+<%init>
+
+my %opt = @_;
+foreach my $opt (qw( county state country prefix onchange disabled )) {
+ $opt{$_} = '' unless exists($opt{$_}) && defined($opt{$_});
+}
+
+my @counties = ();
+if ( $countyflag ) {
+
+ @counties = counties( $opt{'state'}, $opt{'country'} );
+
+ # this is very hacky
+ unless ( scalar(@counties) > 1 ) {
+ if ( $opt{'disabled'} =~ /STYLE=/i ) {
+ $opt{'disabled'} =~ s/STYLE="([^"]+)"/STYLE="$1; display:none"/i;
+ } else {
+ $opt{'disabled'} .= ' STYLE="display:none"';
+ }
+ }
+
+}
+
+</%init>
+<%once>
+
+my $sql = "SELECT COUNT(*) FROM cust_main_county".
+ " WHERE county IS NOT NULL AND county != ''";
+my $sth = dbh->prepare($sql) or die dbh->errstr;
+$sth->execute or die $sth->errstr;
+my $countyflag = $sth->fetchrow_arrayref->[0];
+
+</%once>
diff --git a/httemplate/edit/cust_main/select-domain.html b/httemplate/edit/cust_main/select-domain.html
new file mode 100644
index 000000000..3d42eb8b1
--- /dev/null
+++ b/httemplate/edit/cust_main/select-domain.html
@@ -0,0 +1,66 @@
+
+<% include('/elements/xmlhttp.html',
+ 'url' => $p.'misc/svc_acct-domains.cgi',
+ 'subs' => [ $opt{'prefix'}. 'get_domains' ],
+ )
+%>
+
+<SCRIPT TYPE="text/javascript">
+
+ function selopt(what,value,text,selected) {
+ var optionName = new Option(text, value, false, selected);
+ var length = what.length;
+ what.options[length] = optionName;
+ }
+
+ function <% $opt{'prefix'} %>pkgpart_svcpart_changed(what,selected) {
+
+ pkgpart_svcpart = what.options[what.selectedIndex].value;
+
+ function <% $opt{'prefix'} %>update_domains(domains) {
+
+ // blank the current domain list
+ for ( var i = what.form.<% $opt{'prefix'} %>domsvc.length; i >= 0; i-- )
+ what.form.<% $opt{'prefix'} %>domsvc.options[i] = null;
+
+ // add the new domains
+ var domainArray = eval('(' + domains + ')' );
+ for ( var s = 0; s < domainArray.length; s=s+2 ) {
+ var domainLabel = domainArray[s+1];
+ if ( domainLabel == "" )
+ domainLabel = '(n/a)';
+ selopt(what.form.<% $opt{'prefix'} %>domsvc, domainArray[s], domainLabel, (domainArray[s] == selected) ? true : false);
+ }
+
+ }
+
+ // go get the new domains
+ <% $opt{'prefix'} %>get_domains( pkgpart_svcpart, <% $opt{'prefix'} %>update_domains );
+
+ }
+
+</SCRIPT>
+
+<SELECT NAME="<% $opt{'prefix'} %>pkgpart_svcpart" onchange="<% $opt{'prefix'} %>pkgpart_svcpart_changed(this,0);" >
+
+% foreach my $part_pkg ( @part_pkg ) {
+
+ <OPTION VALUE="<% $part_pkg->pkgpart. "_". $part_pkg->svcpart('svc_acct') %>"<% ( $opt{saved_pkgpart} && $part_pkg->pkgpart == $opt{saved_pkgpart} ) ? ' SELECTED' : '' %>><% $part_pkg->pkg. " - ". $part_pkg->comment %>
+
+% }
+
+</SELECT>
+<SCRIPT>
+ pkgpart_svcpart_changed(document.bottomform.pkgpart_svcpart, <% $opt{saved_domsvc} %>);
+</SCRIPT>
+
+<%init>
+my %opt = @_;
+foreach my $opt (qw( svc_part pkgparts saved_pkgpart saved_domsvc prefix)) {
+ $opt{$_} = '' unless exists($opt{$_}) && defined($opt{$_});
+}
+$opt{saved_domsvc} = 0 unless $opt{saved_domsvc};
+my @part_pkg = @{$opt{'pkgparts'}};
+
+</%init>
+
diff --git a/httemplate/edit/cust_main/select-state.html b/httemplate/edit/cust_main/select-state.html
new file mode 100644
index 000000000..87546e5e3
--- /dev/null
+++ b/httemplate/edit/cust_main/select-state.html
@@ -0,0 +1,20 @@
+<SELECT NAME="<% $opt{'prefix'} %>state" onChange="<% $opt{'prefix'} %>state_changed(this); <% $opt{'onchange'} %>" <% $opt{'disabled'} %>>
+
+% foreach my $state ( keys %states ) {
+
+ <OPTION VALUE="<% $state %>"<% $state eq $opt{'state'} ? ' SELECTED' : '' %>><% $states{$state} || '(n/a)' %>
+
+% }
+
+
+</SELECT>
+
+<%init>
+my %opt = @_;
+foreach my $opt (qw( county state country prefix onchange disabled )) {
+ $opt{$_} = '' unless exists($opt{$_}) && defined($opt{$_});
+}
+
+tie my %states, 'Tie::IxHash', states_hash( $opt{'country'} );
+</%init>
+
diff --git a/httemplate/edit/cust_main_county-expand.cgi b/httemplate/edit/cust_main_county-expand.cgi
new file mode 100755
index 000000000..f56d31941
--- /dev/null
+++ b/httemplate/edit/cust_main_county-expand.cgi
@@ -0,0 +1,59 @@
+<!-- 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";
+%if ( $taxclass ) {
+% die "Can't expand entry!" if $cust_main_county->getfield('taxclass');
+%} else {
+% 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 000000000..7d1354d3e
--- /dev/null
+++ b/httemplate/edit/cust_main_county.cgi
@@ -0,0 +1,99 @@
+<!-- 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_main_note.cgi b/httemplate/edit/cust_main_note.cgi
new file mode 100755
index 000000000..303895bd8
--- /dev/null
+++ b/httemplate/edit/cust_main_note.cgi
@@ -0,0 +1,51 @@
+<% include('/elements/header-popup.html', "$action Customer Note") %>
+
+% if ( $cgi->param('error') ) {
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+
+<FORM ACTION="<% popurl(1) %>process/cust_main_note.cgi" METHOD=POST>
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
+<INPUT TYPE="hidden" NAME="notenum" VALUE="<% $notenum %>">
+
+
+<BR><BR>
+<TEXTAREA NAME="comment" ROWS="12" COLS="60">
+<% $comment %>
+</TEXTAREA>
+
+<BR><BR>
+<INPUT TYPE="submit" VALUE="<% $notenum ? "Apply Changes" : "Add Note" %>">
+
+</FORM>
+</BODY>
+</HTML>
+
+<%init>
+my($custnum, $comment, $notenum, $action);
+$comment = '';
+
+if ( $cgi->param('error') ) {
+ $comment = $cgi->param('comment');
+}elsif ($cgi->param('notenum')) {
+ $cgi->param('notenum') =~ /^(\d+)$/;
+ $notenum = $1;
+ die "illegal query ". $cgi->keywords unless $notenum;
+ my $note = qsearchs('cust_main_note', { 'notenum' => $notenum });
+ die "no such note: ". $notenum unless $note;
+ $comment = $note->comments;
+}
+
+$cgi->param('notenum') =~ /^(\d+)$/;
+$notenum = $1;
+
+$cgi->param('custnum') =~ /^(\d+)$/;
+$custnum = $1;
+
+die "illegal query ". $cgi->keywords unless $custnum;
+
+$action = $notenum ? 'Edit' : 'Add';
+
+</%init>
+
diff --git a/httemplate/edit/cust_pay.cgi b/httemplate/edit/cust_pay.cgi
new file mode 100755
index 000000000..855fbfcf1
--- /dev/null
+++ b/httemplate/edit/cust_pay.cgi
@@ -0,0 +1,145 @@
+% if ( $link eq 'popup' ) {
+ <% include('/elements/header-popup.html', $title ) %>
+% } else {
+ <% include("/elements/header.html", $title, '') %>
+% }
+
+% if ( $cgi->param('error') ) {
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+
+<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 ACTION="<% popurl(1) %>process/cust_pay.cgi" METHOD=POST>
+<INPUT TYPE="hidden" NAME="link" VALUE="<% $link %>">
+<INPUT TYPE="hidden" NAME="linknum" VALUE="<% $linknum %>">
+
+% unless ( $link eq 'popup' ) {
+ <% small_custview($custnum, $conf->config('countrydefault')) %>
+% }
+
+<INPUT TYPE="hidden" NAME="payby" VALUE="<% $payby %>">
+
+<BR><BR>
+Payment
+<% ntable("#cccccc", 2) %>
+
+<TR>
+ <TD ALIGN="right">Date</TD>
+ <TD COLSPAN=2>
+ <INPUT TYPE="text" NAME="_date" ID="_date_text" VALUE="<% time2str("%m/%d/%Y %r",$_date) %>">
+ <IMG SRC="../images/calendar.png" ID="_date_button" STYLE="cursor: pointer" TITLE="Select date">
+ </TD>
+</TR>
+
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "_date_text",
+ ifFormat: "%m/%d/%Y",
+ button: "_date_button",
+ align: "BR"
+ });
+</SCRIPT>
+
+<TR>
+ <TD ALIGN="right">Amount</TD>
+ <TD BGCOLOR="#ffffff" ALIGN="right"><% $money_char %></TD>
+ <TD><INPUT TYPE="text" NAME="paid" VALUE="<% $paid %>" SIZE=8 MAXLENGTH=8> by <B><% $payby{$payby} %></B></TD>
+</TR>
+
+% if ( $payby eq 'BILL' ) {
+ <TR>
+ <TD ALIGN="right">Check #</TD>
+ <TD COLSPAN=2><INPUT TYPE="text" NAME="payinfo" VALUE="<% $payinfo %>" SIZE=10></TD>
+ </TR>
+% }
+
+<TR>
+% if ( $link eq 'custnum' || $link eq 'popup' ) {
+
+ <TD ALIGN="right">Auto-apply<BR>to invoices</TD>
+ <TD COLSPAN=2>
+ <SELECT NAME="apply">
+ <OPTION VALUE="yes" SELECTED>yes
+ <OPTION>no</SELECT>
+ </TD>
+
+% } elsif ( $link eq 'invnum' ) {
+
+ <TD ALIGN="right">Apply to</TD>
+ <TD COLSPAN=2 BGCOLOR="#ffffff">Invoice #<B><% $linknum %></B> only</TD>
+ <INPUT TYPE="hidden" NAME="apply" VALUE="no">
+
+% }
+</TR>
+
+</TABLE>
+
+<INPUT TYPE="hidden" NAME="paybatch" VALUE="<% $paybatch %>">
+
+<BR>
+<INPUT TYPE="submit" VALUE="Post payment">
+
+</FORM>
+</BODY>
+</HTML>
+
+<%once>
+my $conf = new FS::Conf;
+
+my %payby = (
+ 'BILL' => 'Check',
+ 'CASH' => 'Cash',
+ 'WEST' => 'Western Union',
+ 'MCRD' => 'Manual credit card',
+);
+
+my $money_char = $conf->config('money_char') || '$';
+</%once>
+
+<%init>
+my($link, $linknum, $paid, $payby, $payinfo, $_date);
+if ( $cgi->param('error') ) {
+ $link = $cgi->param('link');
+ $linknum = $cgi->param('linknum');
+ $paid = $cgi->param('paid');
+ $payby = $cgi->param('payby');
+ $payinfo = $cgi->param('payinfo');
+ $_date = $cgi->param('_date') ? str2time($cgi->param('_date')) : time;
+} elsif ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+ $link = $cgi->param('popup') ? 'popup' : 'custnum';
+ $linknum = $1;
+ $paid = '';
+ $payby = $cgi->param('payby') || 'BILL';
+ $payinfo = '';
+ $_date = time;
+} elsif ( $cgi->param('invnum') =~ /^(\d+)$/ ) {
+ $link = 'invnum';
+ $linknum = $1;
+ $paid = '';
+ $payby = $cgi->param('payby') || 'BILL';
+ $payinfo = "";
+ $_date = time;
+} else {
+ die "illegal query ". $cgi->keywords;
+}
+
+my $paybatch = "webui-$_date-$$-". rand() * 2**32;
+
+my $title = 'Post '. $payby{$payby}. ' payment';
+$title .= " against Invoice #$linknum" if $link eq 'invnum';
+
+my $custnum;
+if ( $link eq 'invnum' ) {
+ my $cust_bill = qsearchs('cust_bill', { 'invnum' => $linknum } )
+ or die "unknown invnum $linknum";
+ $custnum = $cust_bill->custnum;
+} elsif ( $link eq 'custnum' ) {
+ $custnum = $linknum;
+}
+</%init>
+
diff --git a/httemplate/edit/cust_pkg.cgi b/httemplate/edit/cust_pkg.cgi
new file mode 100755
index 000000000..7a0432c5d
--- /dev/null
+++ b/httemplate/edit/cust_pkg.cgi
@@ -0,0 +1,152 @@
+%
+%
+%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);
+%
+%
+<% include('/elements/header.html', "Add/Edit Packages", '') %>
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+% }
+
+
+<FORM ACTION="<% $p1 %>process/cust_pkg.cgi" METHOD=POST>
+
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
+%
+%#current packages
+%my @cust_pkg = qsearch('cust_pkg', { 'custnum' => $custnum, 'cancel' => '' } );
+%
+%if (@cust_pkg) {
+%
+
+
+ Current packages - select to remove (services are moved to a new package below)
+ <TABLE>
+ <TR STYLE="background-color: #cccccc;">
+ <TH COLSPAN="2">Pkg #</TH>
+ <TH>Package description</TH>
+ </TR>
+ <BR><BR>
+%
+%
+% foreach ( sort { $all_pkg{ $a->getfield('pkgpart') }
+% cmp $all_pkg{ $b->getfield('pkgpart') }
+% }
+% @cust_pkg
+% )
+% {
+% my($pkgnum,$pkgpart)=( $_->getfield('pkgnum'), $_->getfield('pkgpart') );
+% my $checked = $remove_pkg{$pkgnum} ? ' CHECKED' : '';
+%
+%
+
+
+ <TR>
+ <TD><INPUT TYPE="checkbox" NAME="remove_pkg" VALUE="<% $pkgnum %>"<% $checked %>></TD>
+ <TD ALIGN="right"><% $pkgnum %>:</TD>
+ <TD><% $all_pkg{$pkgpart} %> - <% $all_comment{$pkgpart} %></TD>
+ </TR>
+% }
+
+
+ </TABLE>
+ <BR><BR>
+% }
+
+
+Order new packages
+<BR><BR>
+%
+%my $cust_main = qsearchs('cust_main',{'custnum'=>$custnum});
+%my $agent = qsearchs('agent',{'agentnum'=> $cust_main->agentnum });
+%
+%my %agent_pkgs = map { ( $_->pkgpart , $all_pkg{$_->pkgpart} ) }
+% qsearch('type_pkgs',{'typenum'=> $agent->typenum });
+%
+%my $count = 0;
+%my $pkgparts = 0;
+%
+
+
+<TABLE>
+ <TR STYLE="background-color: #cccccc;">
+ <TH>Qty.</TH>
+ <TH COLSPAN="2">Package Description</TH>
+ </TR>
+%
+%#foreach my $type_pkgs ( qsearch('type_pkgs',{'typenum'=> $agent->typenum }) ) {
+%foreach my $pkgpart ( sort { $agent_pkgs{$a} cmp $agent_pkgs{$b} }
+% keys(%agent_pkgs) ) {
+% $pkgparts++;
+% next unless exists $pkg{$pkgpart}; #skip disabled ones
+% #print qq!<TR>! if ( $count == 0 );
+% my $value = $cgi->param("pkg$pkgpart") || 0;
+%
+
+
+ <TR>
+ <TD>
+ <INPUT TYPE="text" NAME="<% "pkg$pkgpart" %>" VALUE="<% $value %>" SIZE="2" MAXLENGTH="2">
+ </TD>
+ <TD ALIGN="right"><% $pkgpart %>:</TD>
+ <TD><% $pkg{$pkgpart} %> - <% $comment{$pkgpart}%></TD>
+ </TR>
+%
+% $count ++ ;
+% #if ( $count == 2 ) {
+% # print qq!</TR>\n! ;
+% # $count = 0;
+% #}
+%}
+%
+
+
+</TABLE>
+% unless ( $pkgparts ) {
+% my $p2 = popurl(2);
+% my $typenum = $agent->typenum;
+% my $agent_type = qsearchs( 'agent_type', { 'typenum' => $typenum } );
+% my $atype = $agent_type->atype;
+%
+
+
+ (No <A HREF="<% $p2 %>browse/part_pkg.cgi">package definitions</A>,
+ or agent type
+ <A HREF="<% $p2 %>edit/agent_type.cgi?<% $typenum %>"><% $atype %></a>
+ is not allowed to purchase any packages.)
+% }
+
+
+<P><INPUT TYPE="submit" VALUE="Order">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
diff --git a/httemplate/edit/cust_refund.cgi b/httemplate/edit/cust_refund.cgi
new file mode 100755
index 000000000..aa825af94
--- /dev/null
+++ b/httemplate/edit/cust_refund.cgi
@@ -0,0 +1,126 @@
+%
+%
+%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);
+%
+%
+
+
+<% include('/elements/header.html', 'Refund '. ucfirst(lc($payby)). ' payment', '') %>
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+
+
+<% small_custview($custnum, $conf->config('countrydefault')) %>
+
+<FORM NAME="RefundForm" ACTION="<% $p1 %>process/cust_refund.cgi" METHOD=POST onSubmit="document.RefundForm.submit.disabled=true">
+<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>
+% if ( $cust_pay ) {
+%
+% #false laziness w/FS/FS/cust_pay.pm
+% my $payby = $cust_pay->payby;
+% my $paymask = $cust_pay->paymask;
+% $payby =~ s/^BILL$/Check/ if $paymask;
+% $payby =~ s/^CHEK$/Electronic check/;
+%
+%
+
+
+ <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)) %> # <% $paymask %></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 );
+%
+
+
+ <TR>
+ <TD ALIGN="right">Processor</TD><TD BGCOLOR="#ffffff"><% $processor %></TD>
+ </TR>
+% if ( length($auth) ) {
+
+ <TR>
+ <TD ALIGN="right">Authorization</TD><TD BGCOLOR="#ffffff"><% $auth %></TD>
+ </TR>
+% }
+% if ( length($order_number) ) {
+
+ <TR>
+ <TD ALIGN="right">Order number</TD><TD BGCOLOR="#ffffff"><% $order_number %></TD>
+ </TR>
+% }
+% }
+
+ </TABLE>
+% }
+
+
+<BR>Refund
+<% ntable("#cccccc", 2) %>
+
+ <TR>
+ <TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff"><% time2str("%D",$_date) %></TD>
+ </TR>
+
+ <TR>
+ <TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">$<INPUT TYPE="text" NAME="refund" VALUE="<% $refund %>" SIZE=8 MAXLENGTH=8></TD>
+ </TR>
+
+ <TR>
+ <TD ALIGN="right">Reason</TD><TD BGCOLOR="#ffffff"><INPUT TYPE="text" NAME="reason" VALUE="<% $reason %>"></TD>
+ </TR>
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" NAME="submit" VALUE="Post refund">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+
diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html
new file mode 100644
index 000000000..17c5ad3eb
--- /dev/null
+++ b/httemplate/edit/elements/edit.html
@@ -0,0 +1,234 @@
+%
+%
+% # options example...
+% #
+% # 'name' =>
+% # 'table' =>
+% # #? 'primary_key' => #required when the dbdef doesn't know...???
+% # 'labels' => {
+% # 'column' => 'Label',
+% # }
+% #
+% # listref - each item is a literal column name (or method) or hashref
+% # or (notyet) coderef
+% # if not specified all columns (except for the primary key) will be editable
+% # 'fields' => [
+% # 'columname',
+% # { 'field' => 'another_columname',
+% # 'type' => 'text', #text
+% # #checkbox
+% # #select
+% # #hidden - hidden value from object
+% # #fixed - display fixed value from here
+% # #fixedhidden - hidden value from here
+% # 'value' => 'Y', #for checkbox, fixed, fixedhidden
+% # },
+% # ]
+% #
+% # 'menubar' => '', #menubar arrayref
+% #
+% # #run when re-displaying with an error
+% # 'error_callback' => sub { my( $cgi, $object ) = @_; },
+% #
+% # #run when editing
+% # 'edit_callback' => sub { my( $cgi, $object ) = @_; },
+% #
+% # # returns a hashref for the new object
+% # 'new_hashref_callback'
+% #
+% # #run when adding
+% # 'new_callback' => sub { my( $cgi, $object ) = @_; },
+% #
+% # #XXX describe
+% # 'field_callback' => sub { },
+% #
+% # #string or coderef of additional HTML to add before </TABLE>
+% # 'html_table_bottom' => '',
+% #
+% # 'viewall_dir' => '', #'search' or 'browse', defaults to 'search'
+% #
+% # 'html_bottom' => '', #string
+% # 'html_bottom' => sub {
+% # my $object = shift;
+% # # ...
+% # "html_string";
+% # },
+% #
+% # # overrides default popurl(1)."process/$table.html"
+% # 'post_url' => popurl(1).'process/something',
+%
+% my(%opt) = @_;
+%
+% #false laziness w/process.html
+% my $table = $opt{'table'};
+% my $class = "FS::$table";
+% my $pkey = dbdef->table($table)->primary_key; #? $opt{'primary_key'} ||
+% my $fields = $opt{'fields'}
+% #|| [ grep { $_ ne $pkey } dbdef->table($table)->columns ];
+% || [ grep { $_ ne $pkey } fields($table) ];
+% #my @actualfields = map { ref($_) ? $_->{'field'} : $_ } @$fields;
+%
+% my $object;
+% if ( $cgi->param('error') ) {
+%
+% $object = $class->new( {
+% map { $_ => scalar($cgi->param($_)) } fields($table)
+% });
+%
+% &{$opt{'error_callback'}}($cgi, $object)
+% if $opt{'error_callback'};
+%
+% } elsif ( $cgi->keywords || $cgi->param($pkey) ) { #editing
+%
+% my $value;
+% if ( $cgi->param($pkey) ) {
+% $value = $cgi->param($pkey)
+% } else {
+% my( $query ) = $cgi->keywords;
+% $value = $query;
+% }
+% $value =~ /^(\d+)$/ or die "unparsable $pkey";
+% $object = qsearchs( $table, { $pkey => $1 } );
+% warn "$table $pkey => $1"
+% if $opt{'debug'};
+%
+% &{$opt{'edit_callback'}}($cgi, $object)
+% if $opt{'edit_callback'};
+%
+% } else { #adding
+%
+% my $hashref = $opt{'new_hashref_callback'}
+% ? &{$opt{'new_hashref_callback'}}
+% : {};
+%
+% $object = $class->new( $hashref );
+%
+% &{$opt{'new_callback'}}($cgi, $object)
+% if $opt{'new_callback'};
+%
+% }
+%
+% my $action = $object->$pkey() ? 'Edit' : 'Add';
+%
+% my $title = "$action $opt{'name'}";
+%
+% my $viewall_url = $p . ( $opt{'viewall_dir'} || 'search' ) . "/$table.html";
+% $viewall_url = $opt{'viewall_url'} if $opt{'viewall_url'};
+%
+% my @menubar = ();
+% if ( $opt{'menubar'} ) {
+% @menubar = @{ $opt{'menubar'} };
+% } else {
+% @menubar = (
+% 'Main menu' => $p, #eventually get rid of this when the ACL/UI update is done
+% #eventually use Lingua::bs to pluralize
+% "View all $opt{'name'}s" => $viewall_url,
+% );
+% }
+%
+%
+<% include("/elements/header.html", $title,
+ include( '/elements/menubar.html', @menubar )
+ )
+%>
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+
+% my $url = $opt{'post_url'} || popurl(1)."process/$table.html";
+
+<FORM ACTION="<% $url %>" METHOD=POST>
+<INPUT TYPE="hidden" NAME="svcdb" VALUE="<% $table %>">
+<INPUT TYPE="hidden" NAME="<% $pkey %>" VALUE="<% $object->$pkey() %>">
+<% ( $opt{labels} && exists $opt{labels}->{$pkey} )
+ ? $opt{labels}->{$pkey}
+ : $pkey
+%>
+#<% $object->$pkey() || "(NEW)" %>
+
+<% ntable("#cccccc",2) %>
+% foreach my $f ( map { ref($_) ? $_ : {'field'=>$_} }
+% @$fields
+% ) {
+%
+% &{ $opt{'field_callback'} }( $f )
+% if $opt{'field_callback'};
+%
+% my $field = $f->{'field'};
+% my $type = $f->{'type'} ||= 'text';
+%
+%
+
+
+ <TR>
+
+ <TD ALIGN="right">
+ <% ( $opt{labels} && exists $opt{labels}->{$field} )
+ ? $opt{labels}->{$field}
+ : $field
+ %>
+ </TD>
+
+% if ( $type eq 'fixed' ) {
+
+ <TD BGCOLOR="#dddddd"><% $f->{'value'} %></TD>
+ <INPUT TYPE="hidden" NAME="<% $field %>" VALUE="<% $f->{'value'} %>">
+
+% } elsif ( $type eq 'fixedhidden' ) {
+
+ <INPUT TYPE="hidden" NAME="<% $field %>" VALUE="<% $f->{'value'} %>">
+
+% } elsif ( $type eq 'checkbox' ) {
+
+ <TD>
+ <INPUT TYPE="checkbox" NAME="<% $field %>" VALUE="<% $f->{'value'} %>" <% $object->$field() eq $f->{'value'} ? ' CHECKED' : '' %>>
+ </TD>
+
+% } elsif ( $type eq 'select' ) {
+
+ <TD>
+ <SELECT NAME="<% $field %>"
+% my $aref = $f->{'value'}{'values'};
+% my $vkey = $f->{'value'}{'vcolumn'};
+% my $ckey = $f->{'value'}{'ccolumn'};
+% foreach my $v (@$aref) {
+ <OPTION <% ($object->$field() eq $v->$vkey) ? 'SELECTED' : '' %>
+ VALUE="<% $v->$vkey %>"><% $v->$ckey %></OPTION>
+% }
+ </SELECT>
+ </TD>
+
+% } else {
+
+ <TD>
+ <INPUT TYPE="<% $type %>" NAME="<% $field %>" VALUE="<% $object->$field() %>">
+ <TD>
+
+% }
+
+ </TR>
+
+% }
+
+<% ref( $opt{'html_table_bottom'} )
+ ? &{ $opt{'html_table_bottom'} }( $object )
+ : $opt{'html_table_bottom'}
+%>
+
+</TABLE>
+
+<% ref( $opt{'html_bottom'} )
+ ? &{ $opt{'html_bottom'} }( $object )
+ : $opt{'html_bottom'}
+%>
+
+<BR>
+
+<INPUT TYPE="submit" VALUE="<% $object->$pkey() ? "Apply changes" : "Add $opt{'name'}" %>">
+
+</FORM>
+
+<% include("/elements/footer.html") %>
+
diff --git a/httemplate/edit/elements/svc_Common.html b/httemplate/edit/elements/svc_Common.html
new file mode 100644
index 000000000..1fd66c251
--- /dev/null
+++ b/httemplate/edit/elements/svc_Common.html
@@ -0,0 +1,101 @@
+%
+% my %opt = @_;
+%
+% #my( $svcnum, $pkgnum, $svcpart, $part_svc );
+% my( $pkgnum, $svcpart, $part_svc );
+%
+% #get & untaint pkgnum & svcpart
+% if ( ! $cgi->param('error')
+% && $cgi->param('pkgnum') && $cgi->param('svcpart')
+% )
+% {
+% $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+% $pkgnum = $1;
+% $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+% $svcpart = $1;
+% $cgi->delete_all(); #so edit.html treats this correctly as new??
+% }
+%
+<% include( 'edit.html',
+
+ 'menubar' => [],
+
+ 'error_callback' => sub {
+ my( $cgi, $svc_x ) = @_;
+ #$svcnum = $svc_x->svcnum;
+ $pkgnum = $cgi->param('pkgnum');
+ $svcpart = $cgi->param('svcpart');
+
+ $part_svc = qsearchs( 'part_svc', { svcpart=>$svcpart });
+ die "No part_svc entry!" unless $part_svc;
+ },
+
+ 'edit_callback' => sub {
+ my( $cgi, $svc_x ) = @_;
+ #$svcnum = $svc_x->svcnum;
+ my $cust_svc = $svc_x->cust_svc
+ 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;
+ },
+
+ 'new_hash_callback' => sub {
+ #my( $cgi, $svc_x ) = @_;
+
+ { svcpart => $svcpart };
+
+ },
+
+ 'new_callback' => sub {
+ my( $cgi, $svc_x ) = @_;;
+
+ $part_svc = qsearchs( 'part_svc', { svcpart=>$svcpart });
+ die "No part_svc entry!" unless $part_svc;
+
+ #$svcnum='';
+
+ $svc_x->set_default_and_fixed;
+
+ },
+
+ 'field_callback' => sub {
+ my $f = shift;
+ my $columndef = $part_svc->part_svc_column($f->{'field'});
+ my $flag = $columndef->columnflag;
+ if ( $flag eq 'F' ) {
+ $f->{'type'} = 'fixed';
+ $f->{'value'} = $columndef->columnvalue;
+ }
+ },
+
+ 'html_table_bottom' => sub {
+ my $svc_x = shift;
+ my $html = '';
+ foreach my $field ($svc_x->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.
+ $html .=
+ $svc_x->pvf($field)->widget( 'HTML',
+ 'edit',
+ $svc_x->getfield($field)
+ );
+ }
+ }
+ $html;
+ },
+
+ 'html_bottom' => sub {
+ qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!.
+ qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!;
+ },
+
+ 'debug' => 1,
+
+ %opt #pass through/override params
+ )
+%>
diff --git a/httemplate/edit/inventory_class.html b/httemplate/edit/inventory_class.html
new file mode 100644
index 000000000..beefcd580
--- /dev/null
+++ b/httemplate/edit/inventory_class.html
@@ -0,0 +1,10 @@
+<% include( 'elements/edit.html',
+ 'name' => 'Inventory Class',
+ 'table' => 'inventory_class',
+ 'labels' => {
+ 'classnum' => 'Class number',
+ 'classname' => 'Class name',
+ },
+ 'viewall_dir' => 'browse',
+ )
+%>
diff --git a/httemplate/edit/msgcat.cgi b/httemplate/edit/msgcat.cgi
new file mode 100755
index 000000000..b46cdfd46
--- /dev/null
+++ b/httemplate/edit/msgcat.cgi
@@ -0,0 +1,57 @@
+<% header("Edit Message catalog" ) %>
+<BR>
+
+% if ( $cgi->param('error') ) {
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+
+<% $widget->html %>
+
+ </TABLE>
+ </BODY>
+</HTML>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+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;
+ },
+
+);
+
+</%init>
diff --git a/httemplate/edit/part_bill_event.cgi b/httemplate/edit/part_bill_event.cgi
new file mode 100755
index 000000000..0921a9577
--- /dev/null
+++ b/httemplate/edit/part_bill_event.cgi
@@ -0,0 +1,531 @@
+<!--mason kludge-->
+%
+%
+%if ( $cgi->param('eventpart') && $cgi->param('eventpart') =~ /^(\d+)$/ ) {
+% $cgi->param('eventpart', $1);
+%} else {
+% $cgi->param('eventpart', '');
+%}
+%
+%my ($creason, $newcreasonT, $newcreason);
+%my ($sreason, $newsreasonT, $newsreason);
+%
+%
+%my ($query) = $cgi->keywords;
+%my $action = '';
+%my $part_bill_event = '';
+%my $currentreasonclass = '';
+%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;
+%
+%
+
+
+<% include('/elements/header.html',
+ "$action Invoice Event Definition",
+ menubar(
+ 'Main Menu' => popurl(2),
+ 'View all invoice events' => popurl(2). 'browse/part_bill_event.cgi',
+ )
+ )
+%>
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+% }
+
+
+<FORM ACTION="<% popurl(1) %>process/part_bill_event.cgi" NAME="editEvent" METHOD=POST>
+<INPUT TYPE="hidden" NAME="eventpart" VALUE="<% $part_bill_event->eventpart %>">
+Invoice Event #<% $hashref->{eventpart} ? $hashref->{eventpart} : "(NEW)" %>
+
+<% ntable("#cccccc",2) %>
+
+ <TR>
+ <TD ALIGN="right">Event name </TD>
+ <TD><INPUT TYPE="text" NAME="event" VALUE="<% $hashref->{event} %>"></TD>
+ </TR>
+
+ <TR>
+ <TD ALIGN="right">For </TD>
+ <TD>
+ <SELECT NAME="payby">
+% tie my %payby, 'Tie::IxHash', FS::payby->cust_payby2longname;
+% foreach my $payby ( keys %payby ) {
+%
+
+
+ <OPTION VALUE="<% $payby %>"<% ($part_bill_event->payby eq $payby) ? ' SELECTED' : '' %>><% $payby{$payby} %></OPTION>
+% }
+
+
+ </SELECT> customers
+ </TD>
+ </TR>
+% my $days = $hashref->{seconds}/86400;
+
+
+ <TR>
+ <TD ALIGN="right">After</TD>
+ <TD><INPUT TYPE="text" NAME="days" VALUE="<% $days %>"> days</TD>
+ </TR>
+
+ <TR>
+ <TD ALIGN="right">Test event</TD>
+ <TD>
+ <SELECT NAME="freq">
+% tie my %freq, 'Tie::IxHash', '1d' => 'daily', '1m' => 'monthly';
+% foreach my $freq ( keys %freq ) {
+%
+
+
+ <OPTION VALUE="<% $freq %>"<% ($part_bill_event->freq eq $freq) ? ' SELECTED' : '' %>><% $freq{$freq} %></OPTION>
+% }
+
+
+ </SELECT>
+ </TD>
+ </TR>
+
+
+ <TR>
+ <TD ALIGN="right">Disabled</TD>
+ <TD>
+ <INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<% $hashref->{disabled} eq 'Y' ? ' CHECKED' : '' %>>
+ </TD>
+ </TR>
+
+ <TR>
+ <TD VALIGN="top" 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'};
+% my %agentnums = map { $_=>1 } split(/,\s*/, $plandata->{'agentnum'});
+% '<SELECT NAME="agentnum" MULTIPLE>'.
+% join("\n", map {
+% '<OPTION VALUE="'. $_->agentnum. '"'.
+% ( $agentnums{$_->agentnum} ? ' SELECTED' : '' ).
+% '>'. $_->agent
+% } qsearch('agent', { 'disabled' => '' } ) ).
+% '</SELECT>';
+%}
+%
+%my $conf = new FS::Conf;
+%my $money_char = $conf->config('money_char') || '$';
+%
+%#this is pretty kludgy right here.
+%tie my %events, 'Tie::IxHash',
+%
+% 'fee' => {
+% 'name' => 'Late fee (flat)',
+% '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,
+% },
+% 'fee_percent' => {
+% 'name' => 'Late fee (percentage)',
+% 'code' => '$cust_main->charge( sprintf(\'%.2f\', $cust_bill->owed * %%%percent%%% / 100 ), \'%%%reason%%%\' );',
+% 'html' =>
+% 'Percent <INPUT TYPE="text" SIZE="2" NAME="percent" VALUE="%%%percent%%%">%'.
+% '<BR>Reason <INPUT TYPE="text" NAME="reason" VALUE="%%%reason%%%">',
+% 'weight' => 10,
+% },
+% 'suspend' => {
+% 'name' => 'Suspend',
+% 'code' => '$cust_main->suspend(reason => %%%sreason%%%);',
+% 'weight' => 10,
+% 'reason' => 'S',
+% },
+% 'suspend-if-balance' => {
+% 'name' => 'Suspend if balance (this invoice and previous) over',
+% 'code' => '$cust_bill->cust_suspend_if_balance_over( %%%balanceover%%%, reason => %%%sreason%%%, );',
+% 'html' => " $money_char ". '<INPUT TYPE="text" SIZE="7" NAME="balanceover" VALUE="%%%balanceover%%%">',
+% 'weight' => 10,
+% 'reason' => 'S',
+% },
+% 'suspend-if-pkgpart' => {
+% 'name' => 'Suspend packages',
+% 'code' => '$cust_main->suspend_if_pkgpart({pkgparts => [%%%if_pkgpart%%%,], reason => %%%sreason%%%,});',
+% 'html' => sub { &select_pkgpart('if_pkgpart', @_) },
+% 'weight' => 10,
+% 'reason' => 'S',
+% },
+% 'suspend-unless-pkgpart' => {
+% 'name' => 'Suspend packages except',
+% 'code' => '$cust_main->suspend_unless_pkgpart({unless_pkgpart => [%%%unless_pkgpart%%%], reason => %%%sreason%%%,});',
+% 'html' => sub { &select_pkgpart('unless_pkgpart', @_) },
+% 'weight' => 10,
+% 'reason' => 'S',
+% },
+% 'cancel' => {
+% 'name' => 'Cancel',
+% 'code' => '$cust_main->cancel(reason => %%%creason%%%);',
+% 'weight' => 10,
+% 'reason' => 'C',
+% },
+%
+% '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,
+% },
+%
+% 'credit' => {
+% 'name' => "Create and apply a credit for the customer's balance (i.e. write off as bad debt)",
+% 'code' => '$cust_main->credit( $cust_main->balance, \'%%%reason%%%\' );',
+% 'html' => '<INPUT TYPE="text" NAME="reason" VALUE="%%%reason%%%">',
+% '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 or check to a pending batch',
+% 'code' => '$cust_bill->batch_card(%options);',
+% 'weight' => 40,
+% },
+%
+%
+% #'retriable' => {
+% # 'name' => 'Mark batched card event as retriable',
+% # 'code' => '$cust_pay_batch->retriable();',
+% # 'weight' => 60,
+% #},
+%
+% 'send' => {
+% 'name' => 'Send invoice (email/print/fax)',
+% 'code' => '$cust_bill->send();',
+% 'weight' => 50,
+% },
+%
+% 'send_email' => {
+% 'name' => 'Send invoice (email only)',
+% 'code' => '$cust_bill->email();',
+% 'weight' => 50,
+% },
+%
+% 'send_alternate' => {
+% 'name' => 'Send invoice (email/print/fax) with alternate template',
+% 'code' => '$cust_bill->send(\'%%%templatename%%%\');',
+% 'html' =>
+% '<INPUT TYPE="text" NAME="templatename" VALUE="%%%templatename%%%">',
+% 'weight' => 50,
+% },
+%
+% 'send_if_newest' => {
+% 'name' => 'Send invoice (email/print/fax) with alternate template, if it is still the newest invoice (useful for late notices - set to 31 days or later)',
+% 'code' => '$cust_bill->send_if_newest(\'%%%if_newest_templatename%%%\');',
+% 'html' =>
+% '<INPUT TYPE="text" NAME="if_newest_templatename" VALUE="%%%if_newest_templatename%%%">',
+% 'weight' => 50,
+% },
+%
+% 'send_agent' => {
+% 'name' => 'Send invoice (email/print/fax) ',
+% 'code' => '$cust_bill->send(\'%%%agent_templatename%%%\', [ %%%agentnum%%% ], \'%%%agent_invoice_from%%%\');',
+% 'html' => sub {
+% '<TABLE BORDER=0>
+% <TR>
+% <TD ALIGN="right">only for agent(s) </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%%%\',
+% \'format\' => \'%%%ftpformat%%%\',
+% );',
+% 'html' =>
+% '<TABLE BORDER=0>'.
+% '<TR><TD ALIGN="right">Format ("default" or "billco"): </TD>'.
+% '<TD>'.
+% '<!--'.
+% '<SELECT NAME="ftpformat">'.
+% '<OPTION VALUE="default">Default'.
+% '<OPTION VALUE="billco">Billco'.
+% '</SELECT>'.
+% '-->'.
+% '<INPUT TYPE="text" NAME="ftpformat" VALUE="%%%ftpformat%%%">'.
+% '</TD></TR>'.
+% '<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,
+% },
+%
+% 'spool_csv' => {
+% 'name' => 'Spool CSV invoice data',
+% 'code' => '$cust_bill->spool_csv(
+% \'format\' => \'%%%spoolformat%%%\',
+% \'dest\' => \'%%%spooldest%%%\',
+% \'balanceover\' => \'%%%spoolbalanceover%%%\',
+% \'agent_spools\' => \'%%%spoolagent_spools%%%\',
+% );',
+% 'html' => sub {
+% my $plandata = shift;
+%
+% my $html =
+% '<TABLE BORDER=0>'.
+% '<TR><TD ALIGN="right">Format: </TD>'.
+% '<TD>'.
+% '<SELECT NAME="spoolformat">';
+%
+% foreach my $option (qw( default billco )) {
+% $html .= qq(<OPTION VALUE="$option");
+% $html .= ' SELECTED' if $option eq $plandata->{'spoolformat'};
+% $html .= ">\u$option";
+% }
+%
+% $html .=
+% '</SELECT>'.
+% '</TD></TR>'.
+% '<TR><TD ALIGN="right">For destination: </TD>'.
+% '<TD>'.
+% '<SELECT NAME="spooldest">';
+%
+% tie my %dest, 'Tie::IxHash',
+% '' => '(all)',
+% 'POST' => 'Postal Mail',
+% 'EMAIL' => 'Email',
+% 'FAX' => 'Fax',
+% ;
+%
+% foreach my $dest (keys %dest) {
+% $html .= qq(<OPTION VALUE="$dest");
+% $html .= ' SELECTED' if $dest eq $plandata->{'spooldest'};
+% $html .= '>'. $dest{$dest};
+% }
+%
+% $html .=
+% '</SELECT>'.
+% '</TD></TR>'.
+%
+% '<TR>'.
+% '<TD ALIGN="right">if balance (this invoice and previous) over </TD>'.
+% '<TD>'.
+% "$money_char ".
+% '<INPUT TYPE="text" SIZE="7" NAME="spoolbalanceover" VALUE="%%%spoolbalanceover%%%">'.
+% '</TD>'.
+% '<TR><TD ALIGN="right">Individual per-agent spools? </TD>'.
+% '<TD><INPUT TYPE="checkbox" NAME="spoolagent_spools" VALUE="1" '.
+% ( $plandata->{'spoolagent_spools'} ? 'CHECKED' : '' ).
+% '>'.
+% '</TD></TR>'.
+% '</TABLE>';
+%
+% $html;
+% },
+% '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_and_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,
+% },
+%
+%;
+%
+<SCRIPT TYPE="text/javascript">var myreasons = new Array();</SCRIPT>
+%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}/;
+% }
+%
+<SCRIPT TYPE="text/javascript">myreasons.push('<% $events{$event}{reason} %>');
+</SCRIPT>
+% if ($event eq $part_bill_event->plan){
+% $currentreasonclass=$events{$event}{reason};
+% }
+% 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!onClick="showhide_table()" !;
+% 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>';
+%}
+%
+% if ($currentreasonclass eq 'C'){
+% if ($cgi->param('creason') =~ /^(-?\d+)$/){
+% $creason = $1;
+% }else{
+% $creason = $part_bill_event->reason;
+% }
+% if ($cgi->param('newcreasonT') =~ /^(\d+)$/){
+% $newcreasonT = $1;
+% }
+% if ($cgi->param('newcreason') =~ /^([\w\s]+)$/){
+% $newcreason = $1;
+% }
+% }elsif ($currentreasonclass eq 'S'){
+% if ($cgi->param('sreason') =~ /^(-?\d+)$/){
+% $sreason = $1;
+% }else{
+% $sreason = $part_bill_event->reason;
+% }
+% if ($cgi->param('newsreasonT') =~ /^(\d+)$/){
+% $newsreasonT = $1;
+% }
+% if ($cgi->param('newsreason') =~ /^([\w\s]+)$/){
+% $newsreason = $1;
+% }
+% }
+%
+
+</TD></TR>
+</TABLE>
+
+<SCRIPT TYPE="text/javascript">
+ function showhide_table()
+ {
+ for(i=0;i<document.editEvent.plan_weight_eventcode.length;i++){
+ if (document.editEvent.plan_weight_eventcode[i].checked == true){
+ currentevent=i;
+ }
+ }
+ if(myreasons[currentevent] == 'C'){
+ document.getElementById('Ctable').style.display = 'inline';
+ document.getElementById('Stable').style.display = 'none';
+ }else if(myreasons[currentevent] == 'S'){
+ document.getElementById('Ctable').style.display = 'none';
+ document.getElementById('Stable').style.display = 'inline';
+ }else{
+ document.getElementById('Ctable').style.display = 'none';
+ document.getElementById('Stable').style.display = 'none';
+ }
+ }
+</SCRIPT>
+
+<TABLE BGCOLOR="#cccccc" BORDER=0 WIDTH="100%">
+<TR><TD>
+<TABLE BORDER=0 id="Ctable" style="display:<% $currentreasonclass eq 'C' ? 'inline' : 'none' %>">
+<% include('/elements/tr-select-reason.html', 'creason', 'C', $creason, $newcreasonT, $newcreason) %>
+</TABLE>
+</TR></TD>
+</TABLE>
+
+<TABLE BGCOLOR="#cccccc" BORDER=0 WIDTH="100%">
+<TR><TD>
+<TABLE BORDER=0 id="Stable" style="display:<% $currentreasonclass eq 'S' ? 'inline' : 'none' %>">
+<% include('/elements/tr-select-reason.html', 'sreason', 'S', $sreason, $newsreasonT, $newsreason) %>
+</TABLE>
+</TR></TD>
+</TABLE>
+
+%
+%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 000000000..6717471dd
--- /dev/null
+++ b/httemplate/edit/part_export.cgi
@@ -0,0 +1,130 @@
+<!-- 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;
+% },
+%);
+%
+%
+
+<% include("/elements/header.html","$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 000000000..77822d7e0
--- /dev/null
+++ b/httemplate/edit/part_pkg.cgi
@@ -0,0 +1,400 @@
+%
+%
+%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 $part_pkg = '';
+%my @agent_type = ();
+%if ( $cgi->param('error') ) {
+% $part_pkg = new FS::part_pkg ( {
+% map { $_, scalar($cgi->param($_)) } fields('part_pkg')
+% } );
+% (@agent_type) = $cgi->param('agent_type');
+%}
+%
+%my $action = '';
+%my $clone_part_pkg = '';
+%my $pkgpart = '';
+%if ( $cgi->param('clone') ) {
+% $pkgpart = $cgi->param('clone');
+% $action = 'Custom Pricing';
+% $clone_part_pkg= qsearchs('part_pkg', { 'pkgpart' => $cgi->param('clone') } );
+% $part_pkg ||= $clone_part_pkg->clone;
+% $part_pkg->disabled('Y'); #isn't sticky on errors
+%} elsif ( $query && $query =~ /^(\d+)$/ ) {
+% (@agent_type) = map {$_->typenum} qsearch('type_pkgs',{'pkgpart'=>$1})
+% unless $part_pkg;
+% $part_pkg ||= qsearchs('part_pkg',{'pkgpart'=>$1});
+% $pkgpart = $part_pkg->pkgpart;
+%} 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;
+%
+%
+
+
+<% include("/elements/header.html","$action Package Definition", menubar(
+ 'Main Menu' => popurl(2),
+ 'View all packages' => popurl(2). 'browse/part_pkg.cgi',
+)) %>
+% #), ' onLoad="visualize()"');
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+% }
+
+
+<FORM NAME="dummy">
+
+<% itable('',8,1) %><TR><TD VALIGN="top">
+
+Package information
+
+<% ntable("#cccccc",2) %>
+ <TR>
+ <TD ALIGN="right">Package Definition #</TD>
+ <TD BGCOLOR="#ffffff">
+ <% $hashref->{pkgpart} ? $hashref->{pkgpart} : "(NEW)" %>
+ </TD>
+ </TR>
+ <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>
+ <% include( '/elements/tr-select-pkg_class.html', $part_pkg->classnum ) %>
+ <TR>
+ <TD ALIGN="right">Promotional code</TD>
+ <TD>
+ <INPUT TYPE="text" NAME="promo_code" SIZE=32 VALUE="<%$part_pkg->promo_code%>">
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Disable new orders</TD>
+ <TD>
+ <INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<% $hashref->{disabled} eq 'Y' ? ' CHECKED' : '' %>
+ </TD>
+ </TR>
+
+</TABLE>
+
+</TD><TD VALIGN="top">
+
+Tax information
+<% ntable("#cccccc", 2) %>
+ <TR>
+ <TD ALIGN="right">Setup fee tax exempt</TD>
+ <TD>
+ <INPUT TYPE="checkbox" NAME="setuptax" VALUE="Y" <% $hashref->{setuptax} eq 'Y' ? ' CHECKED' : '' %>>
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Recurring fee tax exempt</TD>
+ <TD>
+ <INPUT TYPE="checkbox" NAME="recurtax" VALUE="Y" <% $hashref->{recurtax} eq 'Y' ? ' CHECKED' : '' %>>
+ </TD>
+ </TR>
+
+% my $conf = new FS::Conf;
+% if ( $conf->exists('enable_taxclasses') ) {
+
+ <TR>
+ <TD align="right">Tax class</TD>
+ <TD>
+ <% include('/elements/select-taxclass.html', $hashref->{taxclass} ) %>
+ </TD>
+ </TR>
+
+% } else {
+
+ <% include('/elements/select-taxclass.html', $hashref->{taxclass} ) %>
+
+% }
+
+</TABLE>
+<BR>
+
+Line-item revenue recognition
+<% ntable("#cccccc", 2) %>
+% tie my %weight, 'Tie::IxHash',
+% 'pay_weight' => 'Payment',
+% 'credit_weight' => 'Credit'
+% ;
+% foreach my $weight (keys %weight) {
+ <TR>
+ <TD ALIGN="right"><% $weight{$weight} %> weight</TD>
+ <TD>
+ <INPUT TYPE="text" NAME="<% $weight %>" SIZE=6 VALUE=<% $hashref->{$weight} || 0 %>>
+ </TD>
+ </TR>
+% }
+</TABLE>
+
+</TD><TD VALIGN="top">
+
+%#Reseller information # after 1.7.2
+%#<% ntable("#cccccc", 2) %>
+%# <TR>
+%# <TD ALIGN="right"><% 'Agent Types' %></TD>
+%# <TD>
+%# <% include( '/elements/select-table.html',
+%# 'element_name' => 'agent_type',
+%# 'table' => 'agent_type',
+%# 'name_col' => 'atype',
+%# 'value' => \@agent_type,
+%# 'empty_label' => '(none)',
+%# 'element_etc' => 'multiple size="10"',
+%# )
+%# %>
+%# </TD>
+%# </TR>
+%#</TABLE>
+</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>';
+%
+%
+
+
+<BR><BR>Services included
+<% itable('', 4, 1) %><TR><TD VALIGN="top">
+<% $thead %>
+%
+%
+%my $where = "WHERE disabled IS NULL OR disabled = ''";
+%if ( $pkgpart ) {
+% $where .= " OR 0 < ( SELECT quantity FROM pkg_svc
+% WHERE pkg_svc.svcpart = part_svc.svcpart
+% AND pkgpart = $pkgpart
+% )";
+%}
+%my @part_svc = qsearch('part_svc', {}, '', $where);
+%my $q_part_pkg = $clone_part_pkg || $part_pkg;
+%my %pkg_svc = map { $_->svcpart => $_ } $q_part_pkg->pkg_svc;
+%
+%my @fixups = ();
+%my $count = 0;
+%my $columns = 3;
+%foreach my $part_svc ( @part_svc ) {
+% my $svcpart = $part_svc->svcpart;
+% my $pkg_svc = $pkg_svc{$svcpart}
+% || new FS::pkg_svc ( {
+% 'pkgpart' => $pkgpart,
+% 'svcpart' => $svcpart,
+% 'quantity' => 0,
+% 'primary_svc' => '',
+% } );
+%
+% push @fixups, "pkg_svc$svcpart";
+%
+%
+
+
+ <TR>
+ <TD>
+ <INPUT TYPE="text" NAME="pkg_svc<% $svcpart %>" SIZE=4 MAXLENGTH=3 VALUE="<% $cgi->param("pkg_svc$svcpart") || $pkg_svc->quantity || 0 %>">
+ </TD>
+
+ <TD>
+ <INPUT TYPE="radio" NAME="pkg_svc_primary" VALUE="<% $svcpart %>" <% $pkg_svc->primary_svc =~ /^Y/i ? ' CHECKED' : '' %>>
+ </TD>
+
+ <TD>
+ <A HREF="part_svc.cgi?<% $part_svc->svcpart %>"><% $part_svc->svc %></A> <% $part_svc->disabled =~ /^Y/i ? ' (DISABLED' : '' %>
+ </TD>
+ </TR>
+% foreach ( 1 .. $columns-1 ) {
+% if ( $count == int( $_ * scalar(@part_svc) / $columns ) ) {
+%
+
+ </TABLE></TD><TD VALIGN="top"><% $thead %>
+% }
+% }
+% $count++;
+%
+% }
+
+
+</TR></TABLE></TD></TR></TABLE>
+% foreach my $f ( qw( clone pkgnum ) ) {
+
+ <INPUT TYPE="hidden" NAME="<% $f %>" VALUE="<% $cgi->param($f) %>">
+% }
+
+<INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $part_pkg->pkgpart %>">
+%
+%
+%# prolly should be in database
+%tie my %plans, 'Tie::IxHash', %{ FS::part_pkg::plan_info() };
+%
+%my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
+% split("\n", ($clone_part_pkg||$part_pkg)->plandata );
+%#warn join("\n", map { "$_: $plandata{$_}" } keys %plandata ). "\n";
+%
+%tie my %options, 'Tie::IxHash', map { $_=>$plans{$_}->{'name'} } keys %plans;
+%
+%#my @form_select = ('classnum');
+%#if ( $conf->exists('enable_taxclasses') ) {
+%# push @form_select, 'taxclass';
+%#} else {
+%# push @fixups, 'taxclass'; #hidden
+%#}
+%my @form_elements = ( 'classnum', 'taxclass' );
+%# copying non-existant elements is probably harmless, but after 1.7.2
+%#my @form_elements = ( 'classnum', 'taxclass', 'agent_type' );
+%
+%my @form_radio = ();
+%if ( dbdef->table('pkg_svc')->column('primary_svc') ) {
+% push @form_radio, 'pkg_svc_primary';
+%}
+%
+%tie my %freq, 'Tie::IxHash', %{FS::part_pkg->freqs_href()};
+%if ( $part_pkg->dbdef_table->column('freq')->type =~ /(int)/i ) {
+% delete $freq{$_} foreach grep { ! /^\d+$/ } keys %freq;
+%}
+%
+%my $widget = new HTML::Widgets::SelectLayers(
+% 'selected_layer' => $part_pkg->plan,
+% 'options' => \%options,
+% 'form_name' => 'dummy',
+% 'form_action' => 'process/part_pkg.cgi',
+% 'form_elements' => \@form_elements,
+% 'form_text' => [ qw(pkg comment promo_code clone pkgnum pkgpart),
+% qw(pay_weight credit_weight),
+% @fixups,
+% ],
+% 'form_checkbox' => [ qw(setuptax recurtax disabled) ],
+% 'form_radio' => \@form_radio,
+% 'layer_callback' => sub {
+% my $layer = shift;
+% my $html = qq!<INPUT TYPE="hidden" NAME="plan" VALUE="$layer">!.
+% ntable("#cccccc",2);
+% $html .= '
+% <TR>
+% <TD ALIGN="right">Recurring fee frequency </TD>
+% <TD><SELECT NAME="freq">
+% ';
+%
+% my @freq = keys %freq;
+% @freq = grep { /^\d+$/ } @freq
+% if exists($plans{$layer}->{'freq'}) && $plans{$layer}->{'freq'} eq 'm';
+% foreach my $freq ( @freq ) {
+% $html .= qq(<OPTION VALUE="$freq");
+% $html .= ' SELECTED' if $freq eq $part_pkg->freq;
+% $html .= ">$freq{$freq}";
+% }
+% $html .= '</SELECT></TD></TR>';
+%
+% 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)">!; #after 1.7.2
+% } elsif ( $href->{$field}{'type'} eq 'checkbox' ) {
+% $html .= qq!<INPUT TYPE="checkbox" NAME="$field" VALUE=1 !.
+% ( exists($plandata{$field}) && $plandata{$field}
+% ? ' CHECKED'
+% : ''
+% ). '>';
+% } elsif ( $href->{$field}{'type'} =~ /^select/ ) {
+% $html .= '<SELECT';
+% $html .= ' MULTIPLE'
+% if $href->{$field}{'type'} eq 'select_multiple';
+% $html .= qq! NAME="$field" onChange="fchanged(this)">!; # after 1.7.2
+%
+% if ( $href->{$field}{'select_table'} ) {
+% 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'});
+% }
+% } elsif ( $href->{$field}{'select_options'} ) {
+% foreach my $key ( keys %{ $href->{$field}{'select_options'} } ) {
+% my $value = $href->{$field}{'select_options'}{$key};
+% $html .= qq!<OPTION VALUE="$key"!.
+% ( $plandata{$field} =~ /(^|, *)$value *(,|$)/
+% ? ' SELECTED'
+% : ''
+% ).
+% '>'. $value;
+% }
+%
+% } else {
+% $html .= '<font color="#ff0000">warning: '.
+% "don't know how to retreive options for $field select field".
+% '</font>';
+% }
+% $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)">'; #after 1.7.2
+%
+% $html;
+%
+% },
+%);
+%
+%
+
+
+<BR><BR>Price plan <% $widget->html %>
+ </BODY>
+</HTML>
diff --git a/httemplate/edit/part_referral.html b/httemplate/edit/part_referral.html
new file mode 100755
index 000000000..7ce52174d
--- /dev/null
+++ b/httemplate/edit/part_referral.html
@@ -0,0 +1,9 @@
+<% include( 'elements/edit.html',
+ 'name' => 'Advertising source',
+ 'table' => 'part_referral',
+ 'fields' => [ 'referral' ],
+ 'labels' => { 'referral' => 'Advertising source' },
+ 'viewall_dir' => 'browse',
+ 'html_table_bottom' => include('/elements/tr-select-agent.html'),
+ )
+%>
diff --git a/httemplate/edit/part_svc.cgi b/httemplate/edit/part_svc.cgi
new file mode 100755
index 000000000..6ba9240e3
--- /dev/null
+++ b/httemplate/edit/part_svc.cgi
@@ -0,0 +1,354 @@
+%
+%my $part_svc;
+%my $clone = '';
+%if ( $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()\""
+%
+
+<% include("/elements/header.html","$action Service Definition",
+ menubar( 'Main Menu' => $p,
+ 'View all service definitions' => "${p}browse/part_svc.cgi"
+ ),
+ )
+%>
+
+<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="svcpart" VALUE="<% $hashref->{svcpart} %>">
+<BR>
+Service definitions are the templates for items you offer to your customers.
+<UL><LI>svc_acct - Accounts - anything with a username (Mailboxes, PPP accounts, shell accounts, RADIUS entries for broadband, etc.)
+ <LI>svc_domain - Domains
+ <LI>svc_forward - mail forwarding
+ <LI>svc_www - Virtual domain website
+ <LI>svc_broadband - Broadband/High-speed Internet service (always-on)
+ <LI>svc_phone - Customer phone numbers
+ <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, or select an inventory class to manually or automatically fill in
+that field.
+<BR><BR>
+
+% #YUCK. false laziness w/part_svc.pm. go away virtual fields, please
+% my %vfields;
+% foreach my $svcdb ( FS::part_svc->svc_tables() ) {
+% eval "use FS::$svcdb;";
+% 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);
+% $vfields{$svcdb}->{$field} = $pvf;
+% #warn "\$vfields{$svcdb}->{$field} = $pvf";
+% } #next $field
+% } #next $svcdb
+%
+% #code duplication w/ edit/part_svc.cgi, should move this hash to part_svc.pm
+% # and generalize the subs
+% # condition sub is tested to see whether to disable display of this choice
+% # params: ( $def, $layer, $field ) (see SUB below)
+% my $inv_sub = sub {
+% $_[0]->{disable_inventory}
+% || $_[0]->{'type'} ne 'text'
+% };
+% tie my %flag, 'Tie::IxHash',
+% '' => { 'desc' => 'No default', },
+% 'D' => { 'desc' => 'Default',
+% 'condition' =>
+% sub { $_[0]->{disable_default} },
+% },
+% 'F' => { 'desc' => 'Fixed (unchangeable)',
+% 'condition' =>
+% sub { $_[0]->{disable_fixed} },
+% },
+% 'S' => { 'desc' => 'Selectable Choice',
+% 'condition' =>
+% sub { !ref($_[0]) || $_[0]->{disable_select} },
+% },
+%# need to template-ize httemplate/edit/svc_* first
+%# 'M' => { 'desc' => 'Manual selection from inventory',
+%# 'condition' => $inv_sub,
+%# },
+% 'A' => { 'desc' => 'Automatically fill in from inventory',
+% 'condition' => $inv_sub,
+% },
+% 'X' => { 'desc' => 'Excluded',
+% 'condition' =>
+% sub { ! $vfields{$_[1]}->{$_[2]} },
+%
+% },
+% ;
+%
+% my @dbs = $hashref->{svcdb}
+% ? ( $hashref->{svcdb} )
+% : FS::part_svc->svc_tables();
+%
+% 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( 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().
+% "<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 .= include('/elements/table-grid.html', 'cellpadding' => 4 ).
+% '<TR>'.
+% '<TH CLASS="grid" BGCOLOR="#cccccc">Field</TH>'.
+% '<TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=2>Modifier</TH>'.
+% '</TR>';
+%
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor;
+%
+% #yucky kludge
+% my @fields = defined( 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 = $part_svc_column->columnvalue;
+% my $flag = $part_svc_column->columnflag;
+% #my $def = $defs{$layer}{$field};
+% my $def = FS::part_svc->svc_table_fields($layer)->{$field};
+% my $label = $def->{'def_label'} || $def->{'label'};
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+% $html .= qq!<TR><TD CLASS="grid" BGCOLOR="$bgcolor" ALIGN="right">!.
+% ( $label || $field ).
+% "</TD>";
+% $flag = '' if $def->{type} eq 'disabled';
+%
+% $html .= qq!<TD CLASS="grid" BGCOLOR="$bgcolor">!;
+%
+% if ( $def->{type} eq 'disabled' ) {
+%
+% $html .= 'No default';
+%
+% } else {
+%
+% $html .= qq!<SELECT NAME="${layer}__${field}_flag"!.
+% qq! onChange="${layer}__${field}_flag_changed(this)">!;
+%
+% foreach my $f ( keys %flag ) {
+%
+% #here is where the SUB from above is called, to skip some choices
+% next if $flag{$f}->{condition}
+% && &{ $flag{$f}->{condition} }( $def, $layer, $field );
+%
+% $html .= qq!<OPTION VALUE="$f"!.
+% ' SELECTED'x($flag eq $f ).
+% '>'. $flag{$f}->{desc};
+%
+% }
+%
+% $html .= '</SELECT>';
+%
+% $html .= join("\n",
+% '<SCRIPT>',
+% " function ${layer}__${field}_flag_changed(what) {",
+% ' var f = what.options[what.selectedIndex].value;',
+% ' if ( f == "" || f == "X" ) { //disable',
+% " what.form.${layer}__${field}.disabled = true;".
+% " what.form.${layer}__${field}.style.backgroundColor = '#dddddd';".
+% " if ( what.form.${layer}__${field}_classnum ) {".
+% " what.form.${layer}__${field}_classnum.disabled = true;".
+% " what.form.${layer}__${field}_classnum.style.backgroundColor = '#dddddd';".
+% " }".
+% ' } else if ( f == "D" || f == "F" || f =="S" ) { //enable, text box',
+% " what.form.${layer}__${field}.disabled = false;".
+% " what.form.${layer}__${field}.style.backgroundColor = '#ffffff';".
+% " if ( f == 'S' || '${field}' == 'usergroup' ) {". # kludge
+% " what.form.${layer}__${field}.multiple = true;".
+% " } else {".
+% " what.form.${layer}__${field}.multiple = false;".
+% " }".
+% " what.form.${layer}__${field}.style.display = '';".
+% " if ( what.form.${layer}__${field}_classnum ) {".
+% " what.form.${layer}__${field}_classnum.disabled = false;".
+% " what.form.${layer}__${field}_classnum.style.backgroundColor = '#ffffff';".
+% " what.form.${layer}__${field}_classnum.style.display = 'none';".
+% " }".
+% ' } else if ( f == "M" || f == "A" ) { //enable, inventory',
+% " what.form.${layer}__${field}.disabled = false;".
+% " what.form.${layer}__${field}.style.backgroundColor = '#ffffff';".
+% " what.form.${layer}__${field}.style.display = 'none';".
+% " if ( what.form.${layer}__${field}_classnum ) {".
+% " what.form.${layer}__${field}_classnum.disabled = false;".
+% " what.form.${layer}__${field}_classnum.style.backgroundColor = '#ffffff';".
+% " what.form.${layer}__${field}_classnum.style.display = '';".
+% " }".
+% ' }',
+% ' }',
+% '</SCRIPT>',
+% );
+%
+% }
+%
+% $html .= qq!</TD><TD CLASS="grid" BGCOLOR="$bgcolor">!;
+%
+% my $disabled = $flag ? ''
+% : 'DISABLED STYLE="background-color: #dddddd"';
+%
+% if ( !$def->{type} || $def->{type} eq 'text' ) {
+%
+% my $nodisplay = ' STYLE="display:none"';
+% my $is_inv = ( $flag =~ /^[MA]$/ );
+%
+% $html .=
+% qq!<INPUT TYPE="text" NAME="${layer}__${field}" VALUE="$value" !.
+% $disabled.
+% ( $is_inv ? $nodisplay : $disabled ).
+% '>';
+%
+% $html .= include('/elements/select-table.html',
+% 'element_name' => "${layer}__${field}_classnum",
+% 'element_etc' => ( $is_inv
+% ? $disabled
+% : $nodisplay
+% ),
+% 'table' => 'inventory_class',
+% 'name_col' => 'classname',
+% 'value' => $value,
+% 'empty_label' => 'Select inventory class',
+% );
+%
+% } elsif ( $def->{type} eq 'select' ) {
+%
+% $html .= qq!<SELECT NAME="${layer}__${field}" $disabled!;
+% $html .= ' MULTIPLE' if $flag eq 'S';
+% $html .= '>';
+% $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"!.
+% (grep(/^$rvalue$/, split(',',$value)) ? ' SELECTED>' : '>' ).
+% $record->getfield($def->{select_label}). '</OPTION>';
+% } #next $record
+% } else { # select_list
+% foreach my $item ( @{$def->{select_list}} ) {
+% $html .= qq!<OPTION VALUE="$item"!.
+% (grep(/^$item$/, split(',',$value)) ? ' SELECTED>' : '>' ).
+% $item. '</OPTION>';
+% } #next $item
+% } #endif
+% $html .= '</SELECT>';
+%
+% } elsif ( $def->{type} eq 'radius_usergroup_selector' ) {
+%
+% #XXX disable the RADIUS usergroup selector? ugh it sure does need
+% #an overhaul, people have dum group problems because of it
+%
+% $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};
+%
+% }
+%
+% $html .= "</TD></TR>\n";
+%
+% } #foreach my $field (@fields) {
+%
+% $part_svc->svcpart('') if $clone; #undone
+% $html .= "</TABLE>";
+%
+% $html .= include('/elements/progress-init.html',
+% $layer, #form name
+% [ qw(svc svcpart disabled exportnum), @fields ],
+% 'process/part_svc.cgi',
+% $p.'browse/part_svc.cgi',
+% $layer,
+% );
+% $html .= '<BR><INPUT NAME="submit" TYPE="button" VALUE="'.
+% ($hashref->{svcpart} ? 'Apply changes' : 'Add service'). '" '.
+% ' onClick="document.'. "$layer.submit.disabled=true; ".
+% "fixup(document.$layer); $layer". 'process();">';
+%
+% #$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 000000000..9dda4ebf9
--- /dev/null
+++ b/httemplate/edit/part_virtual_field.cgi
@@ -0,0 +1,103 @@
+%
+%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);
+%
+%
+<% include('/elements/header.html', "$action Virtual Field Definition") %>
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+
+
+<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) {
+% foreach my $dbtable (qw( svc_broadband )) {
+% 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>
+<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>
+
+<% include('/elements/footer.html') %>
diff --git a/httemplate/edit/payment_gateway.html b/httemplate/edit/payment_gateway.html
new file mode 100644
index 000000000..59970c3f6
--- /dev/null
+++ b/httemplate/edit/payment_gateway.html
@@ -0,0 +1,134 @@
+%
+%
+%my $payment_gateway;
+%if ( $cgi->param('error') ) {
+% $payment_gateway = new FS::payment_gateway ( {
+% map { $_, scalar($cgi->param($_)) } fields('payment_gateway')
+% } );
+%} elsif ( $cgi->keywords ) {
+% my($query) = $cgi->keywords;
+% $query =~ /^(\d+)$/;
+% $payment_gateway = qsearchs( 'payment_gateway', { 'gatewaynum' => $1 } );
+%} else { #adding
+% $payment_gateway = new FS::payment_gateway {};
+%}
+%my $action = $payment_gateway->gatewaynum ? 'Edit' : 'Add';
+%#my $hashref = $payment_gateway->hashref;
+%
+%
+
+
+<% include("/elements/header.html","$action Payment gateway", menubar(
+ 'Main Menu' => $p,
+ 'View all payment gateways' => $p. 'browse/payment_gateway.html',
+)) %>
+% if ( $cgi->param('error') ) {
+
+<FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+% }
+
+
+<FORM ACTION="<%popurl(1)%>process/payment_gateway.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="gatewaynum" VALUE="<% $payment_gateway->gatewaynum %>">
+Gateway #<% $payment_gateway->gatewaynum || "(NEW)" %>
+
+<% ntable('#cccccc', 2, '') %>
+
+<TR>
+ <TH ALIGN="right">Gateway: </TH>
+ <TD>
+% if ( $payment_gateway->gatewaynum ) {
+
+
+ <% $payment_gateway->gateway_module %>
+ <INPUT TYPE="hidden" NAME="gateway_module" VALUE="<% $payment_gateway->gateway_module %>">
+% } else {
+
+
+ <SELECT NAME="gateway_module" SIZE=1>
+% foreach my $module ( qw(
+% 2CheckOut
+% AuthorizeNet
+% BankOfAmerica
+% Beanstream
+% Capstone
+% Cardstream
+% CashCow
+% CyberSource
+% eSec
+% eSelectPlus
+% Exact
+% iAuthorizer
+% IPaymentTPG
+% Jettis
+% LinkPoint
+% MerchantCommerce
+% Network1Financial
+% OCV
+% OpenECHO
+% PayConnect
+% PayflowPro
+% PaymentsGateway
+% PXPost
+% SecureHostingUPG
+% Skipjack
+% StGeorge
+% SurePay
+% TCLink
+% TransactionCentral
+% VirtualNet
+% ) ) {
+%
+
+ <OPTION VALUE="<% $module %>"><% $module %>
+% }
+
+ </SELECT>
+% }
+
+
+ </TD>
+</TR>
+
+<TR>
+ <TH ALIGN="right">Username: </TH>
+ <TD><INPUT TYPE="text" NAME="gateway_username" VALUE="<% $payment_gateway->gateway_username %>"></TD>
+</TR>
+
+<TR>
+ <TH ALIGN="right">Password: </TH>
+ <TD><INPUT TYPE="text" NAME="gateway_password" VALUE="<% $payment_gateway->gateway_password %>"></TD>
+</TR>
+
+<TR>
+ <TH ALIGN="right">Action: </TH>
+ <TD>
+ <SELECT NAME="gateway_action" SIZE=1>
+% foreach my $action (
+% 'Normal Authorization',
+% 'Authorization Only',
+% 'Authorization Only, Post Authorization',
+% ) {
+%
+
+ <OPTION VALUE="<% $action %>"<% $action eq $payment_gateway->gateway_action ? ' SELECTED' : '' %>><% $action %>
+% }
+
+ </SELECT>
+ </TD>
+</TR>
+
+<TR>
+ <TH ALIGN="right">Options: (Name/Value pairs, one element per line)</TH>
+ <TD>
+ <TEXTAREA ROWS="5" NAME="gateway_options"><% join("\r", $payment_gateway->options ) %></TEXTAREA>
+ </TD>
+</TR>
+
+</TABLE>
+
+<BR><INPUT TYPE="submit" VALUE="<% $payment_gateway->gatewaynum ? "Apply changes" : "Add gateway" %>">
+ </FORM>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/edit/pkg_class.html b/httemplate/edit/pkg_class.html
new file mode 100644
index 000000000..6f2b072f1
--- /dev/null
+++ b/httemplate/edit/pkg_class.html
@@ -0,0 +1,16 @@
+<% include( 'elements/edit.html',
+ 'name' => 'Package Class',
+ 'table' => 'pkg_class',
+ 'fields' => [
+ 'classname',
+ { field=>'disabled', type=>'checkbox', value=>'Y', },
+ ],
+ 'labels' => {
+ 'classnum' => 'Class number',
+ 'classname' => 'Class name',
+ 'disabled' => 'Disable class',
+ },
+ 'viewall_dir' => 'browse',
+ )
+
+%>
diff --git a/httemplate/edit/prepay_credit.cgi b/httemplate/edit/prepay_credit.cgi
new file mode 100644
index 000000000..c22904d6c
--- /dev/null
+++ b/httemplate/edit/prepay_credit.cgi
@@ -0,0 +1,111 @@
+%
+%my $agent = '';
+%my $agentnum = '';
+%if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+% $agent = qsearchs('agent', { 'agentnum' => $agentnum=$1 } );
+%}
+%
+%tie my %multiplier, 'Tie::IxHash',
+% 1 => 'seconds',
+% 60 => 'minutes',
+% 3600 => 'hours',
+%;
+%
+%tie my %bytemultiplier, 'Tie::IxHash',
+% 1 => 'bytes',
+% 1000 => 'Kbytes',
+% 1000000 => 'Mbytes',
+% 1000000000 => 'Gbytes',
+%;
+%
+%$cgi->param('multiplier', '60') unless $cgi->param('multiplier');
+%$cgi->param('upmultiplier', '1000000') unless $cgi->param('upmultiplier');
+%$cgi->param('downmultiplier', '1000000') unless $cgi->param('downmultiplier');
+%$cgi->param('totalmultiplier','1000000') unless $cgi->param('totalmultiplier');
+%
+%
+
+
+<% include("/elements/header.html",'Generate prepaid cards'. ($agent ? ' for '. $agent->agent : ''),
+ menubar( 'Main Menu' => $p, ))
+%>
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#FF0000">Error: <% $cgi->param('error') %></FONT>
+% }
+
+
+<FORM ACTION="<%popurl(1)%>process/prepay_credit.cgi" METHOD="POST" NAME="OneTrueForm" onSubmit="document.OneTrueForm.submit.disabled=true">
+
+Generate
+<INPUT TYPE="text" NAME="num" VALUE="<% $cgi->param('num') || '(quantity)' %>" SIZE=10 MAXLENGTH=10 onFocus="if ( this.value == '(quantity)' ) { this.value = ''; }">
+<SELECT NAME="type">
+% foreach (qw(alpha alphanumeric numeric)) {
+
+ <OPTION<% $cgi->param('type') eq $_ ? ' SELECTED' : '' %>><% $_ %>
+% }
+
+</SELECT>
+ prepaid cards
+
+<BR>for <SELECT NAME="agentnum"><OPTION>(any agent)
+% foreach my $opt_agent ( qsearch('agent', { 'disabled' => '' } ) ) {
+
+ <OPTION VALUE="<% $opt_agent->agentnum %>"<% $opt_agent->agentnum == $agentnum ? ' SELECTED' : '' %>><% $opt_agent->agent %>
+% }
+
+</SELECT>
+
+<TABLE>
+<TR><TD>Value:
+$<INPUT TYPE="text" NAME="amount" SIZE=8 MAXLENGTH=7 VALUE="<% $cgi->param('amount') %>">
+</TD>
+<TD>and/or
+<INPUT TYPE="text" NAME="seconds" SIZE=6 MAXLENGTH=5 VALUE="<% $cgi->param('seconds') %>">
+<SELECT NAME="multiplier">
+% foreach my $multiplier ( keys %multiplier ) {
+
+ <OPTION VALUE="<% $multiplier %>"<% $cgi->param('multiplier') eq $multiplier ? ' SELECTED' : '' %>><% $multiplier{$multiplier} %>
+% }
+
+</SELECT>
+</TD></TR>
+<TR><TD></TD>
+<TD>and/or
+<INPUT TYPE="text" NAME="upbytes" SIZE=6 MAXLENGTH=5 VALUE="<% $cgi->param('upbytes') %>">
+<SELECT NAME="upmultiplier">
+% foreach my $multiplier ( keys %bytemultiplier ) {
+
+ <OPTION VALUE="<% $multiplier %>"<% $cgi->param('upmultiplier') eq $multiplier ? ' SELECTED' : '' %>><% $bytemultiplier{$multiplier} %>
+% }
+
+</SELECT> upload
+</TD></TR>
+<TR><TD></TD>
+<TD>and/or
+<INPUT TYPE="text" NAME="downbytes" SIZE=6 MAXLENGTH=5 VALUE="<% $cgi->param('downbytes') %>">
+<SELECT NAME="downmultiplier">
+% foreach my $multiplier ( keys %bytemultiplier ) {
+
+ <OPTION VALUE="<% $multiplier %>"<% $cgi->param('downmultiplier') eq $multiplier ? ' SELECTED' : '' %>><% $bytemultiplier{$multiplier} %>
+% }
+
+</SELECT> download
+</TD></TR>
+<TR><TD></TD>
+<TD>and/or
+<INPUT TYPE="text" NAME="totalbytes" SIZE=6 MAXLENGTH=5 VALUE="<% $cgi->param('totalbytes') %>">
+<SELECT NAME="totalmultiplier">
+% foreach my $multiplier ( keys %bytemultiplier ) {
+
+ <OPTION VALUE="<% $multiplier %>"<% $cgi->param('totalmultiplier') eq $multiplier ? ' SELECTED' : '' %>><% $bytemultiplier{$multiplier} %>
+% }
+
+</SELECT> total transfer
+</TD></TR>
+</TABLE>
+<BR><BR>
+<INPUT TYPE="submit" NAME="submit" VALUE="Generate" onSubmit="this.disabled = true">
+
+</FORM></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 000000000..26e234fb0
--- /dev/null
+++ b/httemplate/edit/process/REAL_cust_pkg.cgi
@@ -0,0 +1,35 @@
+%
+%
+%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;
+%my $error;
+%if ( $hash{'bill'} != $old->bill # if the next bill date was changed
+% && $hash{'bill'} < time # to a date in the past
+% && ! $cgi->param('bill_areyousure') # and it wasn't confirmed
+% )
+%{
+% $error = '_bill_areyousure';
+%} else {
+% $new = new FS::cust_pkg \%hash;
+% $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/access_group.html b/httemplate/edit/process/access_group.html
new file mode 100644
index 000000000..c80311586
--- /dev/null
+++ b/httemplate/edit/process/access_group.html
@@ -0,0 +1,15 @@
+<% include( 'elements/process.html',
+ 'table' => 'access_group',
+ 'viewall_dir' => 'browse',
+ 'process_m2m' => { 'link_table' => 'access_groupagent',
+ 'target_table' => 'agent',
+ },
+ 'process_m2name' => {
+ 'link_table' => 'access_right',
+ 'link_static' => { 'righttype' => 'FS::access_group', },
+ 'num_col' => 'rightobjnum',
+ 'name_col' => 'rightname',
+ 'names_list' => [ FS::AccessRight->rights() ],
+ },
+ )
+%>
diff --git a/httemplate/edit/process/access_user.html b/httemplate/edit/process/access_user.html
new file mode 100644
index 000000000..9f7c4ddbf
--- /dev/null
+++ b/httemplate/edit/process/access_user.html
@@ -0,0 +1,15 @@
+% if ( $cgi->param('_password') ne $cgi->param('_password2') ) {
+% $cgi->param('error', "The passwords do not match");
+% print $cgi->redirect(popurl(2) . "access_user.html?" . $cgi->query_string);
+% } else {
+<% include( 'elements/process.html',
+ 'table' => 'access_user',
+ 'viewall_dir' => 'browse',
+ 'copy_on_empty' => [ '_password' ],
+ 'clear_on_error' => [ '_password', '_password2' ],
+ 'process_m2m' => { 'link_table' => 'access_usergroup',
+ 'target_table' => 'access_group',
+ },
+ )
+%>
+% }
diff --git a/httemplate/edit/process/addr_block/add.cgi b/httemplate/edit/process/addr_block/add.cgi
new file mode 100755
index 000000000..85780c678
--- /dev/null
+++ b/httemplate/edit/process/addr_block/add.cgi
@@ -0,0 +1,21 @@
+%
+%
+%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 000000000..a94c0320f
--- /dev/null
+++ b/httemplate/edit/process/addr_block/allocate.cgi
@@ -0,0 +1,26 @@
+%
+%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 000000000..494c19f75
--- /dev/null
+++ b/httemplate/edit/process/addr_block/deallocate.cgi
@@ -0,0 +1,25 @@
+%
+%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 000000000..617c3f8ce
--- /dev/null
+++ b/httemplate/edit/process/addr_block/split.cgi
@@ -0,0 +1,20 @@
+%
+%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 000000000..5128d7ae8
--- /dev/null
+++ b/httemplate/edit/process/agent.cgi
@@ -0,0 +1,29 @@
+%
+%
+%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_payment_gateway.html b/httemplate/edit/process/agent_payment_gateway.html
new file mode 100644
index 000000000..436317ec4
--- /dev/null
+++ b/httemplate/edit/process/agent_payment_gateway.html
@@ -0,0 +1,26 @@
+%
+%
+%$cgi->param('agentnum') =~ /(\d+)$/ or die "illegal agentnum";
+%my $agent = qsearchs('agent', { 'agentnum' => $1 } );
+%die "agentnum $1 not found" unless $agent;
+%
+%#my $old
+%
+%my @new = map {
+% my $cardtype = $_;
+% new FS::agent_payment_gateway {
+% ( map { $_ => scalar($cgi->param($_)) }
+% fields('agent_payment_gateway')
+% ),
+% 'cardtype' => $cardtype,
+% };
+% }
+% $cgi->param('cardtype');
+%
+%foreach my $new (@new) {
+% my $error = $new->insert;
+% die $error if $error;
+%}
+%
+%
+<% $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 000000000..b8d03705c
--- /dev/null
+++ b/httemplate/edit/process/agent_type.cgi
@@ -0,0 +1,37 @@
+%
+%
+%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');
+%}
+%#$error ||= $new->process_m2m( );
+%
+%if ( $error ) {
+% $cgi->param('error', $error);
+% print $cgi->redirect(popurl(2). "agent_type.cgi?". $cgi->query_string );
+%} else {
+%
+% my $error = $new->process_m2m(
+% 'link_table' => 'type_pkgs',
+% 'target_table' => 'part_pkg',
+% 'params' => scalar($cgi->Vars)
+% );
+% die $error if $error;
+%
+% print $cgi->redirect(popurl(3). "browse/agent_type.cgi");
+%}
+%
+%
+
diff --git a/httemplate/edit/process/bulk-cust_svc.cgi b/httemplate/edit/process/bulk-cust_svc.cgi
new file mode 100644
index 000000000..ad4d67307
--- /dev/null
+++ b/httemplate/edit/process/bulk-cust_svc.cgi
@@ -0,0 +1,4 @@
+%
+% my $server = new FS::UI::Web::JSRPC 'FS::part_svc::process_bulk_cust_svc', $cgi;
+%
+<% $server->process %>
diff --git a/httemplate/edit/process/cust_bill_pay.cgi b/httemplate/edit/process/cust_bill_pay.cgi
new file mode 100755
index 000000000..962fc4eb9
--- /dev/null
+++ b/httemplate/edit/process/cust_bill_pay.cgi
@@ -0,0 +1,54 @@
+%
+%
+%$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);
+%
+<% $cgi->redirect(popurl(2). "cust_bill_pay.cgi?". $cgi->query_string ) %>
+%
+%
+%} else {
+%
+% #print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+%
+%
+<% header('Payment application sucessful') %>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+
+ </BODY></HTML>
+% }
+
diff --git a/httemplate/edit/process/cust_credit.cgi b/httemplate/edit/process/cust_credit.cgi
new file mode 100755
index 000000000..19faca47a
--- /dev/null
+++ b/httemplate/edit/process/cust_credit.cgi
@@ -0,0 +1,38 @@
+%
+%
+%$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);
+%
+%
+<% $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");
+%
+%
+<% header('Credit sucessful') %>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+
+ </BODY></HTML>
+% }
+
diff --git a/httemplate/edit/process/cust_credit_bill.cgi b/httemplate/edit/process/cust_credit_bill.cgi
new file mode 100755
index 000000000..7509a3f02
--- /dev/null
+++ b/httemplate/edit/process/cust_credit_bill.cgi
@@ -0,0 +1,55 @@
+%
+%
+%$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 from credit' ),
+% 'refund' => $cgi->param('amount'),
+% 'payby' => 'BILL',
+% #'_date' => $cgi->param('_date'),
+% #'payinfo' => 'Cash',
+% 'payinfo' => 'Refund',
+% '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);
+%
+<% $cgi->redirect(popurl(2). "cust_credit_bill.cgi?". $cgi->query_string ) %>
+%
+%
+%} else {
+%
+% #print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+%
+%
+<% header('Credit application sucessful') %>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+
+ </BODY></HTML>
+% }
+
diff --git a/httemplate/edit/process/cust_main.cgi b/httemplate/edit/process/cust_main.cgi
new file mode 100755
index 000000000..a4a70ca22
--- /dev/null
+++ b/httemplate/edit/process/cust_main.cgi
@@ -0,0 +1,176 @@
+%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');
+%my $payby = $cgi->param('select'); # XXX key
+%
+%my %noauto = (
+% 'CARD' => 'DCRD',
+% 'CHEK' => 'DCHK',
+%);
+%$payby = $noauto{$payby}
+% if ! $cgi->param('payauto') && exists $noauto{$payby};
+%
+%$cgi->param('payby', $payby);
+%
+%if ( $payby ) {
+% if ( $payby eq 'CHEK' || $payby eq 'DCHK' ) {
+% $cgi->param('payinfo',
+% $cgi->param('payinfo1'). '@'. $cgi->param('payinfo2') );
+% }
+% $cgi->param('paydate',
+% $cgi->param( 'exp_month' ). '-'. $cgi->param( 'exp_year' ) );
+%}
+%
+%my @invoicing_list = split( /\s*\,\s*/, $cgi->param('invoicing_list') );
+%push @invoicing_list, 'POST' if $cgi->param('invoicing_list_POST');
+%push @invoicing_list, 'FAX' if $cgi->param('invoicing_list_FAX');
+%$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')
+%} );
+%
+% delete( $new->hashref->{'agent_custid'} )
+% unless $new->hashref->{'agent_custid'};
+%
+%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
+% );
+%}
+%
+%if ( $cgi->param('birthdate') && $cgi->param('birthdate') =~ /^([ 0-9\-\/]{0,10})$/) {
+% my $conf = new FS::Conf;
+% my $format = $conf->config('date_format') || "%m/%d/%Y";
+% my $parser = DateTime::Format::Strptime->new(pattern => $format,
+% time_zone => 'floating',
+% );
+% my $dt = $parser->parse_datetime($1);
+% if ($dt) {
+% $new->setfield('birthdate', $dt->epoch);
+% $cgi->param('birthdate', $dt->epoch);
+% } else {
+%# $error ||= $cgi->param('birthdate') . " is an invalid birthdate:" . $parser->errmsg;
+% $error ||= "Invalid birthdate: " . $cgi->param('birthdate') . ".";
+% $cgi->param('birthdate', '');
+% }
+%}
+%
+%$new->setfield('paid', $cgi->param('paid') )
+% if $cgi->param('paid');
+%
+%#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;
+%
+% my %svc_acct = (
+% 'svcpart' => $svcpart,
+% 'username' => $cgi->param('username'),
+% '_password' => $cgi->param('_password'),
+% 'popnum' => $cgi->param('popnum'),
+% );
+% $svc_acct{'domsvc'} = $cgi->param('domsvc')
+% if $cgi->param('domsvc');
+%
+% $svc_acct = new FS::svc_acct \%svc_acct;
+%
+% #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 );
+%
+% my $conf = new FS::Conf;
+% if ( $conf->exists('backend-realtime') && ! $error ) {
+%
+% my $berror = $new->bill;
+% $new->apply_payments_and_credits;
+% $berror ||= $new->collect( 'realtime' => 1 );
+% warn "Warning, error billing during backend-realtime: $berror" if $berror;
+%
+% }
+%
+%} 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);
+% }
+% if ($new->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ && $new->payinfo =~ /xx/) {
+% $new->payinfo($old->payinfo);
+% }
+% $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 000000000..4bcaf1de3
--- /dev/null
+++ b/httemplate/edit/process/cust_main_county-collapse.cgi
@@ -0,0 +1,36 @@
+%
+%
+%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 000000000..e550e8b4a
--- /dev/null
+++ b/httemplate/edit/process/cust_main_county-expand.cgi
@@ -0,0 +1,59 @@
+%
+%
+%$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 000000000..2c3ebe866
--- /dev/null
+++ b/httemplate/edit/process/cust_main_county.cgi
@@ -0,0 +1,31 @@
+%
+%
+%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_main_note.cgi b/httemplate/edit/process/cust_main_note.cgi
new file mode 100755
index 000000000..8b9105bd8
--- /dev/null
+++ b/httemplate/edit/process/cust_main_note.cgi
@@ -0,0 +1,52 @@
+%
+%
+%$cgi->param('custnum') =~ /^(\d+)$/
+% or die "Illegal custnum: ". $cgi->param('custnum');
+%my $custnum = $1;
+%
+%$cgi->param('notenum') =~ /^(\d*)$/
+% or die "Illegal notenum: ". $cgi->param('notenum');
+%my $notenum = $1;
+%
+%my $otaker = $FS::CurrentUser::CurrentUser->name;
+%$otaker = $FS::CurrentUser::CurrentUser->username
+% if ($otaker eq "User, Legacy");
+%
+%my $new = new FS::cust_main_note ( {
+% notenum => $notenum,
+% custnum => $custnum,
+% _date => time,
+% otaker => $otaker,
+% comments => $cgi->param('comment'),
+%} );
+%
+%my $error;
+%if ($notenum){
+% my $old = qsearchs('cust_main_note', { 'notenum' => $notenum });
+% $error = "No such note: $notenum" unless $old;
+% unless($error){
+% map { $new->$_($old->$_) } ('_date', 'otaker');
+% $error = $new->replace($old);
+% }
+%}else{
+% $error = $new->insert;
+%}
+%
+%if ($error) {
+% $cgi->param('error', $error);
+% print $cgi->redirect(popurl(2). 'cust_main_note.cgi?'. $cgi->query_string );
+%}
+%
+%
+<% header('Note ' . ($notenum ? 'updated' : 'added') ) %>
+ <SCRIPT TYPE="text/javascript">
+ parent.cust_main_notes.location.reload();
+ try{parent.cust_main_notes.cClick()}
+ catch(err){}
+ try{parent.cClick()}
+ catch(err){}
+ </SCRIPT>
+ </BODY></HTML>
+%
+%
+
diff --git a/httemplate/edit/process/cust_pay.cgi b/httemplate/edit/process/cust_pay.cgi
new file mode 100755
index 000000000..a34c88aba
--- /dev/null
+++ b/httemplate/edit/process/cust_pay.cgi
@@ -0,0 +1,56 @@
+%
+%
+%$cgi->param('linknum') =~ /^(\d+)$/
+% or die "Illegal linknum: ". $cgi->param('linknum');
+%my $linknum = $1;
+%
+%$cgi->param('link') =~ /^(custnum|invnum|popup)$/
+% or die "Illegal link: ". $cgi->param('link');
+%my $field = my $link = $1;
+%$field = 'custnum' if $field eq 'popup';
+%
+%my $_date = str2time($cgi->param('_date'));
+%
+%my $new = new FS::cust_pay ( {
+% $field => $linknum,
+% _date => $_date,
+% map {
+% $_, scalar($cgi->param($_));
+% } qw(paid payby payinfo paybatch)
+% #} fields('cust_pay')
+%} );
+%
+%my $error = $new->insert( 'manual' => 1 );
+%
+%if ($error) {
+% $cgi->param('error', $error);
+% print $cgi->redirect(popurl(2). 'cust_pay.cgi?'. $cgi->query_string );
+%} elsif ( $field eq 'invnum' ) {
+% print $cgi->redirect(popurl(3). "view/cust_bill.cgi?$linknum");
+%} elsif ( $field 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 ( $link eq 'popup' ) {
+%
+%
+<% header('Payment entered') %>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+
+ </BODY></HTML>
+%
+%
+% } elsif ( $link eq 'custnum' ) {
+% print $cgi->redirect(popurl(3). "view/cust_main.cgi?$linknum");
+% } else {
+% die "unknown link $link";
+% }
+%
+%}
+%
+%
+
diff --git a/httemplate/edit/process/cust_pkg.cgi b/httemplate/edit/process/cust_pkg.cgi
new file mode 100755
index 000000000..817c88087
--- /dev/null
+++ b/httemplate/edit/process/cust_pkg.cgi
@@ -0,0 +1,44 @@
+%
+%
+%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 000000000..a579a02d8
--- /dev/null
+++ b/httemplate/edit/process/cust_refund.cgi
@@ -0,0 +1,34 @@
+%$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 $bop = $FS::payby::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 000000000..3a07d1e7a
--- /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 000000000..87bdf6835
--- /dev/null
+++ b/httemplate/edit/process/domain_record.cgi
@@ -0,0 +1,36 @@
+%
+%
+%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/elements/process.html b/httemplate/edit/process/elements/process.html
new file mode 100644
index 000000000..e388c678b
--- /dev/null
+++ b/httemplate/edit/process/elements/process.html
@@ -0,0 +1,111 @@
+%
+%
+% # options example...
+% #
+% ###
+% ##req
+% ##
+% #
+% # 'table' =>
+% #
+% # #? 'primary_key' => #required when the dbdef doesn't know...???
+% # #? 'fields' => []
+% #
+% ###
+% ##opt
+% ###
+% #
+% # 'viewall_dir' => '', #'search' or 'browse', defaults to 'search'
+% # OR
+% # 'redirect' => 'view/table.cgi?', # value of primary key is appended
+% #
+% # 'error_redirect' => popurl(2).'edit/table.cgi?', #query string appended
+% #
+% # 'edit_ext' => 'html', #defaults to 'html', you might want 'cgi' while the
+% # #naming is still inconsistent
+% #
+% # 'copy_on_empty' => [ 'old_field_name', 'another_old_field', ... ],
+% #
+% # 'clear_on_error' => [ 'form_field1', 'form_field2', ... ],
+% #
+% # 'process_m2m' => { 'link_table' => 'link_table_name',
+% # 'target_table' => 'target_table_name',
+% # },
+% # 'process_m2name' => { 'link_table' => 'link_table_name',
+% # 'link_static' => { 'column' => 'value' },
+% # 'num_col' => 'column', #if column name is different in
+% # #link_table than source_table
+% # 'name_col' => 'name_column',
+% # 'names_list' => [ 'list', 'names' ],
+% # },
+%
+% my(%opt) = @_;
+%
+% #false laziness w/edit.html
+% my $table = $opt{'table'};
+% my $class = "FS::$table";
+% my $pkey = dbdef->table($table)->primary_key; #? $opt{'primary_key'} ||
+% my $fields = $opt{'fields'}
+% #|| [ grep { $_ ne $pkey } dbdef->table($table)->columns ];
+% || [ fields($table) ];
+%
+% my $pkeyvalue = $cgi->param($pkey);
+%
+% my $old = qsearchs( $table, { $pkey => $pkeyvalue } ) if $pkeyvalue;
+%
+% my $new = $class->new( {
+% map {
+% $_, scalar($cgi->param($_));
+% } @$fields
+% } );
+%
+% if ($old && exists($opt{'copy_on_empty'})) {
+% foreach my $field (@{$opt{'copy_on_empty'}}) {
+% $new->set($field, $old->get($field))
+% unless scalar($cgi->param($field));
+% }
+% }
+%
+% my $error;
+% if ( $pkeyvalue ) {
+% $error = $new->replace($old);
+% } else {
+% $error = $new->insert;
+% $pkeyvalue = $new->getfield($pkey);
+% }
+%
+% if ( !$error && $opt{'process_m2m'} ) {
+% $error = $new->process_m2m( %{ $opt{'process_m2m'} },
+% 'params' => scalar($cgi->Vars),
+% );
+% }
+%
+% if ( !$error && $opt{'process_m2name'} ) {
+% $error = $new->process_m2name( %{ $opt{'process_m2name'} },
+% 'params' => scalar($cgi->Vars),
+% );
+% }
+%
+% # XXX print?!?!
+%
+% if ( $error ) {
+% $cgi->param('error', $error);
+% if (scalar(@{$opt{'clear_on_error'}})) {
+% foreach my $field (@{$opt{'clear_on_error'}}) {
+% $cgi->param($field, '')
+% }
+% }
+% my $edit_ext = $opt{'edit_ext'} || 'html';
+% my $url = $opt{'error_redirect'} || popurl(2)."$table.$edit_ext?";
+% print $cgi->redirect($url. $cgi->query_string );
+% } elsif ( $opt{'redirect'} ) {
+% print $cgi->redirect( $opt{'redirect'}. $pkeyvalue );
+% } else {
+% print $cgi->redirect( popurl(3).
+% ( $opt{'viewall_dir'} || 'search' ).
+% "/$table.html"
+% );
+% }
+%
+%
+
diff --git a/httemplate/edit/process/elements/svc_Common.html b/httemplate/edit/process/elements/svc_Common.html
new file mode 100644
index 000000000..8e8c99a42
--- /dev/null
+++ b/httemplate/edit/process/elements/svc_Common.html
@@ -0,0 +1,15 @@
+%
+%
+% my %opt = @_;
+% my $table = $opt{'table'};
+% $opt{'fields'} ||= [ fields($table) ];
+% push @{ $opt{'fields'} }, qw( pkgnum svcpart );
+%
+%
+<% include( 'process.html',
+ 'edit_ext' => 'cgi',
+ 'redirect' => popurl(3)."view/$table.cgi?",
+ %opt,
+ )
+%>
+
diff --git a/httemplate/edit/process/generic.cgi b/httemplate/edit/process/generic.cgi
new file mode 100644
index 000000000..e3ac113ae
--- /dev/null
+++ b/httemplate/edit/process/generic.cgi
@@ -0,0 +1,73 @@
+%# 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.)
+%#
+%# also see elements/process.html, newer and somewhat along the same lines,
+%# though it still makes you setup a process file for the table.
+%# perhaps safer, perhaps more of a pain in the ass.
+%#
+%# 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/inventory_class.html b/httemplate/edit/process/inventory_class.html
new file mode 100644
index 000000000..c7be9e8dd
--- /dev/null
+++ b/httemplate/edit/process/inventory_class.html
@@ -0,0 +1,5 @@
+<% include( 'elements/process.html',
+ 'table' => 'inventory_class',
+ 'viewall_dir' => 'browse',
+ )
+%>
diff --git a/httemplate/edit/process/msgcat.cgi b/httemplate/edit/process/msgcat.cgi
new file mode 100644
index 000000000..9711143d6
--- /dev/null
+++ b/httemplate/edit/process/msgcat.cgi
@@ -0,0 +1,21 @@
+%
+%
+%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 000000000..af594f264
--- /dev/null
+++ b/httemplate/edit/process/part_bill_event.cgi
@@ -0,0 +1,89 @@
+%
+%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 = '';
+%
+% my $rnum;
+% my $rtype;
+% my $reasonm;
+% my $class = '';
+% $class='c' if ($eventcode =~ /cancel/);
+% $class='s' if ($eventcode =~ /suspend/);
+% if ($class) {
+% $cgi->param("${class}reason") =~ /^(-?\d+)$/
+% or $error = "Invalid ${class}reason";
+% $rnum = $1;
+% if ($rnum == -1) {
+% $cgi->param("new${class}reasonT") =~ /^(\d+)$/
+% or $error = "Invalid new${class}reasonT";
+% $rtype = $1;
+% $cgi->param("new${class}reason") =~ /^([\s\w]+)$/
+% or $error = "Invalid new${class}reason";
+% $reasonm = $1;
+% }
+% }
+%
+% if ($rnum == -1 && !$error) {
+% my $reason = new FS::reason ({ 'reason' => $reasonm,
+% 'reason_type' => $rtype,
+% });
+% $error = $reason->insert;
+% unless ($error) {
+% $rnum = $reason->reasonnum;
+% $cgi->param("${class}reason", $rnum);
+% $cgi->param("new${class}reason", '');
+% $cgi->param("new${class}reasonT", '');
+% }
+% }
+%
+% 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);
+%
+% unless($error){
+% my $new = new FS::part_bill_event ( {
+% map {
+% $_, scalar($cgi->param($_));
+% } fields('part_bill_event'),
+% } );
+% $new->setfield('reason', $rnum);
+%
+% 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 000000000..0dd9eabae
--- /dev/null
+++ b/httemplate/edit/process/part_export.cgi
@@ -0,0 +1,40 @@
+%
+%
+%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 000000000..55e7e05ae
--- /dev/null
+++ b/httemplate/edit/process/part_pkg.cgi
@@ -0,0 +1,75 @@
+%
+%
+%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 @agents;
+%foreach ($cgi->param('agent_type')) {
+% /^(\d+)$/;
+% push @agents, $1 if $1;
+%}
+%
+%my $new = new FS::part_pkg ( {
+% map {
+% $_ => scalar($cgi->param($_));
+% } fields('part_pkg')
+%} );
+%
+%my %pkg_svc = map { $_ => scalar($cgi->param("pkg_svc$_")) }
+% map { $_->svcpart }
+% qsearch('part_svc', {} );
+%
+%my $error;
+%my $custnum = '';
+%if ( $cgi->param('taxclass') eq '(select)' ) {
+%
+% $error = 'Must select a tax class';
+%
+%} elsif ( $pkgpart ) {
+%
+% $error = $new->replace( $old,
+% pkg_svc => \%pkg_svc,
+% primary_svc => scalar($cgi->param('pkg_svc_primary')),
+% );
+%} else {
+%
+% $error = $new->insert( pkg_svc => \%pkg_svc,
+% primary_svc => scalar($cgi->param('pkg_svc_primary')),
+% cust_pkg => $cgi->param('pkgnum'),
+% custnum_ref => \$custnum,
+% );
+% $pkgpart = $new->pkgpart;
+%}
+%
+%unless (1 || $error) { # after 1.7.2
+% my $error = $new->process_m2m(
+% 'link_table' => 'type_pkgs',
+% 'target_table' => 'agent_type',
+% 'params' => \@agents,
+% );
+%}
+%if ( $error ) {
+% $cgi->param('error', $error );
+% print $cgi->redirect(popurl(2). "part_pkg.cgi?". $cgi->query_string );
+%} elsif ( $custnum ) {
+% print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+%} else {
+% print $cgi->redirect(popurl(3). "browse/part_pkg.cgi");
+%}
+%
+%
+
diff --git a/httemplate/edit/process/part_referral.html b/httemplate/edit/process/part_referral.html
new file mode 100755
index 000000000..14c1b7001
--- /dev/null
+++ b/httemplate/edit/process/part_referral.html
@@ -0,0 +1,5 @@
+<% include( 'elements/process.html',
+ 'table' => 'part_referral',
+ 'viewall_dir' => 'browse',
+ )
+%>
diff --git a/httemplate/edit/process/part_svc.cgi b/httemplate/edit/process/part_svc.cgi
new file mode 100755
index 000000000..97abc5baf
--- /dev/null
+++ b/httemplate/edit/process/part_svc.cgi
@@ -0,0 +1,4 @@
+%
+% my $server = new FS::UI::Web::JSRPC 'FS::part_svc::process', $cgi;
+%
+<% $server->process %>
diff --git a/httemplate/edit/process/payment_gateway.html b/httemplate/edit/process/payment_gateway.html
new file mode 100644
index 000000000..0b7e31395
--- /dev/null
+++ b/httemplate/edit/process/payment_gateway.html
@@ -0,0 +1,34 @@
+%
+%
+%my $gatewaynum = $cgi->param('gatewaynum');
+%
+%my $old = qsearchs('payment_gateway',{'gatewaynum'=>$gatewaynum}) if $gatewaynum;
+%
+%my $new = new FS::payment_gateway ( {
+% map {
+% $_, scalar($cgi->param($_));
+% } fields('payment_gateway')
+%} );
+%
+%my @options = split(/\r?\n/, $cgi->param('gateway_options') );
+%pop @options
+% if scalar(@options) % 2 && $options[-1] =~ /^\s*$/;
+%my %options = @options;
+%
+%my $error;
+%if ( $gatewaynum ) {
+% $error=$new->replace($old, \%options);
+%} else {
+% $error=$new->insert(\%options);
+% $gatewaynum=$new->getfield('gatewaynum');
+%}
+%
+%if ( $error ) {
+% $cgi->param('error', $error);
+% print $cgi->redirect(popurl(2). "payment_gateway.html?". $cgi->query_string );
+%} else {
+% print $cgi->redirect(popurl(3). "browse/payment_gateway.html");
+%}
+%
+%
+
diff --git a/httemplate/edit/process/pkg_class.html b/httemplate/edit/process/pkg_class.html
new file mode 100644
index 000000000..183da805c
--- /dev/null
+++ b/httemplate/edit/process/pkg_class.html
@@ -0,0 +1,5 @@
+<% include( 'elements/process.html',
+ 'table' => 'pkg_class',
+ 'viewall_dir' => 'browse',
+ )
+%>
diff --git a/httemplate/edit/process/prepay_credit.cgi b/httemplate/edit/process/prepay_credit.cgi
new file mode 100644
index 000000000..6bf46bf7c
--- /dev/null
+++ b/httemplate/edit/process/prepay_credit.cgi
@@ -0,0 +1,63 @@
+%
+%my $hashref = {};
+%
+%my $agent = '';
+%if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+% $agent = qsearchs('agent', { 'agentnum' => $hashref->{agentnum}=$1 } );
+%}
+%
+%my $error = '';
+%
+%my $num = 0;
+%if ( $cgi->param('num') =~ /^\s*(\d+)\s*$/ ) {
+% $num = $1;
+%} else {
+% $error = 'Illegal number of prepaid cards: '. $cgi->param('num');
+%}
+%
+%$hashref->{amount} = $cgi->param('amount');
+%$hashref->{seconds} = $cgi->param('seconds') * $cgi->param('multiplier');
+%$hashref->{upbytes} = $cgi->param('upbytes') * $cgi->param('upmultiplier');
+%$hashref->{downbytes} = $cgi->param('downbytes') * $cgi->param('downmultiplier');
+%$hashref->{totalbytes} = $cgi->param('totalbytes') * $cgi->param('totalmultiplier');
+%
+%$error ||= FS::prepay_credit::generate( $num,
+% scalar($cgi->param('type')),
+% $hashref
+% );
+%
+%unless ( ref($error) ) {
+% $cgi->param('error', $error );
+%
+<%
+ $cgi->redirect(popurl(3). "edit/prepay_credit.cgi?". $cgi->query_string )
+%>
+% } else {
+
+
+<% include("/elements/header.html", "$num prepaid cards generated".
+ ( $agent ? ' for '.$agent->agent : '' ),
+ menubar( 'Main menu' => popurl(3) )
+ )
+%>
+
+<FONT SIZE="+1">
+% foreach my $card ( @$error ) {
+
+ <code><% $card %></code>
+ -
+ <% $hashref->{amount} ? sprintf('$%.2f', $hashref->{amount} ) : '' %>
+ <% $hashref->{amount} && $hashref->{seconds} ? 'and' : '' %>
+ <% $hashref->{seconds} ? duration_exact($hashref->{seconds}) : '' %>
+ <% $hashref->{upbytes} ? FS::UI::Web::bytecount_unexact($hashref->{upbytes}) : '' %>
+ <% $hashref->{downbytes} ? FS::UI::Web::bytecount_unexact($hashref->{downbytes}) : '' %>
+ <% $hashref->{totalbytes} ? FS::UI::Web::bytecount_unexact($hashref->{totalbytes}) : '' %>
+ <br>
+% }
+
+
+</FONT>
+
+</BODY></HTML>
+% }
+
diff --git a/httemplate/edit/process/quick-charge.cgi b/httemplate/edit/process/quick-charge.cgi
new file mode 100644
index 000000000..024a281e0
--- /dev/null
+++ b/httemplate/edit/process/quick-charge.cgi
@@ -0,0 +1,47 @@
+%
+% my $error = '';
+% my $param = $cgi->Vars;
+%
+% my @description = ();
+% for ( my $row = 0; exists($param->{"description$row"}); $row++ ) {
+% push @description, $param->{"description$row"}
+% if ($param->{"description$row"} =~ /\S/);
+% }
+%
+% $param->{"custnum"} =~ /^(\d+)$/
+% or $error .= "Illegal customer number " . $param->{"custnum"} . " ";
+% my $custnum = $1;
+%
+% $param->{"amount"} =~ /^\s*(\d+(\.\d{1,2})?)\s*$/
+% or $error .= "Illegal amount " . $param->{"amount"} . " ";
+% my $amount = $1;
+%
+% if ( $param->{'taxclass'} eq '(select)' ) {
+% $error .= "Must select a tax class. ";
+% }
+%
+% unless ( $error ) {
+% my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+% or $error .= "Unknown customer number $custnum. ";
+%
+% $error ||= $cust_main->charge({ 'amount' => $amount,
+% 'pkg' => $cgi->param('pkg'),
+% 'taxclass' => $cgi->param('taxclass'),
+% 'additional' => \@description,
+% }
+% );
+% }
+%
+% if ( $error ) {
+%
+% $cgi->param('error', "$error" );
+%
+<% $cgi->redirect($p.'quick-charge.html?'. $cgi->query_string) %>
+%
+% }
+<% header("One-time charge added") %>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+ </BODY></HTML>
+
diff --git a/httemplate/edit/process/quick-cust_pkg.cgi b/httemplate/edit/process/quick-cust_pkg.cgi
new file mode 100644
index 000000000..7afc9f2bb
--- /dev/null
+++ b/httemplate/edit/process/quick-cust_pkg.cgi
@@ -0,0 +1,27 @@
+%
+%
+%#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/rate.cgi b/httemplate/edit/process/rate.cgi
new file mode 100755
index 000000000..c81f883b7
--- /dev/null
+++ b/httemplate/edit/process/rate.cgi
@@ -0,0 +1,4 @@
+%
+% my $server = new FS::UI::Web::JSRPC 'FS::rate::process', $cgi;
+%
+<% $server->process %>
diff --git a/httemplate/edit/process/rate_region.cgi b/httemplate/edit/process/rate_region.cgi
new file mode 100755
index 000000000..753224565
--- /dev/null
+++ b/httemplate/edit/process/rate_region.cgi
@@ -0,0 +1,52 @@
+%
+%
+%my $regionnum = $cgi->param('regionnum');
+%
+%my $old = qsearchs('rate_region', { 'regionnum' => $regionnum } ) if $regionnum;
+%
+%my $new = new FS::rate_region ( {
+% map {
+% $_, scalar($cgi->param($_));
+% } ( fields('rate_region') )
+%} );
+%
+%my $countrycode = $cgi->param('countrycode');
+%my @npa = split(/\s*,\s*/, $cgi->param('npa'));
+%$npa[0] = '' unless @npa;
+%my @rate_prefix = map {
+% new FS::rate_prefix {
+% 'countrycode' => $countrycode,
+% 'npa' => $_,
+% }
+% } @npa;
+%
+%my @dest_detail = map {
+% my $ratenum = $_->ratenum;
+% new FS::rate_detail {
+% 'ratenum' => $ratenum,
+% map { $_ => $cgi->param("$_$ratenum") }
+% qw( min_included min_charge sec_granularity )
+% };
+%} qsearch('rate', {} );
+%
+%
+%my $error;
+%if ( $regionnum ) {
+% $error = $new->replace($old, 'rate_prefix' => \@rate_prefix,
+% 'dest_detail' => \@dest_detail, );
+%} else {
+% $error = $new->insert( 'rate_prefix' => \@rate_prefix,
+% 'dest_detail' => \@dest_detail, );
+% $regionnum = $new->getfield('regionnum');
+%}
+%
+%if ( $error ) {
+% $cgi->param('error', $error);
+% print $cgi->redirect(popurl(2). "rate_region.cgi?". $cgi->query_string );
+%} else {
+% #print $cgi->redirect(popurl(3). "browse/rate_region.cgi");
+% print $cgi->redirect(popurl(3). "browse/rate.cgi");
+%}
+%
+%
+
diff --git a/httemplate/edit/process/reason.html b/httemplate/edit/process/reason.html
new file mode 100644
index 000000000..55c1ea958
--- /dev/null
+++ b/httemplate/edit/process/reason.html
@@ -0,0 +1,6 @@
+<% include( 'elements/process.html',
+ 'table' => 'reason',
+ 'redirect' => popurl(3) . 'browse/reason.html?class=' .
+ $cgi->param('class') . '&',
+ )
+%>
diff --git a/httemplate/edit/process/reason_type.html b/httemplate/edit/process/reason_type.html
new file mode 100644
index 000000000..4ccccaddd
--- /dev/null
+++ b/httemplate/edit/process/reason_type.html
@@ -0,0 +1,6 @@
+<% include( 'elements/process.html',
+ 'table' => 'reason_type',
+ 'redirect' => popurl(3) . 'browse/reason_type.html?class=' .
+ $cgi->param('class') . '&',
+ )
+%>
diff --git a/httemplate/edit/process/reg_code.cgi b/httemplate/edit/process/reg_code.cgi
new file mode 100644
index 000000000..4fdea60fc
--- /dev/null
+++ b/httemplate/edit/process/reg_code.cgi
@@ -0,0 +1,50 @@
+%
+%
+%$cgi->param('agentnum') =~ /^(\d+)$/
+% or eidiot 'illegal agentnum '. $cgi->param('agentnum');
+%my $agentnum = $1;
+%my $agent = qsearchs('agent', { 'agentnum' => $agentnum } );
+%
+%my $error = '';
+%
+%my $num = 0;
+%if ( $cgi->param('num') =~ /^\s*(\d+)\s*$/ ) {
+% $num = $1;
+%} else {
+% $error = 'Illegal number of codes: '. $cgi->param('num');
+%}
+%
+%my @pkgparts =
+% map { /^pkgpart(.*)$/; $1 }
+% grep { $cgi->param($_) }
+% grep { /^pkgpart/ }
+% $cgi->param;
+%
+%$error ||= $agent->generate_reg_codes($num, \@pkgparts);
+%
+%unless ( ref($error) ) {
+% $cgi->param('error'. $error );
+%
+<%
+ $cgi->redirect(popurl(3). "edit/reg_code.cgi?". $cgi->query_string )
+%>
+% } else {
+
+
+<% include("/elements/header.html","$num registration codes generated for ". $agent->agent, menubar(
+ 'Main menu' => popurl(3),
+ 'View all agents' => popurl(3). 'browse/agent.cgi',
+) ) %>
+
+<PRE><FONT SIZE="+1">
+% foreach my $code ( @$error ) {
+
+ <% $code %>
+% }
+
+
+</FONT></PRE>
+
+</BODY></HTML>
+% }
+
diff --git a/httemplate/edit/process/router.cgi b/httemplate/edit/process/router.cgi
new file mode 100644
index 000000000..c69114ea4
--- /dev/null
+++ b/httemplate/edit/process/router.cgi
@@ -0,0 +1,68 @@
+%
+%
+%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_Common.html b/httemplate/edit/process/svc_Common.html
new file mode 100644
index 000000000..f5c869a12
--- /dev/null
+++ b/httemplate/edit/process/svc_Common.html
@@ -0,0 +1,13 @@
+<%init>
+
+$cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unparsable svcdb";
+my $table = $1;
+require "FS/$table.pm";
+
+</%init>
+<% include( 'elements/svc_Common.html',
+ 'table' => $table,
+ 'redirect' => popurl(3)."view/svc_Common.html?svcdb=$table;svcnum=",
+ 'error_redirect' => popurl(3)."edit/svc_Common.html?svcdb=$table;",
+ )
+%>
diff --git a/httemplate/edit/process/svc_acct.cgi b/httemplate/edit/process/svc_acct.cgi
new file mode 100755
index 000000000..30552c846
--- /dev/null
+++ b/httemplate/edit/process/svc_acct.cgi
@@ -0,0 +1,50 @@
+%
+%
+%$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 %hash = $svcnum ? $old->hash : ();
+%map {
+% $hash{$_} = 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 $new = new FS::svc_acct ( \%hash );
+%
+%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 000000000..9e9df7bf0
--- /dev/null
+++ b/httemplate/edit/process/svc_acct_pop.cgi
@@ -0,0 +1,29 @@
+%
+%
+%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 000000000..cf4604639
--- /dev/null
+++ b/httemplate/edit/process/svc_broadband.cgi
@@ -0,0 +1,37 @@
+%
+%
+%$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);
+% print $cgi->redirect(popurl(2). "svc_broadband.cgi?". $cgi->query_string );
+%} else {
+% 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 000000000..773143fe3
--- /dev/null
+++ b/httemplate/edit/process/svc_domain.cgi
@@ -0,0 +1,32 @@
+%
+%
+%#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 000000000..97da6ba87
--- /dev/null
+++ b/httemplate/edit/process/svc_external.cgi
@@ -0,0 +1,30 @@
+%
+%
+%$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 000000000..3205312f1
--- /dev/null
+++ b/httemplate/edit/process/svc_forward.cgi
@@ -0,0 +1,30 @@
+%
+%
+%$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_phone.html b/httemplate/edit/process/svc_phone.html
new file mode 100644
index 000000000..44235de63
--- /dev/null
+++ b/httemplate/edit/process/svc_phone.html
@@ -0,0 +1,4 @@
+<% include( 'elements/svc_Common.html',
+ 'table' => 'svc_phone',
+ )
+%>
diff --git a/httemplate/edit/process/svc_www.cgi b/httemplate/edit/process/svc_www.cgi
new file mode 100644
index 000000000..e9a52aff2
--- /dev/null
+++ b/httemplate/edit/process/svc_www.cgi
@@ -0,0 +1,37 @@
+%
+%
+%$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/quick-charge.html b/httemplate/edit/quick-charge.html
new file mode 100644
index 000000000..94682d0a6
--- /dev/null
+++ b/httemplate/edit/quick-charge.html
@@ -0,0 +1,166 @@
+<% include("/elements/header-popup.html", 'One-time charge entry', '',
+ ( $cgi->param('error') ? '' : 'onload="addRow()"' ),
+ )
+%>
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#ff0000"><% $cgi->param('error') %></FONT><BR><BR>
+% }
+
+<SCRIPT TYPE="text/javascript">
+
+function enable_quick_charge () {
+ if ( document.QuickChargeForm.amount.value
+ && document.QuickChargeForm.pkg.value ) {
+ document.QuickChargeForm.submit.disabled = false;
+ } else {
+ document.QuickChargeForm.submit.disabled = true;
+ }
+}
+
+function enable_quick_charge_desc () {
+ if ( document.QuickChargeForm.amount.value && document.QuickChargeForm.pkg.value ) {
+ document.QuickChargeForm.submit.disabled = false;
+ } else {
+ document.QuickChargeForm.submit.disabled = true;
+ }
+}
+
+function enable_quick_charge_amount () {
+ if ( document.QuickChargeForm.amount.value && document.QuickChargeForm.pkg.value ) {
+ document.QuickChargeForm.submit.disabled = false;
+ } else {
+ document.QuickChargeForm.submit.disabled = true;
+ }
+}
+
+function validate_quick_charge () {
+ var pkg = document.QuickChargeForm.pkg.value;
+ var pkg_regex = /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]]*)$/ ;
+ var amount = document.QuickChargeForm.amount.value;
+ var amount_regex = /^\s*\$?\s*(\d+(\.\d{1,2})?)\s*$/ ;
+ var rval = true;
+
+ if ( ! amount_regex.test(amount) ) {
+ alert('Illegal amount - enter an amount to charge, for example, "5" or "43" or "21.46".');
+ return false;
+ }
+ if ( String(pkg).length < 1 ) {
+ rval = false;
+ }
+ if ( ! pkg_regex.test(pkg) ) {
+ rval = false;
+ }
+ var i=0;
+ for (i=0; i < rownum; i++) {
+ if (! eval('pkg_regex.test(document.QuickChargeForm.description' + i + '.value)')){
+ rval = false;
+ break;
+ }
+ }
+ if (rval == true) {
+ return true;
+ }
+
+ if ( ! pkg ) {
+ alert('Enter a description for the one-time charge');
+ return false;
+ }
+
+ alert('Illegal description - spaces, letters, numbers, and the following punctuation characters are allowed: . , ! ? @ # $ % & ( ) - + ; : ' + "'" + ' " = [ ]' );
+ return false;
+}
+
+</SCRIPT>
+
+
+
+<FORM ACTION="process/quick-charge.cgi" NAME="QuickChargeForm" METHOD="POST" onsubmit="document.QuickChargeForm.submit.disabled=true;return validate_quick_charge();">
+
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $cgi->param('custnum') %>">
+<TABLE ID="QuickChargeTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 STYLE="background-color: #cccccc">
+
+<TR>
+ <TD ALIGN="right">Amount:</TD>
+ <TD>
+ $<INPUT TYPE="text" NAME="amount" SIZE=6 VALUE="<% $cgi->param('amount') %>" onChange="enable_quick_charge()" onKeyPress="enable_quick_charge_amount()">
+ </TD>
+<% include('/elements/tr-select-taxclass.html') %>
+</TR>
+ <TD>Description:</TD>
+ <TD>
+ <INPUT TYPE="text" NAME="pkg" SIZE="60" MAXLENGTH="65" VALUE="<% $cgi->param('pkg') %>" onChange="enable_quick_charge()" onKeyPress="enable_quick_charge_desc()">
+ </TD>
+</TR>
+<TR>
+ <TD></TD>
+ <TD><FONT SIZE="-1">Optional additional description: </FONT></TD>
+</TR>
+
+% my $row = 0;
+% if ( $cgi->param('error') ) {
+% my $param = $cgi->Vars;
+%
+% for ( $row = 0; exists($param->{"description$row"}); $row++ ) {
+
+ <TR>
+ <TD></TD>
+ <TD>
+ <INPUT TYPE="text" NAME="description<% $row %>" SIZE="60" MAXLENGTH="65" VALUE="<% $param->{"description$row"} %>" rownum="<% $row %>" onkeyup = "possiblyAddRow;" >
+ </TD>
+ </TR>
+% }
+% }
+
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" NAME="submit" VALUE="Add one-time charge" <% $cgi->param('error') ? '' :' DISABLED' %>>
+
+</FORM>
+
+
+<SCRIPT TYPE="text/javascript">
+
+ var rownum = <% $row %>;
+
+ function possiblyAddRow() {
+ if ( ( rownum - this.getAttribute('rownum') ) == 1 ) {
+ addRow();
+ }
+ }
+
+ function addRow() {
+
+ var table = document.getElementById('QuickChargeTable');
+ var tablebody = table.getElementsByTagName('tbody').item(0);
+
+ var row = document.createElement('TR');
+
+ var empty_cell = document.createElement('TD');
+ row.appendChild(empty_cell);
+
+ var description_cell = document.createElement('TD');
+
+ var description_input = document.createElement('INPUT');
+ description_input.setAttribute('name', 'description'+rownum);
+ description_input.setAttribute('id', 'description'+rownum);
+ description_input.setAttribute('size', 60);
+ description_input.setAttribute('maxlength', 65);
+ description_input.setAttribute('rownum', rownum);
+ description_input.onkeyup = possiblyAddRow;
+ description_cell.appendChild(description_input);
+
+ row.appendChild(description_cell);
+
+ tablebody.appendChild(row);
+
+ rownum++;
+
+ }
+
+</SCRIPT>
+
+</BODY>
+</HTML>
diff --git a/httemplate/edit/rate.cgi b/httemplate/edit/rate.cgi
new file mode 100644
index 000000000..72a04c339
--- /dev/null
+++ b/httemplate/edit/rate.cgi
@@ -0,0 +1,116 @@
+%
+%
+%my $rate;
+%if ( $cgi->keywords ) {
+% my($query) = $cgi->keywords;
+% $query =~ /^(\d+)$/;
+% $rate = qsearchs( 'rate', { 'ratenum' => $1 } );
+%} else { #adding
+% $rate = new FS::rate {};
+%}
+%my $action = $rate->ratenum ? 'Edit' : 'Add';
+%
+%my $p1 = popurl(1);
+%
+%my %granularity = (
+% '1', => '1 second',
+% '6' => '6 second',
+% '30' => '30 second', # '1/2 minute',
+% '60' => 'minute',
+%);
+%
+%#my $nous = <<END;
+%# WHERE 0 < ( SELECT COUNT(*) FROM rate_prefix
+%# WHERE rate_region.regionnum = rate_prefix.regionnum
+%# AND countrycode != '1'
+%# )
+%#END
+%
+%
+
+
+<% include("/elements/header.html","$action Rate plan", menubar(
+ 'Main Menu' => $p,
+ 'View all rate plans' => "${p}browse/rate.cgi",
+ ))
+%>
+
+<% include('/elements/progress-init.html',
+ 'OneTrueForm',
+ [ 'rate', 'min_', 'sec_' ],
+ 'process/rate.cgi',
+ $p.'browse/rate.cgi',
+ )
+%>
+<FORM NAME="OneTrueForm">
+<INPUT TYPE="hidden" NAME="ratenum" VALUE="<% $rate->ratenum %>">
+
+Rate plan
+<INPUT TYPE="text" NAME="ratename" SIZE=32 VALUE="<% $rate->ratename %>">
+<BR><BR>
+
+<% table() %>
+<TR>
+ <TH>Region</TH>
+ <TH>Prefix(es)</TH>
+ <TH><FONT SIZE=-1>Included<BR>minutes</FONT></TH>
+ <TH><FONT SIZE=-1>Charge per<BR>minute</FONT></TH>
+ <TH><FONT SIZE=-1>Granularity</FONT></TH>
+</TR>
+% foreach my $rate_region (
+% sort { lc($a->regionname) cmp lc($b->regionname) }
+% qsearch({
+% 'select' => 'DISTINCT ON ( regionnum ) rate_region.*',
+% 'table' => 'rate_region',
+% 'hashref' => {},
+% #'addl_from' => 'INNER JOIN rate_prefix USING ( regionnum )',
+% #'extra_sql' => "WHERE countrycode != '1'",
+%
+% # 'ORDER BY regionname'
+% # ERROR: SELECT DISTINCT ON expressions must
+% # match initial ORDER BY expressions
+% })
+% ) {
+% my $n = $rate_region->regionnum;
+% my $rate_detail =
+% $rate->dest_detail($rate_region)
+% || new FS::rate_detail { 'min_included' => 0,
+% 'min_charge' => 0,
+% 'sec_granularity' => '60'
+% };
+%
+
+
+ <TR>
+ <TD><A HREF="<%$p%>edit/rate_region.cgi?<% $rate_region->regionnum %>"><% $rate_region->regionname %></A></TD>
+ <TD><% $rate_region->prefixes_short %></TD>
+ <TD><INPUT TYPE="text" SIZE=5 NAME="min_included<%$n%>" VALUE="<% $cgi->param("min_included$n") || $rate_detail->min_included %>"></TD>
+ <TD>$<INPUT TYPE="text" SIZE=4 NAME="min_charge<%$n%>" VALUE="<% sprintf('%.2f', $cgi->param("min_charge$n") || $rate_detail->min_charge ) %>"></TD>
+ <TD>
+ <SELECT NAME="sec_granularity<%$n%>">
+% foreach my $granularity ( keys %granularity ) {
+
+ <OPTION VALUE="<%$granularity%>"<% $granularity == ( $cgi->param("sec_granularity$n") || $rate_detail->sec_granularity ) ? ' SELECTED' : '' %>><%$granularity{$granularity}%>
+% }
+
+ </SELECT>
+ </TR>
+% }
+
+
+<TR>
+ <TD COLSPAN=5 ALIGN="center">
+ <A HREF="<%$p%>edit/rate_region.cgi"><I>Add a region</I></A>
+ </TD>
+</TR>
+
+</TABLE>
+
+<BR><INPUT NAME="submit" TYPE="button" VALUE="<%
+ $rate->ratenum ? "Apply changes" : "Add rate plan"
+%>" onClick="document.OneTrueForm.submit.disabled=true; process();">
+
+ </FORM>
+ </BODY>
+</HTML>
+
diff --git a/httemplate/edit/rate_region.cgi b/httemplate/edit/rate_region.cgi
new file mode 100644
index 000000000..12cb180de
--- /dev/null
+++ b/httemplate/edit/rate_region.cgi
@@ -0,0 +1,119 @@
+<!-- mason kludge -->
+%
+%
+%my $rate_region;
+%if ( $cgi->param('error') ) {
+% $rate_region = new FS::rate_region ( {
+% map { $_, scalar($cgi->param($_)) } fields('rate_region')
+% } );
+%} elsif ( $cgi->keywords ) {
+% my($query) = $cgi->keywords;
+% $query =~ /^(\d+)$/;
+% $rate_region = qsearchs( 'rate_region', { 'regionnum' => $1 } );
+%} else { #adding
+% $rate_region = new FS::rate_region {};
+%}
+%my $action = $rate_region->regionnum ? 'Edit' : 'Add';
+%
+%my $p1 = popurl(1);
+%
+%my %granularity = (
+% '6' => '6 second',
+% '60' => 'minute',
+%);
+%
+%my @rate_prefix = $rate_region->rate_prefix;
+%my $countrycode = '';
+%if ( @rate_prefix ) {
+% $countrycode = $rate_prefix[0]->countrycode;
+% foreach my $rate_prefix ( @rate_prefix ) {
+% eidiot 'multiple country codes per region not yet supported by web UI'
+% unless $rate_prefix->countrycode eq $countrycode;
+% }
+%}
+%
+%
+
+
+<% include("/elements/header.html","$action Region", menubar(
+ 'Main Menu' => $p,
+ #'View all regions' => "${p}browse/rate_region.cgi",
+ ))
+%>
+% if ( $cgi->param('error') ) {
+
+<FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT><BR>
+% }
+
+
+<FORM ACTION="<%$p1%>process/rate_region.cgi" METHOD=POST>
+
+<INPUT TYPE="hidden" NAME="regionnum" VALUE="<% $rate_region->regionnum %>">
+
+<% ntable('#cccccc') %>
+<TR>
+ <TH ALIGN="right">Region name</TH>
+ <TD><INPUT TYPE="text" NAME="regionname" SIZE=32 VALUE="<% $rate_region->regionname %>"></TR>
+</TR>
+
+<TR>
+ <TH ALIGN="right">Country code</TH>
+ <TD><INPUT TYPE="text" NAME="countrycode" SIZE=4 MAXLENGTH=3 VALUE="<% $countrycode %>"></TR>
+</TR>
+
+
+<TR>
+ <TH ALIGN="right">Prefixes</TH>
+ <TD>
+ <TEXTAREA NAME="npa" WRAP=SOFT><% join(', ', map $_->npa, @rate_prefix ) %></TEXTAREA>
+ </TD>
+</TR>
+
+</TABLE>
+
+<BR>
+<% table() %>
+<TR>
+ <TH>Rate plan</TH>
+ <TH><FONT SIZE=-1>Included<BR>minutes</FONT></TH>
+ <TH><FONT SIZE=-1>Charge per<BR>minute</FONT></TH>
+ <TH><FONT SIZE=-1>Granularity</FONT></TH>
+</TR>
+% foreach my $rate ( qsearch('rate', {}) ) {
+%
+% my $n = $rate->ratenum;
+% my $rate_detail = $rate->dest_detail($rate_region)
+% || new FS::rate_region { 'min_included' => 0,
+% 'min_charge' => 0,
+% 'sec_granularity' => '60'
+% };
+%
+%
+
+ <TR>
+ <TD><A HREF="<%$p%>edit/rate.cgi?<% $rate->ratenum %>"><% $rate->ratename %></TD>
+ <TD><INPUT TYPE="text" SIZE=5 NAME="min_included<%$n%>" VALUE="<% $cgi->param("min_included$n") || $rate_detail->min_included %>"></TD>
+ <TD>$<INPUT TYPE="text" SIZE=4 NAME="min_charge<%$n%>" VALUE="<% sprintf('%.2f', $cgi->param("min_charge$n") || $rate_detail->min_charge ) %>"></TD>
+ <TD>
+ <SELECT NAME="sec_granularity<%$n%>">
+% foreach my $granularity ( keys %granularity ) {
+
+ <OPTION VALUE="<%$granularity%>"<% $granularity == ( $cgi->param("sec_granularity$n") || $rate_detail->sec_granularity ) ? ' SELECTED' : '' %>><%$granularity{$granularity}%>
+% }
+
+ </SELECT>
+ </TR>
+% }
+
+
+</TABLE>
+
+<BR><BR><INPUT TYPE="submit" VALUE="<%
+ $rate_region->regionnum ? "Apply changes" : "Add region"
+%>">
+
+ </FORM>
+ </BODY>
+</HTML>
+
+
diff --git a/httemplate/edit/reason.html b/httemplate/edit/reason.html
new file mode 100644
index 000000000..7c0722cea
--- /dev/null
+++ b/httemplate/edit/reason.html
@@ -0,0 +1,45 @@
+%
+% $cgi->param('class') =~ /^(\w)$/ or die "illegal class";
+% my $class=$1;
+%
+% my %classmap = ('C' => 'cancel',
+% 'S' => 'suspend',
+% );
+% my $classname = $classmap{$class};
+%
+% my (@types) = qsearch( 'reason_type', { 'class' => $class } );
+%
+% unless (scalar(@types)) {
+% print $cgi->redirect( "reason_type.html?class=$class" );
+% }
+<% include( 'elements/edit.html',
+ 'name' => ucfirst($classname) . ' Reason',
+ 'table' => 'reason',
+ 'labels' => {
+ 'reasonnum' => ucfirst($classname) . ' Reason',
+ 'reason_type' => ucfirst($classname) . ' Reason type',
+ 'reason' => ucfirst($classname) . ' Reason',
+ 'disabled' => 'Disabled',
+ 'class' => '',
+ },
+ 'fields' => [
+ { 'field' => 'reason_type',
+ 'type' => 'select',
+ 'value' => { 'vcolumn' => 'typenum',
+ 'ccolumn' => 'type',
+ 'values' => \@types,
+ },
+ },
+ 'reason',
+ { 'field' => 'class',
+ 'type' => 'fixedhidden',
+ 'value' => $class,
+ },
+ { 'field' => 'disabled',
+ 'type' => 'checkbox',
+ 'value' => 'Y'
+ },
+ ],
+ 'viewall_url' => $p . "browse/reason.html?class=$class",
+ )
+%>
diff --git a/httemplate/edit/reason_type.html b/httemplate/edit/reason_type.html
new file mode 100644
index 000000000..970529e35
--- /dev/null
+++ b/httemplate/edit/reason_type.html
@@ -0,0 +1,28 @@
+%
+%$cgi->param('class') =~ /^(\w)$/;
+%my $class = $1;
+%
+%my %classmap = ( 'C' => 'Cancel',
+% 'S' => 'Suspend',
+% );
+%
+%my $classname = $classmap{$class};
+%
+<% include( 'elements/edit.html',
+ 'name' => $classname . ' Reason Type',
+ 'table' => 'reason_type',
+ 'labels' => {
+ 'typenum' => $classname . ' reason type',
+ 'type' => $classname . ' reason type name',
+ 'class' => '',
+ },
+ 'fields' => [
+ 'type',
+ { 'field' => 'class',
+ 'type' => 'hidden',
+ },
+ ],
+ 'viewall_url' => $p . "browse/reason_type.html?class=$class",
+ 'new_hashref_callback' => sub {{ 'class' => $class }},
+ )
+%>
diff --git a/httemplate/edit/reg_code.cgi b/httemplate/edit/reg_code.cgi
new file mode 100644
index 000000000..06bef4879
--- /dev/null
+++ b/httemplate/edit/reg_code.cgi
@@ -0,0 +1,39 @@
+%
+%my $agentnum = $cgi->param('agentnum');
+%$agentnum =~ /^(\d+)$/ or eidiot "illegal agentnum $agentnum";
+%$agentnum = $1;
+%my $agent = qsearchs('agent', { 'agentnum' => $agentnum } );
+%
+%
+
+
+<% include("/elements/header.html",'Generate registration codes for '. $agent->agent, menubar(
+ 'Main Menu' => $p,
+ ))
+%>
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#FF0000">Error: <% $cgi->param('error') %></FONT>
+% }
+
+
+<FORM ACTION="<%popurl(1)%>process/reg_code.cgi" METHOD="POST" NAME="OneTrueForm" onSubmit="document.OneTrueForm.submit.disabled=true">
+<INPUT TYPE="hidden" NAME="agentnum" VALUE="<% $agent->agentnum %>">
+
+Generate
+<INPUT TYPE="text" NAME="num" VALUE="<% $cgi->param('num') %>" SIZE=5 MAXLENGTH=4>
+registration codes for <B><% $agent->agent %></B> allowing the following packages:
+<BR><BR>
+% foreach my $part_pkg ( qsearch('part_pkg', { 'disabled' => '' } ) ) {
+
+ <INPUT TYPE="checkbox" NAME="pkgpart<% $part_pkg->pkgpart %>">
+ <% $part_pkg->pkg %> - <% $part_pkg->comment %>
+ <BR>
+% }
+
+
+<BR>
+<INPUT TYPE="submit" NAME="submit" VALUE="Generate">
+
+</FORM></BODY></HTML>
+
diff --git a/httemplate/edit/router.cgi b/httemplate/edit/router.cgi
new file mode 100755
index 000000000..0da45c00e
--- /dev/null
+++ b/httemplate/edit/router.cgi
@@ -0,0 +1,78 @@
+<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_Common.html b/httemplate/edit/svc_Common.html
new file mode 100644
index 000000000..6393f9ebc
--- /dev/null
+++ b/httemplate/edit/svc_Common.html
@@ -0,0 +1,30 @@
+<%init>
+
+# false laziness w/view/svc_Common.html
+
+$cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unparsable svcdb";
+my $table = $1;
+require "FS/$table.pm";
+
+my %opt;
+if ( UNIVERSAL::can("FS::$table", 'table_info') ) {
+ $opt{'name'} = "FS::$table"->table_info->{'name'};
+
+ my $fields = "FS::$table"->table_info->{'fields'};
+ my %labels = map { $_ => ( ref($fields->{$_})
+ ? $fields->{$_}{'label'}
+ : $fields->{$_}
+ );
+ }
+ keys %$fields;
+ $opt{'labels'} = \%labels;
+
+}
+
+</%init>
+<% include('elements/svc_Common.html',
+ 'table' => $table,
+ 'post_url' => popurl(1). "process/svc_Common.html",
+ %opt,
+ )
+%>
diff --git a/httemplate/edit/svc_acct.cgi b/httemplate/edit/svc_acct.cgi
new file mode 100755
index 000000000..ebaa4b2ba
--- /dev/null
+++ b/httemplate/edit/svc_acct.cgi
@@ -0,0 +1,456 @@
+%
+%
+%my $conf = new FS::Conf;
+%my @shells = $conf->config('shells');
+%
+%my $curuser = $FS::CurrentUser::CurrentUser;
+%
+%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');
+%
+%} elsif ( $cgi->param('pkgnum') && $cgi->param('svcpart') ) { #adding
+%
+% $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+% $pkgnum = $1;
+% $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+% $svcpart = $1;
+%
+% $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+% die "No part_svc entry!" unless $part_svc;
+%
+% $svc_acct = new FS::svc_acct({svcpart => $svcpart});
+%
+% $svcnum='';
+%
+%} else { #editing
+%
+% my($query) = $cgi->keywords;
+% $query =~ /^(\d+)$/ or die "unparsable svcnum";
+% $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;
+%
+%}
+%
+%my( $cust_pkg, $cust_main ) = ( '', '' );
+%if ( $pkgnum ) {
+% $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $pkgnum } );
+% $cust_main = $cust_pkg->cust_main;
+%}
+%
+%unless ( $svcnum || $cgi->param('error') ) { #adding
+%
+% #set gecos
+% if ($cust_main) {
+% unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
+% $svc_acct->setfield('finger',
+% $cust_main->getfield('first') . " " . $cust_main->getfield('last')
+% );
+% }
+% }
+%
+% $svc_acct->set_default_and_fixed( {
+% #false laziness w/svc-acct::_fieldhandlers
+% 'usergroup' => sub {
+% my( $self, $groups ) = @_;
+% if ( ref($groups) eq 'ARRAY' ) {
+% @groups = @$groups;
+% $groups;
+% } elsif ( length($groups) ) {
+% @groups = split(/\s*,\s*/, $groups);
+% [ @groups ];
+% } else {
+% @groups = ();
+% [];
+% }
+% }
+% } );
+%
+%}
+%
+%#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->exists('usernamemax')
+% ? $conf->config('usernamemax')
+% : dbdef->table('svc_acct')->column('username')->length;
+%my $ulen2 = $ulen+2;
+%
+%my $pmax = $conf->config('passwordmax') || 8;
+%my $pmax2 = $pmax+2;
+%
+%my $p1 = popurl(1);
+%
+%
+
+
+<% include("/elements/header.html","$action $svc account") %>
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+% if ( $cust_main ) {
+
+ <% include( '/elements/small_custview.html', $cust_main, '', 1,
+ popurl(2) . "view/cust_main.cgi") %>
+ <BR>
+% }
+
+
+<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 %>">
+
+Service # <% $svcnum ? "<B>$svcnum</B>" : " (NEW)" %><BR>
+
+<% ntable("#cccccc",2) %>
+
+<TR>
+ <TD ALIGN="right">Service</TD>
+ <TD BGCOLOR="#eeeeee"><% $part_svc->svc %></TD>
+</TR>
+
+<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>
+%
+%my $sec_phrase = $svc_acct->sec_phrase;
+%if ( $conf->exists('security_phrase') ) {
+%
+
+
+ <TR>
+ <TD ALIGN="right">Security phrase</TD>
+ <TD>
+ <INPUT TYPE="text" NAME="sec_phrase" VALUE="<% $sec_phrase %>" SIZE=32>
+ (for forgotten passwords)
+ </TD>
+ </TD>
+% } else {
+
+
+ <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' ) {
+%
+
+
+ <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;
+% }
+% }
+%
+% if ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S' ) {
+% foreach my $domain
+% (split(',',$part_svc->part_svc_column('domsvc')->columnvalue)) {
+% my $svc_domain =
+% qsearchs('svc_domain', { 'svcnum' => $domain } );
+% $svc_domain{$svc_domain->svcnum} = $svc_domain if $svc_domain;
+% }
+% }elsif ($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', {} );
+% }
+%
+%
+
+
+ <TR>
+ <TD ALIGN="right">Domain</TD>
+ <TD>
+ <SELECT NAME="domsvc" SIZE=1>
+% foreach my $svcnum (
+% sort { $svc_domain{$a}->domain cmp $svc_domain{$b}->domain }
+% keys %svc_domain
+% ) {
+% my $svc_domain = $svc_domain{$svcnum};
+%
+
+
+ <OPTION VALUE="<% $svc_domain->svcnum %>" <% $svc_domain->svcnum == $domsvc ? ' SELECTED' : '' %>><% $svc_domain->domain %>
+% }
+
+ </SELECT>
+ </TD>
+ </TR>
+% }
+%
+%#pop
+%my $popnum = $svc_acct->popnum || 0;
+%if ( $part_svc->part_svc_column('popnum')->columnflag eq 'F' ) {
+%
+
+
+ <INPUT TYPE="hidden" NAME="popnum" VALUE="<% $popnum %>">
+% } else {
+
+
+ <TR>
+ <TD ALIGN="right">Access number</TD>
+ <TD><% FS::svc_acct_pop::popselector($popnum) %></TD>
+ </TR>
+% }
+% #uid/gid
+% foreach my $xid (qw( uid gid )) {
+%
+% if ( $part_svc->part_svc_column($xid)->columnflag =~ /^[FA]$/
+% || ! $conf->exists("svc_acct-edit_$xid")
+% ) {
+%
+% if ( length($svc_acct->$xid()) ) {
+
+
+ <TR>
+ <TD ALIGN="right"><% uc($xid) %></TD>
+ <TD BGCOLOR="#eeeeee"><% $svc_acct->$xid() %></TD>
+ <TD>
+ </TD>
+ </TR>
+% }
+
+
+ <INPUT TYPE="hidden" NAME="<% $xid %>" VALUE="<% $svc_acct->$xid() %>">
+% } else {
+
+
+ <TR>
+ <TD ALIGN="right"><% uc($xid) %></TD>
+ <TD>
+ <INPUT TYPE="text" NAME="<% $xid %>" SIZE=8 MAXLENGTH=6 VALUE="<% $svc_acct->$xid() %>">
+ </TD>
+ </TR>
+% }
+% }
+%
+%#finger
+%if ( $part_svc->part_svc_column('uid')->columnflag eq 'F'
+% && ! $svc_acct->finger ) {
+%
+
+
+ <INPUT TYPE="hidden" NAME="finger" VALUE="">
+% } else {
+
+
+ <TR>
+ <TD ALIGN="right">GECOS</TD>
+ <TD>
+ <INPUT TYPE="text" NAME="finger" VALUE="<% $svc_acct->finger %>">
+ </TD>
+ </TR>
+% }
+
+
+
+<INPUT TYPE="hidden" NAME="dir" VALUE="<% $svc_acct->dir %>">
+%
+%#shell
+%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' )
+% ) {
+%
+
+
+ <INPUT TYPE="hidden" NAME="shell" VALUE="<% $shell %>">
+% } else {
+
+
+ <TR>
+ <TD ALIGN="right">Shell</TD>
+ <TD>
+ <SELECT NAME="shell" SIZE=1>
+%
+% my($etc_shell);
+% foreach $etc_shell (@shells) {
+%
+
+
+ <OPTION<% $etc_shell eq $shell ? ' SELECTED' : '' %>><% $etc_shell %>
+% }
+
+
+ </SELECT>
+ </TD>
+ </TR>
+% }
+% if ( $part_svc->part_svc_column('quota')->columnflag eq 'F' ) {
+
+
+ <INPUT TYPE="hidden" NAME="quota" VALUE="<% $svc_acct->quota %>">
+% } else {
+
+
+ <TR>
+ <TD ALIGN="right">Quota:</TD>
+ <TD><INPUT TYPE="text" NAME="quota" VALUE="<% $svc_acct->quota %>"></TD>
+ </TR>
+% }
+% if ( $part_svc->part_svc_column('slipip')->columnflag =~ /^[FA]$/ ) {
+
+
+ <INPUT TYPE="hidden" NAME="slipip" VALUE="<% $svc_acct->slipip %>">
+% } else {
+
+
+ <TR>
+ <TD ALIGN="right">IP</TD>
+ <TD><INPUT TYPE="text" NAME="slipip" VALUE="<% $svc_acct->slipip %>"></TD>
+ </TR>
+% }
+%
+% if ( $curuser->access_right('Edit usage') ) {
+% my %label = ( seconds => 'Seconds',
+% upbytes => 'Upload bytes',
+% downbytes => 'Download bytes',
+% totalbytes => 'Total bytes',
+% );
+% foreach my $uf (keys %label) {
+% my $tf = $uf . "_threshold";
+% if ( $svc_acct->$tf ne '' ) {
+
+ <TR>
+ <TD ALIGN="right"><% $label{$uf} %> remaining</TD>
+ <TD><INPUT TYPE="text" NAME="<% $uf %>" VALUE="<% $svc_acct->$uf %>"></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 =~ /^[FA]$/ ) {
+
+
+ <INPUT TYPE="hidden" NAME="<% $r %>" VALUE="<% $svc_acct->getfield($r) %>">
+% } else {
+
+
+ <TR>
+ <TD ALIGN="right"><% $FS::raddb::attrib{$a} %></TD>
+ <TD><INPUT TYPE="text" NAME="<% $r %>" VALUE="<% $svc_acct->getfield($r) %>"></TD>
+ </TR>
+% }
+% }
+
+
+
+<TR>
+ <TD ALIGN="right">RADIUS groups</TD>
+% if ( $part_svc->part_svc_column('usergroup')->columnflag eq 'F' ) {
+
+
+ <TD BGCOLOR="#eeeeee"><% join('<BR>', @groups) %></TD>
+% } else {
+
+
+ <TD><% FS::svc_acct::radius_usergroup_selector( \@groups ) %></TD>
+% }
+
+
+</TR>
+% foreach my $field ($svc_acct->virtual_fields) {
+% # If the flag is X, it won't even show up in $svc_acct->virtual_fields.
+% if ( $part_svc->part_svc_column($field)->columnflag ne 'F' ) {
+
+
+ <% $svc_acct->pvf($field)->widget('HTML', 'edit', $svc_acct->getfield($field)) %>
+% }
+% }
+
+
+</TABLE>
+<BR>
+
+<INPUT TYPE="submit" VALUE="Submit">
+
+</FORM></BODY></HTML>
diff --git a/httemplate/edit/svc_acct_pop.cgi b/httemplate/edit/svc_acct_pop.cgi
new file mode 100755
index 000000000..641aa0378
--- /dev/null
+++ b/httemplate/edit/svc_acct_pop.cgi
@@ -0,0 +1,57 @@
+<!-- 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 000000000..2a5a6509a
--- /dev/null
+++ b/httemplate/edit/svc_broadband.cgi
@@ -0,0 +1,254 @@
+%# 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;
+%
+%} elsif ( $cgi->param('pkgnum') && $cgi->param('svcpart') ) { #adding
+%
+% $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+% $pkgnum = $1;
+% $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+% $svcpart = $1;
+%
+% $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+% die "No part_svc entry!" unless $part_svc;
+%
+% $svc_broadband = new FS::svc_broadband({ svcpart => $svcpart });
+%
+% $svcnum='';
+%
+% $svc_broadband->set_default_and_fixed;
+%
+%} else { #editing
+%
+% my($query) = $cgi->keywords;
+% $query =~ /^(\d+)$/ or die "unparsable svcnum";
+% $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;
+%
+%}
+%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, $mac_addr,
+% $latitude, $longitude, $altitude, $vlan_profile, $auth_key,
+% $description) =
+% ($svc_broadband->ip_addr,
+% $svc_broadband->speed_up,
+% $svc_broadband->speed_down,
+% $svc_broadband->blocknum,
+% $svc_broadband->mac_addr,
+% $svc_broadband->latitude,
+% $svc_broadband->longitude,
+% $svc_broadband->altitude,
+% $svc_broadband->vlan_profile,
+% $svc_broadband->auth_key,
+% $svc_broadband->description,
+% );
+%
+%
+
+
+<%include("/elements/header.html","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">Description</TD>
+ <TD BGCOLOR="#ffffff">
+% if ( $part_svc->part_svc_column('description')->columnflag eq 'F' ) {
+
+ <INPUT TYPE="hidden" NAME="description" VALUE="<%$description%>"><%$description%>
+% } else {
+
+ <INPUT TYPE="text" NAME="description" VALUE="<%$description%>">
+% }
+
+ </TD>
+ </TR>
+ <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>
+% }
+ <TR>
+ <TD ALIGN="right">MAC Address</TD>
+ <TD BGCOLOR="#ffffff">
+ <INPUT TYPE="text" NAME="mac_addr" VALUE="<%$mac_addr%>">
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Latitude</TD>
+ <TD BGCOLOR="#ffffff">
+ <INPUT TYPE="text" NAME="latitude" VALUE="<%$latitude%>">
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Longitude</TD>
+ <TD BGCOLOR="#ffffff">
+ <INPUT TYPE="text" NAME="longitude" VALUE="<%$longitude%>">
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Altitude</TD>
+ <TD BGCOLOR="#ffffff">
+ <INPUT TYPE="text" NAME="altitude" VALUE="<%$altitude%>">
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">VLAN Profile</TD>
+ <TD BGCOLOR="#ffffff">
+% if ( $part_svc->part_svc_column('vlan_profile')->columnflag eq 'F' ) {
+
+ <INPUT TYPE="hidden" NAME="vlan_profile" VALUE="<%$vlan_profile%>"><%$vlan_profile%>
+% } else {
+
+ <INPUT TYPE="text" NAME="vlan_profile" VALUE="<%$vlan_profile%>">
+% }
+
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Authentication Key</TD>
+ <TD BGCOLOR="#ffffff">
+% if ( $part_svc->part_svc_column('auth_key')->columnflag eq 'F' ) {
+
+ <INPUT TYPE="hidden" NAME="auth_key" VALUE="<%$auth_key%>"><%$auth_key%>
+% } else {
+
+ <INPUT TYPE="text" NAME="auth_key" VALUE="<%$auth_key%>">
+% }
+
+ </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 000000000..5ec074bda
--- /dev/null
+++ b/httemplate/edit/svc_domain.cgi
@@ -0,0 +1,90 @@
+%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;
+%
+%} elsif ( $cgi->param('pkgnum') && $cgi->param('svcpart') ) { #adding
+%
+% $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+% $pkgnum = $1;
+% $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+% $svcpart = $1;
+%
+% $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+% die "No part_svc entry!" unless $part_svc;
+%
+% $svc_domain = new FS::svc_domain({});
+%
+% $svcnum='';
+%
+% $svc_domain->set_default_and_fixed;
+%
+%} else { #editing
+%
+% $kludge_action = '';
+% $purpose = '';
+% my($query) = $cgi->keywords;
+% $query =~ /^(\d+)$/ or die "unparsable svcnum";
+% $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;
+%
+%}
+%my $action = $svcnum ? 'Edit' : 'Add';
+%
+%my $svc = $part_svc->getfield('svc');
+%
+%my $otaker = getotaker;
+%
+%my $domain = $svc_domain->domain;
+%
+%my $p1 = popurl(1);
+%
+%
+
+
+<% include('/elements/header.html', "$action $svc", '') %>
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+% }
+
+
+<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 %>">
+
+<INPUT TYPE="radio" NAME="action" VALUE="N"<% $kludge_action eq 'N' ? ' CHECKED' : '' %>>New
+<BR>
+
+<INPUT TYPE="radio" NAME="action" VALUE="M"<% $kludge_action eq 'M' ? ' CHECKED' : '' %>>Transfer
+
+<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>
+
+<% include('/elements/footer.html') %>
diff --git a/httemplate/edit/svc_external.cgi b/httemplate/edit/svc_external.cgi
new file mode 100644
index 000000000..393e71c38
--- /dev/null
+++ b/httemplate/edit/svc_external.cgi
@@ -0,0 +1,99 @@
+%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;
+%
+%} elsif ( $cgi->param('pkgnum') && $cgi->param('svcpart') ) { #adding
+%
+% $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+% $pkgnum = $1;
+% $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+% $svcpart = $1;
+%
+% $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+% die "No part_svc entry!" unless $part_svc;
+%
+% $svc_external = new FS::svc_external { svcpart => $svcpart };
+%
+% $svcnum='';
+%
+% $svc_external->set_default_and_fixed;
+%
+%} else { #adding
+%
+% my($query) = $cgi->keywords;
+% $query =~ /^(\d+)$/ or die "unparsable svcnum";
+% $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;
+%
+%}
+%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 000000000..ef08ffc16
--- /dev/null
+++ b/httemplate/edit/svc_forward.cgi
@@ -0,0 +1,180 @@
+<!-- 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;
+%
+%} elsif ( $cgi->param('pkgnum') && $cgi->param('svcpart') ) { #adding
+%
+% $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+% $pkgnum = $1;
+% $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+% $svcpart = $1;
+%
+% $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+% die "No part_svc entry!" unless $part_svc;
+%
+% $svc_forward = new FS::svc_forward({});
+%
+% $svcnum='';
+%
+% $svc_forward->set_default_and_fixed;
+%
+%} else { #editing
+%
+% my($query) = $cgi->keywords;
+%
+% $query =~ /^(\d+)$/ or die "unparsable svcnum";
+% $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;
+%
+%}
+%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
+%
+%
+
+
+<% include("/elements/header.html","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_phone.cgi b/httemplate/edit/svc_phone.cgi
new file mode 100644
index 000000000..ca62b6416
--- /dev/null
+++ b/httemplate/edit/svc_phone.cgi
@@ -0,0 +1,11 @@
+<% include( 'elements/svc_Common.html',
+ 'name' => 'Phone number',
+ 'table' => 'svc_phone',
+ 'fields' => [qw( countrycode phonenum )], #pin
+ 'labels' => {
+ 'countrycode' => 'Country code',
+ 'phonenum' => 'Phone number',
+ 'pin' => 'PIN',
+ },
+ )
+%>
diff --git a/httemplate/edit/svc_www.cgi b/httemplate/edit/svc_www.cgi
new file mode 100644
index 000000000..4b27752ff
--- /dev/null
+++ b/httemplate/edit/svc_www.cgi
@@ -0,0 +1,220 @@
+%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;
+%
+%} elsif ( $cgi->param('pkgnum') && $cgi->param('svcpart') ) { #adding
+%
+% $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+% $pkgnum = $1;
+% $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+% $svcpart = $1;
+%
+% $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+% die "No part_svc entry!" unless $part_svc;
+%
+% $svc_www = new FS::svc_www { svcpart => $svcpart };
+%
+% $svcnum='';
+%
+% $svc_www->set_default_and_fixed;
+%
+%} else { #editing
+%
+% my($query) = $cgi->keywords;
+% $query =~ /^(\d+)$/ or die "unparsable svcnum";
+% $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;
+%
+%}
+%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>";
+%
+%if ( $part_svc->part_svc_column('usersvc')->columnflag ne 'F'
+% || $part_svc->part_svc_column('usersvc')->columnvalue !~ /^\s*$/) {
+% print '<TR><TD ALIGN="right">Username</TD><TD><SELECT NAME="usersvc" SIZE=1>';
+% print '<OPTION VALUE="">(none)';
+% 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 000000000..0dbde793d
--- /dev/null
+++ b/httemplate/elements/calendar-en.js
@@ -0,0 +1,127 @@
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mihai_bazon@yahoo.com>
+// 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");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 0;
+
+// 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-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\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 000000000..b27d9bed0
--- /dev/null
+++ b/httemplate/elements/calendar-setup.js
@@ -0,0 +1,200 @@
+/* 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.5 2006-02-09 07:18:08 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("dateText", null);
+ param_default("firstDay", null);
+ 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);
+ param_default("multiple", null);
+
+ 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.multiple || 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.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 && typeof p.onUpdate == "function")
+ p.onUpdate(cal);
+ if (update && p.flat) {
+ if (typeof p.flatCallback == "function")
+ p.flatCallback(cal);
+ }
+ if (update && p.singleClick && cal.dateClicked)
+ cal.callCloseHandler();
+ };
+
+ 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.showsOtherMonths = params.showOthers;
+ 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.getDateText = params.dateText;
+ if (params.ifFormat) {
+ cal.setDateFormat(params.ifFormat);
+ }
+ if (params.inputField && typeof params.inputField.value == "string") {
+ cal.parseDate(params.inputField.value);
+ }
+ 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 (dateEl)
+ params.date = Date.parseDate(dateEl.value || dateEl.innerHTML, dateFmt);
+ 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();
+ }
+ if (params.multiple) {
+ cal.multiple = {};
+ for (var i = params.multiple.length; --i >= 0;) {
+ var d = params.multiple[i];
+ var ds = d.print("%Y%m%d");
+ cal.multiple[ds] = d;
+ }
+ }
+ cal.showsOtherMonths = params.showOthers;
+ cal.yearStep = params.step;
+ cal.setRange(params.range[0], params.range[1]);
+ cal.params = params;
+ cal.setDateStatusHandler(params.dateStatusFunc);
+ cal.getDateText = params.dateText;
+ cal.setDateFormat(dateFmt);
+ if (mustCreate)
+ cal.create();
+ 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;
+ };
+
+ return cal;
+};
diff --git a/httemplate/elements/calendar-win2k-2.css b/httemplate/elements/calendar-win2k-2.css
new file mode 100644
index 000000000..6f37b7dcd
--- /dev/null
+++ b/httemplate/elements/calendar-win2k-2.css
@@ -0,0 +1,271 @@
+/* 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;
+ z-index: 100;
+}
+
+.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 000000000..f5c74f608
--- /dev/null
+++ b/httemplate/elements/calendar.js
@@ -0,0 +1,1806 @@
+/* Copyright Mihai Bazon, 2002-2005 | www.bazon.net/mishoo
+ * -----------------------------------------------------------
+ *
+ * The DHTML Calendar, version 1.0 "It is happening again"
+ *
+ * Details and latest version at:
+ * www.dynarch.com/projects/calendar
+ *
+ * This script is developed by Dynarch.com. Visit us at www.dynarch.com.
+ *
+ * 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.5 2006-02-09 07:18:08 ivan Exp $
+
+/** The Calendar object constructor. */
+Calendar = function (firstDayOfWeek, dateStr, onSelected, onClose) {
+ // member variables
+ this.activeDiv = null;
+ this.currentDateEl = null;
+ this.getDateStatus = null;
+ this.getDateToolTip = null;
+ this.getDateText = 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 = typeof firstDayOfWeek == "number" ? firstDayOfWeek : Calendar._FD; // 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;
+ this.hiliteToday = true;
+ this.multiple = null;
+ // 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;
+};
+
+// FIXME: the following 2 functions totally suck, are useless and should be replaced immediately.
+Calendar.getElement = function(ev) {
+ var f = Calendar.is_ie ? window.event.srcElement : ev.currentTarget;
+ while (f.nodeType != 1 || /^div$/i.test(f.tagName))
+ f = f.parentNode;
+ return f;
+};
+
+Calendar.getTargetElement = function(ev) {
+ var f = Calendar.is_ie ? window.event.srcElement : ev.target;
+ while (f.nodeType != 1)
+ f = f.parentNode;
+ return f;
+};
+
+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.innerHTML = 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.innerHTML = 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.innerHTML;
+ 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.innerHTML = 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");
+ if (el.calendar)
+ el.calendar.tooltips.innerHTML = _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") {
+ if (cal.currentDateEl) {
+ Calendar.removeClass(cal.currentDateEl, "selected");
+ Calendar.addClass(el, "selected");
+ closing = (cal.currentDateEl == el);
+ if (!closing) {
+ cal.currentDateEl = el;
+ }
+ }
+ cal.date.setDateOnly(el.caldate);
+ date = cal.date;
+ var other_month = !(cal.dateClicked = !el.otherMonth);
+ if (!other_month && !cal.currentDateEl)
+ cal._toggleMultipleDate(new Date(date));
+ else
+ newdate = !el.disabled;
+ // a date was clicked
+ if (other_month)
+ cal._init(cal.firstDayOfWeek, date);
+ } else {
+ if (el.navtype == 200) {
+ Calendar.removeClass(el, "hilite");
+ cal.callCloseHandler();
+ return;
+ }
+ date = new Date(cal.date);
+ if (el.navtype == 0)
+ date.setDateOnly(new Date()); // TODAY
+ // 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 <mihai_bazon@yahoo.com> 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.innerHTML;
+ 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.innerHTML = 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())) {
+ return false;
+ }
+ break;
+ }
+ if (!date.equalsTo(cal.date)) {
+ cal.setDate(date);
+ newdate = true;
+ } else if (el.navtype == 0)
+ newdate = closing = true;
+ }
+ if (newdate) {
+ ev && cal.callHandler();
+ }
+ if (closing) {
+ Calendar.removeClass(el, "hilite");
+ ev && 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;
+ cell.innerHTML = "<div unselectable='on'>" + text + "</div>";
+ 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.innerHTML = Calendar._TT["WK"];
+ }
+ for (var i = 7; i > 0; --i) {
+ cell = Calendar.createElement("td", row);
+ 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);
+ }
+ for (var j = 7; j > 0; --j) {
+ cell = Calendar.createElement("td", row);
+ 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.innerHTML = 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.innerHTML = ":";
+ 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 pm, hrs = this.date.getHours(),
+ mins = this.date.getMinutes();
+ if (t12) {
+ pm = (hrs >= 12);
+ if (pm) hrs -= 12;
+ if (hrs == 0) hrs = 12;
+ AP.innerHTML = pm ? "pm" : "am";
+ }
+ H.innerHTML = (hrs < 10) ? ("0" + hrs) : hrs;
+ M.innerHTML = (mins < 10) ? ("0" + mins) : mins;
+ };
+
+ cal.onUpdateTime = function() {
+ var date = this.date;
+ var h = parseInt(H.innerHTML, 10);
+ if (t12) {
+ if (/pm/i.test(AP.innerHTML) && h < 12)
+ h += 12;
+ else if (/am/i.test(AP.innerHTML) && h == 12)
+ h = 0;
+ }
+ var d = date.getDate();
+ var m = date.getMonth();
+ var y = date.getFullYear();
+ date.setHours(h);
+ date.setMinutes(parseInt(M.innerHTML, 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.innerHTML = 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";
+ div.appendChild(yr);
+ }
+
+ this._init(this.firstDayOfWeek, this.date);
+ parent.appendChild(this.element);
+};
+
+/** keyboard navigation, only for popup calendars */
+Calendar._keyEvent = function(ev) {
+ var cal = window._dynarch_popupCalendar;
+ if (!cal || cal.multiple)
+ return false;
+ (Calendar.is_ie) && (ev = window.event);
+ var act = (Calendar.is_ie || ev.type == "keypress"),
+ K = ev.keyCode;
+ if (ev.ctrlKey) {
+ switch (K) {
+ 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 (K) {
+ 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 prev, x, y, ne, el, step;
+ prev = K == 37 || K == 38;
+ step = (K == 37 || K == 39) ? 1 : 7;
+ function setVars() {
+ el = cal.currentDateEl;
+ var p = el.pos;
+ x = p & 15;
+ y = p >> 4;
+ ne = cal.ar_days[y][x];
+ };setVars();
+ function prevMonth() {
+ var date = new Date(cal.date);
+ date.setDate(date.getDate() - step);
+ cal.setDate(date);
+ };
+ function nextMonth() {
+ var date = new Date(cal.date);
+ date.setDate(date.getDate() + step);
+ cal.setDate(date);
+ };
+ while (1) {
+ switch (K) {
+ case 37: // KEY left
+ if (--x >= 0)
+ ne = cal.ar_days[y][x];
+ else {
+ x = 6;
+ K = 38;
+ continue;
+ }
+ break;
+ case 38: // KEY up
+ if (--y >= 0)
+ ne = cal.ar_days[y][x];
+ else {
+ prevMonth();
+ setVars();
+ }
+ break;
+ case 39: // KEY right
+ if (++x < 7)
+ ne = cal.ar_days[y][x];
+ else {
+ x = 0;
+ K = 40;
+ continue;
+ }
+ break;
+ case 40: // KEY down
+ if (++y < cal.ar_days.length)
+ ne = cal.ar_days[y][x];
+ else {
+ nextMonth();
+ setVars();
+ }
+ break;
+ }
+ break;
+ }
+ if (ne) {
+ if (!ne.disabled)
+ Calendar.cellClick(ne);
+ else if (prev)
+ prevMonth();
+ else
+ nextMonth();
+ }
+ }
+ break;
+ case 13: // KEY enter
+ if (act)
+ Calendar.cellClick(cal.currentDateEl, ev);
+ 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(),
+ TY = today.getFullYear(),
+ TM = today.getMonth(),
+ TD = today.getDate();
+ 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 = this.ar_days = new Array();
+ var weekend = Calendar._TT["WEEKEND"];
+ var dates = this.multiple ? (this.datesCells = {}) : null;
+ for (var i = 0; i < 6; ++i, row = row.nextSibling) {
+ var cell = row.firstChild;
+ if (this.weekNumbers) {
+ cell.className = "day wn";
+ cell.innerHTML = date.getWeekNumber();
+ cell = cell.nextSibling;
+ }
+ row.className = "daysrow";
+ var hasdays = false, iday, dpos = ar_days[i] = [];
+ for (var j = 0; j < 7; ++j, cell = cell.nextSibling, date.setDate(iday + 1)) {
+ iday = date.getDate();
+ var wday = date.getDay();
+ cell.className = "day";
+ cell.pos = i << 4 | j;
+ dpos[j] = cell;
+ 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.innerHTML = this.getDateText ? this.getDateText(date, iday) : iday;
+ if (dates)
+ dates[date.print("%Y%m%d")] = cell;
+ if (this.getDateStatus) {
+ var status = this.getDateStatus(date, year, month, iday);
+ if (this.getDateToolTip) {
+ var toolTip = this.getDateToolTip(date, year, month, iday);
+ if (toolTip)
+ cell.title = toolTip;
+ }
+ if (status === true) {
+ cell.className += " disabled";
+ cell.disabled = true;
+ } else {
+ if (/disabled/i.test(status))
+ cell.disabled = true;
+ cell.className += " " + status;
+ }
+ }
+ if (!cell.disabled) {
+ cell.caldate = new Date(date);
+ cell.ttip = "_";
+ if (!this.multiple && current_month
+ && iday == mday && this.hiliteToday) {
+ cell.className += " selected";
+ this.currentDateEl = cell;
+ }
+ if (date.getFullYear() == TY &&
+ date.getMonth() == TM &&
+ iday == TD) {
+ 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.title.innerHTML = Calendar._MN[month] + ", " + year;
+ this.onSetTime();
+ this.table.style.visibility = "visible";
+ this._initMultipleDates();
+ // PROFILE
+ // this.tooltips.innerHTML = "Generated in " + ((new Date()) - today) + " ms";
+};
+
+Calendar.prototype._initMultipleDates = function() {
+ if (this.multiple) {
+ for (var i in this.multiple) {
+ var cell = this.datesCells[i];
+ var d = this.multiple[i];
+ if (!d)
+ continue;
+ if (cell)
+ cell.className += " selected";
+ }
+ }
+};
+
+Calendar.prototype._toggleMultipleDate = function(date) {
+ if (this.multiple) {
+ var ds = date.print("%Y%m%d");
+ var cell = this.datesCells[ds];
+ if (cell) {
+ var d = this.multiple[ds];
+ if (!d) {
+ Calendar.addClass(cell, "selected");
+ this.multiple[ds] = date;
+ } else {
+ Calendar.removeClass(cell, "selected");
+ delete this.multiple[ds];
+ }
+ }
+ }
+};
+
+Calendar.prototype.setDateToolTipHandler = function (unaryFunction) {
+ this.getDateToolTip = unaryFunction;
+};
+
+/**
+ * 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._dynarch_popupCalendar = 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) {
+ var calendar = window._dynarch_popupCalendar;
+ if (!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._dynarch_popupCalendar.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._dynarch_popupCalendar = 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 "l": p.x += el.offsetWidth - w; break;
+ case "r": 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) {
+ if (!fmt)
+ fmt = this.dateFormat;
+ this.setDate(Date.parseDate(str, fmt));
+};
+
+Calendar.prototype.hideShowCovered = function () {
+ if (!Calendar.is_ie && !Calendar.is_opera)
+ return;
+ 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 = this.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 (this.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";
+ }
+ }
+ }
+};
+
+/** 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.innerHTML = 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;
+
+Date.parseDate = function(str, fmt) {
+ var today = new Date();
+ var y = 0;
+ var m = -1;
+ var d = 0;
+ var a = str.split(/\W+/);
+ 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;
+ else if (/am/i.test(a[i]) && hr >= 12)
+ hr -= 12;
+ break;
+
+ case "%M":
+ min = parseInt(a[i], 10);
+ break;
+ }
+ }
+ if (isNaN(y)) y = today.getFullYear();
+ if (isNaN(m)) m = today.getMonth();
+ if (isNaN(d)) d = today.getDate();
+ if (isNaN(hr)) hr = today.getHours();
+ if (isNaN(min)) min = today.getMinutes();
+ if (y != 0 && m != -1 && d != 0)
+ return new Date(y, m, d, hr, min, 0);
+ 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)
+ y = today.getFullYear();
+ if (m != -1 && d != 0)
+ return new Date(y, m, d, hr, min, 0);
+ return today;
+};
+
+/** 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 date and time equality */
+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()));
+};
+
+/** Set only the year, month, date parts (keep existing time) */
+Date.prototype.setDateOnly = function(date) {
+ var tmp = new Date(date);
+ this.setDate(1);
+ this.setFullYear(tmp.getFullYear());
+ this.setMonth(tmp.getMonth());
+ this.setDate(tmp.getDate());
+};
+
+/** 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 && !Calendar.is_khtml)
+ 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._dynarch_popupCalendar = null;
diff --git a/httemplate/elements/calendar_stripped.js b/httemplate/elements/calendar_stripped.js
new file mode 100644
index 000000000..4fe03f1ea
--- /dev/null
+++ b/httemplate/elements/calendar_stripped.js
@@ -0,0 +1,14 @@
+/* Copyright Mihai Bazon, 2002-2005 | www.bazon.net/mishoo
+ * -----------------------------------------------------------
+ *
+ * The DHTML Calendar, version 1.0 "It is happening again"
+ *
+ * Details and latest version at:
+ * www.dynarch.com/projects/calendar
+ *
+ * This script is developed by Dynarch.com. Visit us at www.dynarch.com.
+ *
+ * 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.getDateToolTip=null;this.getDateText=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=typeof firstDayOfWeek=="number"?firstDayOfWeek:Calendar._FD;this.showsOtherMonths=false;this.dateStr=dateStr;this.ar_days=null;this.showsTime=false;this.time24=true;this.yearStep=2;this.hiliteToday=true;this.multiple=null;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){var f=Calendar.is_ie?window.event.srcElement:ev.currentTarget;while(f.nodeType!=1||/^div$/i.test(f.tagName))f=f.parentNode;return f;};Calendar.getTargetElement=function(ev){var f=Calendar.is_ie?window.event.srcElement:ev.target;while(f.nodeType!=1)f=f.parentNode;return f;};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.innerHTML=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.innerHTML=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.innerHTML;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.innerHTML=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");if(el.calendar)el.calendar.tooltips.innerHTML=_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"){if(cal.currentDateEl){Calendar.removeClass(cal.currentDateEl,"selected");Calendar.addClass(el,"selected");closing=(cal.currentDateEl==el);if(!closing){cal.currentDateEl=el;}}cal.date.setDateOnly(el.caldate);date=cal.date;var other_month=!(cal.dateClicked=!el.otherMonth);if(!other_month&&!cal.currentDateEl)cal._toggleMultipleDate(new Date(date));else newdate=!el.disabled;if(other_month)cal._init(cal.firstDayOfWeek,date);}else{if(el.navtype==200){Calendar.removeClass(el,"hilite");cal.callCloseHandler();return;}date=new Date(cal.date);if(el.navtype==0)date.setDateOnly(new 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 <mihai_bazon@yahoo.com> 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.innerHTML;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.innerHTML=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;}else if(el.navtype==0)newdate=closing=true;}if(newdate){ev&&cal.callHandler();}if(closing){Calendar.removeClass(el,"hilite");ev&&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;cell.innerHTML="<div unselectable='on'>"+text+"</div>";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.innerHTML=Calendar._TT["WK"];}for(var i=7;i>0;--i){cell=Calendar.createElement("td",row);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);}for(var j=7;j>0;--j){cell=Calendar.createElement("td",row);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.innerHTML=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.innerHTML=":";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 pm,hrs=this.date.getHours(),mins=this.date.getMinutes();if(t12){pm=(hrs>=12);if(pm)hrs-=12;if(hrs==0)hrs=12;AP.innerHTML=pm?"pm":"am";}H.innerHTML=(hrs<10)?("0"+hrs):hrs;M.innerHTML=(mins<10)?("0"+mins):mins;};cal.onUpdateTime=function(){var date=this.date;var h=parseInt(H.innerHTML,10);if(t12){if(/pm/i.test(AP.innerHTML)&&h<12)h+=12;else if(/am/i.test(AP.innerHTML)&&h==12)h=0;}var d=date.getDate();var m=date.getMonth();var y=date.getFullYear();date.setHours(h);date.setMinutes(parseInt(M.innerHTML,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.innerHTML=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";div.appendChild(yr);}this._init(this.firstDayOfWeek,this.date);parent.appendChild(this.element);};Calendar._keyEvent=function(ev){var cal=window._dynarch_popupCalendar;if(!cal||cal.multiple)return false;(Calendar.is_ie)&&(ev=window.event);var act=(Calendar.is_ie||ev.type=="keypress"),K=ev.keyCode;if(ev.ctrlKey){switch(K){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(K){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 prev,x,y,ne,el,step;prev=K==37||K==38;step=(K==37||K==39)?1:7;function setVars(){el=cal.currentDateEl;var p=el.pos;x=p&15;y=p>>4;ne=cal.ar_days[y][x];};setVars();function prevMonth(){var date=new Date(cal.date);date.setDate(date.getDate()-step);cal.setDate(date);};function nextMonth(){var date=new Date(cal.date);date.setDate(date.getDate()+step);cal.setDate(date);};while(1){switch(K){case 37:if(--x>=0)ne=cal.ar_days[y][x];else{x=6;K=38;continue;}break;case 38:if(--y>=0)ne=cal.ar_days[y][x];else{prevMonth();setVars();}break;case 39:if(++x<7)ne=cal.ar_days[y][x];else{x=0;K=40;continue;}break;case 40:if(++y<cal.ar_days.length)ne=cal.ar_days[y][x];else{nextMonth();setVars();}break;}break;}if(ne){if(!ne.disabled)Calendar.cellClick(ne);else if(prev)prevMonth();else nextMonth();}}break;case 13:if(act)Calendar.cellClick(cal.currentDateEl,ev);break;default:return false;}return Calendar.stopEvent(ev);};Calendar.prototype._init=function(firstDayOfWeek,date){var today=new Date(),TY=today.getFullYear(),TM=today.getMonth(),TD=today.getDate();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=this.ar_days=new Array();var weekend=Calendar._TT["WEEKEND"];var dates=this.multiple?(this.datesCells={}):null;for(var i=0;i<6;++i,row=row.nextSibling){var cell=row.firstChild;if(this.weekNumbers){cell.className="day wn";cell.innerHTML=date.getWeekNumber();cell=cell.nextSibling;}row.className="daysrow";var hasdays=false,iday,dpos=ar_days[i]=[];for(var j=0;j<7;++j,cell=cell.nextSibling,date.setDate(iday+1)){iday=date.getDate();var wday=date.getDay();cell.className="day";cell.pos=i<<4|j;dpos[j]=cell;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.innerHTML=this.getDateText?this.getDateText(date,iday):iday;if(dates)dates[date.print("%Y%m%d")]=cell;if(this.getDateStatus){var status=this.getDateStatus(date,year,month,iday);if(this.getDateToolTip){var toolTip=this.getDateToolTip(date,year,month,iday);if(toolTip)cell.title=toolTip;}if(status===true){cell.className+=" disabled";cell.disabled=true;}else{if(/disabled/i.test(status))cell.disabled=true;cell.className+=" "+status;}}if(!cell.disabled){cell.caldate=new Date(date);cell.ttip="_";if(!this.multiple&&current_month&&iday==mday&&this.hiliteToday){cell.className+=" selected";this.currentDateEl=cell;}if(date.getFullYear()==TY&&date.getMonth()==TM&&iday==TD){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.title.innerHTML=Calendar._MN[month]+", "+year;this.onSetTime();this.table.style.visibility="visible";this._initMultipleDates();};Calendar.prototype._initMultipleDates=function(){if(this.multiple){for(var i in this.multiple){var cell=this.datesCells[i];var d=this.multiple[i];if(!d)continue;if(cell)cell.className+=" selected";}}};Calendar.prototype._toggleMultipleDate=function(date){if(this.multiple){var ds=date.print("%Y%m%d");var cell=this.datesCells[ds];if(cell){var d=this.multiple[ds];if(!d){Calendar.addClass(cell,"selected");this.multiple[ds]=date;}else{Calendar.removeClass(cell,"selected");delete this.multiple[ds];}}}};Calendar.prototype.setDateToolTipHandler=function(unaryFunction){this.getDateToolTip=unaryFunction;};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._dynarch_popupCalendar=null;};Calendar.prototype.reparent=function(new_parent){var el=this.element;el.parentNode.removeChild(el);new_parent.appendChild(el);};Calendar._checkCalendar=function(ev){var calendar=window._dynarch_popupCalendar;if(!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._dynarch_popupCalendar.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._dynarch_popupCalendar=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 "l":p.x+=el.offsetWidth-w;break;case "r":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){if(!fmt)fmt=this.dateFormat;this.setDate(Date.parseDate(str,fmt));};Calendar.prototype.hideShowCovered=function(){if(!Calendar.is_ie&&!Calendar.is_opera)return;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=this.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(this.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";}}}};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.innerHTML=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.parseDate=function(str,fmt){var today=new Date();var y=0;var m=-1;var d=0;var a=str.split(/\W+/);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;else if(/am/i.test(a[i])&&hr>=12)hr-=12;break;case "%M":min=parseInt(a[i],10);break;}}if(isNaN(y))y=today.getFullYear();if(isNaN(m))m=today.getMonth();if(isNaN(d))d=today.getDate();if(isNaN(hr))hr=today.getHours();if(isNaN(min))min=today.getMinutes();if(y!=0&&m!=-1&&d!=0)return new Date(y,m,d,hr,min,0);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)y=today.getFullYear();if(m!=-1&&d!=0)return new Date(y,m,d,hr,min,0);return today;};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.setDateOnly=function(date){var tmp=new Date(date);this.setDate(1);this.setFullYear(tmp.getFullYear());this.setMonth(tmp.getMonth());this.setDate(tmp.getDate());};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&&!Calendar.is_khtml)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._dynarch_popupCalendar=null; \ No newline at end of file
diff --git a/httemplate/elements/checkboxes-table-name.html b/httemplate/elements/checkboxes-table-name.html
new file mode 100644
index 000000000..0a92e4548
--- /dev/null
+++ b/httemplate/elements/checkboxes-table-name.html
@@ -0,0 +1,85 @@
+%
+%
+% ##
+% # required
+% ##
+% # 'link_table' => 'table_name',
+% #
+% # 'name_col' => 'name_column',
+% # #or
+% # 'name_callback' => sub { },
+% #
+% # 'names_list' => [ 'value', 'other value' ],
+% #
+% ##
+% # recommended (required?)
+% ##
+% # 'source_obj' => $obj,
+% # #or?
+% # #'source_table' => 'table_name',
+% # #'sourcenum' => '4', #current value of primary key in source_table
+% # # # (none is okay, just pass it if you have it)
+% ##
+% # optional
+% ##
+% # 'num_col' => 'col_name' #if column name is different in link_table than
+% # #source_table
+% # 'link_static' => { 'column' => 'value' },
+%
+% my( %opt ) = @_;
+%
+% my( $source_pkey, $sourcenum, $source_obj );
+% if ( $opt{'source_obj'} ) {
+%
+% $source_obj = $opt{'source_obj'};
+% #$source_table = $source_obj->dbdef_table->table;
+% $source_pkey = $source_obj->dbdef_table->primary_key;
+% $sourcenum = $source_obj->$source_pkey();
+%
+% } else {
+%
+% #$source_obj?
+% $source_pkey = $opt{'source_table'}
+% ? dbdef->table($opt{'source_table'})->primary_key
+% : '';
+% $sourcenum = $opt{'sourcenum'};
+% }
+%
+% $source_pkey = $opt{'num_col'} || $source_pkey;
+%
+% my $link_static = $opt{'link_static'} || {};
+%
+%
+% foreach my $name ( @{ $opt{'names_list'} } ) {
+%
+% my $checked;
+% if ( $cgi->param('error') ) {
+%
+% $checked = $cgi->param($opt{'link_table'}. ".$name" )
+% ? 'CHECKED'
+% : '';
+%
+% } else {
+%
+% $checked =
+% qsearchs( $opt{'link_table'}, {
+% $source_pkey => $sourcenum,
+% $opt{'name_col'} => $name,
+% %$link_static,
+% } )
+% ? 'CHECKED'
+% : ''
+%
+% }
+%
+%
+
+
+ <INPUT TYPE="checkbox" NAME="<% $opt{'link_table'}. ".$name" %>" <% $checked %> VALUE="ON">
+
+ <% $name %>
+
+ <BR>
+% }
+
+
diff --git a/httemplate/elements/checkboxes-table.html b/httemplate/elements/checkboxes-table.html
new file mode 100644
index 000000000..cdfa58eca
--- /dev/null
+++ b/httemplate/elements/checkboxes-table.html
@@ -0,0 +1,123 @@
+%
+%
+% ##
+% # required
+% ##
+% # 'target_table' => 'table_name',
+% # 'link_table' => 'table_name',
+% #
+% # 'name_col' => 'name_column',
+% # #or
+% # 'name_callback' => sub { },
+% #
+% ##
+% # recommended (required?)
+% ##
+% # 'source_obj' => $obj,
+% # #or?
+% # #'source_table' => 'table_name',
+% # #'sourcenum' => '4', #current value of primary key in source_table
+% # # # (none is okay, just pass it if you have it)
+% ##
+% # optional
+% ##
+% # 'disable-able' => 1,
+%
+% my( %opt ) = @_;
+%
+% my $target_pkey = dbdef->table($opt{'target_table'})->primary_key;
+%
+% my( $source_pkey, $sourcenum, $source_obj );
+% if ( $opt{'source_obj'} ) {
+%
+% $source_obj = $opt{'source_obj'};
+% #$source_table = $source_obj->dbdef_table->table;
+% $source_pkey = $source_obj->dbdef_table->primary_key;
+% $sourcenum = $source_obj->$source_pkey();
+%
+% } else {
+%
+% #$source_obj?
+% $source_pkey = $opt{'source_table'}
+% ? dbdef->table($opt{'source_table'})->primary_key
+% : '';
+% $sourcenum = $opt{'sourcenum'};
+% }
+%
+% my $hashref = $opt{'hashref'} || {};
+%
+% my $extra_sql = '';
+%
+% if ( $opt{'disable-able'} ) {
+% $hashref->{'disabled'} = '';
+%
+% $extra_sql .= ( $sourcenum && $source_pkey )
+% ? "OR $source_pkey = $sourcenum"
+% : '';
+% }
+%
+%
+% foreach my $target_obj (
+% qsearch({ 'table' => $opt{'target_table'},
+% 'hashref' => $hashref,
+% 'select' => $opt{'target_table'}. '.*',
+% 'addl_from' => "LEFT JOIN $opt{'link_table'} USING ( $target_pkey )",
+% 'extra_sql' => $extra_sql,
+% })
+% ) {
+%
+% my $targetnum = $target_obj->$target_pkey();
+%
+% my $checked;
+% if ( $cgi->param('error') ) {
+%
+% $checked = $cgi->param($target_pkey.$targetnum)
+% ? 'CHECKED'
+% : '';
+%
+% } else {
+%
+% $checked = qsearchs( $opt{'link_table'}, {
+% $source_pkey => $sourcenum,
+% $target_pkey => $targetnum,
+% } )
+% ? 'CHECKED'
+% : ''
+%
+% }
+%
+%
+
+
+ <INPUT TYPE="checkbox" NAME="<% $target_pkey. $targetnum %>" <% $checked %> VALUE="ON">
+% if ( $opt{'target_link'} ) {
+
+
+ <A HREF="<% $opt{'target_link'} %><% $targetnum %>">
+%
+%
+% }
+%
+<% $targetnum %>:
+% if ( $opt{'name_callback'} ) {
+
+
+ <% &{ $opt{'name_callback'} }( $target_obj ) %><% $opt{'target_link'} ? '</A>' : '' %>
+% } else {
+% my $name_col = $opt{'name_col'};
+%
+
+
+ <% $target_obj->$name_col() %><% $opt{'target_link'} ? '</A>' : '' %>
+% }
+% if ( $opt{'disable-able'} ) {
+
+
+ <% $target_obj->disabled =~ /^Y/i ? ' (DISABLED)' : '' %>
+% }
+
+
+ <BR>
+% }
+
+
diff --git a/httemplate/elements/cssexpr.js b/httemplate/elements/cssexpr.js
new file mode 100644
index 000000000..c434d8da0
--- /dev/null
+++ b/httemplate/elements/cssexpr.js
@@ -0,0 +1,66 @@
+function constExpression(x) {
+ return x;
+}
+
+function simplifyCSSExpression() {
+ try {
+ var ss,sl, rs, rl;
+ ss = document.styleSheets;
+ sl = ss.length
+
+ for (var i = 0; i < sl; i++) {
+ simplifyCSSBlock(ss[i]);
+ }
+ }
+ catch (exc) {
+ //alert("Got an error while processing css. The page should still work but might be a bit slower");
+ throw exc;
+ }
+}
+
+function simplifyCSSBlock(ss) {
+ var rs, rl;
+
+ for (var i = 0; i < ss.imports.length; i++)
+ simplifyCSSBlock(ss.imports[i]);
+
+ if (ss.cssText.indexOf("expression(constExpression(") == -1)
+ return;
+
+ rs = ss.rules;
+ rl = rs.length;
+ for (var j = 0; j < rl; j++)
+ simplifyCSSRule(rs[j]);
+
+}
+
+function simplifyCSSRule(r) {
+ var str = r.style.cssText;
+ var str2 = str;
+ var lastStr;
+ do {
+ lastStr = str2;
+ str2 = simplifyCSSRuleHelper(lastStr);
+ } while (str2 != lastStr)
+
+ if (str2 != str)
+ r.style.cssText = str2;
+}
+
+function simplifyCSSRuleHelper(str) {
+ var i, i2;
+ i = str.indexOf("expression(constExpression(");
+ if (i == -1) return str;
+ i2 = str.indexOf("))", i);
+ var hd = str.substring(0, i);
+ var tl = str.substring(i2 + 2);
+ var exp = str.substring(i + 27, i2);
+ var val = eval(exp)
+ return hd + val + tl;
+}
+
+if (/msie/i.test(navigator.userAgent) && window.attachEvent != null) {
+ window.attachEvent("onload", function () {
+ simplifyCSSExpression();
+ });
+}
diff --git a/httemplate/elements/dashboard-toplist.html b/httemplate/elements/dashboard-toplist.html
new file mode 100644
index 000000000..7ee6f2d43
--- /dev/null
+++ b/httemplate/elements/dashboard-toplist.html
@@ -0,0 +1,109 @@
+% if ( $conf->exists('dashboard-toplist') ) {
+
+ <% include('/elements/table-grid.html') %>
+
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor = $bgcolor2;
+
+% foreach my $line ( $conf->config('dashboard-toplist') ) {
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+
+% if ( $line =~ /^\s*cust_main:\s*(\d+)\s*$/ ) { #customer line
+% my $custnum = $1;
+% my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+% if ( $cust_main ) {
+
+ <TR>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <A HREF="view/cust_main.cgi?<% $custnum %>"><% $cust_main->name %></A>
+ </TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
+ <FONT SIZE="-1"><A HREF="<% FS::TicketSystem->href_new_ticket($cust_main, join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list ) ) %>">(new ticket)</A></FONT>
+ </TD>
+
+% foreach my $priority ( @custom_priorities, '' ) {
+% my $num =
+% FS::TicketSystem->num_customer_tickets($custnum,$priority);
+% my $ahref = '';
+% $ahref= '<A HREF="'.
+% FS::TicketSystem->href_customer_tickets($custnum,$priority).
+% '">'
+% if $num;
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
+ <% $ahref.$num %></A>
+ </TD>
+% }
+ </TR>
+
+% } else {
+
+ <TR>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ Unknown customer number <% $custnum %>
+ </TD>
+ </TR>
+
+% }
+%
+% } elsif ( $line =~ /^\-\-+$/ ) { #divider
+%
+ <TR>
+ <TH CLASS="grid" COLSPAN="<% scalar(@custom_priorities) + 3 %>"></TH>
+ </TR>
+
+% next;
+%
+% } elsif ( $line =~ /^\s*$/ ) {
+
+ <TR>
+ <TD CLASS="grid" COLSPAN="<% scalar(@custom_priorities) + 3 %>" BGCOLOR="<% $bgcolor %>">&nbsp;</TD>
+ </TR>
+
+% } elsif ( $line =~ /^\S/ ) { #label line
+
+ <TR>
+ <TH CLASS="grid" BGCOLOR="#cccccc"><% $line %></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
+% foreach my $priority ( @custom_priorities, '' ) {
+ <TH CLASS="grid" BGCOLOR="#cccccc">
+ <% $priority || '<i>(none)</i>'%>
+ </TH>
+% }
+ </TR>
+
+% } else { #regular line
+
+ <TR>
+ <TD CLASS="grid" COLSPAN="<% scalar(@custom_priorities) + 3 %>" BGCOLOR="<% $bgcolor %>"><% $line %></TD>
+ </TR>
+
+% }
+
+%
+% }
+
+ </TABLE>
+ <BR>
+
+% }
+<%init>
+
+my $conf = new FS::Conf;
+
+#false laziness w/httemplate/search/cust_main.cgi... care if
+# custom_priority_field becomes anything but a local hack...
+my @custom_priorities = ();
+if ( $conf->config('ticket_system-custom_priority_field')
+ && @{[ $conf->config('ticket_system-custom_priority_field-values') ]} ) {
+ @custom_priorities =
+ $conf->config('ticket_system-custom_priority_field-values');
+}
+
+</%init>
diff --git a/httemplate/elements/error.html b/httemplate/elements/error.html
new file mode 100644
index 000000000..e8ba93010
--- /dev/null
+++ b/httemplate/elements/error.html
@@ -0,0 +1,4 @@
+% if ( $cgi->param('error') ) {
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
diff --git a/httemplate/elements/footer.html b/httemplate/elements/footer.html
new file mode 100644
index 000000000..32d121996
--- /dev/null
+++ b/httemplate/elements/footer.html
@@ -0,0 +1,5 @@
+ </TD>
+ </TR>
+ </TABLE>
+ </BODY>
+</HTML>
diff --git a/httemplate/elements/freeside.css b/httemplate/elements/freeside.css
new file mode 100644
index 000000000..6f7cadb2e
--- /dev/null
+++ b/httemplate/elements/freeside.css
@@ -0,0 +1,15 @@
+* {
+ font-family: Arial, Verdana, Helvetica, sans-serif;
+}
+
+A:link IMG, A:visited { border-style: none }
+/* A:focus {text-decoration: underline } */
+
+a:link, a:visited {
+ /* text-decoration: none; */
+ color: #000000;
+}
+/* a:hover { text-decoration: underline } */
+
+/* a:focus { background-color: #ccccee } */
+
diff --git a/httemplate/elements/header-popup.html b/httemplate/elements/header-popup.html
new file mode 100644
index 000000000..43d9bc3af
--- /dev/null
+++ b/httemplate/elements/header-popup.html
@@ -0,0 +1,23 @@
+%
+% my($title, $menubar) = ( shift, shift ); #$menubar is unused here though
+% my $etc = @_ ? shift : ''; #$etc is for things like onLoad= etc.
+% my $head = @_ ? shift : ''; #$head is for things that go in the <HEAD> section
+% my $conf = new FS::Conf;
+%
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<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>
+ <CENTER><% $title %></CENTER>
+ </FONT>
+ <BR><!--<BR>-->
diff --git a/httemplate/elements/header.html b/httemplate/elements/header.html
new file mode 100644
index 000000000..1f5674885
--- /dev/null
+++ b/httemplate/elements/header.html
@@ -0,0 +1,244 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<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">
+
+ <% include('menu.html', 'freeside_baseurl' => $fsurl,
+ 'position' => $menu_position,
+ )
+ %>
+
+ <SCRIPT TYPE="text/javascript">
+ function clearhint_search_cust (what) {
+ if ( what.value == '(cust #, name, company or phone)' )
+ what.value = '';
+ }
+
+ function clearhint_search_invoice (what) {
+ if ( what.value == '(inv #)' )
+ what.value = '';
+ }
+
+ function clearhint_search_svc (what) {
+ if ( what.value == '(user, user@domain or domain)' )
+ what.value = '';
+ }
+
+ function clearhint_search_ticket (what) {
+ if ( what.value == '(ticket # or subject string)' )
+ what.value = '';
+ }
+ </SCRIPT>
+
+ <% $head %>
+
+ </HEAD>
+ <BODY <% $menu_position eq 'left' ? qq( BACKGROUND="${fsurl}images/background-cheat.png" ) : ' BGCOLOR="#e8e8e8" ' %> <% $etc %> STYLE="margin-top:0; margin-bottom:0; margin-left:0; margin-right:0">
+ <table width="100%" CELLPADDING=0 CELLSPACING=0 STYLE="padding-left:0; padding-right:4">
+ <tr>
+ <td rowspan=2 BGCOLOR="#ffffff"><IMG BORDER=0 ALT="freeside" SRC="<%$fsurl%>images/small-logo.png"></td>
+ <td align=left rowspan=2 BGCOLOR="#ffffff"> <!-- valign="top" -->
+ <font size=6><% $conf->config('company_name') || 'ExampleCo' %></font>
+ </td>
+ <td align=right valign=top BGCOLOR="#ffffff"><FONT SIZE="-1">Logged in as <b><% getotaker %>&nbsp;</b><br></FONT><FONT SIZE="-2"><a href="<%$fsurl%>pref/pref.html">Preferences</a>&nbsp;<BR></FONT>
+ </td>
+ </tr>
+ <tr>
+ <td align=right valign=bottom BGCOLOR="#ffffff">
+
+ <table>
+ <tr>
+ <td align=right BGCOLOR="#ffffff">
+ <FONT SIZE="-2">
+ <A HREF="http://www.sisd.com/freeside">Freeside</A>&nbsp;v<% $FS::VERSION %><BR>
+ <A HREF="<% $conf->config('support-key') ? "http://www.sisd.com/mediawiki/index.php/Supported:Documentation" : "http://www.sisd.com/mediawiki/index.php/Freeside:1.7:Documentation" %>">Documentation</A><BR>
+ </FONT>
+ </td>
+% if ( $conf->config('ticket_system') eq 'RT_Internal' ) {
+% eval "use RT;";
+
+ <td bgcolor=#000000></td>
+ <td align=left>
+ <FONT SIZE="-2">
+ <A HREF="http://www.bestpractical.com/rt">RT<A>&nbsp;v<% $RT::VERSION %><BR>
+ <A HREF="http://wiki.bestpractical.com/">Documentation</A><BR>
+ </FONT>
+ </td>
+% }
+
+
+ </tr>
+ </table>
+
+ </td>
+ </tr>
+ </table>
+
+<style type="text/css">
+input.fsblackbutton {
+ background-color:#333333;
+ color: #ffffff;
+ border:1px solid;
+ border-top-color:#cccccc;
+ border-left-color:#cccccc;
+ border-right-color:#aaaaaa;
+ border-bottom-color:#aaaaaa;
+ font-weight:bold;
+ padding-left:12px;
+ padding-right:12px;
+ overflow:visible;
+ filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr='#ff333333',EndColorStr='#ff666666')
+}
+
+input.fsblackbuttonselected {
+ background-color:#7e0079;
+ color: #ffffff;
+ border:1px solid;
+ border-top-color:#cccccc;
+ border-left-color:#cccccc;
+ border-right-color:#aaaaaa;
+ border-bottom-color:#aaaaaa;
+ font-weight:bold;
+ padding-left:12px;
+ padding-right:12px;
+ overflow:visible;
+ filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr='#ff330033',EndColorStr='#ff7e0079')
+}
+</style>
+
+ <TABLE WIDTH="100%" CELLSPACING=0 CELLPADDING=0>
+ <TR>
+ <TD COLSPAN=5 WIDTH="100%" STYLE="padding:0"><IMG BORDER=0 ALT="" SRC="<%$fsurl%>images/black-gradient.png" HEIGHT="13" WIDTH="100%"></TD>
+ </TR>
+
+% if ( $menu_position eq 'top' ) {
+
+ <TR>
+
+ <TD COLSPAN="5" WIDTH="100%" STYLE="padding:0">
+ <SCRIPT TYPE="text/javascript">
+ document.write(myBar);
+ </SCRIPT>
+ </TD>
+
+ </TR>
+
+ <TR>
+ <TD COLSPAN="5" WIDTH="100%" HEIGHT="2px" STYLE="padding:0" BGCOLOR="#000000">
+ </TD>
+ </TR>
+
+ <TR>
+ <TD COLSPAN="5" WIDTH="100%" HEIGHT="4px" STYLE="padding:0" BGCOLOR="#000000">
+ </TD>
+ </TR>
+
+% }
+
+ <TR>
+
+ <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right">
+ <FORM ACTION="<%$fsurl%>edit/cust_main.cgi" METHOD="GET" STYLE="margin:0">
+ <INPUT TYPE="submit" VALUE="New customer" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="vertical-align:bottom">
+ </FORM>
+ </TD>
+
+ <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right">
+ <FORM ACTION="<%$fsurl%>search/cust_main.cgi" METHOD="GET" STYLE="margin:0">
+ <INPUT NAME="search_cust" TYPE="text" VALUE="(cust #, name, company or phone)" SIZE="28" onFocus="clearhint_search_cust(this);" onClick="clearhint_search_cust(this);" STYLE="vertical-align:bottom;text-align:right"><BR>
+ <A NOTYET="<%$fsurl%>search/cust_main.html" STYLE="color: #000000; font-size: 70%">Advanced</A>
+ <INPUT TYPE="submit" VALUE="Search customers" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:70%">
+ </FORM>
+ </TD>
+
+ <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right">
+% if ( $FS::CurrentUser::CurrentUser->access_right('View invoices') ) {
+
+ <FORM ACTION="<%$fsurl%>search/cust_bill.html" METHOD="GET" STYLE="margin:0;display:inline">
+ <INPUT NAME="invnum" TYPE="text" VALUE="(inv #)" SIZE="4" onFocus="clearhint_search_invoice(this);" onClick="clearhint_search_invoice(this);" STYLE="vertical-align:bottom;text-align:right;margin-bottom:1px">
+% if ( $FS::CurrentUser::CurrentUser->access_right('List invoices') ) {
+
+ <A HREF="<%$fsurl%>search/report_cust_bill.html" STYLE="color: #ffffff; font-size: 70%">Advanced</A>
+% }
+
+ <BR>
+ <INPUT TYPE="submit" VALUE="Search invoices" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:70%">
+ </FORM>
+% }
+
+ </TD>
+
+ <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right">
+ <FORM ACTION="<%$fsurl%>search/cust_svc.html" METHOD="GET" STYLE="margin:0">
+ <INPUT NAME="search_svc" TYPE="text" VALUE="(user, user@domain or domain)" SIZE="26" onFocus="clearhint_search_svc(this);" onClick="clearhint_search_svc(this);" STYLE="vertical-align:bottom;text-align:right"><BR>
+ <A NOTYET="<%$fsurl%>search/svc_Smarter.html" STYLE="color: #000000; font-size: 70%">Advanced</A>
+ <INPUT TYPE="submit" VALUE="Search services" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:70%">
+ </FORM>
+ </TD>
+
+ <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right" STYLE="padding-right:4px">
+ <FORM ACTION="<%$fsurl%>rt/index.html" METHOD="GET" STYLE="margin:0">
+ <INPUT NAME="q" TYPE="text" VALUE="(ticket # or subject string)" onFocus="clearhint_search_ticket(this);" onClick="clearhint_search_ticket(this);" STYLE="vertical-align:bottom;text-align:right"><BR>
+ <A HREF="<%$fsurl%>rt/Search/Build.html" STYLE="color: #ffffff; font-size: 70%">Advanced</A>
+ <INPUT TYPE="submit" VALUE="Search tickets" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:70%;padding-left:2px;padding-right:2px">
+ </FORM>
+ </TD>
+
+ </TR>
+ </TABLE>
+
+ <TABLE WIDTH="100%" HEIGHT="100%" CELLSPACING=0 CELLPADDING=4>
+
+ <TR>
+
+% if ( $menu_position eq 'left' ) {
+
+ <TD BGCOLOR="#000000" STYLE="padding:0" WIDTH="154"></TD>
+ <TD STYLE="padding:0" WIDTH="13"><IMG BORDER=0 ALT="" SRC="<%$fsurl%>images/black-gray-corner.png"></TD>
+
+% }
+
+ <TD STYLE="padding:0" WIDTH="100%"><IMG BORDER=0 ALT="" SRC="<%$fsurl%>images/black-gray-top.png" HEIGHT="13" WIDTH="100%"></TD>
+
+ </TR>
+
+ <TR HEIGHT="100%">
+
+% if ( $menu_position eq 'left' ) {
+
+ <TD BGCOLOR="#000000" ALIGN="left" HEIGHT="100%" WIDTH="154" VALIGN="top" ALIGN="right">
+ <SCRIPT TYPE="text/javascript">
+ document.write(myBar);
+ </SCRIPT>
+ <BR>
+ <IMG SRC="<%$fsurl%>images/32clear.gif" HEIGHT="1" WIDTH="154">
+
+ </TD>
+ <TD STYLE="padding:0" HEIGHT="100%" WIDTH=13 VALIGN="top"><IMG WIDTH="13" HEIGHT="100%" BORDER=0 ALT="" SRC="<%$fsurl%>images/black-gray-side.png"></TD>
+
+% }
+
+ <TD BGCOLOR="#e8e8e8" HEIGHT="100%" VALIGN="top"> <!-- WIDTH="100%"> -->
+
+ <FONT SIZE=6>
+ <% $title %>
+ </FONT>
+
+ <BR><BR>
+ <% $menubar !~ /^\s*$/ ? "$menubar<BR><BR>" : '' %>
+<%init>
+
+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
+my $conf = new FS::Conf;
+
+my $menu_position = $FS::CurrentUser::CurrentUser->option('menu_position')
+ || 'left';
+
+</%init>
diff --git a/httemplate/elements/iframecontentmws.js b/httemplate/elements/iframecontentmws.js
new file mode 100644
index 000000000..c80998957
--- /dev/null
+++ b/httemplate/elements/iframecontentmws.js
@@ -0,0 +1,20 @@
+/*
+ iframecontentmws.js - Foteos Macrides
+ Initial: October 10, 2004 - Last Revised: May 9, 2005
+ Simple script for using an HTML file as iframe content in overlibmws popups.
+ Include WRAP and TEXTPADDING,0 in the overlib call to ensure that the width
+ arg is respected (unless the CAPTION plus CLOSETEXT widths add up to more than
+ the width arg, in which case you should increase the width arg). The name arg
+ should be a unique string for each popup with iframe content in the document.
+ The frameborder arg should be 1 (browser default if omitted) or 0.
+
+ See http://www.macridesweb.com/oltest/IFRAME.html for demonstration.
+*/
+
+function OLiframeContent(src, width, height, name, frameborder) {
+ return ('<iframe src="'+src+'" width="'+width+'" height="'+height+'"'
+ +(name!=null?' name="'+name+'" id="'+name+'"':'')
+ +(frameborder!=null?' frameborder="'+frameborder+'"':'')
+ +' scrolling="auto">'
+ +'<div>[iframe not supported]</div></iframe>');
+}
diff --git a/httemplate/elements/jsrsClient.js b/httemplate/elements/jsrsClient.js
new file mode 100644
index 000000000..3a2572ccb
--- /dev/null
+++ b/httemplate/elements/jsrsClient.js
@@ -0,0 +1,356 @@
+//
+// jsrsClient.js - javascript remote scripting client include
+//
+// Author: Brent Ashley [jsrs@megahuge.com]
+//
+// make asynchronous remote calls to server without client page refresh
+//
+// see license.txt for copyright and license information
+
+/*
+see history.txt for full history
+2.0 26 Jul 2001 - added POST capability for IE/MOZ
+2.2 10 Aug 2003 - added Opera support
+2.3(beta) 10 Oct 2003 - added Konqueror support - **needs more testing**
+*/
+
+// callback pool needs global scope
+var jsrsContextPoolSize = 0;
+var jsrsContextMaxPool = 10;
+var jsrsContextPool = new Array();
+var jsrsBrowser = jsrsBrowserSniff();
+var jsrsPOST = true;
+var containerName;
+
+// constructor for context object
+function jsrsContextObj( contextID ){
+
+ // properties
+ this.id = contextID;
+ this.busy = true;
+ this.callback = null;
+ this.container = contextCreateContainer( contextID );
+
+ // methods
+ this.GET = contextGET;
+ this.POST = contextPOST;
+ this.getPayload = contextGetPayload;
+ this.setVisibility = contextSetVisibility;
+}
+
+// method functions are not privately scoped
+// because Netscape's debugger chokes on private functions
+function contextCreateContainer( containerName ){
+ // creates hidden container to receive server data
+ var container;
+ switch( jsrsBrowser ) {
+ case 'NS':
+ container = new Layer(100);
+ container.name = containerName;
+ container.visibility = 'hidden';
+ container.clip.width = 100;
+ container.clip.height = 100;
+ break;
+
+ case 'IE':
+ document.body.insertAdjacentHTML( "afterBegin", '<span id="SPAN' + containerName + '"></span>' );
+ var span = document.all( "SPAN" + containerName );
+ var html = '<iframe name="' + containerName + '" src=""></iframe>';
+ span.innerHTML = html;
+ span.style.display = 'none';
+ container = window.frames[ containerName ];
+ break;
+
+ case 'MOZ':
+ var span = document.createElement('SPAN');
+ span.id = "SPAN" + containerName;
+ document.body.appendChild( span );
+ var iframe = document.createElement('IFRAME');
+ iframe.name = containerName;
+ iframe.id = containerName;
+ span.appendChild( iframe );
+ container = iframe;
+ break;
+
+ case 'OPR':
+ var span = document.createElement('SPAN');
+ span.id = "SPAN" + containerName;
+ document.body.appendChild( span );
+ var iframe = document.createElement('IFRAME');
+ iframe.name = containerName;
+ iframe.id = containerName;
+ span.appendChild( iframe );
+ container = iframe;
+ break;
+
+ case 'KONQ':
+ var span = document.createElement('SPAN');
+ span.id = "SPAN" + containerName;
+ document.body.appendChild( span );
+ var iframe = document.createElement('IFRAME');
+ iframe.name = containerName;
+ iframe.id = containerName;
+ span.appendChild( iframe );
+ container = iframe;
+
+ // Needs to be hidden for Konqueror, otherwise it'll appear on the page
+ span.style.display = none;
+ iframe.style.display = none;
+ iframe.style.visibility = hidden;
+ iframe.height = 0;
+ iframe.width = 0;
+
+ break;
+ }
+ return container;
+}
+
+function contextPOST( rsPage, func, parms ){
+
+ var d = new Date();
+ var unique = d.getTime() + '' + Math.floor(1000 * Math.random());
+ var doc = (jsrsBrowser == "IE" ) ? this.container.document : this.container.contentDocument;
+ doc.open();
+ doc.write('<html><body>');
+ doc.write('<form name="jsrsForm" method="post" target="" ');
+ doc.write(' action="' + rsPage + '?U=' + unique + '">');
+ doc.write('<input type="hidden" name="C" value="' + this.id + '">');
+
+ // func and parms are optional
+ if (func != null){
+ doc.write('<input type="hidden" name="F" value="' + func + '">');
+
+ if (parms != null){
+ if (typeof(parms) == "string"){
+ // single parameter
+ doc.write( '<input type="hidden" name="P0" '
+ + 'value="[' + jsrsEscapeQQ(parms) + ']">');
+ } else {
+ // assume parms is array of strings
+ for( var i=0; i < parms.length; i++ ){
+ doc.write( '<input type="hidden" name="P' + i + '" '
+ + 'value="[' + jsrsEscapeQQ(parms[i]) + ']">');
+ }
+ } // parm type
+ } // parms
+ } // func
+
+ doc.write('</form></body></html>');
+ doc.close();
+ doc.forms['jsrsForm'].submit();
+}
+
+function contextGET( rsPage, func, parms ){
+
+ // build URL to call
+ var URL = rsPage;
+
+ // always send context
+ URL += "?C=" + this.id;
+
+ // func and parms are optional
+ if (func != null){
+ URL += "&F=" + escape(func);
+
+ if (parms != null){
+ if (typeof(parms) == "string"){
+ // single parameter
+ URL += "&P0=[" + escape(parms+'') + "]";
+ } else {
+ // assume parms is array of strings
+ for( var i=0; i < parms.length; i++ ){
+ URL += "&P" + i + "=[" + escape(parms[i]+'') + "]";
+ }
+ } // parm type
+ } // parms
+ } // func
+
+ // unique string to defeat cache
+ var d = new Date();
+ URL += "&U=" + d.getTime();
+
+ // make the call
+ switch( jsrsBrowser ) {
+ case 'NS':
+ this.container.src = URL;
+ break;
+ case 'IE':
+ this.container.document.location.replace(URL);
+ break;
+ case 'MOZ':
+ this.container.src = '';
+ this.container.src = URL;
+ break;
+ case 'OPR':
+ this.container.src = '';
+ this.container.src = URL;
+ break;
+ case 'KONQ':
+ this.container.src = '';
+ this.container.src = URL;
+ break;
+ }
+}
+
+function contextGetPayload(){
+ switch( jsrsBrowser ) {
+ case 'NS':
+ return this.container.document.forms['jsrs_Form'].elements['jsrs_Payload'].value;
+ case 'IE':
+ return this.container.document.forms['jsrs_Form']['jsrs_Payload'].value;
+ case 'MOZ':
+ return window.frames[this.container.name].document.forms['jsrs_Form']['jsrs_Payload'].value;
+ case 'OPR':
+ var textElement = window.frames[this.container.name].document.getElementById("jsrs_Payload");
+ case 'KONQ':
+ var textElement = window.frames[this.container.name].document.getElementById("jsrs_Payload");
+ return textElement.value;
+ }
+}
+
+function contextSetVisibility( vis ){
+ switch( jsrsBrowser ) {
+ case 'NS':
+ this.container.visibility = (vis)? 'show' : 'hidden';
+ break;
+ case 'IE':
+ document.all("SPAN" + this.id ).style.display = (vis)? '' : 'none';
+ break;
+ case 'MOZ':
+ document.getElementById("SPAN" + this.id).style.visibility = (vis)? '' : 'hidden';
+ case 'OPR':
+ document.getElementById("SPAN" + this.id).style.visibility = (vis)? '' : 'hidden';
+ this.container.width = (vis)? 250 : 0;
+ this.container.height = (vis)? 100 : 0;
+ break;
+ }
+}
+
+// end of context constructor
+
+function jsrsGetContextID(){
+ var contextObj;
+ for (var i = 1; i <= jsrsContextPoolSize; i++){
+ contextObj = jsrsContextPool[ 'jsrs' + i ];
+ if ( !contextObj.busy ){
+ contextObj.busy = true;
+ return contextObj.id;
+ }
+ }
+ // if we got here, there are no existing free contexts
+ if ( jsrsContextPoolSize <= jsrsContextMaxPool ){
+ // create new context
+ var contextID = "jsrs" + (jsrsContextPoolSize + 1);
+ jsrsContextPool[ contextID ] = new jsrsContextObj( contextID );
+ jsrsContextPoolSize++;
+ return contextID;
+ } else {
+ alert( "jsrs Error: context pool full" );
+ return null;
+ }
+}
+
+function jsrsExecute( rspage, callback, func, parms, visibility ){
+ // call a server routine from client code
+ //
+ // rspage - href to asp file
+ // callback - function to call on return
+ // or null if no return needed
+ // (passes returned string to callback)
+ // func - sub or function name to call
+ // parm - string parameter to function
+ // or array of string parameters if more than one
+ // visibility - optional boolean to make container visible for debugging
+
+ // get context
+ var contextObj = jsrsContextPool[ jsrsGetContextID() ];
+ contextObj.callback = callback;
+
+ var vis = (visibility == null)? false : visibility;
+ contextObj.setVisibility( vis );
+
+ if ( jsrsPOST && ((jsrsBrowser == 'IE') || (jsrsBrowser == 'MOZ'))){
+ contextObj.POST( rspage, func, parms );
+ } else {
+ contextObj.GET( rspage, func, parms );
+ }
+
+ return contextObj.id;
+}
+
+function jsrsLoaded( contextID ){
+ // get context object and invoke callback
+ var contextObj = jsrsContextPool[ contextID ];
+ if( contextObj.callback != null){
+ contextObj.callback( jsrsUnescape( contextObj.getPayload() ), contextID );
+ }
+ // clean up and return context to pool
+ contextObj.callback = null;
+ contextObj.busy = false;
+}
+
+function jsrsError( contextID, str ){
+ alert( unescape(str) );
+ jsrsContextPool[ contextID ].busy = false
+}
+
+function jsrsEscapeQQ( thing ){
+ return thing.replace(/'"'/g, '\\"');
+}
+
+function jsrsUnescape( str ){
+ // payload has slashes escaped with whacks
+ return str.replace( /\\\//g, "/" );
+}
+
+function jsrsBrowserSniff(){
+ if (document.layers) return "NS";
+ if (document.all) {
+ // But is it really IE?
+ // convert all characters to lowercase to simplify testing
+ var agt=navigator.userAgent.toLowerCase();
+ var is_opera = (agt.indexOf("opera") != -1);
+ var is_konq = (agt.indexOf("konqueror") != -1);
+ if(is_opera) {
+ return "OPR";
+ } else {
+ if(is_konq) {
+ return "KONQ";
+ } else {
+ // Really is IE
+ return "IE";
+ }
+ }
+ }
+ if (document.getElementById) return "MOZ";
+ return "OTHER";
+}
+
+/////////////////////////////////////////////////
+//
+// user functions
+
+function jsrsArrayFromString( s, delim ){
+ // rebuild an array returned from server as string
+ // optional delimiter defaults to ~
+ var d = (delim == null)? '~' : delim;
+ return s.split(d);
+}
+
+function jsrsDebugInfo(){
+ // use for debugging by attaching to f1 (works with IE)
+ // with onHelp = "return jsrsDebugInfo();" in the body tag
+ var doc = window.open().document;
+ doc.open;
+ doc.write( 'Pool Size: ' + jsrsContextPoolSize + '<br><font face="arial" size="2"><b>' );
+ for( var i in jsrsContextPool ){
+ var contextObj = jsrsContextPool[i];
+ doc.write( '<hr>' + contextObj.id + ' : ' + (contextObj.busy ? 'busy' : 'available') + '<br>');
+ doc.write( contextObj.container.document.location.pathname + '<br>');
+ doc.write( contextObj.container.document.location.search + '<br>');
+ doc.write( '<table border="1"><tr><td>' + contextObj.container.document.body.innerHTML + '</td></tr></table>' );
+ }
+ doc.write('</table>');
+ doc.close();
+ return false;
+}
diff --git a/httemplate/elements/jsrsServer.html b/httemplate/elements/jsrsServer.html
new file mode 100644
index 000000000..f37b0aaee
--- /dev/null
+++ b/httemplate/elements/jsrsServer.html
@@ -0,0 +1,4 @@
+%
+% my $server = new FS::UI::Web::JSRPC '', $cgi;
+%
+<% $server->process %>
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
new file mode 100644
index 000000000..9565ff2d0
--- /dev/null
+++ b/httemplate/elements/menu.html
@@ -0,0 +1,353 @@
+<script type="text/javascript" src="<%$fsurl%>elements/cssexpr.js"></script>
+
+% if ( $opt{'position'} eq 'top' ) {
+
+ <script type="text/javascript" src="<%$fsurl%>elements/xmenu.top.js"></script>
+ <link href="<%$fsurl%>elements/xmenu.top.css" type="text/css" rel="stylesheet">
+
+% } else { # elsif ( $opt{'position'} eq 'left' ) {
+
+ <script type="text/javascript" src="<%$fsurl%>elements/xmenu.js"></script>
+ <link href="<%$fsurl%>elements/xmenu.css" type="text/css" rel="stylesheet">
+
+% }
+
+<link href="<%$fsurl%>elements/freeside.css" type="text/css" rel="stylesheet">
+
+<SCRIPT TYPE="text/javascript">
+
+ webfxMenuImagePath = "<%$fsurl%>images/";
+ webfxMenuUseHover = 1;
+ webfxMenuShowTime = 300;
+ webfxMenuHideTime = 500;
+
+ var myBar = new WebFXMenuBar;
+
+% foreach my $item ( keys %menu ) {
+%
+% my( $url_or_submenu, $tooltip ) = @{ $menu{$item} };
+%
+% if ( ref($url_or_submenu) ) {
+%
+% #warn $item;
+%
+% my( $subhtml, $submenuname ) = submenu($url_or_submenu, $item);
+
+ <% $subhtml %>
+ myBar.add(new WebFXMenuButton("<% $item %>", null, "<% $tooltip %>", <% $submenuname %> ));
+
+% } else {
+
+ myBar.add(new WebFXMenuButton("<% $item %>", "<% $url_or_submenu %>", "<% $tooltip %>" ));
+
+% }
+%
+% }
+
+ myBar.show( null, 'vertical' );
+ myBar.width = 154;
+
+</SCRIPT>
+
+<%init>
+my( %opt ) = @_;
+my $conf = new FS::Conf;
+my $fsurl = $opt{'freeside_baseurl'};
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+#Active tickets not assigned to a customer
+
+tie my %report_customers_lists, 'Tie::IxHash',
+ 'by customer number' => [ $fsurl. 'search/cust_main.cgi?browse=custnum', '' ],
+ 'by last name' => [ $fsurl. 'search/cust_main.cgi?browse=last', '' ],
+ 'by company name' => [ $fsurl. 'search/cust_main.cgi?browse=company', '' ],
+;
+$report_customers_lists{'by active trouble tickets'} = [ $fsurl. 'search/cust_main.cgi?browse=tickets', '' ]
+ if $conf->config('ticket_system');
+
+tie my %report_customers_search, 'Tie::IxHash';
+$report_customers_search{'by ordering employee'} = [ $fsurl. 'search/cust_main-otaker.cgi' ]
+ if $curuser->access_right('Configuration');
+
+tie my %report_customers, 'Tie::IxHash',
+ 'List customers' => [ \%report_customers_lists, 'List customers' ],
+;
+$report_customers{'Search customers'} = [ \%report_customers_search, 'Search customers' ]
+ if keys %report_customers_search;
+$report_customers{'Zip code distribution'} = [ $fsurl.'search/report_cust_main-zip.html', 'Zip codes by number of customers' ];
+
+tie my %report_invoices_open, 'Tie::IxHash',
+ 'All open invoices' => [ $fsurl.'search/cust_bill.html?OPEN_date', 'All invoices with an unpaid balance' ],
+ '15 day open invoices' => [ $fsurl.'search/cust_bill.html?OPEN15_date', 'Invoices 15 days or older with an unpaid balance' ],
+ '30 day open invoices' => [ $fsurl.'search/cust_bill.html?OPEN30_date', 'Invoices 30 days or older with an unpaid balance' ],
+ '60 day open invoices' => [ $fsurl.'search/cust_bill.html?OPEN60_date', 'Invoices 60 days or older with an unpaid balance' ],
+ '90 day open invoices' => [ $fsurl.'search/cust_bill.html?OPEN90_date', 'Invoices 90 days or older with an unpaid balance' ],
+ '120 day open invoices' => [ $fsurl.'search/cust_bill.html?OPEN120_date', 'Invoices 120 days or older with an unpaid balance' ],
+;
+
+tie my %report_invoices, 'Tie::IxHash',
+ 'Open invoices' => [ \%report_invoices_open, 'Open invoices' ],
+ 'All invoices' => [ $fsurl. 'search/cust_bill.html?date', 'List all invoices' ],
+ 'Advanced invoice reports' => [ $fsurl.'search/report_cust_bill.html', 'by agent, date range, etc.' ],
+;
+
+tie my %report_services, 'Tie::IxHash';
+if ( $curuser->access_right('Configuration') ) {
+ $report_services{'Service definitions'} = [ $fsurl.'browse/part_svc.cgi?orderby=active', 'Service definitions by number of active packages' ];
+ $report_services{'separator'} = '';
+}
+foreach my $svcdb ( FS::part_svc->svc_tables() ) {
+
+ my $name = "FS::$svcdb"->table_info->{'name_plural'}
+ || PL( "FS::$svcdb"->table_info->{'name'} );
+ my $lcname = lc($name);
+ my $longname = "FS::$svcdb"->table_info->{'longname_plural'} || $name;
+ my $lclongname = lc($longname);
+ my $sorts = "FS::$svcdb"->table_info->{'sorts'} || [ 'svcnum' ];
+ $sorts = [ $sorts ] unless ref($sorts);
+ my %svc_url = ( 'm' => $m,
+ 'action' => 'search',
+ 'svcdb' => $svcdb,
+ );
+
+ tie my %report_svc, 'Tie::IxHash';
+
+ foreach my $sort ( @$sorts ) {
+
+ my $title = "All $lcname";
+ $title .= " by ". FS::part_svc->svc_table_fields($svcdb)->{$sort}->{'label'}
+ if scalar(@$sorts) > 1;
+
+ $report_svc{$title} =
+ [ FS::UI::Web::svc_url( %svc_url, 'query' => "magic=all;sortby=$sort" ),
+ '',
+ ];
+ }
+
+ if ( $curuser->access_right('View/link unlinked services') ) {
+ $report_svc{"Unlinked $lcname"} =
+ [ FS::UI::Web::svc_url( %svc_url, 'query' => "magic=unlinked;sortby=". $sorts->[0] ),
+ "Pre-Freeside $lcname without a customer record",
+ ];
+ }
+
+ $report_services{$name} = [ \%report_svc, $longname ];
+
+}
+
+tie my %report_packages, 'Tie::IxHash';
+if ( $curuser->access_right('Configuration') ) {
+ $report_packages{'Package definitions'} = [ $fsurl.'browse/part_pkg.cgi?active=1', 'Package definitions by number of active packages' ];
+ $report_packages{'separator'} = '';
+}
+$report_packages{'All customer packages'} = [ $fsurl.'search/cust_pkg.cgi?pkgnum', 'List all customer packages', ];
+$report_packages{'Suspended customer packages'} = [ $fsurl.'search/cust_pkg.cgi?magic=suspended', 'List suspended packages' ];
+$report_packages{'Customer packages with unconfigured services'} = [ $fsurl.'search/cust_pkg.cgi?APKG_pkgnum', 'List packages which have provisionable services' ];
+$report_packages{'Advanced package reports'} = [ $fsurl.'search/report_cust_pkg.html', 'by agent, date range, status, package definition' ];
+
+tie my %report_rating, 'Tie::IxHash',
+ 'RADIUS sessions' => [ $fsurl.'search/sqlradius.html', '' ],
+ 'Call Detail Records (CDRs)' => [ $fsurl.'search/report_cdr.html', '' ],
+;
+
+tie my %report_bill_event, 'Tie::IxHash',
+ 'All billing events' => [ $fsurl.'search/cust_bill_event.html', 'All billing events for a date range' ],
+ 'Invoice event errors' => [ $fsurl.'search/cust_bill_event.html?failed=1', 'failed credit cards, processor or printer problems, etc.' ],
+;
+
+tie my %report_financial, 'Tie::IxHash',
+ 'Sales, Credits and Receipts' => [ $fsurl.'graph/report_money_time.html', 'Sales, credits and receipts summary graph' ],
+ 'Sales Report' => [ $fsurl.'graph/report_cust_bill_pkg.html', 'Sales report and graph (by agent, package class and/or date range)' ],
+ 'Credit Report' => [ $fsurl.'search/report_cust_credit.html', 'Credit report (by employee and/or date range)' ],
+ 'Payment Report' => [ $fsurl.'search/report_cust_pay.html', 'Payment report (by type and/or date range)' ],
+;
+$report_financial{'Payment Batch Report'} = [ $fsurl.'search/pay_batch.html', 'Payment batches (by status and/or date range)' ]
+ if $conf->exists('batch-enable');
+$report_financial{'A/R Aging'} = [ $fsurl.'search/report_receivables.html', 'Accounts Receivable Aging report' ];
+$report_financial{'Prepaid Income'} = [ $fsurl.'search/report_prepaid_income.html', 'Prepaid income (unearned revenue) report' ];
+$report_financial{'Sales Tax Liability'} = [ $fsurl.'search/report_tax.html', 'Sales tax liability report' ];
+;
+
+tie my %report_menu, 'Tie::IxHash';
+$report_menu{'Customers'} = [ \%report_customers, 'Customer reports' ]
+ if $curuser->access_right('List customers');
+$report_menu{'Invoices'} = [ \%report_invoices, 'Invoice reports' ]
+ if $curuser->access_right('List invoices');
+$report_menu{'Packages'} = [ \%report_packages, 'Package reports' ]
+ if $curuser->access_right('List packages');
+$report_menu{'Services'} = [ \%report_services, 'Services reports' ]
+ if $curuser->access_right('List services');
+$report_menu{'Usage'} = [ \%report_rating, 'Usage reports' ]
+ if $curuser->access_right('List rating data');
+$report_menu{'Billing events'} = [ \%report_bill_event, 'Billing events' ]
+ if $curuser->access_right('Billing event reports');
+$report_menu{'Financial'} = [ \%report_financial, 'Financial reports' ]
+ if $curuser->access_right('Financial reports');
+
+tie my %tools_importing, 'Tie::IxHash',
+ 'Import customers from CSV file' => [ $fsurl.'misc/cust_main-import.cgi', '' ],
+ 'Import one-time charges from CSV file' => [ $fsurl.'misc/cust_main-import_charges.cgi', '' ],
+ 'Import Call Detail Records (CDRs) from CSV file' => [ $fsurl.'misc/cdr-import.html', '' ],
+;
+
+tie my %tools_exporting, 'Tie::IxHash',
+ 'Download database dump' => [ $fsurl. 'misc/dump.cgi', '' ],
+;
+
+# <!-- <BR>View active NAS ports:
+# <A HREF="browse/nas.cgi">session server</A> -->
+# <!-- or <A HREF="browse/nas-sqlradius.cgi">RADIUS</A>
+# <BR> -->
+
+tie my %tools_menu, 'Tie::IxHash', ();
+$tools_menu{'Quick payment entry'} = [ $fsurl.'misc/batch-cust_pay.html', 'Enter multiple payments in a batch' ]
+ if $curuser->access_right('Post payment batch');
+$tools_menu{'Process payment batches'} = [ $fsurl.'search/pay_batch.cgi?magic=_date;open=1;intransit=1', 'Process credit card and electronic check batches' ]
+ if $conf->exists('batch-enable') && $curuser->access_right('Process batches');
+$tools_menu{'Job Queue'} = [ $fsurl.'search/queue.html', 'View pending job queue' ]
+ if $curuser->access_right('Job queue');
+$tools_menu{'Importing'} = [ \%tools_importing, 'Import tools' ]
+ if $curuser->access_right('Import');
+$tools_menu{'Exporting'} = [ \%tools_exporting, 'Export tools' ]
+ if $curuser->access_right('Export');
+
+tie my %config_employees, 'Tie::IxHash',
+ 'View/Edit employees' => [ $fsurl.'browse/access_user.html', 'Setup internal users' ],
+ 'View/Edit employee groups' => [ $fsurl.'browse/access_group.html', 'Employee groups allow you to control access to the backend' ],
+;
+
+tie my %config_export_svc_pkg, 'Tie::IxHash',
+ 'View/Edit exports' => [ $fsurl.'browse/part_export.cgi', 'Provisioning services to external machines, databases and APIs' ],
+ 'View/Edit service definitions' => [ $fsurl.'browse/part_svc.cgi', 'Services are items you offer to your customers' ],
+ 'View/Edit package definitions' => [ $fsurl.'browse/part_pkg.cgi', 'One or more services are grouped together into a package and given pricing information. Customers purchase packages, not services' ],
+ 'View/Edit package classes' => [ $fsurl.'browse/pkg_class.html', 'Package classes define groups of packages, for reporting and convenience purposes.' ],
+ 'View/Edit cancel reason types' => [ $fsurl.'browse/reason_type.html?class=C', 'Cancel reason types define groups of reasons, for reporting and convenience purposes.' ],
+ 'View/Edit cancel reasons' => [ $fsurl.'browse/reason.html?class=C', 'Cancel reasons explain why a service was cancelled.' ],
+ 'View/Edit suspend reason types' => [ $fsurl.'browse/reason_type.html?class=S', 'Suspend reason types define groups of reasons, for reporting and convenience purposes.' ],
+ 'View/Edit suspend reasons' => [ $fsurl.'browse/reason.html?class=S', 'Suspend reasons explain why a service was suspended.' ],
+;
+
+tie my %config_agent, 'Tie::IxHash',
+ 'View/Edit agent types' => [ $fsurl.'browse/agent_type.cgi', 'Agent types define groups of package definitions that you can then assign to particular agents' ],
+ 'View/Edit agents' => [ $fsurl.'browse/agent.cgi', 'Agents are resellers of your service. Agents may be limited to a subset of your full offerings (via their type)' ],
+;
+
+tie my %config_billing, 'Tie::IxHash',
+ 'View/Edit payment gateways' => [ $fsurl.'browse/payment_gateway.html', 'Credit card and electronic check processors' ],
+ 'View/Edit invoice events' => [ $fsurl.'browse/part_bill_event.cgi', 'Actions for overdue invoices' ],
+ 'View/Edit prepaid cards' => [ $fsurl.'search/prepay_credit.html', 'View outstanding cards, generate new cards' ],
+ 'View/Edit call rates and regions' => [ $fsurl.'browse/rate.cgi', 'Manage rate plans, regions and prefixes for VoIP and call billing' ],
+ 'View/Edit locales and tax rates' => [ $fsurl.'browse/cust_main_county.cgi', 'Change tax rates, or break down a country into states, or a state into counties and assign different tax rates to each' ],
+;
+
+tie my %config_dialup, 'Tie::IxHash',
+ 'View/Edit access numbers' => [ $fsurl.'browse/svc_acct_pop.cgi', 'Points of Presence' ],
+;
+
+tie my %config_broadband, 'Tie::IxHash',
+ 'View/Edit routers' => [ $fsurl.'browse/router.cgi', 'Broadband access routers' ],
+ 'View/Edit address blocks' => [ $fsurl.'browse/addr_block.cgi', 'Manage address blocks and block assignments to broadband routers' ],
+;
+
+tie my %config_misc, 'Tie::IxHash';
+$config_misc{'View/Edit advertising sources'} = [ $fsurl.'browse/part_referral.html', 'Where a customer heard about your service. Tracked for informational purposes' ]
+ if $curuser->access_right('Configuration')
+ || $curuser->access_right('Edit advertising sources')
+ || $curuser->access_right('Edit global advertising sources');
+if ( $curuser->access_right('Configuration') ) {
+ $config_misc{'View/Edit virtual fields'} = [ $fsurl.'browse/part_virtual_field.cgi', 'Locally defined fields', ];
+ $config_misc{'View/Edit message catalog'} = [ $fsurl.'browse/msgcat.cgi', 'Change error messages and other customizable labels' ];
+ $config_misc{'View/Edit inventory classes and inventory'} = [ $fsurl.'browse/inventory_class.html', 'Setup inventory classes and stock inventory' ];
+}
+
+tie my %config_menu, 'Tie::IxHash';
+if ( $curuser->access_right('Configuration' ) ) {
+ %config_menu = (
+ 'Settings' => [ $fsurl.'config/config-view.cgi', '' ],
+ 'separator' => '', #its a separator!
+ 'Employees' => [ \%config_employees, '' ],
+ 'Provisioning, services and packages'
+ => [ \%config_export_svc_pkg, '' ],
+ 'Resellers' => [ \%config_agent, '' ],
+ 'Billing' => [ \%config_billing, '' ],
+ 'Dialup' => [ \%config_dialup, '' ],
+ 'Fixed (username-less) broadband'
+ => [ \%config_broadband, '' ],
+ );
+}
+$config_menu{'Miscellaneous'} = [ \%config_misc, '' ]
+ if $curuser->access_right('Configuration')
+ || $curuser->access_right('Edit advertising sources')
+ || $curuser->access_right('Edit global advertising sources');
+
+tie my %menu, 'Tie::IxHash',
+ 'Billing Main' => [ $fsurl, 'Billing start page', ],
+;
+if ( $conf->config('ticket_system') ) {
+ $menu{'Ticketing Main'} =
+ [
+ ( $conf->config('ticket_system') eq 'RT_External'
+ ? FS::TicketSystem->baseurl()
+ : $fsurl.'rt/'
+ ),
+ 'Ticketing start page',
+ ],
+}
+$menu{'Reports'} = [ \%report_menu, 'Lists, reporting and graphing' ]
+ if keys %report_menu;
+$menu{'Tools'} = [ \%tools_menu, 'Tools' ]
+ if keys %tools_menu;
+$menu{'Configuration'} = [ \%config_menu, 'Configuraiton and setup' ]
+ if $curuser->access_right('Configuration')
+ || $curuser->access_right('Edit advertising sources')
+ || $curuser->access_right('Edit global advertising sources');
+
+use vars qw($gmenunum);
+$gmenunum = 0;
+
+sub submenu {
+ my($submenu, $title) = @_;
+ my $menunum = $gmenunum++;
+
+ #return two args: html, menuname
+
+ "var myMenu$menunum = new WebFXMenu;\n".
+ #"myMenu$menunum.useAutoPosition = true;\n".
+ "myMenu$menunum.emptyText = '$title';\n".
+
+ (
+ join("\n", map {
+
+ if ( !ref( $submenu->{$_} ) ) {
+
+ "myMenu$menunum.add(new WebFXMenuSeparator());";
+
+ } else {
+
+ my($url_or_submenu, $tooltip ) = @{ $submenu->{$_} };
+ if ( ref($url_or_submenu) ) {
+
+ my($subhtml, $submenuname ) = submenu($url_or_submenu, $_); #mmm, recursion
+
+ "$subhtml\n".
+ "myMenu$menunum.add(new WebFXMenuItem(\"$_\", null, \"$tooltip\", $submenuname ));";
+
+ } else {
+
+ "myMenu$menunum.add(new WebFXMenuItem(\"$_\", \"$url_or_submenu\", \"$tooltip\" ));";
+
+ }
+
+ }
+
+ } keys %$submenu )
+ ). "\n".
+ "myMenu$menunum.width = 280;\n",
+
+ "myMenu$menunum";
+
+}
+
+</%init>
+
diff --git a/httemplate/elements/menubar.html b/httemplate/elements/menubar.html
new file mode 100644
index 000000000..ec6c13fea
--- /dev/null
+++ b/httemplate/elements/menubar.html
@@ -0,0 +1,10 @@
+%
+% my($item, $url, @html);
+% while (@_) {
+% ($item, $url) = splice(@_,0,2);
+% next if $item =~ /^\s*Main\s+Menu\s*$/i;
+% push @html, qq!<A HREF="$url">$item</A>!;
+% }
+%
+
+<% join(' | ', @html) %>
diff --git a/httemplate/elements/overlibmws.js b/httemplate/elements/overlibmws.js
new file mode 100644
index 000000000..5ef50d68f
--- /dev/null
+++ b/httemplate/elements/overlibmws.js
@@ -0,0 +1,697 @@
+/*
+ Do not remove or change this notice.
+ overlibmws.js core module - Copyright Foteos Macrides 2002-2005. All rights reserved.
+ Initial: August 18, 2002 - Last Revised: May 30, 2006
+ This module is subject to the same terms of usage as for Erik Bosrup's overLIB,
+ though only a minority of the code and API now correspond with Erik's version.
+ See the overlibmws Change History and Command Reference via:
+
+ http://www.macridesweb.com/oltest/
+
+ Published under an open source license: http://www.macridesweb.com/oltest/license.html
+ Give credit on sites that use overlibmws and submit changes so others can use them as well.
+ You can get Erik's version via: http://www.bosrup.com/web/overlib/
+*/
+
+// PRE-INIT -- Ignore these lines, configuration is below.
+var OLloaded=0,pmCnt=1,pMtr=new Array(),OLcmdLine=new Array(),OLrunTime=new Array(),OLv,OLudf,
+OLpct=new Array("83%","67%","83%","100%","117%","150%","200%","267%"),OLrefXY,
+OLbubblePI=0,OLcrossframePI=0,OLdebugPI=0,OLdraggablePI=0,OLexclusivePI=0,OLfilterPI=0,
+OLfunctionPI=0,OLhidePI=0,OLiframePI=0,OLovertwoPI=0,OLscrollPI=0,OLshadowPI=0,OLprintPI=0;
+if(typeof OLgateOK=='undefined')var OLgateOK=1;
+var OLp1or2c='inarray,caparray,caption,closetext,right,left,center,autostatuscap,padx,pady,'
++'below,above,vcenter,donothing',OLp1or2co='nofollow,background,offsetx,offsety,fgcolor,'
++'bgcolor,cgcolor,textcolor,capcolor,width,wrap,wrapmax,height,border,base,status,autostatus,'
++'snapx,snapy,fixx,fixy,relx,rely,midx,midy,ref,refc,refp,refx,refy,fgbackground,bgbackground,'
++'cgbackground,fullhtml,capicon,textfont,captionfont,textsize,captionsize,timeout,delay,hauto,'
++'vauto,nojustx,nojusty,fgclass,bgclass,cgclass,capbelow,textpadding,textfontclass,'
++'captionpadding,captionfontclass,sticky,noclose,mouseoff,offdelay,closecolor,closefont,'
++'closesize,closeclick,closetitle,closefontclass,decode',OLp1or2o='text,cap,close,hpos,vpos,'
++'padxl,padxr,padyt,padyb',OLp1co='label',OLp1or2=OLp1or2co+','+OLp1or2o,OLp1=OLp1co+','+'frame';
+OLregCmds(OLp1or2c+','+OLp1or2co+','+OLp1co);
+function OLud(v){return eval('typeof ol_'+v+'=="undefined"')?1:0;}
+
+// DEFAULT CONFIGURATION -- See overlibConfig.txt for descriptions
+if(OLud('fgcolor'))var ol_fgcolor="#ccccff";
+if(OLud('bgcolor'))var ol_bgcolor="#333399";
+if(OLud('cgcolor'))var ol_cgcolor="#333399";
+if(OLud('textcolor'))var ol_textcolor="#000000";
+if(OLud('capcolor'))var ol_capcolor="#ffffff";
+if(OLud('closecolor'))var ol_closecolor="#eeeeff";
+if(OLud('textfont'))var ol_textfont="Verdana,Arial,Helvetica";
+if(OLud('captionfont'))var ol_captionfont="Verdana,Arial,Helvetica";
+if(OLud('closefont'))var ol_closefont="Verdana,Arial,Helvetica";
+if(OLud('textsize'))var ol_textsize=1;
+if(OLud('captionsize'))var ol_captionsize=1;
+if(OLud('closesize'))var ol_closesize=1;
+if(OLud('fgclass'))var ol_fgclass="";
+if(OLud('bgclass'))var ol_bgclass="";
+if(OLud('cgclass'))var ol_cgclass="";
+if(OLud('textpadding'))var ol_textpadding=2;
+if(OLud('textfontclass'))var ol_textfontclass="";
+if(OLud('captionpadding'))var ol_captionpadding=2;
+if(OLud('captionfontclass'))var ol_captionfontclass="";
+if(OLud('closefontclass'))var ol_closefontclass="";
+if(OLud('close'))var ol_close="Close";
+if(OLud('closeclick'))var ol_closeclick=0;
+if(OLud('closetitle'))var ol_closetitle="Click to Close";
+if(OLud('text'))var ol_text="Default Text";
+if(OLud('cap'))var ol_cap="";
+if(OLud('capbelow'))var ol_capbelow=0;
+if(OLud('background'))var ol_background="";
+if(OLud('width'))var ol_width=200;
+if(OLud('wrap'))var ol_wrap=0;
+if(OLud('wrapmax'))var ol_wrapmax=0;
+if(OLud('height'))var ol_height= -1;
+if(OLud('border'))var ol_border=1;
+if(OLud('base'))var ol_base=0;
+if(OLud('offsetx'))var ol_offsetx=10;
+if(OLud('offsety'))var ol_offsety=10;
+if(OLud('sticky'))var ol_sticky=0;
+if(OLud('nofollow'))var ol_nofollow=0;
+if(OLud('noclose'))var ol_noclose=0;
+if(OLud('mouseoff'))var ol_mouseoff=0;
+if(OLud('offdelay'))var ol_offdelay=300;
+if(OLud('hpos'))var ol_hpos=RIGHT;
+if(OLud('vpos'))var ol_vpos=BELOW;
+if(OLud('status'))var ol_status="";
+if(OLud('autostatus'))var ol_autostatus=0;
+if(OLud('snapx'))var ol_snapx=0;
+if(OLud('snapy'))var ol_snapy=0;
+if(OLud('fixx'))var ol_fixx= -1;
+if(OLud('fixy'))var ol_fixy= -1;
+if(OLud('relx'))var ol_relx=null;
+if(OLud('rely'))var ol_rely=null;
+if(OLud('midx'))var ol_midx=null;
+if(OLud('midy'))var ol_midy=null;
+if(OLud('ref'))var ol_ref="";
+if(OLud('refc'))var ol_refc='UL';
+if(OLud('refp'))var ol_refp='UL';
+if(OLud('refx'))var ol_refx=0;
+if(OLud('refy'))var ol_refy=0;
+if(OLud('fgbackground'))var ol_fgbackground="";
+if(OLud('bgbackground'))var ol_bgbackground="";
+if(OLud('cgbackground'))var ol_cgbackground="";
+if(OLud('padxl'))var ol_padxl=1;
+if(OLud('padxr'))var ol_padxr=1;
+if(OLud('padyt'))var ol_padyt=1;
+if(OLud('padyb'))var ol_padyb=1;
+if(OLud('fullhtml'))var ol_fullhtml=0;
+if(OLud('capicon'))var ol_capicon="";
+if(OLud('frame'))var ol_frame=self;
+if(OLud('timeout'))var ol_timeout=0;
+if(OLud('delay'))var ol_delay=0;
+if(OLud('hauto'))var ol_hauto=0;
+if(OLud('vauto'))var ol_vauto=0;
+if(OLud('nojustx'))var ol_nojustx=0;
+if(OLud('nojusty'))var ol_nojusty=0;
+if(OLud('label'))var ol_label="";
+if(OLud('decode'))var ol_decode=0;
+// ARRAY CONFIGURATION - See overlibConfig.txt for descriptions.
+if(OLud('texts'))var ol_texts=new Array("Text 0","Text 1");
+if(OLud('caps'))var ol_caps=new Array("Caption 0","Caption 1");
+// END CONFIGURATION -- Don't change anything below, all configuration is above.
+
+// INIT -- Runtime variables.
+var o3_text="",o3_cap="",o3_sticky=0,o3_nofollow=0,o3_background="",o3_noclose=0,o3_mouseoff=0,
+o3_offdelay=300,o3_hpos=RIGHT,o3_offsetx=10,o3_offsety=10,o3_fgcolor="",o3_bgcolor="",
+o3_cgcolor="",o3_textcolor="",o3_capcolor="",o3_closecolor="",o3_width=200,o3_wrap=0,
+o3_wrapmax=0,o3_height= -1,o3_border=1,o3_base=0,o3_status="",o3_autostatus=0,o3_snapx=0,
+o3_snapy=0,o3_fixx= -1,o3_fixy= -1,o3_relx=null,o3_rely=null,o3_midx=null,o3_midy=null,o3_ref="",
+o3_refc='UL',o3_refp='UL',o3_refx=0,o3_refy=0,o3_fgbackground="",o3_bgbackground="",
+o3_cgbackground="",o3_padxl=0,o3_padxr=0,o3_padyt=0,o3_padyb=0,o3_fullhtml=0,o3_vpos=BELOW,
+o3_capicon="",o3_textfont="Verdana,Arial,Helvetica",o3_captionfont="",o3_closefont="",
+o3_textsize=1,o3_captionsize=1,o3_closesize=1,o3_frame=self,o3_timeout=0,o3_delay=0,o3_hauto=0,
+o3_vauto=0,o3_nojustx=0,o3_nojusty=0,o3_close="",o3_closeclick=0,o3_closetitle="",o3_fgclass="",
+o3_bgclass="",o3_cgclass="",o3_textpadding=2,o3_textfontclass="",o3_captionpadding=2,
+o3_captionfontclass="",o3_closefontclass="",o3_capbelow=0,o3_label="",o3_decode=0,
+CSSOFF=DONOTHING,CSSCLASS=DONOTHING,OLdelayid=0,OLtimerid=0,OLshowid=0,OLndt=0,over=null,
+OLfnRef="",OLhover=0,OLx=0,OLy=0,OLshowingsticky=0,OLallowmove=0,OLcC=null,
+OLua=navigator.userAgent.toLowerCase(),
+OLns4=(navigator.appName=='Netscape'&&parseInt(navigator.appVersion)==4),
+OLns6=(document.getElementById)?1:0,
+OLie4=(document.all)?1:0,
+OLgek=(OLv=OLua.match(/gecko\/(\d{8})/i))?parseInt(OLv[1]):0,
+OLmac=(OLua.indexOf('mac')>=0)?1:0,
+OLsaf=(OLua.indexOf('safari')>=0)?1:0,
+OLkon=(OLua.indexOf('konqueror')>=0)?1:0,
+OLkht=(OLsaf||OLkon)?1:0,
+OLopr=(OLua.indexOf('opera')>=0)?1:0,
+OLop7=(OLopr&&document.createTextNode)?1:0;
+if(OLopr){OLns4=OLns6=0;if(!OLop7)OLie4=0;}
+var OLieM=((OLie4&&OLmac)&&!(OLkht||OLopr))?1:0,
+OLie5=0,OLie55=0;if(OLie4&&!OLop7){
+if((OLv=OLua.match(/msie (\d\.\d+)\.*/i))&&(OLv=parseFloat(OLv[1]))>=5.0){
+OLie5=1;OLns6=0;if(OLv>=5.5)OLie55=1;}if(OLns6)OLie4=0;}
+if(OLns4)window.onresize=function(){location.reload();}
+var OLchkMh=1,OLdw;
+if(OLns4||OLie4||OLns6)OLmh();
+else{overlib=nd=cClick=OLpageDefaults=no_overlib;}
+
+/*
+ PUBLIC FUNCTIONS
+*/
+// Loads defaults then args into runtime variables.
+function overlib(){
+if(!(OLloaded&&OLgateOK))return;
+if((OLexclusivePI)&&OLisExclusive(arguments))return true;
+if(OLchkMh)OLmh();
+if(OLndt&&!OLtimerid)OLndt=0;if(over)cClick();
+OLload(OLp1or2);OLload(OLp1);
+OLfnRef="";OLhover=0;
+OLsetRunTimeVar();
+OLparseTokens('o3_',arguments);
+if(!(over=OLmkLyr()))return false;
+if(o3_decode)OLdecode();
+if(OLprintPI)OLchkPrint();
+if(OLbubblePI)OLchkForBubbleEffect();
+if(OLdebugPI)OLsetDebugCanShow();
+if(OLshadowPI)OLinitShadow();
+if(OLiframePI)OLinitIfs();
+if(OLfilterPI)OLinitFilterLyr();
+if(OLexclusivePI&&o3_exclusive&&o3_exclusivestatus!="")o3_status=o3_exclusivestatus;
+else if(o3_autostatus==2&&o3_cap!="")o3_status=o3_cap;
+else if(o3_autostatus==1&&o3_text!="")o3_status=o3_text;
+if(!o3_delay){return OLmain();
+}else{OLdelayid=setTimeout("OLmain()",o3_delay);
+if(o3_status!=""){self.status=o3_status;return true;}
+else if(!(OLop7&&event&&event.type=='mouseover'))return false;}
+}
+
+// Clears popups if appropriate
+function nd(time){
+if(OLloaded&&OLgateOK){if(!((OLexclusivePI)&&OLisExclusive())){
+if(time&&over&&!o3_delay){if(OLtimerid>0)clearTimeout(OLtimerid);
+OLtimerid=(OLhover&&o3_frame==self&&!OLcursorOff())?0:
+setTimeout("cClick()",(o3_timeout=OLndt=time));}else{
+if(!OLshowingsticky){OLallowmove=0;if(over)OLhideObject(over);}}}}
+return false;
+}
+
+// Close function for stickies
+function cClick(){
+if(OLloaded&&OLgateOK){OLhover=0;if(over){
+if(OLovertwoPI&&over==over2)cClick2();OLhideObject(over);OLshowingsticky=0;}}
+return false;
+}
+
+// Sets page-specific defaults.
+function OLpageDefaults(){
+OLparseTokens('ol_',arguments);
+}
+
+// For unsupported browsers.
+function no_overlib(){return false;}
+
+/*
+ OVERLIB MAIN FUNCTION SET
+*/
+function OLmain(){
+o3_delay=0;
+if(o3_frame==self){if(o3_noclose)OLoptMOUSEOFF(0);else if(o3_mouseoff)OLoptMOUSEOFF(1);}
+if(o3_sticky)OLshowingsticky=1;OLdoLyr();OLallowmove=0;if(o3_timeout>0){
+if(OLtimerid>0)clearTimeout(OLtimerid);OLtimerid=setTimeout("cClick()",o3_timeout);}
+if(o3_ref){OLrefXY=OLgetRefXY(o3_ref);if(OLrefXY[0]==null){o3_ref="";o3_midx=0;o3_midy=0;}}
+OLdisp(o3_status);if(OLdraggablePI)OLcheckDrag();
+if(o3_status!="")return true;else if(!(OLop7&&event&&event.type=='mouseover'))return false;
+}
+
+// Loads o3_ variables
+function OLload(c){var i,m=c.split(',');for(i=0;i<m.length;i++)eval('o3_'+m[i]+'=ol_'+m[i]);}
+
+// Chooses LGF
+function OLdoLGF(){
+return (o3_background!=''||o3_fullhtml)?OLcontentBackground(o3_text,o3_background,o3_fullhtml):
+(o3_cap=="")?OLcontentSimple(o3_text):
+(o3_sticky)?OLcontentCaption(o3_text,o3_cap,o3_close):OLcontentCaption(o3_text,o3_cap,'');
+}
+
+// Makes Layer
+function OLmkLyr(id,f,z){
+id=(id||'overDiv');f=(f||o3_frame);z=(z||1000);var fd=f.document,d=OLgetRefById(id,fd);
+if(!d){if(OLns4)d=fd.layers[id]=new Layer(1024,f);else if(OLie4&&!document.getElementById){
+fd.body.insertAdjacentHTML('BeforeEnd','<div id="'+id+'"></div>');d=fd.all[id];
+}else{d=fd.createElement('div');if(d){d.id=id;fd.body.appendChild(d);}}if(!d)return null;
+if(OLns4)d.zIndex=z;else{var o=d.style;o.position='absolute';o.visibility='hidden';o.zIndex=z;}}
+return d;
+}
+
+// Creates and writes layer content
+function OLdoLyr(){
+if(o3_background==''&&!o3_fullhtml){
+if(o3_fgbackground!='')o3_fgbackground=' background="'+o3_fgbackground+'"';
+if(o3_bgbackground!='')o3_bgbackground=' background="'+o3_bgbackground+'"';
+if(o3_cgbackground!='')o3_cgbackground=' background="'+o3_cgbackground+'"';
+if(o3_fgcolor!='')o3_fgcolor=' bgcolor="'+o3_fgcolor+'"';
+if(o3_bgcolor!='')o3_bgcolor=' bgcolor="'+o3_bgcolor+'"';
+if(o3_cgcolor!='')o3_cgcolor=' bgcolor="'+o3_cgcolor+'"';
+if(o3_height>0)o3_height=' height="'+o3_height+'"';else o3_height='';}
+if(!OLns4)OLrepositionTo(over,(OLns6?20:0),0);var lyrHtml=OLdoLGF();
+if(o3_sticky&&OLtimerid>0){clearTimeout(OLtimerid);OLtimerid=0;}
+if(o3_wrap&&!o3_fullhtml){OLlayerWrite(lyrHtml);
+o3_width=(OLns4?over.clip.width:over.offsetWidth);
+if(OLns4&&o3_wrapmax<1)o3_wrapmax=o3_frame.innerWidth-40;
+o3_wrap=0;if(o3_wrapmax>0&&o3_width>o3_wrapmax)o3_width=o3_wrapmax;lyrHtml=OLdoLGF();}
+OLlayerWrite(lyrHtml);o3_width=(OLns4?over.clip.width:over.offsetWidth);
+if(OLbubblePI)OLgenerateBubble(lyrHtml);
+}
+
+/*
+ LAYER GENERATION FUNCTIONS
+*/
+// Makes simple table without caption
+function OLcontentSimple(txt){
+var t=OLbgLGF()+OLfgLGF(txt)+OLbaseLGF();
+OLsetBackground('');return t;
+}
+
+// Makes table with caption and optional close link
+function OLcontentCaption(txt,title,close){
+var closing=(OLprintPI?OLprintCapLGF():''),closeevent='onmouseover',caption,t,
+cC='javascript:return '+OLfnRef+(OLovertwoPI&&over==over2?'cClick2();':'cClick();');
+if(o3_closeclick)closeevent=(o3_closetitle?'title="'+o3_closetitle+'" ':'')+'onclick';
+if(o3_capicon!='')o3_capicon='<img src="'+o3_capicon+'" /> ';
+if(close){closing+='<td align="right"><a href="'+cC+'" '
++closeevent+'="'+cC+'"'+(o3_closefontclass?' class="'+o3_closefontclass
++'">':'>'+OLlgfUtil(0,0,'','span',o3_closecolor,o3_closefont,o3_closesize))+close
++(o3_closefontclass?'':OLlgfUtil(1,0,'','span'))+'</a></td>';}
+caption='<table'+OLwd(0)+' border="0" cellpadding="'+o3_captionpadding+'" cellspacing="0"'
++(o3_cgclass?' class="'+o3_cgclass+'"':o3_cgcolor+o3_cgbackground)+'><tr><td'+OLwd(0)
++(o3_cgclass?' class="'+o3_cgclass+'">':'>')+(o3_captionfontclass?'<div class="'
++o3_captionfontclass+'">':OLlgfUtil(0,1,'','div',o3_capcolor,o3_captionfont,
+o3_captionsize))+o3_capicon+title+OLlgfUtil(1,1,'','div')+'</td>'+closing+'</tr></table>';
+t=OLbgLGF()+(o3_capbelow?OLfgLGF(txt)+caption:caption+OLfgLGF(txt))+OLbaseLGF();
+OLsetBackground('');return t;
+}
+
+// For BACKGROUND and FULLHTML commands
+function OLcontentBackground(txt, image, hasfullhtml){
+var t;if(hasfullhtml){t=txt;}else{t='<table'+OLwd(1)
++' border="0" cellpadding="0" cellspacing="0" '+'height="'+o3_height
++'"><tr><td colspan="3" height="'+o3_padyt+'"></td></tr><tr><td width="'
++o3_padxl+'"></td><td valign="top"'+OLwd(2)+'>'
++OLlgfUtil(0,0,o3_textfontclass,'div',o3_textcolor,o3_textfont,o3_textsize)+txt+
+OLlgfUtil(1,0,'','div')+'</td><td width="'+o3_padxr+'"></td></tr><tr><td colspan="3" height="'
++o3_padyb+'"></td></tr></table>';}
+OLsetBackground(image);return t;
+}
+
+// LGF utilities
+function OLbgLGF(){
+return '<table'+OLwd(1)+o3_height+' border="0" cellpadding="'+o3_border+'" cellspacing="0"'
++(o3_bgclass?' class="'+o3_bgclass+'"':o3_bgcolor+o3_bgbackground)+'><tr><td>';
+}
+function OLfgLGF(t){
+return '<table'+OLwd(0)+o3_height+' border="0" cellpadding="'+o3_textpadding
++'" cellspacing="0"'+(o3_fgclass?' class="'+o3_fgclass+'"':o3_fgcolor+o3_fgbackground)
++'><tr><td valign="top"'+(o3_fgclass?' class="'+o3_fgclass+'"':'')+'>'
++OLlgfUtil(0,0,o3_textfontclass,'div',o3_textcolor,o3_textfont,o3_textsize)+t
++(OLprintPI?OLprintFgLGF():'')+OLlgfUtil(1,0,'','div')+'</td></tr></table>';
+}
+function OLlgfUtil(end,stg,tfc,ele,col,fac,siz){
+if(end)return ('</'+(OLns4?'font'+(stg?'></strong':''):ele)+'>');else return (tfc?'<div '
++'class="' +tfc +'">':('<'+(OLns4?(stg?'strong><':'')+'font color="'+col+'" face="'
++OLquoteMultiNameFonts(fac)+'" size="'+siz:ele+' style="color:'+col
++(stg?';font-weight:bold':'')+';font-family:'+OLquoteMultiNameFonts(fac)+';font-size:'
++siz+';'+(ele=='span'?'text-decoration:underline;':''))+'">'));
+}
+function OLquoteMultiNameFonts(f){
+var i,v,pM=f.split(',');
+for(i=0;i<pM.length;i++){v=pM[i];v=v.replace(/^\s+/,'').replace(/\s+$/,'');
+if(/\s/.test(v) && !/['"]/.test(v)){v="\'"+v+"\'";pM[i]=v;}}
+return pM.join();
+}
+function OLbaseLGF(){
+return ((o3_base>0&&!o3_wrap)?('<table width="100%" border="0" cellpadding="0" cellspacing="0"'
++(o3_bgclass?' class="'+o3_bgclass+'"':'')+'><tr><td height="'+o3_base
++'"></td></tr></table>'):'')+'</td></tr></table>';
+}
+function OLwd(a){
+return(o3_wrap?'':' width="'+(!a?'100%':(a==1?o3_width:(o3_width-o3_padxl-o3_padxr)))+'"');
+}
+
+// Loads image into the div.
+function OLsetBackground(i){
+if(i==''){if(OLns4)over.background.src=null;
+else{if(OLns6)over.style.width='';over.style.backgroundImage='none';}
+}else{if(OLns4)over.background.src=i;
+else{if(OLns6)over.style.width=o3_width+'px';over.style.backgroundImage='url('+i+')';}}
+}
+
+/*
+ HANDLING FUNCTIONS
+*/
+// Displays layer
+function OLdisp(s){
+if(!OLallowmove){if(OLshadowPI)OLdispShadow();if(OLiframePI)OLdispIfs();OLplaceLayer();
+if(OLndt)OLshowObject(over);else OLshowid=setTimeout("OLshowObject(over)",1);
+OLallowmove=(o3_sticky||o3_nofollow)?0:1;}OLndt=0;if(s!="")self.status=s;
+}
+
+// Decides placement of layer.
+function OLplaceLayer(){
+var snp,X,Y,pgLeft,pgTop,pWd=o3_width,pHt,iWd=100,iHt=100,SB=0,LM=0,CX=0,TM=0,BM=0,CY=0,
+o=OLfd(),nsb=(OLgek>=20010505&&!o3_frame.scrollbars.visible)?1:0;
+if(!OLkht&&o&&o.clientWidth)iWd=o.clientWidth;
+else if(o3_frame.innerWidth){SB=Math.ceil(1.4*(o3_frame.outerWidth-o3_frame.innerWidth));
+if(SB>20)SB=20;iWd=o3_frame.innerWidth;}
+pgLeft=(OLie4)?o.scrollLeft:o3_frame.pageXOffset;
+if(OLie55&&OLfilterPI&&o3_filter&&o3_filtershadow)SB=CX=5;else
+if((OLshadowPI)&&bkdrop&&o3_shadow&&o3_shadowx){SB+=((o3_shadowx>0)?o3_shadowx:0);
+LM=((o3_shadowx<0)?Math.abs(o3_shadowx):0);CX=Math.abs(o3_shadowx);}
+if(o3_ref!=""||o3_fixx> -1||o3_relx!=null||o3_midx!=null){
+if(o3_ref!=""){X=OLrefXY[0];if(OLie55&&OLfilterPI&&o3_filter&&o3_filtershadow){
+if(o3_refp=='UR'||o3_refp=='LR')X-=5;}
+else if((OLshadowPI)&&bkdrop&&o3_shadow&&o3_shadowx){
+if(o3_shadowx<0&&(o3_refp=='UL'||o3_refp=='LL'))X-=o3_shadowx;else
+if(o3_shadowx>0&&(o3_refp=='UR'||o3_refp=='LR'))X-=o3_shadowx;}
+}else{if(o3_midx!=null){
+X=parseInt(pgLeft+((iWd-pWd-SB-LM)/2)+o3_midx);
+}else{if(o3_relx!=null){
+if(o3_relx>=0)X=pgLeft+o3_relx+LM;else X=pgLeft+o3_relx+iWd-pWd-SB;
+}else{X=o3_fixx+LM;}}}
+}else{
+if(o3_hauto){
+if(o3_hpos==LEFT&&OLx-pgLeft<iWd/2&&OLx-pWd-o3_offsetx<pgLeft+LM)o3_hpos=RIGHT;else
+if(o3_hpos==RIGHT&&OLx-pgLeft>iWd/2&&OLx+pWd+o3_offsetx>pgLeft+iWd-SB)o3_hpos=LEFT;}
+X=(o3_hpos==CENTER)?parseInt(OLx-((pWd+CX)/2)+o3_offsetx):
+(o3_hpos==LEFT)?OLx-o3_offsetx-pWd:OLx+o3_offsetx;
+if(o3_snapx>1){
+snp=X % o3_snapx;
+if(o3_hpos==LEFT){X=X-(o3_snapx+snp);}else{X=X+(o3_snapx-snp);}}}
+if(!o3_nojustx&&X+pWd>pgLeft+iWd-SB)
+X=iWd+pgLeft-pWd-SB;if(!o3_nojustx&&X-LM<pgLeft)X=pgLeft+LM;
+pgTop=OLie4?o.scrollTop:o3_frame.pageYOffset;
+if(!OLkht&&!nsb&&o&&o.clientHeight)iHt=o.clientHeight;
+else if(o3_frame.innerHeight)iHt=o3_frame.innerHeight;
+if(OLbubblePI&&o3_bubble)pHt=OLbubbleHt;else pHt=OLns4?over.clip.height:over.offsetHeight;
+if((OLshadowPI)&&bkdrop&&o3_shadow&&o3_shadowy){TM=(o3_shadowy<0)?Math.abs(o3_shadowy):0;
+if(OLie55&&OLfilterPI&&o3_filter&&o3_filtershadow)BM=CY=5;else
+BM=(o3_shadowy>0)?o3_shadowy:0;CY=Math.abs(o3_shadowy);}
+if(o3_ref!=""||o3_fixy> -1||o3_rely!=null||o3_midy!=null){
+if(o3_ref!=""){Y=OLrefXY[1];if(OLie55&&OLfilterPI&&o3_filter&&o3_filtershadow){
+if(o3_refp=='LL'||o3_refp=='LR')Y-=5;}else if((OLshadowPI)&&bkdrop&&o3_shadow&&o3_shadowy){
+if(o3_shadowy<0&&(o3_refp=='UL'||o3_refp=='UR'))Y-=o3_shadowy;else
+if(o3_shadowy>0&&(o3_refp=='LL'||o3_refp=='LR'))Y-=o3_shadowy;}
+}else{if(o3_midy!=null){
+Y=parseInt(pgTop+((iHt-pHt-CY)/2)+o3_midy);
+}else{if(o3_rely!=null){
+if(o3_rely>=0)Y=pgTop+o3_rely+TM;else Y=pgTop+o3_rely+iHt-pHt-BM;}else{
+Y=o3_fixy+TM;}}}
+}else{
+if(o3_vauto){
+if(o3_vpos==ABOVE&&OLy-pgTop<iHt/2&&OLy-pHt-o3_offsety<pgTop)o3_vpos=BELOW;else
+if(o3_vpos==BELOW&&OLy-pgTop>iHt/2&&OLy+pHt+o3_offsety+((OLns4||OLkht)?17:0)>pgTop+iHt-BM)
+o3_vpos=ABOVE;}Y=(o3_vpos==VCENTER)?parseInt(OLy-((pHt+CY)/2)+o3_offsety):
+(o3_vpos==ABOVE)?OLy-(pHt+o3_offsety+BM):OLy+o3_offsety+TM;
+if(o3_snapy>1){
+snp=Y % o3_snapy;
+if(pHt>0&&o3_vpos==ABOVE){Y=Y-(o3_snapy+snp);}else{Y=Y+(o3_snapy-snp);}}}
+if(!o3_nojusty&&Y+pHt+BM>pgTop+iHt)Y=pgTop+iHt-pHt-BM;if(!o3_nojusty&&Y-TM<pgTop)Y=pgTop+TM;
+OLrepositionTo(over,X,Y);
+if(OLshadowPI)OLrepositionShadow(X,Y);if(OLiframePI)OLrepositionIfs(X,Y);
+if(OLns6&&o3_frame.innerHeight){iHt=o3_frame.innerHeight;OLrepositionTo(over,X,Y);}
+if(OLscrollPI)OLchkScroll(X-pgLeft,Y-pgTop);
+}
+
+// Chooses body or documentElement
+function OLfd(f){
+var fd=((f)?f:o3_frame).document,fdc=fd.compatMode,fdd=fd.documentElement;
+return (!OLop7&&fdc&&fdc!='BackCompat'&&fdd&&fdd.clientWidth)?fd.documentElement:fd.body;
+}
+
+// Gets location of REFerence object
+function OLgetRefXY(r,d){
+var o=OLgetRef(r,d),ob=o,rXY=[o3_refx,o3_refy],of;
+if(!o)return [null,null];
+if(OLns4){if(typeof o.length!='undefined'&&o.length>1){
+ob=o[0];rXY[0]+=o[0].x+o[1].pageX;rXY[1]+=o[0].y+o[1].pageY;
+}else{if((o.toString().indexOf('Image')!= -1)||(o.toString().indexOf('Anchor')!= -1)){
+rXY[0]+=o.x;rXY[1]+=o.y;}else{rXY[0]+=o.pageX;rXY[1]+=o.pageY;}}
+}else{rXY[0]+=OLpageLoc(o,'Left');rXY[1]+=OLpageLoc(o,'Top');}
+of=OLgetRefOffsets(ob);rXY[0]+=of[0];rXY[1]+=of[1];
+return rXY;
+}
+function OLgetRef(l,d){var r=OLgetRefById(l,d);return (r)?r:OLgetRefByName(l,d);}
+
+// Seeks REFerence by id
+function OLgetRefById(l,d){
+l=(l||'overDiv');d=(d||o3_frame.document);var j,r;
+if(OLie4&&d.all)return d.all[l];if(d.getElementById)return d.getElementById(l);
+if(d.layers&&d.layers.length>0){if(d.layers[l])return d.layers[l];
+for(j=0;j<d.layers.length;j++){r=OLgetRefById(l,d.layers[j].document);if(r)return r;}}
+return null;
+}
+
+// Seeks REFerence by name
+function OLgetRefByName(l,d){
+d=(d||o3_frame.document);var j,r,v=OLie4?d.all.tags('iframe'):
+OLns6?d.getElementsByTagName('iframe'):null;
+if(typeof d.images!='undefined'&&d.images[l])return d.images[l];
+if(typeof d.anchors!='undefined'&&d.anchors[l])return d.anchors[l];
+if(v)for(j=0;j<v.length;j++)if(v[j].name==l)return v[j];
+if(d.layers&&d.layers.length>0)for(j=0;j<d.layers.length;j++){
+r=OLgetRefByName(l,d.layers[j].document);
+if(r&&r.length>0)return r;else if(r)return [r,d.layers[j]];}
+return null;
+}
+
+// Gets layer vs REFerence offsets
+function OLgetRefOffsets(o){
+var c=o3_refc.toUpperCase(),p=o3_refp.toUpperCase(),W=0,H=0,pW=0,pH=0,of=[0,0];
+pW=(OLbubblePI&&o3_bubble)?o3_width:OLns4?over.clip.width:over.offsetWidth;
+pH=(OLbubblePI&&o3_bubble)?OLbubbleHt:OLns4?over.clip.height:over.offsetHeight;
+if((!OLop7)&&o.toString().indexOf('Image')!= -1){W=o.width;H=o.height;
+}else if((!OLop7)&&o.toString().indexOf('Anchor')!= -1){c=o3_refc='UL';}else{
+W=(OLns4)?o.clip.width:o.offsetWidth;H=(OLns4)?o.clip.height:o.offsetHeight;}
+if((OLns4||(OLns6&&OLgek))&&o.border){W+=2*parseInt(o.border);H+=2*parseInt(o.border);}
+if(c=='UL'){of=(p=='UR')?[-pW,0]:(p=='LL')?[0,-pH]:(p=='LR')?[-pW,-pH]:[0,0];
+}else if(c=='UR'){of=(p=='UR')?[W-pW,0]:(p=='LL')?[W,-pH]:(p=='LR')?[W-pW,-pH]:[W,0];
+}else if(c=='LL'){of=(p=='UR')?[-pW,H]:(p=='LL')?[0,H-pH]:(p=='LR')?[-pW,H-pH]:[0,H];
+}else if(c=='LR'){of=(p=='UR')?[W-pW,H]:(p=='LL')?[W,H-pH]:(p=='LR')?[W-pW,H-pH]:
+[W,H];}
+return of;
+}
+
+// Gets x or y location of object
+function OLpageLoc(o,t){
+var l=0;while(o.offsetParent&&o.offsetParent.tagName.toLowerCase()!='html'){
+l+=o['offset'+t];o=o.offsetParent;}l+=o['offset'+t];
+return l;
+}
+
+// Moves layer
+function OLmouseMove(e){
+var e=(e||event);
+OLcC=(OLovertwoPI&&over2&&over==over2?cClick2:cClick);
+OLx=(e.pageX||e.clientX+OLfd().scrollLeft);OLy=(e.pageY||e.clientY+OLfd().scrollTop);
+if((OLallowmove&&over)&&(o3_frame==self||over==OLgetRefById()
+||(OLovertwoPI&&over2==over&&over==OLgetRefById('overDiv2')))){
+OLplaceLayer();if(OLhidePI)OLhideUtil(0,1,1,0,0,0);}
+if(OLhover&&over&&o3_frame==self&&OLcursorOff())if(o3_offdelay<1)OLcC();else
+{if(OLtimerid>0)clearTimeout(OLtimerid);OLtimerid=setTimeout("OLcC()",o3_offdelay);}
+}
+
+// Capture mouse and chain other scripts.
+function OLmh(){
+var fN,f,j,k,s,mh=OLmouseMove,w=(OLns4&&window.onmousemove),re=/function[ ]*(\w*)\(/;
+OLdw=document;if(document.onmousemove||w){if(w)OLdw=window;f=OLdw.onmousemove.toString();
+fN=f.match(re);if(!fN||fN[1]=='anonymous'||fN[1]=='OLmouseMove'){OLchkMh=0;return;}
+if(fN[1])s=fN[1]+'(e)';else{j=f.indexOf('{');k=f.lastIndexOf('}')+1;s=f.substring(j,k);}
+s+=';OLmouseMove(e);';mh=new Function('e',s);}
+OLdw.onmousemove=mh;if(OLns4)OLdw.captureEvents(Event.MOUSEMOVE);
+}
+
+/*
+ PARSING
+*/
+function OLparseTokens(pf,ar){
+var i,v,md= -1,par=(pf!='ol_'),p=OLpar,q=OLparQuo,t=OLtoggle;OLudf=(par&&!ar.length?1:0);
+for(i=0;i< ar.length;i++){if(md<0){if(typeof ar[i]=='number'){OLudf=(par?1:0);i--;}
+else{switch(pf){case 'ol_':ol_text=ar[i];break;default:o3_text=ar[i];}}md=0;
+}else{
+if(ar[i]==INARRAY){OLudf=0;eval(pf+'text=ol_texts['+ar[++i]+']');continue;}
+if(ar[i]==CAPARRAY){eval(pf+'cap=ol_caps['+ar[++i]+']');continue;}
+if(ar[i]==CAPTION){q(ar[++i],pf+'cap');continue;}
+if(Math.abs(ar[i])==STICKY){t(ar[i],pf+'sticky');continue;}
+if(Math.abs(ar[i])==NOFOLLOW){t(ar[i],pf+'nofollow');continue;}
+if(ar[i]==BACKGROUND){q(ar[++i],pf+'background');continue;}
+if(Math.abs(ar[i])==NOCLOSE){t(ar[i],pf+'noclose');continue;}
+if(Math.abs(ar[i])==MOUSEOFF){t(ar[i],pf+'mouseoff');continue;}
+if(ar[i]==OFFDELAY){p(ar[++i],pf+'offdelay');continue;}
+if(ar[i]==RIGHT||ar[i]==LEFT||ar[i]==CENTER){p(ar[i],pf+'hpos');continue;}
+if(ar[i]==OFFSETX){p(ar[++i],pf+'offsetx');continue;}
+if(ar[i]==OFFSETY){p(ar[++i],pf+'offsety');continue;}
+if(ar[i]==FGCOLOR){q(ar[++i],pf+'fgcolor');continue;}
+if(ar[i]==BGCOLOR){q(ar[++i],pf+'bgcolor');continue;}
+if(ar[i]==CGCOLOR){q(ar[++i],pf+'cgcolor');continue;}
+if(ar[i]==TEXTCOLOR){q(ar[++i],pf+'textcolor');continue;}
+if(ar[i]==CAPCOLOR){q(ar[++i],pf+'capcolor');continue;}
+if(ar[i]==CLOSECOLOR){q(ar[++i],pf+'closecolor');continue;}
+if(ar[i]==WIDTH){p(ar[++i],pf+'width');continue;}
+if(Math.abs(ar[i])==WRAP){t(ar[i],pf+'wrap');continue;}
+if(ar[i]==WRAPMAX){p(ar[++i],pf+'wrapmax');continue;}
+if(ar[i]==HEIGHT){p(ar[++i],pf+'height');continue;}
+if(ar[i]==BORDER){p(ar[++i],pf+'border');continue;}
+if(ar[i]==BASE){p(ar[++i],pf+'base');continue;}
+if(ar[i]==STATUS){q(ar[++i],pf+'status');continue;}
+if(Math.abs(ar[i])==AUTOSTATUS){v=pf+'autostatus';
+eval(v+'=('+ar[i]+'<0)?('+v+'==2?2:0):('+v+'==1?0:1)');continue;}
+if(Math.abs(ar[i])==AUTOSTATUSCAP){v=pf+'autostatus';
+eval(v+'=('+ar[i]+'<0)?('+v+'==1?1:0):('+v+'==2?0:2)');continue;}
+if(ar[i]==CLOSETEXT){q(ar[++i],pf+'close');continue;}
+if(ar[i]==SNAPX){p(ar[++i],pf+'snapx');continue;}
+if(ar[i]==SNAPY){p(ar[++i],pf+'snapy');continue;}
+if(ar[i]==FIXX){p(ar[++i],pf+'fixx');continue;}
+if(ar[i]==FIXY){p(ar[++i],pf+'fixy');continue;}
+if(ar[i]==RELX){p(ar[++i],pf+'relx');continue;}
+if(ar[i]==RELY){p(ar[++i],pf+'rely');continue;}
+if(ar[i]==MIDX){p(ar[++i],pf+'midx');continue;}
+if(ar[i]==MIDY){p(ar[++i],pf+'midy');continue;}
+if(ar[i]==REF){q(ar[++i],pf+'ref');continue;}
+if(ar[i]==REFC){q(ar[++i],pf+'refc');continue;}
+if(ar[i]==REFP){q(ar[++i],pf+'refp');continue;}
+if(ar[i]==REFX){p(ar[++i],pf+'refx');continue;}
+if(ar[i]==REFY){p(ar[++i],pf+'refy');continue;}
+if(ar[i]==FGBACKGROUND){q(ar[++i],pf+'fgbackground');continue;}
+if(ar[i]==BGBACKGROUND){q(ar[++i],pf+'bgbackground');continue;}
+if(ar[i]==CGBACKGROUND){q(ar[++i],pf+'cgbackground');continue;}
+if(ar[i]==PADX){p(ar[++i],pf+'padxl');p(ar[++i],pf+'padxr');continue;}
+if(ar[i]==PADY){p(ar[++i],pf+'padyt');p(ar[++i],pf+'padyb');continue;}
+if(Math.abs(ar[i])==FULLHTML){t(ar[i],pf+'fullhtml');continue;}
+if(ar[i]==BELOW||ar[i]==ABOVE||ar[i]==VCENTER){p(ar[i],pf+'vpos');continue;}
+if(ar[i]==CAPICON){q(ar[++i],pf+'capicon');continue;}
+if(ar[i]==TEXTFONT){q(ar[++i],pf+'textfont');continue;}
+if(ar[i]==CAPTIONFONT){q(ar[++i],pf+'captionfont');continue;}
+if(ar[i]==CLOSEFONT){q(ar[++i],pf+'closefont');continue;}
+if(ar[i]==TEXTSIZE){q(ar[++i],pf+'textsize');continue;}
+if(ar[i]==CAPTIONSIZE){q(ar[++i],pf+'captionsize');continue;}
+if(ar[i]==CLOSESIZE){q(ar[++i],pf+'closesize');continue;}
+if(ar[i]==TIMEOUT){p(ar[++i],pf+'timeout');continue;}
+if(ar[i]==DELAY){p(ar[++i],pf+'delay');continue;}
+if(Math.abs(ar[i])==HAUTO){t(ar[i],pf+'hauto');continue;}
+if(Math.abs(ar[i])==VAUTO){t(ar[i],pf+'vauto');continue;}
+if(Math.abs(ar[i])==NOJUSTX){t(ar[i],pf+'nojustx');continue;}
+if(Math.abs(ar[i])==NOJUSTY){t(ar[i],pf+'nojusty');continue;}
+if(Math.abs(ar[i])==CLOSECLICK){t(ar[i],pf+'closeclick');continue;}
+if(ar[i]==CLOSETITLE){q(ar[++i],pf+'closetitle');continue;}
+if(ar[i]==FGCLASS){q(ar[++i],pf+'fgclass');continue;}
+if(ar[i]==BGCLASS){q(ar[++i],pf+'bgclass');continue;}
+if(ar[i]==CGCLASS){q(ar[++i],pf+'cgclass');continue;}
+if(ar[i]==TEXTPADDING){p(ar[++i],pf+'textpadding');continue;}
+if(ar[i]==TEXTFONTCLASS){q(ar[++i],pf+'textfontclass');continue;}
+if(ar[i]==CAPTIONPADDING){p(ar[++i],pf+'captionpadding');continue;}
+if(ar[i]==CAPTIONFONTCLASS){q(ar[++i],pf+'captionfontclass');continue;}
+if(ar[i]==CLOSEFONTCLASS){q(ar[++i],pf+'closefontclass');continue;}
+if(Math.abs(ar[i])==CAPBELOW){t(ar[i],pf+'capbelow');continue;}
+if(ar[i]==LABEL){q(ar[++i],pf+'label');continue;}
+if(Math.abs(ar[i])==DECODE){t(ar[i],pf+'decode');continue;}
+if(ar[i]==DONOTHING){continue;}
+i=OLparseCmdLine(pf,i,ar);}}
+if((OLfunctionPI)&&OLudf&&o3_function)o3_text=o3_function();
+if(pf=='o3_')OLfontSize();
+}
+function OLpar(a,v){eval(v+'='+a);}
+function OLparQuo(a,v){eval(v+"='"+OLescSglQt(a)+"'");}
+function OLescSglQt(s){return s.toString().replace(/'/g,"\\'");}
+function OLtoggle(a,v){eval(v+'=('+v+'==0&&'+a+'>=0)?1:0');}
+function OLhasDims(s){return /[%\-a-z]+$/.test(s);}
+function OLfontSize(){
+var i;if(OLhasDims(o3_textsize)){if(OLns4)o3_textsize="2";}else
+if(!OLns4){i=parseInt(o3_textsize);o3_textsize=(i>0&&i<8)?OLpct[i]:OLpct[0];}
+if(OLhasDims(o3_captionsize)){if(OLns4)o3_captionsize="2";}else
+if(!OLns4){i=parseInt(o3_captionsize);o3_captionsize=(i>0&&i<8)?OLpct[i]:OLpct[0];}
+if(OLhasDims(o3_closesize)){if(OLns4)o3_closesize="2";}else
+if(!OLns4){i=parseInt(o3_closesize);o3_closesize=(i>0&&i<8)?OLpct[i]:OLpct[0];}
+if(OLprintPI)OLprintDims();
+}
+function OLdecode(){
+var re=/%[0-9A-Fa-f]{2,}/,t=o3_text,c=o3_cap,u=unescape,d=!OLns4&&(!OLgek||OLgek>=20020826)
+&&typeof decodeURIComponent?decodeURIComponent:u;if(typeof(window.TypeError)=='function'){
+if(re.test(t)){eval(new Array('try{','o3_text=d(t);','}catch(e){','o3_text=u(t);',
+'}').join('\n'))};if(c&&re.test(c)){eval(new Array('try{','o3_cap=d(c);','}catch(e){',
+'o3_cap=u(c);','}').join('\n'))}}else{if(re.test(t))o3_text=u(t);if(c&&re.test(c))o3_cap=u(c);}
+}
+
+/*
+ LAYER FUNCTIONS
+*/
+// Writes to layer
+function OLlayerWrite(t){
+t+="\n";
+if(OLns4){over.document.write(t);over.document.close();
+}else if(typeof over.innerHTML!='undefined'){if(OLieM)over.innerHTML='';over.innerHTML=t;
+}else{range=o3_frame.document.createRange();range.setStartAfter(over);
+domfrag=range.createContextualFragment(t);
+while(over.hasChildNodes()){over.removeChild(over.lastChild);}
+over.appendChild(domfrag);}
+if(OLprintPI)over.print=o3_print?t:null;
+}
+
+// Makes object visible
+function OLshowObject(o){
+OLshowid=0;o=(OLns4)?o:o.style;
+if(((OLfilterPI)&&!OLchkFilter(o))||!OLfilterPI)o.visibility="visible";
+if(OLshadowPI)OLshowShadow();if(OLiframePI)OLshowIfs();if(OLhidePI)OLhideUtil(1,1,0);
+}
+
+// Hides object
+function OLhideObject(o){
+if(OLshowid>0){clearTimeout(OLshowid);OLshowid=0;}
+if(OLtimerid>0)clearTimeout(OLtimerid);if(OLdelayid>0)clearTimeout(OLdelayid);
+OLtimerid=0;OLdelayid=0;self.status="";o3_label=ol_label;
+if(o3_frame!=self)o=OLgetRefById();
+if(o){if(o.onmouseover)o.onmouseover=null;
+if(OLscrollPI&&o==over)OLclearScroll();
+if(OLdraggablePI)OLclearDrag();
+if(OLfilterPI)OLcleanupFilter(o);if(OLshadowPI)OLhideShadow();
+var os=(OLns4)?o:o.style;os.visibility="hidden";
+if(OLhidePI&&o==over)OLhideUtil(0,0,1);if(OLiframePI)OLhideIfs(o);}
+}
+
+// Moves layer
+function OLrepositionTo(o,xL,yL){
+o=(OLns4)?o:o.style;
+o.left=(OLns4?xL:xL+'px');
+o.top=(OLns4?yL:yL+'px');
+}
+
+// Handle NOCLOSE-MOUSEOFF
+function OLoptMOUSEOFF(c){
+if(!c)o3_close="";
+over.onmouseover=function(){OLhover=1;if(OLtimerid>0){clearTimeout(OLtimerid);OLtimerid=0;}}
+}
+function OLcursorOff(){
+var o=(OLns4?over:over.style),pHt=OLns4?over.clip.height:over.offsetHeight,
+left=parseInt(o.left),top=parseInt(o.top),
+right=left+o3_width,bottom=top+((OLbubblePI&&o3_bubble)?OLbubbleHt:pHt);
+if(OLx<left||OLx>right||OLy<top||OLy>bottom)return true;
+return false;
+}
+
+/*
+ REGISTRATION
+*/
+function OLsetRunTimeVar(){
+if(OLrunTime.length)for(var k=0;k<OLrunTime.length;k++)OLrunTime[k]();
+}
+function OLparseCmdLine(pf,i,ar){
+if(OLcmdLine.length){for(var k=0;k<OLcmdLine.length;k++){
+var j=OLcmdLine[k](pf,i,ar);if(j>-1){i=j;break;}}}
+return i;
+}
+function OLregCmds(c){
+if(typeof c!='string')return;
+var pM=c.split(',');pMtr=pMtr.concat(pM);
+for(var i=0;i<pM.length;i++)eval(pM[i].toUpperCase()+'='+pmCnt++);
+}
+function OLregRunTimeFunc(f){
+if(typeof f=='object')OLrunTime=OLrunTime.concat(f);
+else OLrunTime[OLrunTime.length++]=f;
+}
+function OLregCmdLineFunc(f){
+if(typeof f=='object')OLcmdLine=OLcmdLine.concat(f);
+else OLcmdLine[OLcmdLine.length++]=f;
+}
+
+OLloaded=1;
diff --git a/httemplate/elements/overlibmws_crossframe.js b/httemplate/elements/overlibmws_crossframe.js
new file mode 100644
index 000000000..6b21c42e8
--- /dev/null
+++ b/httemplate/elements/overlibmws_crossframe.js
@@ -0,0 +1,44 @@
+/*
+ overlibmws_crossframe.js plug-in module - Copyright Foteos Macrides 2003-2006
+ For support of FRAME.
+ Initial: August 3, 2003 - Last Revised: November 2, 2004
+ See the Change History and Command Reference for overlibmws via:
+
+ http://www.macridesweb.com/oltest/
+
+ Published under an open source license: http://www.macridesweb.com/oltest/license.html
+*/
+
+OLloaded=0;
+OLregCmds('frame');
+
+function OLparseCrossframe(pf,i,ar){
+var k=i,v;
+if(k<ar.length){
+if(ar[k]==FRAME){v=ar[++k];if(pf=='ol_')ol_frame=v;else OLoptFRAME(v);return k;}}
+return -1;
+}
+
+function OLgetFrameRef(thisFrame,ofrm){
+var i,v,retVal='';for(i=0;i<thisFrame.length;i++){if((((thisFrame[i].length>0)))&&(((OLns4))||
+((OLie4)&&(v=thisFrame[i].document.all.tags('iframe'))!=null&&v.length==0)||
+((OLns6)&&(v=thisFrame[i].document.getElementsByTagName('iframe'))!=null&&v.length==0))){
+retVal=OLgetFrameRef(thisFrame[i],ofrm);if(retVal=='')continue;}
+else if(thisFrame[i]!=ofrm)continue;retVal='['+i+']'+retVal;break;}
+return retVal;
+}
+
+function OLoptFRAME(frm){
+o3_frame=OLmkLyr('overDiv',frm)?frm:self;if(o3_frame!=self){
+var l,tFrm=OLgetFrameRef(top.frames,o3_frame),sFrm=OLgetFrameRef(top.frames,ol_frame);
+if(sFrm.length==tFrm.length) {l=tFrm.lastIndexOf('[');if(l){
+while(sFrm.substring(0,l)!=tFrm.substring(0,l))l=tFrm.lastIndexOf('[',l-1);
+tFrm=tFrm.substr(l);sFrm=sFrm.substr(l);}}var i,k,cnt=0,p='',str=tFrm;
+while((k=str.lastIndexOf('['))!= -1){cnt++;str=str.substring(0,k);}
+for(i=0;i<cnt;i++)p=p+'parent.';OLfnRef=p+'frames'+sFrm+'.';}
+}
+
+OLregCmdLineFunc(OLparseCrossframe);
+
+OLcrossframePI=1;
+OLloaded=1;
diff --git a/httemplate/elements/overlibmws_draggable.js b/httemplate/elements/overlibmws_draggable.js
new file mode 100644
index 000000000..0d25f842e
--- /dev/null
+++ b/httemplate/elements/overlibmws_draggable.js
@@ -0,0 +1,78 @@
+/*
+ overlibmws_draggable.js plug-in module - Copyright Foteos Macrides 2002=2005
+ For support of the DRAGGABLE feature.
+ Initial: August 24, 2002 - Last Revised: March 2, 2006
+ See the Change History and Command Reference for overlibmws via:
+
+ http://www.macridesweb.com/oltest/
+
+ Published under an open source license: http://www.macridesweb.com/oltest/license.html
+*/
+
+OLloaded=0;
+OLregCmds('draggable');
+
+// DEFAULT CONFIGURATION
+if(OLud('draggable'))var ol_draggable=0;
+// END CONFIGURATION
+
+var o3_draggable=0,o3_dragging=0,OLmMv,OLcX,OLcY,OLcbX,OLcbY;
+function OLloadDraggable(){OLload('draggable');}
+function OLparseDraggable(pf,i,ar){
+var k=i;
+if(k<ar.length){if(Math.abs(ar[k])==DRAGGABLE){OLtoggle(ar[k],pf+'draggable');return k;}}
+return -1;
+}
+
+function OLcheckDrag(){
+if(o3_draggable){if(o3_sticky&&(o3_frame==self))initDrag();else o3_draggable=0;}
+}
+function initDrag(){
+OLmMv=OLdw.onmousemove;o3_dragging=0;
+if(OLns4){document.captureEvents(Event.MOUSEDOWN|Event.CLICK);
+document.onmousedown=OLgrabEl;;document.onclick=function(e){return routeEvent(e);}}
+else{over.onmousedown=OLgrabEl;OLsetDrgCur(1);}
+}
+function OLsetDrgCur(d){if(!OLns4)over.style.cursor=(d?'move':'auto');}
+
+function OLgrabEl(e){
+var e=(e||event);
+var cKy=(OLns4?e.modifiers&Event.ALT_MASK:(e.altKey||(OLop7&&e.ctrlKey)));o3_dragging=1;
+if(cKy){OLsetDrgCur(0);document.onmouseup=function(){OLsetDrgCur(1);o3_dragging=0;}
+return(OLns4?routeEvent(e):true);}
+OLx=(e.pageX||e.clientX+OLfd().scrollLeft);OLy=(e.pageY||e.clientY+OLfd().scrollTop);
+if(OLie4)over.onselectstart=function(){return false;}
+if(OLns4){OLcX=OLx;OLcY=OLy;document.captureEvents(Event.MOUSEUP)}else{
+OLcX=OLx-(OLns4?over.left:parseInt(over.style.left));
+OLcY=OLy-(OLns4?over.top:parseInt(over.style.top));
+if((OLshadowPI)&&bkdrop&&o3_shadow){OLcbX=OLx-(parseInt(bkdrop.style.left));
+OLcbY=OLy-(parseInt(bkdrop.style.top));}}OLdw.onmousemove=OLmoveEl;
+document.onmouseup=function(){
+if(OLie4)over.onselectstart=null;o3_dragging=0;OLdw.onmousemove=OLmMv;}
+return(OLns4?routeEvent(e):false);
+}
+
+function OLmoveEl(e){
+var e=(e||event);
+OLx=(e.pageX||e.clientX+OLfd().scrollLeft);OLy=(e.pageY||e.clientY+OLfd().scrollTop);
+if(o3_dragging){if(OLns4){over.moveBy(OLx-OLcX,OLy-OLcY);
+if(OLshadowPI&&bkdrop&&o3_shadow)bkdrop.moveBy(OLx-OLcX,OLy-OLcY);}
+else{OLrepositionTo(over,OLx-OLcX,OLy-OLcY);
+if((OLiframePI)&&OLie55&&OLifsP1)OLrepositionTo(OLifsP1,OLx-OLcX,OLy-OLcY);
+if((OLshadowPI)&&bkdrop&&o3_shadow){OLrepositionTo(bkdrop,OLx-OLcbX,OLy-OLcbY);
+if((OLiframePI)&&OLie55&&OLifsSh)OLrepositionTo(OLifsSh,OLx-OLcbX,OLy-OLcbY);}}
+if(OLhidePI)OLhideUtil(0,1,1,0,0,0);}if(OLns4){OLcX=OLx;OLcY=OLy;}
+return false;
+}
+
+function OLclearDrag(){
+if(OLns4){document.releaseEvents(Event.MOUSEDOWN|Event.MOUSEUP|Event.CLICK);
+document.onmousedown=document.onclick=null;}else{over.onmousedown=null;OLsetDrgCur(0);}
+document.onmouseup=null;o3_dragging=0;
+}
+
+OLregRunTimeFunc(OLloadDraggable);
+OLregCmdLineFunc(OLparseDraggable);
+
+OLdraggablePI=1;
+OLloaded=1;
diff --git a/httemplate/elements/overlibmws_iframe.js b/httemplate/elements/overlibmws_iframe.js
new file mode 100644
index 000000000..e3032f2ee
--- /dev/null
+++ b/httemplate/elements/overlibmws_iframe.js
@@ -0,0 +1,93 @@
+/*
+ overlibmws_iframe.js plug-in module - Copyright Foteos Macrides 2003-2005
+ Masks system controls to prevent obscuring of popops for IE v5.5 or higher.
+ Initial: October 19, 2003 - Last Revised: May 15, 2005
+ See the Change History and Command Reference for overlibmws via:
+
+ http://www.macridesweb.com/oltest/
+
+ Published under an open source license: http://www.macridesweb.com/oltest/license.html
+*/
+
+OLloaded=0;
+
+var OLifsP1=null,OLifsSh=null,OLifsP2=null;
+
+// IFRAME SHIM SUPPORT FUNCTIONS
+function OLinitIfs(){
+if(!OLie55)return;
+if((OLovertwoPI)&&over2&&over==over2){
+var o=o3_frame.document.all['overIframeOvertwo'];
+if(!o||OLifsP2!=o){OLifsP2=null;OLgetIfsP2Ref();}return;}
+o=o3_frame.document.all['overIframe'];
+if(!o||OLifsP1!=o){OLifsP1=null;OLgetIfsRef();}
+if((OLshadowPI)&&o3_shadow){o=o3_frame.document.all['overIframeShadow'];
+if(!o||OLifsSh!=o){OLifsSh=null;OLgetIfsShRef();}}
+}
+
+function OLsetIfsRef(o,i,z){
+o.id=i;o.src='javascript:false;';o.scrolling='no';var os=o.style;
+os.position='absolute';os.top=0;os.left=0;os.width=1;os.height=1;os.visibility='hidden';
+os.zIndex=over.style.zIndex-z;os.filter='Alpha(style=0,opacity=0)';
+}
+
+function OLgetIfsRef(){
+if(OLifsP1||!OLie55)return;
+OLifsP1=o3_frame.document.createElement('iframe');
+OLsetIfsRef(OLifsP1,'overIframe',2);
+o3_frame.document.body.appendChild(OLifsP1);
+}
+
+function OLgetIfsShRef(){
+if(OLifsSh||!OLie55)return;
+OLifsSh=o3_frame.document.createElement('iframe');
+OLsetIfsRef(OLifsSh,'overIframeShadow',3);
+o3_frame.document.body.appendChild(OLifsSh);
+}
+
+function OLgetIfsP2Ref(){
+if(OLifsP2||!OLie55)return;
+OLifsP2=o3_frame.document.createElement('iframe');
+OLsetIfsRef(OLifsP2,'overIframeOvertwo',1);
+o3_frame.document.body.appendChild(OLifsP2);
+}
+
+function OLsetDispIfs(o,w,h){
+var os=o.style;
+os.width=w+'px';os.height=h+'px';os.clip='rect(0px '+w+'px '+h+'px 0px)';
+o.filters.alpha.enabled=true;
+}
+
+function OLdispIfs(){
+if(!OLie55)return;
+var wd=over.offsetWidth,ht=over.offsetHeight;
+if(OLfilterPI&&o3_filter&&o3_filtershadow){wd+=5;ht+=5;}
+if((OLovertwoPI)&&over2&&over==over2){
+if(!OLifsP2)return;
+OLsetDispIfs(OLifsP2,wd,ht);return;}
+if(!OLifsP1)return;
+OLsetDispIfs(OLifsP1,wd,ht);
+if((!OLshadowPI)||!o3_shadow||!OLifsSh)return;
+OLsetDispIfs(OLifsSh,wd,ht);
+}
+
+function OLshowIfs(){
+if(OLifsP1){OLifsP1.style.visibility="visible";
+if((OLshadowPI)&&o3_shadow&&OLifsSh)OLifsSh.style.visibility="visible";}
+}
+
+function OLhideIfs(o){
+if(!OLie55||o!=over)return;
+if(OLifsP1)OLifsP1.style.visibility="hidden";
+if((OLshadowPI)&&o3_shadow&&OLifsSh)OLifsSh.style.visibility="hidden";
+}
+
+function OLrepositionIfs(X,Y){
+if(OLie55){if((OLovertwoPI)&&over2&&over==over2){
+if(OLifsP2)OLrepositionTo(OLifsP2,X,Y);}
+else{if(OLifsP1){OLrepositionTo(OLifsP1,X,Y);if((OLshadowPI)&&o3_shadow&&OLifsSh)
+OLrepositionTo(OLifsSh,X+o3_shadowx,Y+o3_shadowy);}}}
+}
+
+OLiframePI=1;
+OLloaded=1;
diff --git a/httemplate/elements/pager.html b/httemplate/elements/pager.html
new file mode 100644
index 000000000..a53300f53
--- /dev/null
+++ b/httemplate/elements/pager.html
@@ -0,0 +1,55 @@
+% 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;
+% my $prevpage = 0;
+% my $over = 0;
+% my $step = $opt{total} / 10; # 10 evenly spaced
+% for ( my $poff = 0; $poff < $opt{total}; $poff += $opt{maxrecords} ) {
+% $page++;
+%
+% next unless
+% $page <= 4 #first four
+% || $page >= ( $opt{total} / $opt{maxrecords} ) - 3 #last four
+% || abs( ($opt{offset}-$poff) / $opt{maxrecords} ) <= 3 #w/i 3 of current
+% || $poff > $over # evenly spaced
+% ;
+%
+% $over += $step if $poff > $over;
+%
+% if ( $opt{'offset'} == $poff ) {
+
+ <FONT SIZE="+2"><% $page %></FONT>
+
+% } else {
+% $cgi->param('offset', $poff);
+%
+% if ( $page > $prevpage+1 ) {
+ ...
+% }
+
+ <A HREF="<% $cgi->self_url %>"><% $page %></A>
+
+% }
+%
+% $prevpage = $page;
+%
+% }
+%
+% 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/phonenumber.html b/httemplate/elements/phonenumber.html
new file mode 100644
index 000000000..ffbd8c100
--- /dev/null
+++ b/httemplate/elements/phonenumber.html
@@ -0,0 +1,22 @@
+%
+% my( $number, %opt ) = @_;
+% my $conf = new FS::Conf;
+% ( my $snumber = $number ) =~ s/\D//g;
+%
+
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws_iframe.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws_draggable.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/iframecontentmws.js"></SCRIPT>
+% if ( length($number) ) {
+
+ <% $number %>
+% if ( $opt{'callable'} && $conf->config('vonage-username') ) {
+
+ <A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('https://secure.click2callu.com/tpcc/makecall?username=<% $conf->config('vonage-username') %>&password=<% $conf->config('vonage-password') %>&fromnumber=<% $conf->config('vonage-fromnumber')%>&tonumber=1<% $snumber %>', 240, 64, 'call_popup'), CAPTION, 'Initiating call', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE, WIDTH, 240, HEIGHT, 64 ); return false;" TITLE="Call this number"><IMG SRC="<%$fsurl%>images/red_telephone_mimooh_01.png" BORDER=0 ALT="Call this number"></A>
+% }
+% } else {
+
+ &nbsp;
+% }
+
diff --git a/httemplate/elements/progress-init.html b/httemplate/elements/progress-init.html
new file mode 100644
index 000000000..1c96a54ac
--- /dev/null
+++ b/httemplate/elements/progress-init.html
@@ -0,0 +1,85 @@
+%
+% my( $formname, $fields, $action, $url_or_message, $key ) = @_;
+% $key = '' unless defined $key;
+%
+% my $url_or_message_link;
+% if ( ref($url_or_message) ) { #its a message or something
+% $url_or_message_link =
+% 'message='. uri_escape( $url_or_message->{'message'} )
+% } else {
+% $url_or_message_link = "url=$url_or_message";
+% }
+%
+
+
+<% include('/elements/xmlhttp.html',
+ 'method' => 'POST',
+ 'url' => $action,
+ 'subs' => [ 'start_job' ],
+ 'key' => $key,
+ )
+%>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws_iframe.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript">
+function OLiframeContent(src, width, height, name) {
+ return ('<iframe src="'+src+'" width="'+width+'" height="'+height+'"'
+ +(name?' name="'+name+'" id="'+name+'"':'')+' scrolling="auto">'
+ +'<div>[iframe not supported]</div></iframe>');
+}
+
+function <%$key%>process () {
+
+ //alert('<%$key%>process for form <%$formname%>');
+
+ if ( document.<%$formname%>.submit.disabled == false ) {
+ document.<%$formname%>.submit.disabled=true;
+ }
+
+ overlib( 'Submitting job to server...', WIDTH, 444, HEIGHT, 168, CAPTION, 'Please wait...', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', CLOSECLICK, MIDX, 0, MIDY, 0 );
+
+ var Hash = new Array();
+ var x = 0;
+ var fieldName;
+ for (var i = 0; i<document.<%$formname%>.elements.length; i++) {
+ field = document.<%$formname%>.elements[i];
+ if ( <% join(' || ', map { "(field.name.indexOf('$_') > -1)" } @$fields ) %>
+ )
+ {
+ if ( field.type == 'select-multiple' ) {
+ //alert('select-multiple ' + field.name);
+ for (var j=0; j < field.options.length; j++) {
+ if ( field.options[j].selected ) {
+ //alert(field.name + ' => ' + field.options[j].value);
+ Hash[x++] = field.name;
+ Hash[x++] = field.options[j].value;
+ }
+ }
+ } else if ( ( field.type != 'radio' && field.type != 'checkbox' )
+ || ( ( field.type == 'radio' || field.type == 'checkbox' )
+ && document.<%$formname%>.elements[i].checked
+ )
+ )
+ {
+ Hash[x++] = field.name;
+ Hash[x++] = field.value;
+ }
+ }
+ }
+
+ // jsrsPOST = true;
+ // jsrsExecute( '<% $action %>', <%$key%>myCallback, 'start_job', Hash );
+
+ //alert('start_job( ' + Hash + ', <%$key%>myCallback )' );
+ //alert('start_job()' );
+ <%$key%>start_job( Hash, <%$key%>myCallback );
+
+}
+
+function <%$key%>myCallback( jobnum ) {
+
+ overlib( OLiframeContent('<%$p%>elements/progress-popup.html?jobnum=' + jobnum + ';<%$url_or_message_link%>;formname=<%$formname%>' , 444, 168, 'progress_popup'), CAPTION, 'Please wait...', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', CLOSECLICK, MIDX, 0, MIDY, 0 );
+
+}
+
+</SCRIPT>
diff --git a/httemplate/elements/progress-popup.html b/httemplate/elements/progress-popup.html
new file mode 100644
index 000000000..cda704a12
--- /dev/null
+++ b/httemplate/elements/progress-popup.html
@@ -0,0 +1,105 @@
+%
+% my $jobnum = $cgi->param('jobnum');
+% my $url = $cgi->param('url');
+% my $message = $cgi->param('message');
+% my $formname = scalar($cgi->param('formname'));
+%
+
+<HTML>
+ <HEAD>
+ <TITLE></TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#ccccff" onLoad="refreshStatus()">
+
+<% include('/elements/xmlhttp.html',
+ 'url' => $p.'elements/jsrsServer.html',
+ 'subs' => [ 'job_status' ],
+ )
+%>
+<SCRIPT TYPE="text/javascript" src="<%$fsurl%>elements/qlib/control.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" src="<%$fsurl%>elements/qlib/imagelist.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" src="<%$fsurl%>elements/qlib/progress.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript">
+function refreshStatus () {
+ //jsrsExecute( '<%$p%>elements/jsrsServer.html', updateStatus, 'job_status', '<% $jobnum %>' );
+
+ job_status( '<% $jobnum %>', updateStatus );
+}
+function updateStatus( status_statustext ) {
+
+ //var Array = status_statustext.split("\n");
+ var statusArray = eval('(' + status_statustext + ')');
+ var status = statusArray[0];
+ var statustext = statusArray[1];
+
+ //if ( status == 'progress' ) {
+ //IE workaround, no i have no idea why
+ if ( status.indexOf('progress') > -1 ) {
+ document.getElementById("progress_percent").innerHTML = statustext + '%';
+ bar1.set(statustext);
+ bar1.update;
+ //jsrsExecute( '<%$p%>elements/jsrsServer.html', updateStatus, 'job_status', '<% $jobnum %>' );
+ job_status( '<% $jobnum %>', updateStatus );
+ } else if ( status.indexOf('complete') > -1 ) {
+% if ( $message ) {
+
+ document.getElementById("progress_message").innerHTML = "<% $message %>";
+ document.getElementById("progress_bar").innerHTML = '';
+ document.getElementById("progress_percent").innerHTML = '<INPUT TYPE="button" VALUE="OK" onClick="parent.nd(1);">';
+ document.getElementById("progress_jobnum").innerHTML = '';
+ if ( parent.document.<%$formname%>.submit.disabled == true ) {
+ parent.document.<%$formname%>.submit.disabled=false;
+ }
+% } elsif ( $url ) {
+
+ window.top.location.href = '<% $url %>';
+% } else {
+
+ alert('job done but no url or message specified');
+% }
+
+ } else if ( status.indexOf('error') > -1 ) {
+ document.getElementById("progress_message").innerHTML = '<FONT SIZE="+1" COLOR="#FF0000">Error: ' + statustext + '</FONT>';
+ document.getElementById("progress_bar").innerHTML = '';
+ document.getElementById("progress_percent").innerHTML = '<INPUT TYPE="button" VALUE="OK" onClick="parent.nd(1);">';
+ document.getElementById("progress_jobnum").innerHTML = '';
+ if ( parent.document.<%$formname%>.submit.disabled == true ) {
+ parent.document.<%$formname%>.submit.disabled=false;
+ }
+ } else {
+ alert('XXX unknown status returned from server: ' + status);
+ }
+
+}
+</SCRIPT>
+
+ <TABLE WIDTH="100%">
+ <TR>
+ <TD ALIGN="center" ID="progress_message">
+ Server processing job...
+ </TD>
+ </TR><TR>
+ <TD ALIGN="center" ID="progress_bar">
+ <SCRIPT TYPE="text/javascript">
+ // Create imagelist
+ SEGS = new QImageList(4, 23, "<%$fsurl%>images/progressbar-empty.png", "<%$fsurl%>images/progressbar-full.png");
+ // Create bars
+ bar1 = new QProgress(null, "bar1", SEGS, 100);
+ // bar1.set(0);
+ // bar1.update;
+ </SCRIPT>
+ </TD>
+ </TR><TR>
+ <TD ALIGN="center">
+ <DIV ID="progress_percent">%</DIV>
+ </TD>
+ </TR><TR>
+ <TD ALIGN="center" ID="progress_jobnum">
+ (progress of job #<% $jobnum %>)
+ </TD>
+ </TR>
+ </TABLE>
+
+ </BODY>
+</HTML>
+
diff --git a/httemplate/elements/qlib/box.js b/httemplate/elements/qlib/box.js
new file mode 100644
index 000000000..537aac4c8
--- /dev/null
+++ b/httemplate/elements/qlib/box.js
@@ -0,0 +1,29 @@
+/**
+ * QLIB 1.0 Box Control
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QBox(parent, name, res, x, y, width, height, body, visible, effects, opacity, zindex) {
+ this.init(parent, name);
+ if (this.res = res) {
+ this.x = x - 0;
+ this.y = y - 0;
+ this.width = width - 0;
+ this.height = (typeof(height) == "number") ? height : null;
+ this.body = body || "&nbsp;";
+ var j = QBox.arguments.length;
+ this.visible = (j > 8) ? visible : true;
+ this.effects = (j > 9) ? effects : (res.effects || 0);
+ this.opacity = (j > 10) ? opacity : (res.opacity != null ? res.opacity : 100);
+ this.zindex = (j > 11) ? zindex : null;
+ this.create();
+ } else {
+ this.document.write("invalid resource");
+ }
+}
+QBox.prototype = new QBoxCtrl();
diff --git a/httemplate/elements/qlib/boxctrl.js b/httemplate/elements/qlib/boxctrl.js
new file mode 100644
index 000000000..417b204e4
--- /dev/null
+++ b/httemplate/elements/qlib/boxctrl.js
@@ -0,0 +1,48 @@
+/**
+ * QLIB 1.0 Box Abstraction
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QBoxCtrl_content() {
+ with (this) {
+ if (res) {
+ this.cwidth = width - res.L - res.R - 8;
+ this.cheight = height && (height - res.T - res.B - 8);
+ var ec = '"><table border="0" cellspacing="0" cellpadding="0"><tr><td></td></tr></table></td>';
+ document.write('<table class="qbox" border="0" cellspacing="0" cellpadding="0" width="' +
+ (width - 8) + (height != null ? '" height="' + (height - 8) : '') + '"><tr><td width="' +
+ res.L + '" height="' + res.T + '"><img src="' + res.TL.src + '" border="0" width="' +
+ res.L + '" height="' + res.T + '"></td><td width="' + cwidth + '" height="' + res.T +
+ '" background="' + res.TC.src + ec + '<td width="' + res.R + '" height="' + res.T +
+ '"><img src="' + res.TR.src + '" border="0" width="' + res.R + '" height="' + res.T +
+ '"></td></tr><tr><td width="' + res.L + (cheight != null ? '" height="' + cheight : '') +
+ '" background="' + res.ML.src + ec + '<td width="' + cwidth + '" bgcolor="' + res.bgcolor +
+ (cheight != null ? '" height="' + cheight : '') + (res.bgtile ? '" background="' +
+ res.bgtile.src : '') + '" align="left" valign="top" class="body" unselectable="on">');
+ if (typeof(body) == "function") {
+ this.body();
+ } else {
+ document.write(body);
+ }
+ document.write('</td><td width="' + res.R + (cheight != null ? '" height="' + cheight : '') +
+ '" background="' + res.MR.src + ec + '</tr><tr><td width="' + res.L + '" height="' + res.B +
+ '"><img src="' + res.BL.src + '" border="0" width="' + res.L + '" height="' + res.B +
+ '"></td><td width="' + cwidth + '" height="' + res.B + '" background="' + res.BC.src + ec +
+ '<td width="' + res.R + '" height="' + res.B + '"><img src="' + res.BR.src +
+ '" border="0" width="' + res.R + '" height="' + res.B + '"></td></tr></table><br>');
+ }
+ }
+}
+
+function QBoxCtrl() {
+ this.res = false;
+ this.body = "&nbsp;";
+ this.cwidth = this.cheight = 0;
+ this.content = QBoxCtrl_content;
+}
+QBoxCtrl.prototype = new QWndCtrl();
diff --git a/httemplate/elements/qlib/boxres.js b/httemplate/elements/qlib/boxres.js
new file mode 100644
index 000000000..087817211
--- /dev/null
+++ b/httemplate/elements/qlib/boxres.js
@@ -0,0 +1,42 @@
+/**
+ * QLIB 1.0 Box Resource
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QBoxRes(t, r, b, l, tc, tr, mr, br, bc, bl, ml, tl, bgcolor, bgtile, effects, opacity) {
+ var args = QBoxRes.arguments.length;
+ this.T = t;
+ this.R = r;
+ this.B = b;
+ this.L = l;
+ this.TC = new Image();
+ this.TC.src = tc;
+ this.TR = new Image(r, t);
+ this.TR.src = tr;
+ this.MR = new Image();
+ this.MR.src = mr;
+ this.BR = new Image(r, b);
+ this.BR.src = br;
+ this.BC = new Image();
+ this.BC.src = bc;
+ this.BL = new Image(l, b);
+ this.BL.src = bl;
+ this.ML = new Image();
+ this.ML.src = ml;
+ this.TL = new Image(l, t);
+ this.TL.src = tl;
+ this.bgcolor = bgcolor || "#FFFFFF";
+ if (bgtile) {
+ this.bgtile = new Image();
+ this.bgtile.src = bgtile;
+ } else {
+ this.bgtile = false;
+ }
+ this.effects = (args > 13) ? effects : null;
+ this.opacity = (args > 14) ? opacity : null;
+}
diff --git a/httemplate/elements/qlib/button.js b/httemplate/elements/qlib/button.js
new file mode 100644
index 000000000..05247d5f8
--- /dev/null
+++ b/httemplate/elements/qlib/button.js
@@ -0,0 +1,74 @@
+/**
+ * QLIB 1.0 Button Control
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QButton_update() {
+ with (this) {
+ image.src = ((!enabled && res.imgD) || (value ? res.imgP : res.imgN)).src;
+ }
+}
+
+function QButton_doEvent() {
+ with (this) {
+ if (enabled) {
+ if (res.style == 1) {
+ this.value = value ? 0 : 1;
+ update();
+ }
+ onClick(value, tag);
+ }
+ }
+ return false;
+}
+
+function QButton_enable(state) {
+ this.enabled = state;
+ this.update();
+}
+
+function QButton_set(value) {
+ if (this.enabled) {
+ this.value = value ? 1 : 0;
+ this.update();
+ }
+ return true;
+}
+
+function QButton(parent, name, res, tooltip) {
+ this.init(parent, name);
+ if (res) {
+ this.res = res;
+ this.tip = tooltip || "";
+ this.enabled = true;
+ this.value = 0;
+ this.set = QButton_set;
+ this.enable = QButton_enable;
+ this.update = QButton_update;
+ this.doEvent = QButton_doEvent;
+ this.onClick = QControl.event;
+ with (this) {
+ document.write('<a href="#" hidefocus="true" unselectable="on"' +
+ (tip ? ' title="' + tip + '"' : '') + ' onClick="return ' + name +
+ '.doEvent()" onMouseOver="' + (res.style == 2 ? name + '.set(1);' : '') +
+ 'window.top.status=' + name + '.tip;return true" onMouseOut="' +
+ (!res.style || (res.style == 2) ? name + '.set();' : '') + 'window.top.status=\'\'"' +
+ (!res.style ? ' onMouseDown="return ' + name + '.set(1)" onMouseUp="return ' + name + '.set()"' : '') +
+ '><img class="qbutton" name="' + id + '" src="' + res.imgN.src + '" border="0" width="' +
+ res.width + '" height="' + res.height + '"></a>');
+ this.image = document.images[id] || new Image(1, 1);
+ }
+ } else {
+ this.document.write("invalid resource");
+ }
+}
+QButton.prototype = new QControl();
+QButton.NORMAL = 0;
+QButton.CHECKBOX = 1;
+QButton.WEB = 2;
+QButton.SIGNAL = 3;
diff --git a/httemplate/elements/qlib/buttonres.js b/httemplate/elements/qlib/buttonres.js
new file mode 100644
index 000000000..97f6dfccc
--- /dev/null
+++ b/httemplate/elements/qlib/buttonres.js
@@ -0,0 +1,23 @@
+/**
+ * QLIB 1.0 Button Resource
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QButtonRes(style, width, height, normal, pressed, disabled) {
+ this.style = style;
+ this.width = width;
+ this.height = height;
+ this.imgN = new Image(width, height);
+ this.imgN.src = normal;
+ this.imgP = new Image(width, height);
+ this.imgP.src = pressed;
+ if (disabled) {
+ this.imgD = new Image(width, height);
+ this.imgD.src = disabled;
+ }
+}
diff --git a/httemplate/elements/qlib/control.js b/httemplate/elements/qlib/control.js
new file mode 100644
index 000000000..f50206e27
--- /dev/null
+++ b/httemplate/elements/qlib/control.js
@@ -0,0 +1,51 @@
+/**
+ * QLIB 1.0 Base Abstract Control
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QControl_init(parent, name) {
+ this.parent = parent || self;
+ this.window = (parent && parent.window) || self;
+ this.document = (parent && parent.document) || self.document;
+ this.name = (parent && parent.name) ? (parent.name + "." + name) : ("self." + name);
+ this.id = "Q";
+ var h = this.hash(this.name);
+ for (var j=0; j<8; j++) {
+ this.id += QControl.HEXTABLE.charAt(h & 15);
+ h >>>= 4;
+ }
+}
+
+function QControl_hash(str) {
+ var h = 0;
+ if (str) {
+ for (var j=str.length-1; j>=0; j--) {
+ h ^= QControl.ANTABLE.indexOf(str.charAt(j)) + 1;
+ for (var i=0; i<3; i++) {
+ var m = (h = h<<7 | h>>>25) & 150994944;
+ h ^= m ? (m == 150994944 ? 1 : 0) : 1;
+ }
+ }
+ }
+ return h;
+}
+
+function QControl_nop() {
+}
+
+function QControl() {
+ this.init = QControl_init;
+ this.hash = QControl_hash;
+ this.window = self;
+ this.document = self.document;
+ this.tag = null;
+}
+QControl.ANTABLE = "w5Q2KkFts3deLIPg8Nynu_JAUBZ9YxmH1XW47oDpa6lcjMRfi0CrhbGSOTvqzEV";
+QControl.HEXTABLE = "0123456789ABCDEF";
+QControl.nop = QControl_nop;
+QControl.event = QControl_nop;
diff --git a/httemplate/elements/qlib/counter.js b/httemplate/elements/qlib/counter.js
new file mode 100644
index 000000000..72aeddbdb
--- /dev/null
+++ b/httemplate/elements/qlib/counter.js
@@ -0,0 +1,81 @@
+/**
+ * QLIB 1.0 Animated Digital Counter
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QCounter_update() {
+ with (this) {
+ var v = Math.max(value, 0);
+ var mod;
+ for (var j=0; j<size; j++) {
+ mod = Math.floor(v % 10);
+ images[j].src = (v >= 1) || (!j) ? res.list[mod].src : res.list[10].src;
+ v /= 10;
+ }
+ }
+}
+
+function QCounter_count(value, step) {
+ this._cntt = false;
+ this.value += step;
+ if ((step * (this.value - value)) >= 0) {
+ this.value = value - 0; // convert to number
+ } else {
+ this._cntt = setTimeout(this.name + ".count(" + value + "," + step + ")", 50);
+ }
+ this.update();
+}
+
+function QCounter_set(value) {
+ this.setval = value;
+ if (value != this.value) {
+ if (this._cntt) {
+ clearTimeout(this._cntt);
+ this._cntt = false;
+ }
+ var dv = value - this.value;
+ if (this.effect == 2) {
+ dv = dv / Math.min(10, Math.abs(dv));
+ } else if (this.effect == 3) {
+ dv = dv / Math.abs(dv);
+ }
+ this.count(value, dv);
+ }
+}
+
+function QCounter(parent, name, res, size, effect) {
+ this.init(parent, name);
+ if (res) {
+ this.res = res;
+ this.setval = this.value = 0;
+ this.size = size || 4;
+ this.effect = effect || 2;
+ this._cntt = false;
+ this.images = new Array(this.size);
+ this.set = QCounter_set;
+ this.update = QCounter_update;
+ this.count = QCounter_count;
+ with (this) {
+ document.write('<table class="qcounter" width="' + (res.width * size) + '" height="' + res.height +
+ '" border="0" cellspacing="0" cellpadding="0" unselectable="on"><tr>');
+ for (var j=(size - 1); j>=0; j--) {
+ document.write('<td width="' + res.width + '" height="' + res.height +
+ '" unselectable="on"><img name="' + id + j + '" src="' + (j ? res.list[10].src : res.list[0].src) +
+ '" border="0" width="' + res.width + '" height="' + res.height + '"></td>');
+ images[j] = document.images[id + j] || new Image(1, 1);
+ }
+ document.write('</tr></table>');
+ }
+ } else {
+ this.document.write("invalid resource");
+ }
+}
+QCounter.prototype = new QControl();
+QCounter.INSTANT = 1;
+QCounter.FAST = 2;
+QCounter.SLOW = 3;
diff --git a/httemplate/elements/qlib/imagelist.js b/httemplate/elements/qlib/imagelist.js
new file mode 100644
index 000000000..9f12de053
--- /dev/null
+++ b/httemplate/elements/qlib/imagelist.js
@@ -0,0 +1,25 @@
+/**
+ * QLIB 1.0 ImageList Resource
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QImageList(width, height) {
+ var len = QImageList.arguments.length - 2;
+ if (len > 0) {
+ this.list = new Array(len);
+ this.length = len;
+ this.width = width;
+ this.height = height;
+ var im;
+ for (var j=0; j<len; j++) {
+ im = new Image(width, height);
+ im.src = QImageList.arguments[j + 2];
+ this.list[j] = im;
+ }
+ }
+} \ No newline at end of file
diff --git a/httemplate/elements/qlib/label.js b/httemplate/elements/qlib/label.js
new file mode 100644
index 000000000..2d8b1e710
--- /dev/null
+++ b/httemplate/elements/qlib/label.js
@@ -0,0 +1,72 @@
+/**
+ * QLIB 1.0 Text Label
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QLabel_set_ie(value) {
+ this.label.innerText = (this.value = value) || "\xA0";
+}
+
+function QLabel_set_dom2(value) {
+ with (this.label) {
+ replaceChild(this.document.createTextNode((this.value = value) || "\xA0"), firstChild);
+ }
+}
+
+function QLabel_set_ns4(value) {
+ this.value = value || "";
+ with (this) {
+ document.open();
+ document.write('<div class="qlabel">' + (clickable ? '<a href="#" title="' + tooltip + '" onClick="return ' +
+ name + '.doEvent()" onMouseOut="window.top.status=\'\'" onMouseOver="window.top.status=' + name +
+ '.tooltip;return true">' + value + '</a>' : value) + '</div>');
+ document.close();
+ }
+}
+
+function QLabel_doEvent() {
+ this.onClick(this.value, this.tag);
+ return false;
+}
+
+function QLabel(parent, name, value, clickable, tooltip) {
+ this.init(parent, name);
+ this.value = value || "";
+ this.clickable = clickable || false;
+ this.tooltip = tooltip || "";
+ this.doEvent = QLabel_doEvent;
+ this.onClick = QControl.event;
+ with (this) {
+ if (document.getElementById || document.all) {
+ document.write(clickable ? '<div class="qlabel" unselectable="on"><a id="' + id + '" href="#" title="' +
+ tooltip + '" onClick="return ' + name + '.doEvent()" onMouseOver="window.top.status=' + name +
+ '.tooltip;return true" onMouseOut="window.top.status=\'\'" hidefocus="true" unselectable="on">' +
+ (value || '&nbsp;') + '</a></div>' : '<div id="' + id + '" class="qlabel" unselectable="on">' +
+ (value || '&nbsp;') + '</div>');
+ this.label = document.getElementById ? document.getElementById(id) :
+ (document.all.item ? document.all.item(id) : document.all[id]);
+ this.set = (label && (label.innerText ? QLabel_set_ie :
+ (label.replaceChild && QLabel_set_dom2))) || QControl.nop;
+ } else if (document.layers) {
+ var suffix = "";
+ for (var j=value.length; j<QLabel.TEXTQUOTA; j++) suffix += " &nbsp;";
+ document.write('<div><ilayer id="i' + id + '"><layer id="' + id + '"><div class="qlabel">' +
+ (clickable ? '<a href="#" title="' + tooltip + '" onClick="return ' + name +
+ '.doEvent()" onMouseOver="window.top.status=' + name +
+ '.tooltip;return true" onMouseOut="window.top.status=\'\'">' + value + suffix + '</a>' :
+ value + suffix) + '</div></layer></ilayer></div>');
+ this.label = (this.label = document.layers["i" + id]) && label.document.layers[id];
+ this.document = label && label.document;
+ this.set = (label && document) ? QLabel_set_ns4 : QControl.nop;
+ } else {
+ document.write("Object is not supported");
+ }
+ }
+}
+QLabel.prototype = new QControl();
+QLabel.TEXTQUOTA = 50;
diff --git a/httemplate/elements/qlib/messagebox.js b/httemplate/elements/qlib/messagebox.js
new file mode 100644
index 000000000..2e458393d
--- /dev/null
+++ b/httemplate/elements/qlib/messagebox.js
@@ -0,0 +1,57 @@
+/**
+ * QLIB 1.0 Message Box Control
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QMessageBox_alert(msg) {
+ if (typeof(msg) == "string") {
+ this.label.set(this.value = msg);
+ }
+ this.center();
+ this.focus();
+ this.show(true);
+}
+
+function QMessageBox_close() {
+ with (this.parent) {
+ if (!onClose(tag)) show(false);
+ }
+}
+
+function QMessageBox_body() {
+ with (this) {
+ document.write('<table border="0" width="' + cwidth + '"><tr><td align="left" valign="top" unselectable="on">');
+ this.label = new QLabel(this, "label", value);
+ document.write('</td></tr><tr><td height="' + (bres.height + 14) + '" align="center" valign="bottom" unselectable="on">');
+ this.button = new QButton(this, "button", bres, "Close");
+ document.write('</td></tr></table>');
+ button.onClick = QMessageBox_close;
+ }
+}
+
+function QMessageBox(parent, name, box, btn, msg, effects, opacity) {
+ this.init(parent, name);
+ if ((this.res = box) && (this.bres = btn)) {
+ this.value = typeof(msg) == "string" ? msg : "";
+ this.width = Math.max(200, Math.floor(Math.sqrt(555 * this.value.length)));
+ this.height = null;
+ this.x = this.y = 0;
+ this.visible = false;
+ this.zindex = null;
+ this.body = QMessageBox_body;
+ var j = QMessageBox.arguments.length;
+ this.effects = j > 5 ? effects : (box.effects != null ? box.effects : 0);
+ this.opacity = j > 6 ? opacity : (box.opacity != null ? box.opacity : 100);
+ this.create();
+ this.alert = QMessageBox_alert;
+ this.onClose = QControl.event;
+ } else {
+ this.document.write("invalid resource");
+ }
+}
+QMessageBox.prototype = new QBoxCtrl();
diff --git a/httemplate/elements/qlib/progress.js b/httemplate/elements/qlib/progress.js
new file mode 100644
index 000000000..2de077eac
--- /dev/null
+++ b/httemplate/elements/qlib/progress.js
@@ -0,0 +1,73 @@
+/**
+ * QLIB 1.0 Progress Control
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QProgress_update() {
+ with (this) {
+ var i = low;
+ for (var j=0; j<size; j++) {
+ images[j].src = i < value ? imgsrc1 : imgsrc0;
+ i += delta;
+ }
+ }
+}
+
+function QProgress_set(value) {
+ this.value = value - 0;
+ this.update();
+}
+
+function QProgress_setBounds(low, high) {
+ this.low = Math.min(low, high);
+ this.high = Math.max(low, high);
+ this.delta = (this.high - this.low) / this.size;
+ this.update();
+}
+
+function QProgress(parent, name, res, size, style) {
+ this.init(parent, name);
+ if (res) {
+ this.res = res;
+ this.value = 0;
+ this.low = 0;
+ this.high = 100;
+ this.size = size || 10;
+ this.delta = 100 / this.size;
+ this.style = style || 0;
+ this.images = new Array(this.size);
+ this.imgsrc0 = res.list[0] && res.list[0].src;
+ this.imgsrc1 = res.list[1] && res.list[1].src;
+ this.set = QProgress_set;
+ this.update = QProgress_update;
+ this.setBounds = QProgress_setBounds;
+ with (this) {
+ var hor = this.style < 2;
+ var rev = this.style % 2;
+ document.write('<table class="qprogress" border="0" cellspacing="0" cellpadding="0" unselectable="on" ' +
+ (hor ? 'width="' + (size * res.width) + '" height="' + res.height + '"><tr>' : 'width="' + res.width +
+ '" height="' + (size * res.height) + '">'));
+ for (var j=0; j<size; j++) {
+ document.write((hor ? '' : '<tr>') + '<td width="' + res.width + '" height="' + res.height +
+ '" unselectable="on"><img name="' + id + (rev ? size - j - 1 : j) + '" src="' + res.list[0].src +
+ '" border="0" width="' + res.width + '" height="' + res.height + '"></td>' + (hor ? '' : '</tr>'));
+ }
+ document.write((hor ? '</tr>' : '') + '</table>');
+ for (var j=0; j<size; j++) {
+ images[j] = document.images[id + j] || new Image(1, 1);
+ }
+ }
+ } else {
+ this.document.write("invalid resource");
+ }
+}
+QProgress.prototype = new QControl();
+QProgress.NORMAL = 0;
+QProgress.REVERSE = 1;
+QProgress.FALL = 2;
+QProgress.RISE = 3;
diff --git a/httemplate/elements/qlib/sound.js b/httemplate/elements/qlib/sound.js
new file mode 100644
index 000000000..3d1aaf660
--- /dev/null
+++ b/httemplate/elements/qlib/sound.js
@@ -0,0 +1,47 @@
+/**
+ * QLIB 1.0 Preloaded Sound
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QSound_play(loop) {
+ this._out.loop = loop || 0;
+ this._out.src = this._buf.src;
+}
+
+function QSound_stop() {
+ this._out.loop = 0;
+ this._out.src = "";
+}
+
+function QSound_setVolume(volume) {
+ this._out.volume = this.volume = volume;
+}
+
+function QSound(parent, name, src, volume) {
+ this.init(parent, name);
+ this.volume = volume || 0;
+ this.play = this.stop = this.setVolume = QControl.nop;
+ with (this) {
+ document.write('<bgsound id="' + id + '" src="" volume="' + volume + '">');
+ if (document.all && document.all.item) {
+ this._out = document.all.item(id);
+ if (_out && (typeof _out.src != "undefined") && (_out.volume === volume)) {
+ document.write('<bgsound id="b' + id + '" src="' + src + '" volume="-10000">');
+ this._buf = document.all.item("b" + id);
+ if (_buf) {
+ this.play = QSound_play;
+ this.stop = QSound_stop;
+ this.setVolume = QSound_setVolume;
+
+ _out.onreadystatechange = new Function("alert(0)");
+ }
+ }
+ }
+ }
+}
+QSound.prototype = new QControl();
diff --git a/httemplate/elements/qlib/sprite.js b/httemplate/elements/qlib/sprite.js
new file mode 100644
index 000000000..72a68fb7c
--- /dev/null
+++ b/httemplate/elements/qlib/sprite.js
@@ -0,0 +1,125 @@
+/**
+ * QLIB 1.0 Sprite Object
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QSprite_load(src) {
+ if (src) {
+ this.face = new Image(this.cwidth, this.cheight);
+ this.face.src = src;
+ this.valid = false;
+ }
+}
+
+function QSprite_show(show) {
+ if (show && !this.valid && this.face.complete) {
+ this._img.src = this.face.src;
+ this.valid = true;
+ }
+ this._show(show);
+}
+
+function QSprite_moveTo(x, y) {
+ this.stop();
+ this._move(x, y);
+}
+
+function QSprite_slideTo(x, y) {
+ this.stop();
+ if (this.visible) {
+ this.doSlide(++this._spro, x, y);
+ } else {
+ this.moveTo(x, y);
+ }
+}
+
+function QSprite_shake() {
+ this.stop();
+ if (this.visible) {
+ this.doShake(++this._spro, 0, this.x, this.y);
+ }
+}
+
+function QSprite_stop() {
+ this._spro++;
+ if (this._sprt) {
+ clearTimeout(this._sprt);
+ this._sprt = false;
+ }
+}
+
+function QSprite_doSlide(id, x, y) {
+ if (this._spro == id) {
+ this._sprt = false;
+ var dx = Math.round(x - this.x);
+ var dy = Math.round(y - this.y);
+ if (dx || dy) {
+ if (dx) dx = dx > 0 ? Math.ceil(dx/4) : Math.floor(dx/4);
+ if (dy) dy = dy > 0 ? Math.ceil(dy/4) : Math.floor(dy/4);
+ this._move(this.x + dx, this.y + dy);
+ this._sprt = setTimeout(this.name + ".doSlide(" + id + "," + x + "," + y + ")", 30);
+ } else {
+ this._move(x, y);
+ }
+ }
+}
+
+function QSprite_doShake(id, phase, x, y) {
+ if (this._spro == id) {
+ this._sprt = false;
+ if (phase < 20) {
+ var m = 3 * Math.sin(.16 * phase);
+ this._move(x + m * Math.sin(phase), y + m * Math.cos(phase));
+ this._sprt = setTimeout(this.name + ".doShake(" + id + "," + (++phase) + "," + x + "," + y + ")", 20);
+ } else {
+ this._move(x, y);
+ }
+ }
+}
+
+function QSprite_doClick() {
+ if (!this._sprt) {
+ this.onClick(this.tag);
+ }
+ return false;
+}
+
+function QSprite(parent, name, x, y, width, height, src, visible, effects, opacity, zindex) {
+ this.init(parent, name);
+ this.x = x - 0;
+ this.y = y - 0;
+ this.width = (this.cwidth = width - 0) + 8;
+ this.height = (this.cheight = height - 0) + 8;
+ var j = QSprite.arguments.length;
+ this.visible = (j > 7) ? visible : true;
+ this.effects = (j > 8) ? effects : 0;
+ this.opacity = (j > 9) ? opacity : 100;
+ this.zindex = (j > 10) ? zindex : null;
+ this.valid = !!src;
+ this.content = '<a href="#" title="" onclick="return false" onmousedown="return ' + this.name +
+ '.doClick()" onmouseover="window.top.status=\'\';return true" hidefocus="true" unselectable="on"><img name="' +
+ this.id + '" src="' + (src || '') + '" border="0" width="' + this.cwidth + '" height="' + this.cheight +
+ '" alt="" unselectable="on"></a>';
+ this.doClick = QSprite_doClick;
+ this.doSlide = QSprite_doSlide;
+ this.doShake = QSprite_doShake;
+ this.onClick = QControl.event;
+ this.create();
+ this.face = this._img = this.document.images[this.id] || new Image(1, 1);
+ this._spro = 0;
+ this._sprt = false;
+ this._show = this.show;
+ this._move = this.moveTo;
+ this.load = QSprite_load;
+ this.show = QSprite_show;
+ this.moveTo = QSprite_moveTo;
+ this.slideTo = QSprite_slideTo;
+ this.shake = QSprite_shake;
+ this.stop = QSprite_stop;
+}
+QSprite.prototype = new QWndCtrl();
diff --git a/httemplate/elements/qlib/window.js b/httemplate/elements/qlib/window.js
new file mode 100644
index 000000000..6056fda9b
--- /dev/null
+++ b/httemplate/elements/qlib/window.js
@@ -0,0 +1,25 @@
+/**
+ * QLIB 1.0 Window Control
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QWindow(parent, name, x, y, width, height, content, visible, effects, opacity, zindex) {
+ this.init(parent, name);
+ this.x = x - 0;
+ this.y = y - 0;
+ this.width = width - 0;
+ this.height = (typeof(height) == "number") ? height : null;
+ this.content = content;
+ var j = QWindow.arguments.length;
+ this.visible = (j > 7) ? visible : true;
+ this.effects = (j > 8) ? effects : 0;
+ this.opacity = (j > 9) ? opacity : 100;
+ this.zindex = (j > 10) ? zindex : null;
+ this.create();
+}
+QWindow.prototype = new QWndCtrl();
diff --git a/httemplate/elements/qlib/wndctrl.js b/httemplate/elements/qlib/wndctrl.js
new file mode 100644
index 000000000..b3bde4e92
--- /dev/null
+++ b/httemplate/elements/qlib/wndctrl.js
@@ -0,0 +1,322 @@
+/**
+ * QLIB 1.0 Window Abstraction
+ * Copyright (C) 2002 2003, Quazzle.com Serge Dolgov
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * http://qlib.quazzle.com
+ */
+
+function QWndCtrl_center_ie4() {
+ var b = this.document.body;
+ this.moveTo(b.scrollLeft + Math.max(0, Math.floor((b.clientWidth -
+ this.width) / 2)), b.scrollTop + 100);
+}
+
+function QWndCtrl_center_moz() {
+ this.moveTo(self.pageXOffset + Math.max(0, Math.floor((self.innerWidth -
+ this.width) / 2)), self.pageYOffset + 100);
+}
+
+function QWndCtrl_setEffects_ie4(fx) {
+ this.effects = fx;
+ with (this.wnd) {
+ filters[0].enabled = (fx & 256) != 0;
+ filters[1].enabled = (fx & 512) != 0;
+ filters[2].enabled = (fx & 1024) != 0;
+ filters[4].enabled = (fx & 2048) != 0;
+ }
+}
+
+function QWndCtrl_setEffects_moz(fx) {
+ this.effects = fx;
+}
+
+function QWndCtrl_setOpacity_ie4(op) {
+ this.opacity = Math.max(0, Math.min(100, Math.floor(op - 0)));
+ this.wnd.filters[3].opacity = this.opacity;
+ this.wnd.filters[3].enabled = (this.opacity < 100);
+}
+
+function QWndCtrl_setOpacity_moz(op) {
+ this.opacity = Math.max(0, Math.min(100, Math.floor(op - 0)));
+ this.wnd.style.MozOpacity = this.opacity + "%";
+}
+
+function QWndCtrl_setSize_css(w, h) {
+ this.wnd.style.width = (this.width = Math.floor(w - 0)) + "px";
+ this.wnd.style.height = typeof(h) == "number" ? (this.height = Math.floor(h)) + "px" : "auto";
+}
+
+function QWndCtrl_setSize_ns4(w, h) {
+ this.wnd.clip.width = this.width = Math.floor(w - 0);
+ if (typeof(h) == "number") {
+ this.wnd.clip.height = this.height = Math.floor(h);
+ }
+}
+
+function QWndCtrl_focus() {
+ this.setZIndex(QWndCtrl.TOPZINDEX++);
+}
+
+function QWndCtrl_setZIndex_css(z) {
+ this.wnd.style.zIndex = this.zindex = z || 0;
+}
+
+function QWndCtrl_setZIndex_ns4(z) {
+ this.wnd.zIndex = this.zindex = z || 0;
+}
+
+function QWndCtrl_moveTo_css(x, y) {
+ this.wnd.style.left = (this.x = Math.floor(x - 0)) + "px";
+ this.wnd.style.top = (this.y = Math.floor(y - 0)) + "px";
+}
+
+function QWndCtrl_moveTo_ns4(x, y) {
+ this.wnd.moveTo(this.x = Math.floor(x - 0), this.y = Math.floor(y - 0));
+}
+
+function QWndCtrl_fxhandler() {
+ this.fxhandler = QControl.nop;
+ this.onShow(this.visible, this.tag);
+}
+
+function QWndCtrl_show_ie4(show) {
+ if (this.visible != show) {
+ var fx = false;
+ switch (show ? this.effects & 15 : (this.effects & 240) >>> 4) {
+ case 1:
+ fx = this.wnd.filters[5];
+ break;
+ case 2:
+ (fx = this.wnd.filters[6]).transition = show ? 1 : 0;
+ break;
+ case 3:
+ (fx = this.wnd.filters[6]).transition = show ? 3 : 2;
+ break;
+ case 4:
+ (fx = this.wnd.filters[6]).transition = show ? 5 : 4;
+ break;
+ case 5:
+ (fx = this.wnd.filters[6]).transition = show ? 14 : 13;
+ break;
+ case 6:
+ (fx = this.wnd.filters[6]).transition = show ? 16 : 15;
+ break;
+ case 7:
+ (fx = this.wnd.filters[6]).transition = 12;
+ break;
+ case 8:
+ (fx = this.wnd.filters[6]).transition = 8;
+ break;
+ case 9:
+ (fx = this.wnd.filters[6]).transition = 9;
+ }
+ if (fx) {
+ fx.apply();
+ this.wnd.style.visibility = (this.visible = show) ? "visible" : "hidden";
+ this.fxhandler = QWndCtrl_fxhandler;
+ fx.play(0.3);
+ } else {
+ this.wnd.style.visibility = (this.visible = show) ? "visible" : "hidden";
+ this.onShow(show, this.tag);
+ }
+ }
+}
+
+function QWndCtrl_fade_moz(op, step) {
+ this._wndt = false;
+ if (step) {
+ op += step;
+ if ((op > 0) && (op < this.opacity)) {
+ this.wnd.style.MozOpacity = op + "%";
+ this._wndt = setTimeout(this.name + ".fade(" + op + "," + step + ")", 50);
+ } else {
+ if (op <= 0) {
+ this.wnd.style.visibility = "hidden";
+ this.visible = false;
+ }
+ this.wnd.style.MozOpacity = this.opacity + "%";
+ this.onShow(this.visible, this.tag);
+ }
+ }
+}
+
+function QWndCtrl_show_moz(show) {
+ if (this.visible != show) {
+ if (this._wndt) {
+ clearTimeout(this._wndt);
+ this._wndt = false;
+ }
+ var step = show ? ((this.effects & 15) == 1) && Math.floor(this.opacity / 5) :
+ ((this.effects & 240) == 16) && -Math.floor(this.opacity / 5);
+ if (step) {
+ if (this.visible) {
+ this.fade(this.opacity - 0, step);
+ } else {
+ this.wnd.style.MozOpacity = "0%";
+ this.wnd.style.visibility = "visible";
+ this.visible = true;
+ this.fade(0, step);
+ }
+ } else {
+ this.wnd.style.visibility = (this.visible = show) ? "visible" : "hidden";
+ this.onShow(show, this.tag);
+ }
+ }
+}
+
+function QWndCtrl_show_css(show) {
+ if (this.visible != show) {
+ this.wnd.style.visibility = (this.visible = show) ? "visible" : "hidden";
+ this.onShow(show, this.tag);
+ }
+}
+
+function QWndCtrl_show_ns4(show) {
+ if (this.visible != show) {
+ this.wnd.visibility = (this.visible = show) ? "show" : "hidden";
+ this.onShow(show, this.tag);
+ }
+}
+
+function QWndCtrl_create_dom2() {
+ with (this) {
+ this.fxhandler = QControl.nop;
+ var ie4 = document.body && document.body.filters;
+ var moz = document.body && document.body.style &&
+ typeof(document.body.style.MozOpacity) == "string";
+ document.write('<div unselectable="on" id="' + id +
+ (ie4 ? '" onfilterchange="' + name + '.fxhandler()': '') +
+ '" style="position:absolute;left:' + x + 'px;top:' + y +
+ 'px;width:' + width + (height != null ? 'px;height:' + height : '') +
+ 'px;visibility:' + (visible ? 'visible' : 'hidden') +
+ ';overflow:hidden' + (zindex ? ';z-index:' + zindex : '') +
+ (ie4 ? ';filter:Gray(enabled=' + (effects & 256 ? '1' : '0') +
+ ') Xray(enabled=' + (effects & 512 ? '1' : '0') +
+ ') Invert(enabled=' + (effects & 1024 ? '1' : '0') +
+ ') alpha(enabled=' + (opacity < 100 ? '1' : '0') + ',opacity=' + opacity +
+ ') shadow(enabled=' + (effects & 2048 ? '1' : '0') +
+ ',direction=135) BlendTrans(enabled=0) RevealTrans(enabled=0)' : '') +
+ (moz && (opacity < 100) ? ';-moz-opacity:' + opacity + '%' : '') +
+ '"><div unselectable="on" class="qwindow">');
+ if (typeof(content) == "function") {
+ this.content();
+ } else {
+ document.write(content);
+ }
+ document.write('</div></div>');
+ if (this.wnd = document.getElementById ? document.getElementById(id) :
+ (document.all.item ? document.all.item(id) : document.all[id])) {
+ if (wnd.style) {
+ ie4 = ie4 && wnd.filters;
+ moz = moz && typeof(wnd.style.MozOpacity) == "string";
+ this.moveTo = QWndCtrl_moveTo_css;
+ this.setZIndex = QWndCtrl_setZIndex_css;
+ this.focus = QWndCtrl_focus;
+ this.setSize = QWndCtrl_setSize_css;
+ this.show = ie4 ? QWndCtrl_show_ie4 : (moz ? QWndCtrl_show_moz : QWndCtrl_show_css);
+ this.fade = moz ? QWndCtrl_fade_moz : QControl.nop;
+ this.setOpacity = ie4 ? QWndCtrl_setOpacity_ie4 : (moz ? QWndCtrl_setOpacity_moz : QControl.nop);
+ this.setEffects = ie4 ? QWndCtrl_setEffects_ie4 : (moz ? QWndCtrl_setEffects_moz : QControl.nop);
+ this.center = self.innerWidth ? QWndCtrl_center_moz :
+ (document.body && document.body.clientWidth ? QWndCtrl_center_ie4 : QControl.nop);
+ }
+ }
+ }
+}
+
+function QWndCtrl_create_ns4(finalize) {
+ with (this) {
+ if (finalize) {
+ if (_wnde) {
+ parent.window.onload = _wnde;
+ parent.window.onload();
+ }
+ document.open();
+ document.write('<div class="qwindow">');
+ this.content();
+ document.write('</div>');
+ document.close();
+ } else {
+ document.write('<layer id="' + id + '" left="' + x + '" top="' + y +
+ '" width="' + width + '" visibility="' + (visible ? 'show' : 'hidden') +
+ (height != null ? '" height="' + height + '" clip="' + width + ',' + height : '') +
+ (zindex ? '" z-index="' + zindex : '') + (typeof(content) != "function" ?
+ '"><div class="qwindow">' + content + '</div></layer>' : '">&nbsp;</layer>'));
+ if (this.window = this.wnd = document.layers[id]) {
+ if (this.document = wnd.document) {
+ this.show = QWndCtrl_show_ns4;
+ this.moveTo = QWndCtrl_moveTo_ns4;
+ this.setZIndex = QWndCtrl_setZIndex_ns4;
+ this.focus = QWndCtrl_focus;
+ this.center = QWndCtrl_center_moz;
+ this.setSize = QWndCtrl_setSize_ns4;
+ if (typeof(content) == "function") {
+ this._wnde = parent.window.onload;
+ parent.window.onload = new Function(name + ".create(true)");
+ }
+ }
+ }
+ }
+ }
+}
+
+function QWndCtrl_create_na() {
+ this.document.write('Object is not supported.');
+ this.wnd = null;
+}
+
+function QWndCtrl_create() {
+ with (this) {
+ this.create = (document.getElementById || document.all) ? QWndCtrl_create_dom2 :
+ (document.layers ? QWndCtrl_create_ns4 : QWndCtrl_create_na);
+ create();
+ }
+}
+
+function QWndCtrl() {
+ this.x = this.y = 0;
+ this.width = this.height = 0;
+ this.content = "";
+ this.visible = true;
+ this.effects = 0;
+ this.opacity = 100;
+ this.zindex = null;
+ this._wndt = this._wnde = false;
+ this.create = QWndCtrl_create;
+ this.show = QControl.nop;
+ this.focus = QControl.nop;
+ this.center = QControl.nop;
+ this.moveTo = QControl.nop;
+ this.setSize = QControl.nop;
+ this.setOpacity = QControl.nop;
+ this.setEffects = QControl.nop;
+ this.setZIndex = QControl.nop;
+ this.onShow = QControl.event;
+}
+QWndCtrl.prototype = new QControl();
+QWndCtrl.TOPZINDEX = 1000;
+QWndCtrl.GRAY = 256;
+QWndCtrl.XRAY = 512;
+QWndCtrl.INVERT = 1024;
+QWndCtrl.SHADOW = 2048;
+QWndCtrl.FADEIN = 1;
+QWndCtrl.FADEOUT = 16;
+QWndCtrl.BOXIN = 2;
+QWndCtrl.BOXOUT = 32;
+QWndCtrl.CIRCLEIN = 3;
+QWndCtrl.CIRCLEOUT = 48;
+QWndCtrl.WIPEIN = 4;
+QWndCtrl.WIPEOUT = 64;
+QWndCtrl.HBARNIN = 5;
+QWndCtrl.HBARNOUT = 80;
+QWndCtrl.VBARNIN = 6;
+QWndCtrl.VBARNOUT = 96;
+QWndCtrl.DISSOLVEIN = 7;
+QWndCtrl.DISSOLVEOUT = 112;
+QWndCtrl.HBLINDSIN = 8;
+QWndCtrl.HBLINDSOUT = 128;
+QWndCtrl.VBLINDSIN = 9;
+QWndCtrl.VBLINDSOUT = 144;
diff --git a/httemplate/elements/search-cust_main.html b/httemplate/elements/search-cust_main.html
new file mode 100644
index 000000000..f2b17eacb
--- /dev/null
+++ b/httemplate/elements/search-cust_main.html
@@ -0,0 +1,164 @@
+%
+% my( %opt ) = @_;
+% $opt{'field_name'} ||= 'custnum';
+%
+% my $cust_main = '';
+% if ( $opt{'value'} ) {
+% $cust_main = qsearchs(
+% 'table' => 'cust_main',
+% 'hashref' => { 'custnum' => $opt{'value'} },
+% 'extra_sql' => " AND ". $FS::CurrentUser::CurrentUser->agentnums_sql,
+% );
+% }
+%
+
+
+<INPUT TYPE="hidden" NAME="<% $opt{'field_name'} %>" VALUE="<% $opt{'value'} %>">
+
+<!-- some false laziness w/ misc/batch-cust_pay.html, though not as bad as i'd thought at first... -->
+
+<INPUT TYPE="text" NAME="<% $opt{'field_name'} %>_search" ID="<% $opt{'field_name'} %>_search" SIZE="32" VALUE="<% $cust_main ? $cust_main->name : '(cust #, name or company)' %>" onFocus="clearhint_<% $opt{'field_name'} %>_search(this);" onClick="clearhint_<% $opt{'field_name'} %>_search(this);" onChange="smart_<% $opt{'field_name'} %>_search(this);">
+
+<SELECT NAME="<% $opt{'field_name'} %>_select" ID="<% $opt{'field_name'} %>_select" STYLE="color:#ff0000; display:none" onChange="select_<% $opt{'field_name'} %>(this);">
+</SELECT>
+
+<% include('/elements/xmlhttp.html',
+ 'url' => $p. 'misc/xmlhttp-cust_main-search.cgi',
+ 'subs' => [ 'smart_search' ],
+ )
+%>
+
+<SCRIPT TYPE="text/javascript">
+
+ function clearhint_<% $opt{'field_name'} %>_search (what) {
+
+ what.style.color = '#000000';
+
+ if ( what.value == '(cust #, name or company)' )
+ what.value = '';
+
+ if ( what.value.indexOf('Customer not found: ') == 0 )
+ what.value = what.value.substr(20);
+
+ }
+
+ function smart_<% $opt{'field_name'} %>_search(what) {
+
+ var customer = what.value;
+
+ if ( customer == 'searching...' || customer == ''
+ || customer.indexOf('Customer not found: ') == 0 )
+ return;
+
+ if ( what.getAttribute('magic') == 'nosearch' ) {
+ what.setAttribute('magic', '');
+ return;
+ }
+
+ //what.value = 'searching...'
+ what.disabled = true;
+ what.style.color= '#000000';
+ what.style.backgroundColor = '#dddddd';
+
+ var customer_select = document.getElementById('<% $opt{'field_name'} %>_select');
+
+ //alert("search for customer " + customer);
+
+ function <% $opt{'field_name'} %>_search_update(customers) {
+
+ //alert('customers returned: ' + customers);
+
+ var customerArray = eval('(' + customers + ')');
+
+ what.disabled = false;
+ what.style.backgroundColor = '#ffffff';
+
+ if ( customerArray.length == 0 ) {
+
+ what.form.<% $opt{'field_name'} %>.value = '';
+
+ what.value = 'Customer not found: ' + what.value;
+ what.style.color = '#ff0000';
+
+ what.style.display = '';
+ customer_select.style.display = 'none';
+
+ } else if ( customerArray.length == 1 ) {
+
+ //alert('one customer found: ' + customerArray[0]);
+
+ what.form.<% $opt{'field_name'} %>.value = customerArray[0][0];
+ what.value = customerArray[0][1];
+
+ what.style.display = '';
+ customer_select.style.display = 'none';
+
+ } else {
+
+ //alert('multiple customers found, have to create select dropdown');
+
+ //blank the current list
+ for ( var i = customer_select.length; i >= 0; i-- )
+ customer_select.options[i] = null;
+
+ opt(customer_select, '', 'Multiple customers match "' + customer + '" - select one', '#ff0000');
+
+ //add the multiple customers
+ for ( var s = 0; s < customerArray.length; s++ )
+ opt(customer_select, customerArray[s][0], customerArray[s][1], '#000000');
+
+ opt(customer_select, 'cancel', '(Edit search string)', '#000000');
+
+ what.style.display = 'none';
+ customer_select.style.display = '';
+
+ }
+
+ }
+
+ smart_search( customer, <% $opt{'field_name'} %>_search_update );
+
+
+ }
+
+ function select_<% $opt{'field_name'} %> (what) {
+
+ var custnum = what.options[what.selectedIndex].value;
+ var customer = what.options[what.selectedIndex].text;
+
+ var customer_obj = document.getElementById('<% $opt{'field_name'} %>_search');
+
+ if ( custnum == '' ) {
+ //what.style.color = '#ff0000';
+
+ } else if ( custnum == 'cancel' ) {
+
+ customer_obj.style.color = '#000000';
+
+ what.style.display = 'none';
+ customer_obj.style.display = '';
+ customer_obj.focus();
+
+ } else {
+
+ what.form.<% $opt{'field_name'} %>.value = custnum;
+
+ customer_obj.value = customer;
+ customer_obj.style.color = '#000000';
+
+ what.style.display = 'none';
+ customer_obj.style.display = '';
+
+ }
+
+ }
+
+ function opt(what,value,text,color) {
+ var optionName = new Option(text, value, false, false);
+ optionName.style.color = color;
+ var length = what.length;
+ what.options[length] = optionName;
+ }
+
+</SCRIPT>
+
diff --git a/httemplate/elements/select-access_group.html b/httemplate/elements/select-access_group.html
new file mode 100644
index 000000000..299a66a45
--- /dev/null
+++ b/httemplate/elements/select-access_group.html
@@ -0,0 +1,16 @@
+%
+% my( $groupnum, %opt ) = @_;
+%
+% %opt{'records'} = delete $opt{'access_group'}
+% if $opt{'access_group'};
+%
+%
+<% include( '/elements/select-table.html',
+ 'table' => 'access_group',
+ 'name_col' => 'groupname',
+ 'value' => $groupnum,
+ 'empty_label' => '(none)',
+ #'hashref' => { 'disabled' => '' },
+ %opt,
+ )
+%>
diff --git a/httemplate/elements/select-agent.html b/httemplate/elements/select-agent.html
new file mode 100644
index 000000000..3107ff9d4
--- /dev/null
+++ b/httemplate/elements/select-agent.html
@@ -0,0 +1,19 @@
+%
+% my( $agentnum, %opt ) = @_;
+%
+% $opt{'records'} = delete $opt{'agents'}
+% if $opt{'agents'};
+%
+%
+<% include( '/elements/select-table.html',
+ 'table' => 'agent',
+ 'name_col' => 'agent',
+ 'value' => $agentnum || '',
+ 'empty_label' => 'all',
+ 'hashref' => { 'disabled' => '' },
+ 'extra_sql' => ' AND '.
+ $FS::CurrentUser::CurrentUser->agentnums_sql.
+ ' ORDER BY agent',
+ %opt,
+ )
+%>
diff --git a/httemplate/elements/select-cust-fields.html b/httemplate/elements/select-cust-fields.html
new file mode 100644
index 000000000..98feaf85c
--- /dev/null
+++ b/httemplate/elements/select-cust-fields.html
@@ -0,0 +1,24 @@
+%
+% my( $cust_fields, %opt ) = @_;
+%
+% use FS::ConfDefaults;
+% $opt{'avail_fields'} ||= [ FS::ConfDefaults->cust_fields_avail() ];
+%
+% tie my %hash, 'Tie::IxHash', @{ $opt{'avail_fields'} };
+%
+%
+
+
+<SELECT NAME="cust_fields">
+
+ <OPTION VALUE="">(configured default)
+%
+% foreach my $value ( keys %hash ) {
+
+
+ <OPTION VALUE="<% $value %>"><% $hash{$value} %>
+% }
+
+
+</SELECT>
+
diff --git a/httemplate/elements/select-cust_pkg-status.html b/httemplate/elements/select-cust_pkg-status.html
new file mode 100644
index 000000000..71aaa84b6
--- /dev/null
+++ b/httemplate/elements/select-cust_pkg-status.html
@@ -0,0 +1,21 @@
+<SELECT NAME="status" <% $opt{'onchange'} %>>
+
+ <OPTION VALUE="">all
+
+% foreach my $option ( @{ $opt{'statuses'} } ) {
+ <OPTION VALUE="<% $option %>" <% $option eq $status ? 'SELECTED' : '' %>><% $option %>
+% }
+
+</SELECT>
+<%init>
+ my( $status, %opt ) = @_;
+
+ $opt{'statuses'} ||= [ FS::cust_pkg->statuses() ]; # { disabled=>'' } )
+
+ if ( exists $opt{'onchange'} && $opt{'onchange'} ) {
+ $opt{'onchange'} = ' onChange="' . $opt{'onchange'}. '"';
+ } else {
+ $opt{'onchange'} = '';
+ }
+
+</%init>
diff --git a/httemplate/elements/select-month_year.html b/httemplate/elements/select-month_year.html
new file mode 100644
index 000000000..34476bc94
--- /dev/null
+++ b/httemplate/elements/select-month_year.html
@@ -0,0 +1,62 @@
+%
+%
+% my %opt = @_;
+%
+% my $prefix = $opt{'prefix'} || '';
+% my $disabled = $opt{'disabled'} || '';
+% my $empty = $opt{'empty_option'} || '';
+% my $start_year = $opt{'start_year'};
+% my $end_year = $opt{'end_year'} || '2037';
+%
+% my @mon;
+% if ( $opt{'show_month_abbr'} ) {
+% @mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
+% } else {
+% @mon = ( 1 .. 12 );
+% }
+%
+% my $date = $opt{'selected_date'} || '';
+% $date = '' if $date eq '-';
+% #$date ||= '01-2000' unless $empty;
+%
+% my $mon = $opt{'selected_mon'} || 0;
+% my $year = $opt{'selected_year'} || 0;
+% if ( $date ) {
+% if ( $date =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #PostgreSQL date format
+% ( $mon, $year ) = ( $2, $1 );
+% } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
+% ( $mon, $year ) = ( $1, $3 );
+% } else {
+% die "unrecognized expiration date format: $date";
+% }
+% }
+%
+% unless ( $start_year ) {
+% my @t = localtime;
+% $start_year = $t[5] + 1900;
+% }
+% $start_year = $year if $start_year > $year && $year > 0;
+%
+%
+
+
+<SELECT NAME="<% $prefix %>_month" SIZE="1" <% $disabled%>>
+
+<% $empty ? '<OPTION VALUE="">' : '' %>
+% foreach ( 1 .. 12 ) {
+
+ <OPTION<% $_ == $mon ? ' SELECTED' : '' %> VALUE="<% $_ %>"><% $mon[$_-1] %>
+% }
+
+
+</SELECT>/<SELECT NAME="<% $prefix %>_year" SIZE="1" <% $disabled%>>
+
+<% $empty ? '<OPTION VALUE="">' : '' %>
+% for ( $start_year .. $end_year ) {
+
+ <OPTION<% $_ == $year ? ' SELECTED' : '' %> VALUE="<% $_ %>"><% $_ %>
+% }
+
+
+</SELECT>
+
diff --git a/httemplate/elements/select-part_referral.html b/httemplate/elements/select-part_referral.html
new file mode 100644
index 000000000..670b70c7c
--- /dev/null
+++ b/httemplate/elements/select-part_referral.html
@@ -0,0 +1,18 @@
+%
+% my( $refnum, %opt ) = @_;
+%
+% $opt{'records'} = delete $opt{'part_referrals'}
+% if $opt{'part_referrals'};
+%
+%
+<% include( '/elements/select-table.html',
+ 'table' => 'part_referral',
+ 'name_col' => 'referral',
+ 'value' => $refnum,
+ 'empty_label' => 'Select advertising source',
+ 'hashref' => { 'disabled' => '' },
+ 'extra_sql' => ' AND '.
+ FS::part_referral->acl_agentnum_sql(1),
+ %opt,
+ )
+%>
diff --git a/httemplate/elements/select-pkg_class.html b/httemplate/elements/select-pkg_class.html
new file mode 100644
index 000000000..8e873ed66
--- /dev/null
+++ b/httemplate/elements/select-pkg_class.html
@@ -0,0 +1,16 @@
+% my( $classnum, %opt ) = @_;
+%
+% $opt{'records'} = delete $opt{'pkg_class'}
+% if $opt{'pkg_class'};
+%
+% #warn "***** select-pkg-class: \n". Dumper(%opt);
+%
+<% include( '/elements/select-table.html',
+ 'table' => 'pkg_class',
+ 'name_col' => 'classname',
+ 'value' => $classnum,
+ 'empty_label' => '(none)',
+ 'hashref' => { 'disabled' => '' },
+ %opt,
+ )
+%>
diff --git a/httemplate/elements/select-table.html b/httemplate/elements/select-table.html
new file mode 100644
index 000000000..3235ef627
--- /dev/null
+++ b/httemplate/elements/select-table.html
@@ -0,0 +1,85 @@
+<SELECT NAME="<% $opt{'element_name'} || $key %>" <% $opt{'element_etc'} %>>
+
+% while ( @pre_options ) {
+
+ <OPTION VALUE="<% shift(@pre_options) %>"><% shift(@pre_options) %>
+
+% }
+
+ <OPTION VALUE=""><% $opt{'empty_label'} || 'all' %>
+
+% foreach my $record ( sort { $a->$name_col() cmp $b->$name_col() } @records ) {
+% my $matches = 0;
+% ref($opt{'value'}) ? (exists($opt{'value'}->{$record->$key()}) and $matches=1)
+% : ($opt{'value'} == $record->$key() and $matches=1);
+
+ <OPTION VALUE="<% $record->$key() %>"<% $matches ? ' SELECTED' : '' %>><% $record->$name_col() %>
+
+% }
+
+</SELECT>
+<%init>
+
+##required
+# 'table' => 'table_name',
+# 'name_col' => 'name_column',
+#
+##strongly recommended (you want your forms to be "sticky" on errors, right?)
+# 'value' => 'current_value',
+#
+##opt
+# 'empty_label' => '', #better specify it though, the default might change
+# 'hashref' => {},
+# 'extra_sql' => '',
+# 'records' => \@records, #instead of hashref
+# 'pre_options' => [ 'value' => 'option' ], #before normal options
+# 'element_name' => '', #HTML element name, defaults to the name of
+# # the primary key column
+# 'element_etc' => '', #additional attributes (i.e. "DISABLED") for the
+# #<SELECT> element
+# 'debug' => 0, #set true to enable
+
+my( %opt ) = @_;
+
+warn "elements/select-table.html: \n". Dumper(%opt)
+ if exists $opt{debug} && $opt{debug};
+
+my $key = dbdef->table($opt{table})->primary_key; #? $opt{primary_key} ||
+
+my $name_col = $opt{name_col};
+
+$opt{hashref} ||= {};
+
+my @records = ();
+if ( $opt{records} ) {
+ @records = @{ $opt{records} };
+} else {
+ @records = qsearch( {
+ 'table' => $opt{table},
+ 'hashref' => $opt{hashref},
+ 'extra_sql' => ( $opt{extra_sql} || '' ),
+ });
+}
+
+unless ( ! $opt{value}
+ or ref($opt{value})
+ or ! exists( $opt{hashref}->{disabled} ) #??
+ or grep { $opt{value} == $_->$key() } @records
+ ) {
+ delete $opt{hashref}->{disabled};
+ $opt{hashref}->{$key} = $opt{value};
+ my $record = qsearchs( {
+ 'table' => $opt{table},
+ 'hashref' => $opt{hashref},
+ 'extra_sql' => ( $opt{extra_sql} || '' ),
+ });
+ push @records, $record if $record;
+}
+
+if ( ref( $opt{value} ) eq 'ARRAY' ) {
+ $opt{value} = { map { $_ => 1 } @{$opt{value}} };
+}
+
+my @pre_options = $opt{pre_options} ? @{ $opt{pre_options} } : ();
+
+</%init>
diff --git a/httemplate/elements/select-taxclass.html b/httemplate/elements/select-taxclass.html
new file mode 100644
index 000000000..3c1558b72
--- /dev/null
+++ b/httemplate/elements/select-taxclass.html
@@ -0,0 +1,38 @@
+% if ( $conf->exists('enable_taxclasses') ) {
+
+ <SELECT NAME="taxclass">
+
+% if ( $conf->exists('require_taxclasses') ) {
+ <OPTION VALUE="(select)">Select tax class
+% } else {
+ <OPTION VALUE="">
+% }
+
+% foreach my $taxclass ( @{ $opt{'taxclasses'} } ) {
+ <OPTION VALUE="<% $taxclass %>"<% $taxclass eq $selected_taxclass ? ' SELECTED' : '' %>><% $taxclass %>
+% }
+
+ </SELECT>
+
+% } else {
+
+ <INPUT TYPE="hidden" NAME="taxclass" VALUE="<% $selected_taxclass %>">
+
+% }
+
+<%init>
+
+my( $selected_taxclass, %opt ) = @_;
+my $conf = new FS::Conf;
+
+unless ( $opt{'taxclasses'} ) {
+
+ my $sth = dbh->prepare('SELECT DISTINCT taxclass FROM cust_main_county')
+ or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+ my %taxclasses = map { $_->[0] => 1 } @{$sth->fetchall_arrayref};
+ @{ $opt{'taxclasses'} } = grep $_, keys %taxclasses;
+
+}
+
+</%init>
diff --git a/httemplate/elements/small_custview.html b/httemplate/elements/small_custview.html
new file mode 100644
index 000000000..9060d897d
--- /dev/null
+++ b/httemplate/elements/small_custview.html
@@ -0,0 +1,3 @@
+% my $conf = new FS::Conf;
+
+<% small_custview( shift, shift || scalar($conf->config('countrydefault')), @_ ) %>
diff --git a/httemplate/elements/table-grid.html b/httemplate/elements/table-grid.html
new file mode 100644
index 000000000..0f532e86b
--- /dev/null
+++ b/httemplate/elements/table-grid.html
@@ -0,0 +1,21 @@
+%
+% my %opt = @_;
+% $opt{cellspacing} ||= 0;
+% $opt{cellpadding} ||= 0;
+%
+%
+
+<STYLE TYPE="text/css">
+
+.grid table { border: solid; empty-cells: show }
+.grid TH { padding-left: 3px; padding-right: 3px; border: 1px solid #dddddd; border-bottom: dashed 1px black; border-right: none }
+.grid TD { padding-left: 3px; padding-right: 3px; empty-cells: show; border: 1px solid #cccccc; border-bottom: none; border-right: none }
+
+.inv table { border: none }
+.inv TH { border: none }
+.inv TD { border: none }
+
+</STYLE>
+
+<TABLE CLASS="grid" CELLSPACING=<% $opt{cellspacing} %> CELLPADDING=<% $opt{cellpadding} %> BORDER=1 BORDERCOLOR="#000000" STYLE="border: solid 1px black; empty-cells: show">
+
diff --git a/httemplate/elements/table.html b/httemplate/elements/table.html
new file mode 100644
index 000000000..8152b65d8
--- /dev/null
+++ b/httemplate/elements/table.html
@@ -0,0 +1,11 @@
+%
+% 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/elements/tr-input-beginning_ending.html b/httemplate/elements/tr-input-beginning_ending.html
new file mode 100644
index 000000000..9c067dbea
--- /dev/null
+++ b/httemplate/elements/tr-input-beginning_ending.html
@@ -0,0 +1,63 @@
+<LINK REL="stylesheet" TYPE="text/css" HREF="<%$fsurl%>elements/calendar-win2k-2.css" TITLE="win2k-2">
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/calendar_stripped.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/calendar-en.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/calendar-setup.js"></SCRIPT>
+
+<TR>
+ <TD ALIGN="right">From date: </TD>
+ <TD><INPUT TYPE="text" NAME="<% $opt{prefix} %>beginning" ID="<% $opt{prefix} %>beginning_text" VALUE="" SIZE=<%$size%> MAXLENGTH=<%$maxlength%>> <IMG SRC="<%$fsurl%>images/calendar.png" ID="<% $opt{prefix} %>beginning_button" STYLE="cursor: pointer" TITLE="Select date"><IMG SRC="<%$fsurl%>images/calendar-disabled.png" ID="<% $opt{prefix} %>beginning_disabled" STYLE="display:none"><BR><i>m/d/y<% $time_hint %></i></TD>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "<% $opt{prefix} %>beginning_text",
+ ifFormat: "%m/%d/%Y<% $time_format %>",
+ button: "<% $opt{prefix} %>beginning_button",
+ align: "BR"
+ <% $input_time %>
+ });
+</SCRIPT>
+
+% unless ( $opt{layout} =~ /^h/i ) { #horizontal
+
+</TR>
+<TR>
+
+% }
+
+ <TD ALIGN="right">To date: </TD>
+ <TD><INPUT TYPE="text" NAME="<% $opt{prefix} %>ending" ID="<% $opt{prefix} %>ending_text" VALUE="" SIZE=<%$size%> MAXLENGTH=<%$maxlength%>> <IMG SRC="<%$fsurl%>images/calendar.png" ID="<% $opt{prefix} %>ending_button" STYLE="cursor: pointer" TITLE="Select date"><IMG SRC="<%$fsurl%>images/calendar-disabled.png" ID="<% $opt{prefix} %>ending_disabled" STYLE="display:none"><BR><i>m/d/y<% $time_hint %></i></TD>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "<% $opt{prefix} %>ending_text",
+ ifFormat: "%m/%d/%Y<% $time_format %>",
+ button: "<% $opt{prefix} %>ending_button",
+ align: "BR"
+ <% $input_time %>
+ });
+</SCRIPT>
+</TR>
+
+<TR>
+ <TD></TD>
+ <TD COLSPAN=<% $opt{layout} =~ /^h/i ? 3 : 1 %>>
+ <FONT SIZE="-1">(leave one or both dates blank for an open-ended search)</FONT>
+ </TD>
+</TR>
+
+<%init>
+
+my %opt = @_;
+
+$opt{prefix} = '' unless defined $opt{prefix};
+$opt{prefix} .= '_' if $opt{prefix};
+
+my( $input_time, $time_format, $time_hint ) = ( '', '', '' );
+my( $size, $maxlength ) = ( 11, 10 );
+if ( $opt{'input_time'} ) {
+ $input_time = ', showsTime: true, timeFormat: "12"'; # http://www.dynarch.com/demos/jscalendar/doc/html/reference.html#node_sec_2.3
+ $time_format = ' %k:%M:%S'; # http://www.dynarch.com/demos/jscalendar/doc/html/reference.html#node_sec_5.3.5
+ $time_hint = ' h:m:s';
+ $size = 21;
+ $maxlength = 27;
+}
+
+</%init>
diff --git a/httemplate/elements/tr-input-date-field.html b/httemplate/elements/tr-input-date-field.html
new file mode 100644
index 000000000..11581d5bc
--- /dev/null
+++ b/httemplate/elements/tr-input-date-field.html
@@ -0,0 +1,40 @@
+
+<LINK REL="stylesheet" TYPE="text/css" HREF="<%$fsurl%>elements/calendar-win2k-2.css" TITLE="win2k-2">
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/calendar_stripped.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/calendar-en.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/calendar-setup.js"></SCRIPT>
+
+<TR>
+ <TD ALIGN="right"><% $label %></TD>
+ <TD>
+ <INPUT TYPE="text" NAME="<% $name %>" ID="<% $name %>_text" VALUE="<% $value %>">
+ <IMG SRC="<%$fsurl%>images/calendar.png" ID="<% $name %>_button" STYLE="cursor: pointer" TITLE="Select date">
+ </TD>
+</TR>
+
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "<% $name %>_text",
+ ifFormat: "<% $format %>",
+ button: "<% $name %>_button",
+ align: "BR"
+ });
+</SCRIPT>
+
+
+<%init>
+my($name, $value, $label, $format, $usedatetime) = @_;
+
+$format = "%m/%d/%Y" unless $format;
+$label = $name unless $label;
+
+if ($usedatetime) {
+ my $dt = DateTime->from_epoch(epoch => $value, time_zone => 'floating');
+ $value = $dt->strftime($format)
+ unless $value eq '';
+}else{
+ $value = time2str($format, $value);
+}
+
+</%init>
+
diff --git a/httemplate/elements/tr-input-lessthan_greaterthan.html b/httemplate/elements/tr-input-lessthan_greaterthan.html
new file mode 100644
index 000000000..16c2ed9fc
--- /dev/null
+++ b/httemplate/elements/tr-input-lessthan_greaterthan.html
@@ -0,0 +1,13 @@
+<TR>
+ <TD ALIGN="right"><% $opt{label} %> less than: </TD>
+ <TD><INPUT TYPE="text" NAME="<% $opt{field} %>_lt" SIZE=7></TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right"><% $opt{label} %> greater than: </TD>
+ <TD><INPUT TYPE="text" NAME="<% $opt{field} %>_gt" SIZE=7></TD>
+</TR>
+
+<%init>
+ my %opt = @_;
+</%init>
diff --git a/httemplate/elements/tr-select-access_group.html b/httemplate/elements/tr-select-access_group.html
new file mode 100644
index 000000000..e443ad26a
--- /dev/null
+++ b/httemplate/elements/tr-select-access_group.html
@@ -0,0 +1,22 @@
+%
+% my( $groupnum, %opt ) = @_;
+%
+% $opt{'access_group'} ||= [ qsearch( 'access_group', {} ) ]; # { disabled=>'' } )
+%
+% #warn "***** tr-select-access_group: \n". Dumper(%opt);
+%
+% if ( scalar(@{ $opt{'access_group'} }) == 0 ) {
+
+
+ <INPUT TYPE="hidden" NAME="groupnum" VALUE="">
+% } else {
+
+
+ <TR>
+ <TD ALIGN="right"><% $opt{'label'} || 'Access group' %></TD>
+ <TD>
+ <% include( '/elements/select-access_group.html', $groupnum, %opt ) %>
+ </TD>
+ </TR>
+% }
+
diff --git a/httemplate/elements/tr-select-agent.html b/httemplate/elements/tr-select-agent.html
new file mode 100644
index 000000000..37b1c1e88
--- /dev/null
+++ b/httemplate/elements/tr-select-agent.html
@@ -0,0 +1,34 @@
+%
+% my( $agentnum, %opt ) = @_;
+%
+% my @agents;
+% if ( $opt{'agents'} ) {
+% #@agents = @{ $opt{'agents'} };
+% #here is the agent virtualization
+% my $agentnums_href = $FS::CurrentUser::CurrentUser->agentnums_href;
+% @agents = grep $agentnums_href->{$_->agentnum}, @{ $opt{'agents'} };
+% delete $opt{'agents'};
+% } else {
+% @agents = $FS::CurrentUser::CurrentUser->agents;
+% }
+%
+%
+% if ( scalar(@agents) == 1 ) {
+
+
+ <INPUT TYPE="hidden" NAME="agentnum" VALUE="<% $agents[0]->agentnum %>">
+% } else {
+
+
+ <TR>
+ <TD ALIGN="right"><% $opt{'label'} || 'Agent' %></TD>
+ <TD>
+ <% include( '/elements/select-agent.html', $agentnum,
+ 'agents' => \@agents,
+ %opt,
+ )
+ %>
+ </TD>
+ </TR>
+% }
+
diff --git a/httemplate/elements/tr-select-cust-fields.html b/httemplate/elements/tr-select-cust-fields.html
new file mode 100644
index 000000000..80562fe3d
--- /dev/null
+++ b/httemplate/elements/tr-select-cust-fields.html
@@ -0,0 +1,15 @@
+%
+% my( $cust_fields, %opt ) = @_;
+%
+% use FS::ConfDefaults;
+% $opt{'avail_fields'} ||= [ FS::ConfDefaults->cust_fields_avail() ];
+%
+%
+
+
+<TR>
+ <TD ALIGN="right"><% $opt{'label'} || 'Customer fields' %></TD>
+ <TD>
+ <% include( '/elements/select-cust-fields.html', $cust_fields, %opt ) %>
+ </TD>
+</TR>
diff --git a/httemplate/elements/tr-select-cust_pkg-status.html b/httemplate/elements/tr-select-cust_pkg-status.html
new file mode 100644
index 000000000..22ee146cd
--- /dev/null
+++ b/httemplate/elements/tr-select-cust_pkg-status.html
@@ -0,0 +1,14 @@
+%
+% my( $status, %opt ) = @_;
+%
+% $opt{'statuses'} ||= [ FS::cust_pkg->statuses() ]; # { disabled=>'' } )
+%
+%
+
+
+<TR>
+ <TD ALIGN="right"><% $opt{'label'} || 'Status' %></TD>
+ <TD>
+ <% include( '/elements/select-cust_pkg-status.html', $status, %opt ) %>
+ </TD>
+</TR>
diff --git a/httemplate/elements/tr-select-from_to.html b/httemplate/elements/tr-select-from_to.html
new file mode 100644
index 000000000..083243d40
--- /dev/null
+++ b/httemplate/elements/tr-select-from_to.html
@@ -0,0 +1,52 @@
+%
+%
+% #my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
+% my ($curmon,$curyear) = (localtime(time))[4,5];
+%
+% #find first month
+% my $syear = 1899+$curyear;
+% my $smonth = $curmon+1;
+%
+% #want 12 month by default, not 13
+% $smonth++;
+% if ( $smonth > 12 ) { $smonth-=12; $syear++ }
+%
+% #find last month
+% my $eyear = 1900+$curyear;
+% my $emonth = $curmon+1;
+%
+% my %hash = (
+% 'show_month_abbr' => 1,
+% 'start_year' => '1999',
+% 'end_year' => '2012', #haha, well...
+% @_,
+% );
+%
+%
+
+
+<TR>
+ <TD ALIGN="right">From: </TD>
+ <TD>
+ <% include('/elements/select-month_year.html',
+ 'prefix' => 'start',
+ 'selected_mon' => $smonth,
+ 'selected_year' => $syear,
+ %hash,
+ )
+ %>
+ </TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right">To: </TD>
+ <TD>
+ <% include('/elements/select-month_year.html',
+ 'prefix' => 'end',
+ 'selected_mon' => $emonth,
+ 'selected_year' => $eyear,
+ %hash,
+ )
+ %>
+ </TD>
+</TR>
diff --git a/httemplate/elements/tr-select-part_referral.html b/httemplate/elements/tr-select-part_referral.html
new file mode 100644
index 000000000..35c5b8047
--- /dev/null
+++ b/httemplate/elements/tr-select-part_referral.html
@@ -0,0 +1,30 @@
+%
+% my( $refnum, %opt ) = @_;
+%
+% $opt{'part_referrals'} ||=
+% [ FS::part_referral->all_part_referral( 1 ) ]; #1: include global
+%
+% my $r = qq!<font color="#ff0000">*</font>&nbsp;!;
+%
+%
+% if ( scalar( @{$opt{'part_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.html and create one or more advertising sources.";
+% } elsif ( scalar( @{$opt{'part_referrals'}} ) == 1 ) {
+%
+
+
+ <INPUT TYPE="hidden" NAME="refnum" VALUE="<% $opt{'part_referrals'}->[0]->refnum %>">
+% } else {
+
+
+ <TR>
+ <TH ALIGN="right"><%$r%>Advertising source</TH>
+ <TD>
+ <% include( '/elements/select-part_referral.html', $refnum,
+ 'part_referrals' => $opt{'part_referrals'},
+ )
+ %>
+ </TD>
+ </TR>
+% }
+
diff --git a/httemplate/elements/tr-select-pkg_class.html b/httemplate/elements/tr-select-pkg_class.html
new file mode 100644
index 000000000..542142466
--- /dev/null
+++ b/httemplate/elements/tr-select-pkg_class.html
@@ -0,0 +1,20 @@
+% my( $classnum, %opt ) = @_;
+%
+% $opt{'pkg_class'} ||= [ qsearch( 'pkg_class', { disabled=>'' } ) ];
+%
+% #warn "***** tr-select-pkg-class: \n". Dumper(%opt);
+%
+% if ( scalar(@{ $opt{'pkg_class'} }) == 0 ) {
+
+ <INPUT TYPE="hidden" NAME="classnum" VALUE="">
+
+% } else {
+
+ <TR>
+ <TD ALIGN="right"><% $opt{'label'} || 'Package class' %></TD>
+ <TD>
+ <% include( '/elements/select-pkg_class.html', $classnum, %opt ) %>
+ </TD>
+ </TR>
+
+% }
diff --git a/httemplate/elements/tr-select-reason.html b/httemplate/elements/tr-select-reason.html
new file mode 100755
index 000000000..2f8f3a109
--- /dev/null
+++ b/httemplate/elements/tr-select-reason.html
@@ -0,0 +1,101 @@
+
+<SCRIPT TYPE="text/javascript">
+ function sh_add<% $name %>()
+ {
+
+ if (document.getElementById('<% $name %>').selectedIndex == 0){
+ <% $controlledbutton ? $controlledbutton.'.disabled = true;' : ';' %>
+ }else{
+ <% $controlledbutton ? $controlledbutton.'.disabled = false;' : ';' %>
+ }
+
+%if ($curuser->access_right($access_right)){
+
+ if (document.getElementById('<% $name %>').selectedIndex ==
+ (document.getElementById('<% $name %>').length - 1)) {
+ document.getElementById('new<% $name %>').disabled = false;
+ document.getElementById('new<% $name %>').style.display = 'inline';
+ document.getElementById('new<% $name %>Label').style.display = 'inline';
+ document.getElementById('new<% $name %>T').disabled = false;
+ document.getElementById('new<% $name %>T').style.display = 'inline';
+ document.getElementById('new<% $name %>TLabel').style.display = 'inline';
+ }else{
+ document.getElementById('new<% $name %>').disabled = true;
+ document.getElementById('new<% $name %>').style.display = 'none';
+ document.getElementById('new<% $name %>Label').style.display = 'none';
+ document.getElementById('new<% $name %>T').disabled = true;
+ document.getElementById('new<% $name %>T').style.display = 'none';
+ document.getElementById('new<% $name %>TLabel').style.display = 'none';
+ }
+
+%}
+
+ }
+</SCRIPT>
+
+<TR>
+ <TD ALIGN="right">Reason</TD>
+ <TD>
+ <SELECT id="<% $name %>" name="<% $name %>" onFocus="sh_add<% $name %>()" onChange="sh_add<% $name %>()">
+% my @reasons = qsearch( { table =>'reason',
+% hashref => {},
+% extra_sql => $extra_sql,
+% addl_from => 'LEFT JOIN reason_type ON reason_type.typenum = reason.reason_type',
+% });
+ <OPTION VALUE="" <% ($init_reason eq "") ? 'SELECTED' : '' %>>Select Reason...</OPTION>
+% foreach my $reason (@reasons) {
+ <OPTION VALUE="<% $reason->reasonnum %>" <% ($init_reason == $reason->reasonnum) ? 'SELECTED' : '' %>><% $reason->reason %></OPTION>
+% }
+% if ($curuser->access_right($access_right)) {
+ <OPTION VALUE="-1" <% ($init_reason == -1) ? 'SELECTED' : '' %>>Add new reason</OPTION>
+% }
+%
+ </SELECT>
+ </TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right">
+ <P id="new<% $name %>TLabel" style="display:<% $display %>">Reason Type</P>
+ </TD>
+ <TD>
+ <SELECT id="new<% $name %>T" name="new<% $name %>T" disabled="<% $disabled %>" style="display:<% $display %>">
+% for my $type (qsearch( 'reason_type', { 'class' => $class } )){
+ <OPTION VALUE="<% $type->typenum %>" <% ($init_type == $type->typenum) ? 'SELECTED' : '' %>><% $type->type %></OPTION>
+% }
+ </SELECT>
+ </TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right">
+ <P id="new<% $name %>Label" style="display:<% $display %>">New Reason</P>
+ </TD>
+ <TD><INPUT id="new<% $name %>" name="new<% $name %>" type="text" value="<% $init_newreason %>" disabled="<% $disabled %>" style="display:<% $display %>"></TD>
+</TR>
+
+<%init>
+my($name, $class, $init_reason, $init_type, $init_newreason, $controlledbutton) = @_;
+my($extra_sql, $curuser, $access_right, $display, $disabled);
+
+if ($class eq 'C') {
+ $access_right='Add on-the-fly cancel reason';
+}elsif ($class eq 'S') {
+ $access_right='Add on-the-fly suspend reason';
+}else{
+ print "illegal class: $class";
+}
+
+if ($init_reason == -1){
+ $display = 'inline';
+ $disabled = 'false';
+}else{
+ $display = 'none';
+ $disabled = 'true';
+}
+
+$extra_sql = "WHERE class = '$class' ORDER BY reason_type";
+$curuser = $FS::CurrentUser::CurrentUser;
+
+</%init>
+
diff --git a/httemplate/elements/tr-select-taxclass.html b/httemplate/elements/tr-select-taxclass.html
new file mode 100644
index 000000000..424d5ad02
--- /dev/null
+++ b/httemplate/elements/tr-select-taxclass.html
@@ -0,0 +1,32 @@
+% if ( ! $conf->exists('enable_taxclasses')
+% || scalar(@{ $opt{'taxclasses'} }) == 0
+% ) {
+
+ <INPUT TYPE="hidden" NAME="taxclass" VALUE="<% $taxclass %>">
+
+% } else {
+
+ <TR>
+ <TD ALIGN="right"><% $opt{'label'} || 'Tax class: ' %></TD>
+ <TD>
+ <% include( '/elements/select-taxclass.html', $taxclass, %opt ) %>
+ </TD>
+ </TR>
+
+% }
+<%init>
+
+my( $taxclass, %opt ) = @_;
+my $conf = new FS::Conf;
+
+unless ( $opt{'taxclasses'} ) {
+
+ my $sth = dbh->prepare('SELECT DISTINCT taxclass FROM cust_main_county')
+ or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+ my %taxclasses = map { $_->[0] => 1 } @{$sth->fetchall_arrayref};
+ @{ $opt{'taxclasses'} } = grep $_, keys %taxclasses;
+
+}
+
+</%init>
diff --git a/httemplate/elements/tr-selectmultiple-part_pkg.html b/httemplate/elements/tr-selectmultiple-part_pkg.html
new file mode 100644
index 000000000..bd96d1c3a
--- /dev/null
+++ b/httemplate/elements/tr-selectmultiple-part_pkg.html
@@ -0,0 +1,19 @@
+%
+% my( %opt ) = @_;
+%
+
+
+<TR>
+ <TD ALIGN="right"><% $opt{'label'} || 'Packages' %></TD>
+ <TD>
+ <% include( '/elements/select-table.html',
+ 'table' => 'part_pkg',
+ 'name_col' => 'pkg',
+ 'value' => '',
+ 'empty_label' => '(none)',
+ 'element_etc' => 'multiple',
+ %opt,
+ )
+ %>
+ </TD>
+</TR>
diff --git a/httemplate/elements/xmenu.css b/httemplate/elements/xmenu.css
new file mode 100644
index 000000000..97c7da8bb
--- /dev/null
+++ b/httemplate/elements/xmenu.css
@@ -0,0 +1,196 @@
+
+.webfx-menu, .webfx-menu * {
+ /*
+ Set the box sizing to content box
+ in the future when IE6 supports box-sizing
+ there will be an issue to fix the sizes
+
+ There is probably an issue with IE5 mac now
+ because IE5 uses content-box but the script
+ assumes all versions of IE uses border-box.
+
+ At the time of this writing mozilla did not support
+ box-sizing for absolute positioned element.
+
+ Opera only supports content-box
+ */
+ box-sizing: content-box;
+ -moz-box-sizing: content-box;
+}
+
+.webfx-menu {
+ position: absolute;
+ z-index: 100;
+ visibility: hidden;
+ border: 1px solid black;
+ padding: 1px;
+ background: white;
+ filter: progid:DXImageTransform.Microsoft.Shadow(color="#777777", Direction=135, Strength=4)
+ alpha(Opacity=95);
+ -moz-opacity: 0.95;
+ /* a drop shadow would be nice in moz/others too... */
+}
+
+.webfx-menu-empty {
+ display: block;
+ border: 1px solid white;
+ padding: 2px 5px 2px 5px;
+ font-size: 11px;
+ /* font-family: Tahoma, Verdan, Helvetica, Sans-Serif; */
+ color: black;
+}
+
+.webfx-menu a {
+ display: block;
+ /* width: expression(constExpression(ieBox ? "100%": "auto")); /* should be ignored by mz and op */
+ width: expression(constExpression(ie ? "98%": "auto")); /* should be ignored by mz and op */
+ overflow: visible;
+ /* padding: 2px 0px 2px 5px; */
+ padding: 1px 0px 1px 5px;
+ font-size: 14px;
+/* font-family: Verdana, Arial, Helvetica, sans-serif; */
+ font-weight: bold;
+ text-decoration: none;
+ vertical-align: center;
+ color: black;
+ border: 1px solid white;
+}
+
+.webfx-menu a:visited {
+ color: black;
+ border: 1px solid white;
+}
+
+.webfx-menu a:hover {
+ color: black;
+ border: 1px solid #7e0079;
+}
+
+.webfx-menu a:hover {
+ color: black;
+ /* background: #faf7fa; #f5ebf4; #efdfef; white; #BC79B8; */
+ /* background: #ffe6fe; */
+ /* background: #ffc2fe; */
+ background: #fff2fe;
+ border: 1px solid #7e0079; /*rgb(120,172,255);#ff8800;*/
+}
+
+.webfx-menu a .arrow {
+ float: right;
+ border: 0;
+ width: 3px;
+ margin-right: 3px;
+ margin-top: 4px;
+}
+
+/* separtor */
+.webfx-menu div {
+ height: 0;
+ height: expression(constExpression(ieBox ? "2px" : "0"));
+ border-top: 1px solid #7e0079; /* rgb(120,172,255); */
+ border-bottom: 1px solid rgb(234,242,255);
+ overflow: hidden;
+ margin: 2px 0px 2px 0px;
+ font-size: 0mm;
+}
+
+.webfx-menu-bar {
+ /* i want a vertical bar */
+ display: block;
+
+ /* background: rgb(120,172,255);/*rgb(255,128,0);*/
+ /* background: #a097ed; */
+ background: #000000;
+ /* border: 1px solid #7E0079; */
+ /* border: 1px solid #000000; */
+ /* border: none */
+ color: white;
+
+ padding: 2px;
+
+ /* IE5.0 has the wierdest box model for inline elements */
+ padding: expression(constExpression(ie50 ? "0px" : "2px"));
+}
+
+.webfx-menu-bar a,
+.webfx-menu-bar a:visited {
+ /* i want a vertical bar */
+ display: block;
+
+ /* border: 1px solid black; /*rgb(0,0,0);/*rgb(255,128,0);*/
+ /* border: 1px solid black; /* #ffffff; */
+ /* border-bottom: 1px solid black; */
+ border-bottom: 1px solid white;
+ /* border-bottom: 1px solid rgb(0,66,174);
+ /* border-bottom: 1px solid black;
+ border-bottom: 1px solid black;
+ border-bottom: 1px solid black; */
+
+ padding: 1px 5px 1px 5px;
+
+ /* color: black; */
+ color: white;
+ text-decoration: none;
+
+ /* IE5.0 Does not paint borders and padding on inline elements without a height/width */
+ height: expression(constExpression(ie50 ? "17px" : "auto"));
+}
+
+.webfx-menu-bar a:link {
+ color: white;
+}
+
+.webfx-menu-bar a:hover {
+ /* color: black; */
+ color: white;
+ /* background: rgb(120,172,255); */
+ /* background: #BC79B8; */
+ background: #7e0079;
+ /* border-left: 1px solid rgb(234,242,255);
+ border-right: 1px solid rgb(0,66,174);
+ border-top: 1px solid rgb(234,242,255);
+ border-bottom: 1px solid rgb(0,66,174); */
+}
+
+.webfx-menu-bar a .arrow {
+ float: right;
+ border: 0;
+/* vertical-align: top; */
+ width: 3px;
+ margin-right: 3px;
+ margin-top: 4px;
+}
+
+.webfx-menu-bar a:active, .webfx-menu-bar a:focus {
+ -moz-outline: none;
+ outline: none;
+ /*
+ ie does not support outline but ie55 can hide the outline using
+ a proprietary property on HTMLElement. Did I say that IE sucks at CSS?
+ */
+ ie-dummy: expression(this.hideFocus=true);
+
+/* border-left: 1px solid rgb(0,66,174); */
+/* border-right: 1px solid rgb(234,242,255); */
+/* border-top: 1px solid rgb(0,66,174); */
+/* border-bottom: 1px solid rgb(234,242,255); */
+}
+
+.webfx-menu-title {
+ color: black;
+ /* background: #faf7fa; #f5ebf4; #efdfef; white; #BC79B8; */
+ background: #7e0079;
+/* border: 1px solid #7e0079; /*rgb(120,172,255);#ff8800;*/
+ /* padding: 3px 1px 3px 6px; */
+ padding: 3px 1px 3px 5px;
+ display: block;
+ font-size: 16px;
+/* font-family: Verdana, Arial, Helvetica, sans-serif; */
+ font-weight: bold;
+ text-decoration: none;
+ color: white;
+/* border: 1px solid white; */
+ border-bottom: 1px solid white;
+ width: expression(constExpression(ie ? "98%": "auto")); /* should be ignored by mz and op */
+}
+
diff --git a/httemplate/elements/xmenu.js b/httemplate/elements/xmenu.js
new file mode 100644
index 000000000..134265f53
--- /dev/null
+++ b/httemplate/elements/xmenu.js
@@ -0,0 +1,668 @@
+//<script>
+/*
+ * This script was created by Erik Arvidsson (erik@eae.net)
+ * for WebFX (http://webfx.eae.net)
+ * Copyright 2001
+ *
+ * For usage see license at http://webfx.eae.net/license.html
+ *
+ * Created: 2001-01-12
+ * Updates: 2001-11-20 Added hover mode support and removed Opera focus hacks
+ * 2001-12-20 Added auto positioning and some properties to support this
+ * 2002-08-13 toString used ' for attributes. Changed to " to allow in args
+ */
+
+// check browsers
+var ua = navigator.userAgent;
+var opera = /opera [56789]|opera\/[56789]/i.test(ua);
+var ie = !opera && /MSIE/.test(ua);
+var ie50 = ie && /MSIE 5\.[01234]/.test(ua);
+var ie6 = ie && /MSIE [6789]/.test(ua);
+var ieBox = ie && (document.compatMode == null || document.compatMode != "CSS1Compat");
+var moz = !opera && /gecko/i.test(ua);
+var nn6 = !opera && /netscape.*6\./i.test(ua);
+var khtml = /KHTML/i.test(ua);
+
+// define the default values
+
+webfxMenuDefaultWidth = 154;
+
+webfxMenuDefaultBorderLeft = 1;
+webfxMenuDefaultBorderRight = 1;
+webfxMenuDefaultBorderTop = 1;
+webfxMenuDefaultBorderBottom = 1;
+
+webfxMenuDefaultPaddingLeft = 1;
+webfxMenuDefaultPaddingRight = 1;
+webfxMenuDefaultPaddingTop = 1;
+webfxMenuDefaultPaddingBottom = 1;
+
+webfxMenuDefaultShadowLeft = 0;
+webfxMenuDefaultShadowRight = ie && !ie50 && /win32/i.test(navigator.platform) ? 4 :0;
+webfxMenuDefaultShadowTop = 0;
+webfxMenuDefaultShadowBottom = ie && !ie50 && /win32/i.test(navigator.platform) ? 4 : 0;
+
+
+webfxMenuItemDefaultHeight = 18;
+webfxMenuItemDefaultText = "Untitled";
+webfxMenuItemDefaultHref = "javascript:void(0)";
+
+webfxMenuSeparatorDefaultHeight = 6;
+
+webfxMenuDefaultEmptyText = "Empty";
+
+webfxMenuDefaultUseAutoPosition = nn6 ? false : true;
+
+
+
+// other global constants
+
+webfxMenuImagePath = "";
+
+webfxMenuUseHover = opera ? true : false;
+webfxMenuHideTime = 500;
+webfxMenuShowTime = 200;
+
+
+
+var webFXMenuHandler = {
+ idCounter : 0,
+ idPrefix : "webfx-menu-object-",
+ all : {},
+ getId : function () { return this.idPrefix + this.idCounter++; },
+ overMenuItem : function (oItem) {
+ if (this.showTimeout != null)
+ window.clearTimeout(this.showTimeout);
+ if (this.hideTimeout != null)
+ window.clearTimeout(this.hideTimeout);
+ var jsItem = this.all[oItem.id];
+ if (webfxMenuShowTime <= 0)
+ this._over(jsItem);
+ else if ( jsItem )
+ //this.showTimeout = window.setTimeout(function () { webFXMenuHandler._over(jsItem) ; }, webfxMenuShowTime);
+ // I hate IE5.0 because the piece of shit crashes when using setTimeout with a function object
+ this.showTimeout = window.setTimeout("webFXMenuHandler._over(webFXMenuHandler.all['" + jsItem.id + "'])", webfxMenuShowTime);
+ },
+ outMenuItem : function (oItem) {
+ if (this.showTimeout != null)
+ window.clearTimeout(this.showTimeout);
+ if (this.hideTimeout != null)
+ window.clearTimeout(this.hideTimeout);
+ var jsItem = this.all[oItem.id];
+ if (webfxMenuHideTime <= 0)
+ this._out(jsItem);
+ else if ( jsItem )
+ //this.hideTimeout = window.setTimeout(function () { webFXMenuHandler._out(jsItem) ; }, webfxMenuHideTime);
+ this.hideTimeout = window.setTimeout("webFXMenuHandler._out(webFXMenuHandler.all['" + jsItem.id + "'])", webfxMenuHideTime);
+ },
+ blurMenu : function (oMenuItem) {
+ window.setTimeout("webFXMenuHandler.all[\"" + oMenuItem.id + "\"].subMenu.hide();", webfxMenuHideTime);
+ },
+ _over : function (jsItem) {
+ if (jsItem.subMenu) {
+ jsItem.parentMenu.hideAllSubs();
+ jsItem.subMenu.show();
+ }
+ else
+ jsItem.parentMenu.hideAllSubs();
+ },
+ _out : function (jsItem) {
+ // find top most menu
+ var root = jsItem;
+ var m;
+ if (root instanceof WebFXMenuButton)
+ m = root.subMenu;
+ else {
+ m = jsItem.parentMenu;
+ while (m.parentMenu != null && !(m.parentMenu instanceof WebFXMenuBar))
+ m = m.parentMenu;
+ }
+ if (m != null)
+ m.hide();
+ },
+ hideMenu : function (menu) {
+ if (this.showTimeout != null)
+ window.clearTimeout(this.showTimeout);
+ if (this.hideTimeout != null)
+ window.clearTimeout(this.hideTimeout);
+
+ this.hideTimeout = window.setTimeout("webFXMenuHandler.all['" + menu.id + "'].hide()", webfxMenuHideTime);
+ },
+ showMenu : function (menu, src, dir) {
+ if (this.showTimeout != null)
+ window.clearTimeout(this.showTimeout);
+ if (this.hideTimeout != null)
+ window.clearTimeout(this.hideTimeout);
+
+ if (arguments.length < 3)
+ dir = "vertical";
+
+ menu.show(src, dir);
+ }
+};
+
+function WebFXMenu() {
+ this._menuItems = [];
+ this._subMenus = [];
+ this.id = webFXMenuHandler.getId();
+ this.top = 0;
+ this.left = 0;
+ this.shown = false;
+ this.parentMenu = null;
+ webFXMenuHandler.all[this.id] = this;
+}
+
+WebFXMenu.prototype.width = webfxMenuDefaultWidth;
+WebFXMenu.prototype.emptyText = webfxMenuDefaultEmptyText;
+WebFXMenu.prototype.useAutoPosition = webfxMenuDefaultUseAutoPosition;
+
+WebFXMenu.prototype.borderLeft = webfxMenuDefaultBorderLeft;
+WebFXMenu.prototype.borderRight = webfxMenuDefaultBorderRight;
+WebFXMenu.prototype.borderTop = webfxMenuDefaultBorderTop;
+WebFXMenu.prototype.borderBottom = webfxMenuDefaultBorderBottom;
+
+WebFXMenu.prototype.paddingLeft = webfxMenuDefaultPaddingLeft;
+WebFXMenu.prototype.paddingRight = webfxMenuDefaultPaddingRight;
+WebFXMenu.prototype.paddingTop = webfxMenuDefaultPaddingTop;
+WebFXMenu.prototype.paddingBottom = webfxMenuDefaultPaddingBottom;
+
+WebFXMenu.prototype.shadowLeft = webfxMenuDefaultShadowLeft;
+WebFXMenu.prototype.shadowRight = webfxMenuDefaultShadowRight;
+WebFXMenu.prototype.shadowTop = webfxMenuDefaultShadowTop;
+WebFXMenu.prototype.shadowBottom = webfxMenuDefaultShadowBottom;
+
+
+
+WebFXMenu.prototype.add = function (menuItem) {
+ this._menuItems[this._menuItems.length] = menuItem;
+ if (menuItem.subMenu) {
+ this._subMenus[this._subMenus.length] = menuItem.subMenu;
+ menuItem.subMenu.parentMenu = this;
+ }
+
+ menuItem.parentMenu = this;
+};
+
+WebFXMenu.prototype.show = function (relObj, sDir) {
+ if (this.useAutoPosition)
+ this.position(relObj, sDir);
+
+ var divElement = document.getElementById(this.id);
+ if ( divElement ) {
+
+ divElement.style.left = opera ? this.left : this.left + "px";
+ divElement.style.top = opera ? this.top : this.top + "px";
+ divElement.style.visibility = "visible";
+
+ if ( ie ) {
+ var shimElement = document.getElementById(this.id + "Shim");
+ if ( shimElement ) {
+ shimElement.style.width = divElement.offsetWidth;
+ shimElement.style.height = divElement.offsetHeight;
+ shimElement.style.top = divElement.style.top;
+ shimElement.style.left = divElement.style.left;
+ /*shimElement.style.zIndex = divElement.style.zIndex - 1; */
+ shimElement.style.display = "block";
+ shimElement.style.filter='progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)';
+ }
+ }
+
+ }
+
+ this.shown = true;
+
+ if (this.parentMenu)
+ this.parentMenu.show();
+};
+
+WebFXMenu.prototype.hide = function () {
+ this.hideAllSubs();
+ var divElement = document.getElementById(this.id);
+ if ( divElement ) {
+ divElement.style.visibility = "hidden";
+ if ( ie ) {
+ var shimElement = document.getElementById(this.id + "Shim");
+ if ( shimElement ) {
+ shimElement.style.display = "none";
+ }
+ }
+ }
+
+ this.shown = false;
+};
+
+WebFXMenu.prototype.hideAllSubs = function () {
+ for (var i = 0; i < this._subMenus.length; i++) {
+ if (this._subMenus[i].shown)
+ this._subMenus[i].hide();
+ }
+};
+
+WebFXMenu.prototype.toString = function () {
+ var top = this.top + this.borderTop + this.paddingTop;
+ var str = "<div id='" + this.id + "' class='webfx-menu' style='" +
+ "width:" + (!ieBox ?
+ this.width - this.borderLeft - this.paddingLeft - this.borderRight - this.paddingRight :
+ this.width) + "px;" +
+ (this.useAutoPosition ?
+ "left:" + this.left + "px;" + "top:" + this.top + "px;" :
+ "") +
+ (ie50 ? "filter: none;" : "") +
+ "'>";
+
+ if (this._menuItems.length == 0) {
+ str += "<span class='webfx-menu-empty'>" + this.emptyText + "</span>";
+ }
+ else {
+ str += '<span class="webfx-menu-title" onmouseover="webFXMenuHandler.overMenuItem(this)"' +
+ (webfxMenuUseHover ? " onmouseout='webFXMenuHandler.outMenuItem(this)'" : "") +
+ '>' + this.emptyText + '</span>';
+ // str += '<div id="' + this.id + '-title">' + this.emptyText + '</div>';
+ // loop through all menuItems
+ for (var i = 0; i < this._menuItems.length; i++) {
+ var mi = this._menuItems[i];
+ str += mi;
+ if (!this.useAutoPosition) {
+ if (mi.subMenu && !mi.subMenu.useAutoPosition)
+ mi.subMenu.top = top - mi.subMenu.borderTop - mi.subMenu.paddingTop;
+ top += mi.height;
+ }
+ }
+
+ }
+
+ str += "</div>";
+
+ if ( ie ) {
+ str += "<iframe id='" + this.id + "Shim' src='javascript:false;' scrolling='no' frameBorder='0' style='position:absolute; top:0px; left: 0px; display:none;'></iframe>";
+ }
+
+ for (var i = 0; i < this._subMenus.length; i++) {
+ this._subMenus[i].left = this.left + this.width - this._subMenus[i].borderLeft;
+ str += this._subMenus[i];
+ }
+
+ return str;
+};
+// WebFXMenu.prototype.position defined later
+
+function WebFXMenuItem(sText, sHref, sToolTip, oSubMenu) {
+ this.text = sText || webfxMenuItemDefaultText;
+ this.href = (sHref == null || sHref == "") ? webfxMenuItemDefaultHref : sHref;
+ this.subMenu = oSubMenu;
+ if (oSubMenu)
+ oSubMenu.parentMenuItem = this;
+ this.toolTip = sToolTip;
+ this.id = webFXMenuHandler.getId();
+ webFXMenuHandler.all[this.id] = this;
+};
+WebFXMenuItem.prototype.height = webfxMenuItemDefaultHeight;
+WebFXMenuItem.prototype.toString = function () {
+ return "<a" +
+ " id='" + this.id + "'" +
+ " href=\"" + this.href + "\"" +
+ (this.toolTip ? " title=\"" + this.toolTip + "\"" : "") +
+ " onmouseover='webFXMenuHandler.overMenuItem(this)'" +
+ (webfxMenuUseHover ? " onmouseout='webFXMenuHandler.outMenuItem(this)'" : "") +
+ (this.subMenu ? " unselectable='on' tabindex='-1'" : "") +
+ ">" +
+ (this.subMenu ? "<img class='arrow' src=\"" + webfxMenuImagePath + "arrow.right.black.png\">" : "") +
+ this.text +
+ "</a>";
+};
+
+
+function WebFXMenuSeparator() {
+ this.id = webFXMenuHandler.getId();
+ webFXMenuHandler.all[this.id] = this;
+};
+WebFXMenuSeparator.prototype.height = webfxMenuSeparatorDefaultHeight;
+WebFXMenuSeparator.prototype.toString = function () {
+ return "<div" +
+ " id='" + this.id + "'" +
+ (webfxMenuUseHover ?
+ " onmouseover='webFXMenuHandler.overMenuItem(this)'" +
+ " onmouseout='webFXMenuHandler.outMenuItem(this)'"
+ :
+ "") +
+ "></div>"
+};
+
+function WebFXMenuBar() {
+ this._parentConstructor = WebFXMenu;
+ this._parentConstructor();
+}
+WebFXMenuBar.prototype = new WebFXMenu;
+WebFXMenuBar.prototype.toString = function () {
+ var str = "<div id='" + this.id + "' class='webfx-menu-bar'>";
+
+ // loop through all menuButtons
+ for (var i = 0; i < this._menuItems.length; i++)
+ str += this._menuItems[i];
+
+ str += "</div>";
+
+ for (var i = 0; i < this._subMenus.length; i++)
+ str += this._subMenus[i];
+
+ return str;
+};
+
+function WebFXMenuButton(sText, sHref, sToolTip, oSubMenu) {
+ this._parentConstructor = WebFXMenuItem;
+ this._parentConstructor(sText, sHref, sToolTip, oSubMenu);
+}
+WebFXMenuButton.prototype = new WebFXMenuItem;
+WebFXMenuButton.prototype.toString = function () {
+ return "<a" +
+ " id='" + this.id + "'" +
+ " href='" + this.href + "'" +
+ (this.toolTip ? " title='" + this.toolTip + "'" : "") +
+ (webfxMenuUseHover ?
+ (" onmouseover='webFXMenuHandler.overMenuItem(this)'" +
+ " onmouseout='webFXMenuHandler.outMenuItem(this)'") :
+ (
+ " onfocus='webFXMenuHandler.overMenuItem(this)'" +
+ (this.subMenu ?
+ " onblur='webFXMenuHandler.blurMenu(this)'" :
+ ""
+ )
+ )) +
+ ">" +
+ (this.subMenu ? "<img class='arrow' src='" + webfxMenuImagePath + "arrow.right.png'>" : "") +
+ this.text +
+ "</a>";
+};
+
+
+
+
+
+/* Position functions */
+
+
+function getInnerLeft(el) {
+
+ if (el == null) return 0;
+
+ if (ieBox && el == document.body || !ieBox && el == document.documentElement) return 0;
+
+ return parseInt( getLeft(el) + parseInt(getBorderLeft(el)) );
+
+}
+
+
+
+function getLeft(el, debug) {
+
+ if (el == null) return 0;
+
+ //if ( debug )
+ // alert ( el.offsetLeft + ' - ' + getInnerLeft(el.offsetParent) );
+
+ return parseInt( el.offsetLeft + parseInt(getInnerLeft(el.offsetParent)) );
+
+}
+
+
+
+function getInnerTop(el) {
+
+ if (el == null) return 0;
+
+ if (ieBox && el == document.body || !ieBox && el == document.documentElement) return 0;
+
+ return parseInt( getTop(el) + parseInt(getBorderTop(el)) );
+
+}
+
+
+
+function getTop(el) {
+
+ if (el == null) return 0;
+
+ return parseInt( el.offsetTop + parseInt(getInnerTop(el.offsetParent)) );
+
+}
+
+
+
+function getBorderLeft(el) {
+
+ return ie ?
+
+ el.clientLeft :
+
+ ( khtml
+ ? parseInt(document.defaultView.getComputedStyle(el, null).getPropertyValue("border-left-width"))
+ : parseInt(window.getComputedStyle(el, null).getPropertyValue("border-left-width"))
+ );
+
+}
+
+
+
+function getBorderTop(el) {
+
+ return ie ?
+
+ el.clientTop :
+
+ ( khtml
+ ? parseInt(document.defaultView.getComputedStyle(el, null).getPropertyValue("border-left-width"))
+ : parseInt(window.getComputedStyle(el, null).getPropertyValue("border-top-width"))
+ );
+
+}
+
+
+
+function opera_getLeft(el) {
+
+ if (el == null) return 0;
+
+ return el.offsetLeft + opera_getLeft(el.offsetParent);
+
+}
+
+
+
+function opera_getTop(el) {
+
+ if (el == null) return 0;
+
+ return el.offsetTop + opera_getTop(el.offsetParent);
+
+}
+
+
+
+function getOuterRect(el, debug) {
+
+ return {
+
+ left: (opera ? opera_getLeft(el) : getLeft(el, debug)),
+
+ top: (opera ? opera_getTop(el) : getTop(el)),
+
+ width: el.offsetWidth,
+
+ height: el.offsetHeight
+
+ };
+
+}
+
+
+
+// mozilla bug! scrollbars not included in innerWidth/height
+
+function getDocumentRect(el) {
+
+ return {
+
+ left: 0,
+
+ top: 0,
+
+ width: (ie ?
+
+ (ieBox ? document.body.clientWidth : document.documentElement.clientWidth) :
+
+ window.innerWidth
+
+ ),
+
+ height: (ie ?
+
+ (ieBox ? document.body.clientHeight : document.documentElement.clientHeight) :
+
+ window.innerHeight
+
+ )
+
+ };
+
+}
+
+
+
+function getScrollPos(el) {
+
+ return {
+
+ left: (ie ?
+
+ (ieBox ? document.body.scrollLeft : document.documentElement.scrollLeft) :
+
+ window.pageXOffset
+
+ ),
+
+ top: (ie ?
+
+ (ieBox ? document.body.scrollTop : document.documentElement.scrollTop) :
+
+ window.pageYOffset
+
+ )
+
+ };
+
+}
+
+
+/* end position functions */
+
+WebFXMenu.prototype.position = function (relEl, sDir) {
+ var dir = sDir;
+ // find parent item rectangle, piRect
+ var piRect;
+ if (!relEl) {
+ var pi = this.parentMenuItem;
+ if (!this.parentMenuItem)
+ return;
+
+ relEl = document.getElementById(pi.id);
+ if (dir == null)
+ dir = pi instanceof WebFXMenuButton ? "vertical" : "horizontal";
+ //alert('created RelEl from parent: ' + pi.id);
+ piRect = getOuterRect(relEl, 1);
+ }
+ else if (relEl.left != null && relEl.top != null && relEl.width != null && relEl.height != null) { // got a rect
+ //alert('passed a Rect as RelEl: ' + typeof(relEl));
+
+ piRect = relEl;
+ }
+ else {
+ //alert('passed an element as RelEl: ' + typeof(relEl));
+ piRect = getOuterRect(relEl);
+ }
+
+ var menuEl = document.getElementById(this.id);
+ var menuRect = getOuterRect(menuEl);
+ var docRect = getDocumentRect();
+ var scrollPos = getScrollPos();
+ var pMenu = this.parentMenu;
+
+ if (dir == "vertical") {
+ if (piRect.left + menuRect.width - scrollPos.left <= docRect.width) {
+ //alert('piRect.left: ' + piRect.left);
+ this.left = piRect.left;
+ if ( ! ie )
+ this.left = this.left + 138;
+ } else if (docRect.width >= menuRect.width) {
+ //konq (not safari though) winds up here by accident and positions the menus all weird
+ //alert('docRect.width + scrollPos.left - menuRect.width');
+
+ this.left = docRect.width + scrollPos.left - menuRect.width;
+ } else {
+ //alert('scrollPos.left: ' + scrollPos.left);
+ this.left = scrollPos.left;
+ }
+
+ if (piRect.top + piRect.height + menuRect.height <= docRect.height + scrollPos.top)
+
+ this.top = piRect.top + piRect.height;
+
+ else if (piRect.top - menuRect.height >= scrollPos.top)
+
+ this.top = piRect.top - menuRect.height;
+
+ else if (docRect.height >= menuRect.height)
+
+ this.top = docRect.height + scrollPos.top - menuRect.height;
+
+ else
+
+ this.top = scrollPos.top;
+ }
+ else {
+ if (piRect.top + menuRect.height - this.borderTop - this.paddingTop <= docRect.height + scrollPos.top)
+
+ this.top = piRect.top - this.borderTop - this.paddingTop;
+
+ else if (piRect.top + piRect.height - menuRect.height + this.borderTop + this.paddingTop >= 0)
+
+ this.top = piRect.top + piRect.height - menuRect.height + this.borderBottom + this.paddingBottom + this.shadowBottom;
+
+ else if (docRect.height >= menuRect.height)
+
+ this.top = docRect.height + scrollPos.top - menuRect.height;
+
+ else
+
+ this.top = scrollPos.top;
+
+
+
+ var pMenuPaddingLeft = pMenu ? pMenu.paddingLeft : 0;
+
+ var pMenuBorderLeft = pMenu ? pMenu.borderLeft : 0;
+
+ var pMenuPaddingRight = pMenu ? pMenu.paddingRight : 0;
+
+ var pMenuBorderRight = pMenu ? pMenu.borderRight : 0;
+
+
+
+ if (piRect.left + piRect.width + menuRect.width + pMenuPaddingRight +
+
+ pMenuBorderRight - this.borderLeft + this.shadowRight <= docRect.width + scrollPos.left)
+
+ this.left = piRect.left + piRect.width + pMenuPaddingRight + pMenuBorderRight - this.borderLeft;
+
+ else if (piRect.left - menuRect.width - pMenuPaddingLeft - pMenuBorderLeft + this.borderRight + this.shadowRight >= 0)
+
+ this.left = piRect.left - menuRect.width - pMenuPaddingLeft - pMenuBorderLeft + this.borderRight + this.shadowRight;
+
+ else if (docRect.width >= menuRect.width)
+
+ this.left = docRect.width + scrollPos.left - menuRect.width;
+
+ else
+
+ this.left = scrollPos.left;
+ }
+};
diff --git a/httemplate/elements/xmenu.top.css b/httemplate/elements/xmenu.top.css
new file mode 100644
index 000000000..75917031b
--- /dev/null
+++ b/httemplate/elements/xmenu.top.css
@@ -0,0 +1,211 @@
+
+.webfx-menu, .webfx-menu * {
+ /*
+ Set the box sizing to content box
+ in the future when IE6 supports box-sizing
+ there will be an issue to fix the sizes
+
+ There is probably an issue with IE5 mac now
+ because IE5 uses content-box but the script
+ assumes all versions of IE uses border-box.
+
+ At the time of this writing mozilla did not support
+ box-sizing for absolute positioned element.
+
+ Opera only supports content-box
+ */
+ box-sizing: content-box;
+ -moz-box-sizing: content-box;
+}
+
+.webfx-menu {
+ position: absolute;
+ z-index: 100;
+ visibility: hidden;
+ border: 1px solid black;
+ padding: 1px;
+ background: white;
+ filter: progid:DXImageTransform.Microsoft.Shadow(color="#777777", Direction=135, Strength=4)
+ alpha(Opacity=95);
+ -moz-opacity: 0.95;
+ /* a drop shadow would be nice in moz/others too... */
+}
+
+.webfx-menu-empty {
+ display: block;
+ border: 1px solid white;
+ padding: 2px 5px 2px 5px;
+ font-size: 11px;
+ /* font-family: Tahoma, Verdan, Helvetica, Sans-Serif; */
+ color: black;
+}
+
+.webfx-menu a {
+ display: block;
+ /* width: expression(constExpression(ieBox ? "100%": "auto")); /* should be ignored by mz and op */
+ width: expression(constExpression(ie ? "98%": "auto")); /* should be ignored by mz and op */
+ overflow: visible;
+ /* padding: 2px 0px 2px 5px; */
+ padding: 1px 0px 1px 5px;
+ font-size: 14px;
+/* font-family: Verdana, Arial, Helvetica, sans-serif; */
+ font-weight: bold;
+ text-decoration: none;
+ vertical-align: center;
+ color: black;
+ border: 1px solid white;
+}
+
+.webfx-menu a:visited {
+ color: black;
+ border: 1px solid white;
+}
+
+.webfx-menu a:hover {
+ color: black;
+ border: 1px solid #7e0079;
+}
+
+.webfx-menu a:hover {
+ color: black;
+ /* background: #faf7fa; #f5ebf4; #efdfef; white; #BC79B8; */
+ /* background: #ffe6fe; */
+ /* background: #ffc2fe; */
+ background: #fff2fe;
+ border: 1px solid #7e0079; /*rgb(120,172,255);#ff8800;*/
+}
+
+.webfx-menu a .arrow {
+ float: right;
+ border: 0;
+ width: 3px;
+ margin-right: 3px;
+ margin-top: 4px;
+}
+
+/* separtor */
+.webfx-menu div {
+ height: 0;
+ height: expression(constExpression(ieBox ? "2px" : "0"));
+ border-top: 1px solid #7e0079; /* rgb(120,172,255); */
+ border-bottom: 1px solid rgb(234,242,255);
+ overflow: hidden;
+ margin: 2px 0px 2px 0px;
+ font-size: 0mm;
+}
+
+.webfx-menu-bar {
+ /* background: rgb(120,172,255);/*rgb(255,128,0);*/
+ /* background: #a097ed; */
+ background: #000000;
+ /* border: 1px solid #7E0079; */
+ /* border: 1px solid #000000; */
+ /* border: none */
+ color: white;
+
+ padding: 2px;
+
+ /* IE5.0 has the wierdest box model for inline elements */
+ padding: expression(constExpression(ie50 ? "0px" : "2px"));
+}
+
+.webfx-menu-bar a,
+.webfx-menu-bar a:visited {
+ /* i want a vertical bar */
+ /* display: block; */
+
+ /* border: 1px solid black; /*rgb(0,0,0);/*rgb(255,128,0);*/
+ /* border: 1px solid black; /* #ffffff; */
+ /* border-bottom: 1px solid black; */
+ /* border-bottom: 1px solid white; */
+ /* border-bottom: 1px solid rgb(0,66,174);
+ /* border-bottom: 1px solid black;
+ border-bottom: 1px solid black;
+ border-bottom: 1px solid black; */
+
+ padding: 1px 5px 1px 5px;
+
+ /* color: black; */
+ color: white;
+ text-decoration: none;
+
+ /* IE5.0 Does not paint borders and padding on inline elements without a height/width */
+ height: expression(constExpression(ie50 ? "17px" : "auto"));
+
+ background-color:#333333;
+ border:1px solid;
+ border-top-color:#cccccc;
+ border-left-color:#cccccc;
+ border-right-color:#aaaaaa;
+ border-bottom-color:#aaaaaa;
+
+ margin-right: 4px
+
+}
+
+.webfx-menu-bar a:link {
+ color: white;
+}
+
+.webfx-menu-bar a:hover {
+ /* color: black; */
+ color: white;
+ /* background: rgb(120,172,255); */
+ /* background: #BC79B8; */
+ background: #7e0079;
+ /* border-left: 1px solid rgb(234,242,255);
+ border-right: 1px solid rgb(0,66,174);
+ border-top: 1px solid rgb(234,242,255);
+ border-bottom: 1px solid rgb(0,66,174); */
+
+ border:1px solid;
+ border-top-color:#cccccc;
+ border-left-color:#cccccc;
+ border-right-color:#aaaaaa;
+ border-bottom-color:#aaaaaa;
+
+}
+
+.webfx-menu-bar a .arrow {
+ /* float: right; */
+ border: 0;
+/* vertical-align: top; */
+/* width: 3px; */
+/* margin-right: 3px; */
+ margin-bottom: 2px;
+
+}
+
+.webfx-menu-bar a:active, .webfx-menu-bar a:focus {
+ -moz-outline: none;
+ outline: none;
+ /*
+ ie does not support outline but ie55 can hide the outline using
+ a proprietary property on HTMLElement. Did I say that IE sucks at CSS?
+ */
+ ie-dummy: expression(this.hideFocus=true);
+
+/* border-left: 1px solid rgb(0,66,174); */
+/* border-right: 1px solid rgb(234,242,255); */
+/* border-top: 1px solid rgb(0,66,174); */
+/* border-bottom: 1px solid rgb(234,242,255); */
+}
+
+.webfx-menu-title {
+ color: black;
+ /* background: #faf7fa; #f5ebf4; #efdfef; white; #BC79B8; */
+ background: #7e0079;
+/* border: 1px solid #7e0079; /*rgb(120,172,255);#ff8800;*/
+ /* padding: 3px 1px 3px 6px; */
+ padding: 3px 1px 3px 5px;
+ display: block;
+ font-size: 16px;
+/* font-family: Verdana, Arial, Helvetica, sans-serif; */
+ font-weight: bold;
+ text-decoration: none;
+ color: white;
+/* border: 1px solid white; */
+ border-bottom: 1px solid white;
+ width: expression(constExpression(ie ? "98%": "auto")); /* should be ignored by mz and op */
+}
+
diff --git a/httemplate/elements/xmenu.top.js b/httemplate/elements/xmenu.top.js
new file mode 100644
index 000000000..8d81035a2
--- /dev/null
+++ b/httemplate/elements/xmenu.top.js
@@ -0,0 +1,671 @@
+//<script>
+/*
+ * This script was created by Erik Arvidsson (erik@eae.net)
+ * for WebFX (http://webfx.eae.net)
+ * Copyright 2001
+ *
+ * For usage see license at http://webfx.eae.net/license.html
+ *
+ * Created: 2001-01-12
+ * Updates: 2001-11-20 Added hover mode support and removed Opera focus hacks
+ * 2001-12-20 Added auto positioning and some properties to support this
+ * 2002-08-13 toString used ' for attributes. Changed to " to allow in args
+ */
+
+// check browsers
+var ua = navigator.userAgent;
+var opera = /opera [56789]|opera\/[56789]/i.test(ua);
+var ie = !opera && /MSIE/.test(ua);
+var ie50 = ie && /MSIE 5\.[01234]/.test(ua);
+var ie6 = ie && /MSIE [6789]/.test(ua);
+var ieBox = ie && (document.compatMode == null || document.compatMode != "CSS1Compat");
+var moz = !opera && /gecko/i.test(ua);
+var nn6 = !opera && /netscape.*6\./i.test(ua);
+var khtml = /KHTML/i.test(ua);
+
+// define the default values
+
+webfxMenuDefaultWidth = 154;
+
+webfxMenuDefaultBorderLeft = 1;
+webfxMenuDefaultBorderRight = 1;
+webfxMenuDefaultBorderTop = 1;
+webfxMenuDefaultBorderBottom = 1;
+
+webfxMenuDefaultPaddingLeft = 1;
+webfxMenuDefaultPaddingRight = 1;
+webfxMenuDefaultPaddingTop = 1;
+webfxMenuDefaultPaddingBottom = 1;
+
+webfxMenuDefaultShadowLeft = 0;
+webfxMenuDefaultShadowRight = ie && !ie50 && /win32/i.test(navigator.platform) ? 4 :0;
+webfxMenuDefaultShadowTop = 0;
+webfxMenuDefaultShadowBottom = ie && !ie50 && /win32/i.test(navigator.platform) ? 4 : 0;
+
+
+webfxMenuItemDefaultHeight = 18;
+webfxMenuItemDefaultText = "Untitled";
+webfxMenuItemDefaultHref = "javascript:void(0)";
+
+webfxMenuSeparatorDefaultHeight = 6;
+
+webfxMenuDefaultEmptyText = "Empty";
+
+webfxMenuDefaultUseAutoPosition = nn6 ? false : true;
+
+
+
+// other global constants
+
+webfxMenuImagePath = "";
+
+webfxMenuUseHover = opera ? true : false;
+webfxMenuHideTime = 500;
+webfxMenuShowTime = 200;
+
+
+
+var webFXMenuHandler = {
+ idCounter : 0,
+ idPrefix : "webfx-menu-object-",
+ all : {},
+ getId : function () { return this.idPrefix + this.idCounter++; },
+ overMenuItem : function (oItem) {
+ if (this.showTimeout != null)
+ window.clearTimeout(this.showTimeout);
+ if (this.hideTimeout != null)
+ window.clearTimeout(this.hideTimeout);
+ var jsItem = this.all[oItem.id];
+ if (webfxMenuShowTime <= 0)
+ this._over(jsItem);
+ else if ( jsItem )
+ //this.showTimeout = window.setTimeout(function () { webFXMenuHandler._over(jsItem) ; }, webfxMenuShowTime);
+ // I hate IE5.0 because the piece of shit crashes when using setTimeout with a function object
+ this.showTimeout = window.setTimeout("webFXMenuHandler._over(webFXMenuHandler.all['" + jsItem.id + "'])", webfxMenuShowTime);
+ },
+ outMenuItem : function (oItem) {
+ if (this.showTimeout != null)
+ window.clearTimeout(this.showTimeout);
+ if (this.hideTimeout != null)
+ window.clearTimeout(this.hideTimeout);
+ var jsItem = this.all[oItem.id];
+ if (webfxMenuHideTime <= 0)
+ this._out(jsItem);
+ else if ( jsItem )
+ //this.hideTimeout = window.setTimeout(function () { webFXMenuHandler._out(jsItem) ; }, webfxMenuHideTime);
+ this.hideTimeout = window.setTimeout("webFXMenuHandler._out(webFXMenuHandler.all['" + jsItem.id + "'])", webfxMenuHideTime);
+ },
+ blurMenu : function (oMenuItem) {
+ window.setTimeout("webFXMenuHandler.all[\"" + oMenuItem.id + "\"].subMenu.hide();", webfxMenuHideTime);
+ },
+ _over : function (jsItem) {
+ if (jsItem.subMenu) {
+ jsItem.parentMenu.hideAllSubs();
+ jsItem.subMenu.show();
+ }
+ else
+ jsItem.parentMenu.hideAllSubs();
+ },
+ _out : function (jsItem) {
+ // find top most menu
+ var root = jsItem;
+ var m;
+ if (root instanceof WebFXMenuButton)
+ m = root.subMenu;
+ else {
+ m = jsItem.parentMenu;
+ while (m.parentMenu != null && !(m.parentMenu instanceof WebFXMenuBar))
+ m = m.parentMenu;
+ }
+ if (m != null)
+ m.hide();
+ },
+ hideMenu : function (menu) {
+ if (this.showTimeout != null)
+ window.clearTimeout(this.showTimeout);
+ if (this.hideTimeout != null)
+ window.clearTimeout(this.hideTimeout);
+
+ this.hideTimeout = window.setTimeout("webFXMenuHandler.all['" + menu.id + "'].hide()", webfxMenuHideTime);
+ },
+ showMenu : function (menu, src, dir) {
+ if (this.showTimeout != null)
+ window.clearTimeout(this.showTimeout);
+ if (this.hideTimeout != null)
+ window.clearTimeout(this.hideTimeout);
+
+ if (arguments.length < 3)
+ dir = "vertical";
+
+ menu.show(src, dir);
+ }
+};
+
+function WebFXMenu() {
+ this._menuItems = [];
+ this._subMenus = [];
+ this.id = webFXMenuHandler.getId();
+ this.top = 0;
+ this.left = 0;
+ this.shown = false;
+ this.parentMenu = null;
+ webFXMenuHandler.all[this.id] = this;
+}
+
+WebFXMenu.prototype.width = webfxMenuDefaultWidth;
+WebFXMenu.prototype.emptyText = webfxMenuDefaultEmptyText;
+WebFXMenu.prototype.useAutoPosition = webfxMenuDefaultUseAutoPosition;
+
+WebFXMenu.prototype.borderLeft = webfxMenuDefaultBorderLeft;
+WebFXMenu.prototype.borderRight = webfxMenuDefaultBorderRight;
+WebFXMenu.prototype.borderTop = webfxMenuDefaultBorderTop;
+WebFXMenu.prototype.borderBottom = webfxMenuDefaultBorderBottom;
+
+WebFXMenu.prototype.paddingLeft = webfxMenuDefaultPaddingLeft;
+WebFXMenu.prototype.paddingRight = webfxMenuDefaultPaddingRight;
+WebFXMenu.prototype.paddingTop = webfxMenuDefaultPaddingTop;
+WebFXMenu.prototype.paddingBottom = webfxMenuDefaultPaddingBottom;
+
+WebFXMenu.prototype.shadowLeft = webfxMenuDefaultShadowLeft;
+WebFXMenu.prototype.shadowRight = webfxMenuDefaultShadowRight;
+WebFXMenu.prototype.shadowTop = webfxMenuDefaultShadowTop;
+WebFXMenu.prototype.shadowBottom = webfxMenuDefaultShadowBottom;
+
+
+
+WebFXMenu.prototype.add = function (menuItem) {
+ this._menuItems[this._menuItems.length] = menuItem;
+ if (menuItem.subMenu) {
+ this._subMenus[this._subMenus.length] = menuItem.subMenu;
+ menuItem.subMenu.parentMenu = this;
+ }
+
+ menuItem.parentMenu = this;
+};
+
+WebFXMenu.prototype.show = function (relObj, sDir) {
+ if (this.useAutoPosition)
+ this.position(relObj, sDir);
+
+ var divElement = document.getElementById(this.id);
+ if ( divElement ) {
+
+ divElement.style.left = opera ? this.left : this.left + "px";
+ divElement.style.top = opera ? this.top : this.top + "px";
+ divElement.style.visibility = "visible";
+
+ if ( ie ) {
+ var shimElement = document.getElementById(this.id + "Shim");
+ if ( shimElement ) {
+ shimElement.style.width = divElement.offsetWidth;
+ shimElement.style.height = divElement.offsetHeight;
+ shimElement.style.top = divElement.style.top;
+ shimElement.style.left = divElement.style.left;
+ /*shimElement.style.zIndex = divElement.style.zIndex - 1; */
+ shimElement.style.display = "block";
+ shimElement.style.filter='progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)';
+ }
+ }
+
+ }
+
+ this.shown = true;
+
+ if (this.parentMenu)
+ this.parentMenu.show();
+};
+
+WebFXMenu.prototype.hide = function () {
+ this.hideAllSubs();
+ var divElement = document.getElementById(this.id);
+ if ( divElement ) {
+ divElement.style.visibility = "hidden";
+ if ( ie ) {
+ var shimElement = document.getElementById(this.id + "Shim");
+ if ( shimElement ) {
+ shimElement.style.display = "none";
+ }
+ }
+ }
+
+ this.shown = false;
+};
+
+WebFXMenu.prototype.hideAllSubs = function () {
+ for (var i = 0; i < this._subMenus.length; i++) {
+ if (this._subMenus[i].shown)
+ this._subMenus[i].hide();
+ }
+};
+
+WebFXMenu.prototype.toString = function () {
+ var top = this.top + this.borderTop + this.paddingTop;
+ var str = "<div id='" + this.id + "' class='webfx-menu' style='" +
+ "width:" + (!ieBox ?
+ this.width - this.borderLeft - this.paddingLeft - this.borderRight - this.paddingRight :
+ this.width) + "px;" +
+ (this.useAutoPosition ?
+ "left:" + this.left + "px;" + "top:" + this.top + "px;" :
+ "") +
+ (ie50 ? "filter: none;" : "") +
+ "'>";
+
+ if (this._menuItems.length == 0) {
+ str += "<span class='webfx-menu-empty'>" + this.emptyText + "</span>";
+ }
+ else {
+ str += '<span class="webfx-menu-title" onmouseover="webFXMenuHandler.overMenuItem(this)"' +
+ (webfxMenuUseHover ? " onmouseout='webFXMenuHandler.outMenuItem(this)'" : "") +
+ '>' + this.emptyText + '</span>';
+ // str += '<div id="' + this.id + '-title">' + this.emptyText + '</div>';
+ // loop through all menuItems
+ for (var i = 0; i < this._menuItems.length; i++) {
+ var mi = this._menuItems[i];
+ str += mi;
+ if (!this.useAutoPosition) {
+ if (mi.subMenu && !mi.subMenu.useAutoPosition)
+ mi.subMenu.top = top - mi.subMenu.borderTop - mi.subMenu.paddingTop;
+ top += mi.height;
+ }
+ }
+
+ }
+
+ str += "</div>";
+
+ if ( ie ) {
+ str += "<iframe id='" + this.id + "Shim' src='javascript:false;' scrolling='no' frameBorder='0' style='position:absolute; top:0px; left: 0px; display:none;'></iframe>";
+ }
+
+ for (var i = 0; i < this._subMenus.length; i++) {
+ this._subMenus[i].left = this.left + this.width - this._subMenus[i].borderLeft;
+ str += this._subMenus[i];
+ }
+
+ return str;
+};
+// WebFXMenu.prototype.position defined later
+
+function WebFXMenuItem(sText, sHref, sToolTip, oSubMenu) {
+ this.text = sText || webfxMenuItemDefaultText;
+ this.href = (sHref == null || sHref == "") ? webfxMenuItemDefaultHref : sHref;
+ this.subMenu = oSubMenu;
+ if (oSubMenu)
+ oSubMenu.parentMenuItem = this;
+ this.toolTip = sToolTip;
+ this.id = webFXMenuHandler.getId();
+ webFXMenuHandler.all[this.id] = this;
+};
+WebFXMenuItem.prototype.height = webfxMenuItemDefaultHeight;
+WebFXMenuItem.prototype.toString = function () {
+ return "<a" +
+ " id='" + this.id + "'" +
+ " href=\"" + this.href + "\"" +
+ (this.toolTip ? " title=\"" + this.toolTip + "\"" : "") +
+ " onmouseover='webFXMenuHandler.overMenuItem(this)'" +
+ (webfxMenuUseHover ? " onmouseout='webFXMenuHandler.outMenuItem(this)'" : "") +
+ (this.subMenu ? " unselectable='on' tabindex='-1'" : "") +
+ ">" +
+ (this.subMenu ? "<img class='arrow' src=\"" + webfxMenuImagePath + "arrow.right.black.png\">" : "") +
+ this.text +
+ "</a>";
+};
+
+
+function WebFXMenuSeparator() {
+ this.id = webFXMenuHandler.getId();
+ webFXMenuHandler.all[this.id] = this;
+};
+WebFXMenuSeparator.prototype.height = webfxMenuSeparatorDefaultHeight;
+WebFXMenuSeparator.prototype.toString = function () {
+ return "<div" +
+ " id='" + this.id + "'" +
+ (webfxMenuUseHover ?
+ " onmouseover='webFXMenuHandler.overMenuItem(this)'" +
+ " onmouseout='webFXMenuHandler.outMenuItem(this)'"
+ :
+ "") +
+ "></div>"
+};
+
+function WebFXMenuBar() {
+ this._parentConstructor = WebFXMenu;
+ this._parentConstructor();
+}
+WebFXMenuBar.prototype = new WebFXMenu;
+WebFXMenuBar.prototype.toString = function () {
+ var str = "<div id='" + this.id + "' class='webfx-menu-bar'>";
+
+ // loop through all menuButtons
+ for (var i = 0; i < this._menuItems.length; i++)
+ str += this._menuItems[i];
+
+ str += "</div>";
+
+ for (var i = 0; i < this._subMenus.length; i++)
+ str += this._subMenus[i];
+
+ return str;
+};
+
+function WebFXMenuButton(sText, sHref, sToolTip, oSubMenu) {
+ this._parentConstructor = WebFXMenuItem;
+ this._parentConstructor(sText, sHref, sToolTip, oSubMenu);
+}
+WebFXMenuButton.prototype = new WebFXMenuItem;
+WebFXMenuButton.prototype.toString = function () {
+ return "<a" +
+ " id='" + this.id + "'" +
+ " href='" + this.href + "'" +
+ (this.toolTip ? " title='" + this.toolTip + "'" : "") +
+ (webfxMenuUseHover ?
+ (" onmouseover='webFXMenuHandler.overMenuItem(this)'" +
+ " onmouseout='webFXMenuHandler.outMenuItem(this)'") :
+ (
+ " onfocus='webFXMenuHandler.overMenuItem(this)'" +
+ (this.subMenu ?
+ " onblur='webFXMenuHandler.blurMenu(this)'" :
+ ""
+ )
+ )) +
+ ">" +
+ this.text +
+ (this.subMenu ? "<img class='arrow' src='" + webfxMenuImagePath + "arrow.down.png'>" : "") +
+ "</a>";
+};
+
+
+
+
+
+/* Position functions */
+
+
+function getInnerLeft(el, debug) {
+
+ if (el == null) return 0;
+
+ if (ieBox && el == document.body || !ieBox && el == document.documentElement) return 0;
+
+ //if ( debug )
+ // alert ( 'getInnerLeft: ' + getLeft(el) + ' - ' + getBorderLeft(el) );
+
+ return parseInt( getLeft(el) + parseInt(getBorderLeft(el)) );
+
+}
+
+
+
+function getLeft(el, debug) {
+
+ if (el == null) return 0;
+
+ //if ( debug )
+ // alert ( el + ': ' + el.offsetLeft + ' - ' + getInnerLeft(el.offsetParent) );
+
+ return parseInt( el.offsetLeft + parseInt(getInnerLeft(el.offsetParent)) );
+
+}
+
+
+
+function getInnerTop(el) {
+
+ if (el == null) return 0;
+
+ if (ieBox && el == document.body || !ieBox && el == document.documentElement) return 0;
+
+ return parseInt( getTop(el) + parseInt(getBorderTop(el)) );
+
+}
+
+
+
+function getTop(el) {
+
+ if (el == null) return 0;
+
+ return parseInt( el.offsetTop + parseInt(getInnerTop(el.offsetParent)) );
+
+}
+
+
+
+function getBorderLeft(el) {
+
+ return ie ?
+
+ el.clientLeft :
+
+ ( khtml
+ ? parseInt(document.defaultView.getComputedStyle(el, null).getPropertyValue("border-left-width"))
+ : parseInt(window.getComputedStyle(el, null).getPropertyValue("border-left-width"))
+ );
+
+}
+
+
+
+function getBorderTop(el) {
+
+ return ie ?
+
+ el.clientTop :
+
+ ( khtml
+ ? parseInt(document.defaultView.getComputedStyle(el, null).getPropertyValue("border-left-width"))
+ : parseInt(window.getComputedStyle(el, null).getPropertyValue("border-top-width"))
+ );
+
+}
+
+
+
+function opera_getLeft(el) {
+
+ if (el == null) return 0;
+
+ return el.offsetLeft + opera_getLeft(el.offsetParent);
+
+}
+
+
+
+function opera_getTop(el) {
+
+ if (el == null) return 0;
+
+ return el.offsetTop + opera_getTop(el.offsetParent);
+
+}
+
+
+
+function getOuterRect(el, debug) {
+
+ return {
+
+ left: (opera ? opera_getLeft(el) : getLeft(el, debug)),
+
+ top: (opera ? opera_getTop(el) : getTop(el)),
+
+ width: el.offsetWidth,
+
+ height: el.offsetHeight
+
+ };
+
+}
+
+
+
+// mozilla bug! scrollbars not included in innerWidth/height
+
+function getDocumentRect(el) {
+
+ return {
+
+ left: 0,
+
+ top: 0,
+
+ width: (ie ?
+
+ (ieBox ? document.body.clientWidth : document.documentElement.clientWidth) :
+
+ window.innerWidth
+
+ ),
+
+ height: (ie ?
+
+ (ieBox ? document.body.clientHeight : document.documentElement.clientHeight) :
+
+ window.innerHeight
+
+ )
+
+ };
+
+}
+
+
+
+function getScrollPos(el) {
+
+ return {
+
+ left: (ie ?
+
+ (ieBox ? document.body.scrollLeft : document.documentElement.scrollLeft) :
+
+ window.pageXOffset
+
+ ),
+
+ top: (ie ?
+
+ (ieBox ? document.body.scrollTop : document.documentElement.scrollTop) :
+
+ window.pageYOffset
+
+ )
+
+ };
+
+}
+
+
+/* end position functions */
+
+WebFXMenu.prototype.position = function (relEl, sDir) {
+ var dir = sDir;
+ // find parent item rectangle, piRect
+ var piRect;
+ if (!relEl) {
+ var pi = this.parentMenuItem;
+ if (!this.parentMenuItem)
+ return;
+
+ relEl = document.getElementById(pi.id);
+ if (dir == null)
+ dir = pi instanceof WebFXMenuButton ? "vertical" : "horizontal";
+ //alert('created RelEl from parent: ' + pi.id);
+ piRect = getOuterRect(relEl, 1);
+ }
+ else if (relEl.left != null && relEl.top != null && relEl.width != null && relEl.height != null) { // got a rect
+ //alert('passed a Rect as RelEl: ' + typeof(relEl));
+
+ piRect = relEl;
+ }
+ else {
+ //alert('passed an element as RelEl: ' + typeof(relEl));
+ piRect = getOuterRect(relEl);
+ }
+
+ var menuEl = document.getElementById(this.id);
+ var menuRect = getOuterRect(menuEl);
+ var docRect = getDocumentRect();
+ var scrollPos = getScrollPos();
+ var pMenu = this.parentMenu;
+
+ if (dir == "vertical") {
+ if (piRect.left + menuRect.width - scrollPos.left <= docRect.width) {
+ //alert('piRect.left: ' + piRect.left);
+ this.left = piRect.left;
+// if ( ! ie )
+// this.left = this.left + 138;
+ } else if (docRect.width >= menuRect.width) {
+ //konq (not safari though) winds up here by accident and positions the menus all weird
+ //alert('docRect.width + scrollPos.left - menuRect.width');
+
+ this.left = docRect.width + scrollPos.left - menuRect.width;
+ } else {
+ //alert('scrollPos.left: ' + scrollPos.left);
+ this.left = scrollPos.left;
+ }
+
+ if (piRect.top + piRect.height + menuRect.height <= docRect.height + scrollPos.top)
+
+ this.top = piRect.top + piRect.height;
+
+ else if (piRect.top - menuRect.height >= scrollPos.top)
+
+ this.top = piRect.top - menuRect.height;
+
+ else if (docRect.height >= menuRect.height)
+
+ this.top = docRect.height + scrollPos.top - menuRect.height;
+
+ else
+
+ this.top = scrollPos.top;
+ }
+ else {
+ if (piRect.top + menuRect.height - this.borderTop - this.paddingTop <= docRect.height + scrollPos.top)
+
+ this.top = piRect.top - this.borderTop - this.paddingTop;
+
+ else if (piRect.top + piRect.height - menuRect.height + this.borderTop + this.paddingTop >= 0)
+
+ this.top = piRect.top + piRect.height - menuRect.height + this.borderBottom + this.paddingBottom + this.shadowBottom;
+
+ else if (docRect.height >= menuRect.height)
+
+ this.top = docRect.height + scrollPos.top - menuRect.height;
+
+ else
+
+ this.top = scrollPos.top;
+
+
+
+ var pMenuPaddingLeft = pMenu ? pMenu.paddingLeft : 0;
+
+ var pMenuBorderLeft = pMenu ? pMenu.borderLeft : 0;
+
+ var pMenuPaddingRight = pMenu ? pMenu.paddingRight : 0;
+
+ var pMenuBorderRight = pMenu ? pMenu.borderRight : 0;
+
+
+
+ if (piRect.left + piRect.width + menuRect.width + pMenuPaddingRight +
+
+ pMenuBorderRight - this.borderLeft + this.shadowRight <= docRect.width + scrollPos.left)
+
+ this.left = piRect.left + piRect.width + pMenuPaddingRight + pMenuBorderRight - this.borderLeft;
+
+ else if (piRect.left - menuRect.width - pMenuPaddingLeft - pMenuBorderLeft + this.borderRight + this.shadowRight >= 0)
+
+ this.left = piRect.left - menuRect.width - pMenuPaddingLeft - pMenuBorderLeft + this.borderRight + this.shadowRight;
+
+ else if (docRect.width >= menuRect.width)
+
+ this.left = docRect.width + scrollPos.left - menuRect.width;
+
+ else
+
+ this.left = scrollPos.left;
+ }
+};
diff --git a/httemplate/elements/xmlhttp.html b/httemplate/elements/xmlhttp.html
new file mode 100644
index 000000000..6efc395f7
--- /dev/null
+++ b/httemplate/elements/xmlhttp.html
@@ -0,0 +1,111 @@
+%
+% my ( %opt ) = @_;
+%
+% my $url = $opt{'url'};
+% my $method = exists($opt{'method'}) ? $opt{'method'} : 'GET';
+% #my @subs = @{ $opt{'subs'};
+% my $key = exists($opt{'key'}) ? $opt{'key'} : '';
+%
+% $url .= ( ($url =~ /\?/) ? '&' : '?' )
+% if $method eq 'GET';
+%
+%
+
+
+<SCRIPT TYPE="text/javascript">
+
+ function rs_init_object() {
+ var A;
+ try {
+ A=new ActiveXObject("Msxml2.XMLHTTP");
+ } catch (e) {
+ try {
+ A=new ActiveXObject("Microsoft.XMLHTTP");
+ } catch (oc) {
+ A=null;
+ }
+ }
+ if(!A && typeof XMLHttpRequest != "undefined")
+ A = new XMLHttpRequest();
+ if (!A)
+ alert("Can't create XMLHttpRequest object");
+ return A;
+
+ }
+% foreach my $func ( @{$opt{'subs'}} ) {
+%
+% my $furl = $url;
+% $furl =~ s/\"/\\\\\"/; #javascript escape
+%
+%
+
+
+ function <%$key%><%$func%>() {
+ // count args; build URL
+ var url = "<%$furl%>";
+ var a = <%$key%><%$func%>.arguments;
+
+ var args;
+ var len;
+ var content = 'sub=<% uri_escape($func) %>';
+ if ( a && typeof a == 'object' && a[0].constructor == Array ) {
+ args = a[0];
+ len = args.length
+ } else {
+ args = a;
+ len = args.length - 1;
+ }
+ for (var i = 0; i < len; i++)
+ content = content + "&arg=" + escape(args[i]);
+ content = content.replace( /[+]/g, '%2B'); // fix unescaped plus signs
+
+ if ( '<%$method%>' == 'GET' ) {
+ url = url + content;
+ }
+
+ //alert('<%$method%> ' + url);
+
+ var xmlhttp = rs_init_object();
+ xmlhttp.open("<%$method%>", url, true);
+
+ xmlhttp.onreadystatechange = function() {
+ if (xmlhttp.readyState != 4)
+ return;
+
+ if (xmlhttp.status != 200) {
+ alert(xmlhttp.status + " status connecting to " + url);
+ } else {
+ var data = xmlhttp.responseText;
+ //alert('received response: ' + data);
+ a[a.length-1](data);
+ if ( data.indexOf("<b>System error</b>") > -1 ) {
+ var w;
+ if ( w = window.open("about:blank") ) {
+ w.document.write(data);
+ } else {
+ // popup blocking? should use an overlib popup instead
+ alert("Error popup disabled; try disabling popup blocking to see");
+ }
+ }
+ }
+ }
+
+ if ( '<%$method%>' == 'POST' ) {
+
+ xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+ xmlhttp.send(content);
+
+ } else {
+
+ xmlhttp.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
+ xmlhttp.send(null);
+
+ }
+
+ //rs_debug("x_$func_name url = " + url);
+ //rs_debug("x_$func_name waiting..");
+ }
+% }
+
+
+</SCRIPT>
diff --git a/httemplate/graph/cust_bill_pkg.cgi b/httemplate/graph/cust_bill_pkg.cgi
new file mode 100644
index 000000000..4070069ac
--- /dev/null
+++ b/httemplate/graph/cust_bill_pkg.cgi
@@ -0,0 +1,121 @@
+<% include('elements/monthly.html',
+ 'title' => $title. 'Sales Report (Gross)',
+ 'graph_type' => 'Mountain',
+ 'items' => \@items,
+ 'params' => \@params,
+ 'labels' => \@labels,
+ 'graph_labels' => \@labels,
+ 'colors' => \@colors,
+ 'links' => \@links,
+ 'remove_empty' => 1,
+ 'bottom_total' => 1,
+ 'bottom_link' => "$link;",
+ 'start_month' => $smonth,
+ 'start_year' => $syear,
+ 'end_month' => $emonth,
+ 'end_year' => $eyear,
+ 'agentnum' => $agentnum,
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+#find first month
+my $syear = $cgi->param('start_year'); # || 1899+$curyear;
+my $smonth = $cgi->param('start_month'); # || $curmon+1;
+
+#find last month
+my $eyear = $cgi->param('end_year'); # || 1900+$curyear;
+my $emonth = $cgi->param('end_month'); # || $curmon+1;
+
+#XXX or virtual
+my( $agentnum, $sel_agent ) = ('', '');
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+ $sel_agent = qsearchs('agent', { 'agentnum' => $agentnum } );
+ die "agentnum $agentnum not found!" unless $sel_agent;
+}
+my $title = $sel_agent ? $sel_agent->agent.' ' : '';
+
+#false lazinessish w/search/cust_pkg.cgi
+my $classnum = 0;
+my @pkg_class = ();
+if ( $cgi->param('classnum') =~ /^(\d*)$/ ) {
+ $classnum = $1;
+ if ( $classnum ) {
+ @pkg_class = ( qsearchs('pkg_class', { 'classnum' => $classnum } ) );
+ die "classnum $classnum not found!" unless $pkg_class[0];
+ $title .= $pkg_class[0]->classname.' ';
+ } elsif ( $classnum eq '' ) {
+ $title .= 'Empty class ';
+ @pkg_class = ( '(empty class)' );
+ } elsif ( $classnum eq '0' ) {
+ @pkg_class = qsearch('pkg_class', {} ); # { 'disabled' => '' } );
+ push @pkg_class, '(empty class)';
+ }
+}
+#eslaf
+
+my $hue = 0;
+#my $hue_increment = 170;
+#my $hue_increment = 145;
+my $hue_increment = 125;
+
+my @items = ();
+my @params = ();
+my @labels = ();
+my @colors = ();
+my @links = ();
+
+my $link = "${p}search/cust_bill_pkg.cgi?nottax=1;include_comp_cust=1";
+
+foreach my $agent ( $sel_agent || qsearch('agent', { 'disabled' => '' } ) ) {
+
+ my $col_scheme = Color::Scheme->new
+ ->from_hue($hue) #->from_hex($agent->color)
+ ->scheme('analogic')
+ ;
+ my @recur_colors = ();
+ my @onetime_colors = ();
+
+ ### fixup the color handling for package classes...
+ my $n = 0;
+
+ foreach my $pkg_class ( @pkg_class ) {
+
+ push @items, 'cust_bill_pkg';
+
+
+ push @labels,
+ ( $sel_agent ? '' : $agent->agent.' ' ).
+ ( $classnum eq '0'
+ ? ( ref($pkg_class) ? $pkg_class->classname : $pkg_class )
+ : ''
+ );
+
+ my $row_classnum = ref($pkg_class) ? $pkg_class->classnum : 0;
+ my $row_agentnum = $agent->agentnum;
+ push @params, [ 'classnum' => $row_classnum,
+ 'agentnum' => $row_agentnum,
+ ];
+
+ push @links, "$link;agentnum=$row_agentnum;classnum=$row_classnum;";
+
+ @recur_colors = ($col_scheme->colors)[0,4,8,1,5,9]
+ unless @recur_colors;
+ @onetime_colors = ($col_scheme->colors)[2,6,10,3,7,11]
+ unless @onetime_colors;
+ push @colors, shift @recur_colors;
+
+ }
+
+ $hue += $hue_increment;
+
+}
+
+#use Data::Dumper;
+#warn Dumper(\@items);
+
+</%init>
diff --git a/httemplate/graph/elements/monthly.html b/httemplate/graph/elements/monthly.html
new file mode 100644
index 000000000..f5789a2a2
--- /dev/null
+++ b/httemplate/graph/elements/monthly.html
@@ -0,0 +1,207 @@
+%
+%
+% # options example...
+% #
+% # 'title' => 'Page title',
+% # 'items' => \@items,
+% # 'params' => \@params, # opt,
+% # 'labels' => \@labels, # or \%labels (keys are items)
+% # 'graph_labels' => \@graph_labels, # or \%graph_labels,
+% # 'colors' => \@colors, # or \%colors,
+% # 'links => \@links, # or \%link, #opt
+% # 'start_month' => $smonth,
+% # 'start_year' => $syear,
+% # 'end_month' => $emonth,
+% # 'end_year' => $eyear,
+% # 'agentnum' => $agentnum, #opt
+% # 'nototal' => 1, #opt,
+% # 'graph_type' => 'LinesPoints', #opt
+% # 'remove_empty' => 1, #opt,
+% # 'bottom_total' => 1, #opt,
+%
+% my(%opt) = @_;
+% my @items = @{ $opt{'items'} };
+%
+% foreach my $other (qw( labels graph_labels colors links )) {
+% #foreach my $other (qw( labels graph_labels colors )) {
+% if ( ref($opt{$other}) eq 'HASH' ) {
+% $opt{$other} = [ map $opt{$other}{$_}, @items ];
+% }
+% }
+%
+% my $report = new FS::Report::Table::Monthly (
+%
+% #'items' => $opt{'items'},
+% 'items' => \@items,
+% 'params' => $opt{'params'},
+% 'item_labels' => ( $cgi->param('_type') =~ /^(png)$/
+% ? $opt{'graph_labels'}
+% : $opt{'labels'}
+% ),
+% 'colors' => $opt{'colors'},
+% 'links' => $opt{'links'},
+%
+% 'start_month' => $opt{'start_month'},
+% 'start_year' => $opt{'start_year'},
+% 'end_month' => $opt{'end_month'},
+% 'end_year' => $opt{'end_year'},
+%
+% 'agentnum' => $opt{'agentnum'},
+% 'remove_empty' => $opt{'remove_empty'},
+% );
+% my $data = $report->data;
+%
+% if ( $cgi->param('_type') =~ /^(png)$/ ) {
+%
+% #my $chart = Chart::LinesPoints->new(1024,480);
+% #my $chart = Chart::LinesPoints->new(768,480);
+%
+% my $graph_type = 'LinesPoints';
+% if ( $opt{'graph_type'} =~ /^(LinesPoints|Mountain)$/ ) {
+% $graph_type = $1;
+% }
+% my $class = "Chart::$graph_type";
+%
+% my $chart = $class->new(976,384);
+%
+% my $d = 0;
+% $chart->set(
+% #'min_val' => 0,
+% 'legend' => 'bottom',
+% 'colors' => { (
+% map { my $color = $_;
+% 'dataset'.$d++ =>
+% [ map hex($_), unpack 'a2a2a2', $color ]
+% }
+% #@{ $opt{'colors'} }
+% @{ $data->{'colors'} }
+% ),
+% #'grey_background' => [ 211, 211, 211 ],
+% 'grey_background' => 'white',
+% 'background' => [ 0xe8, 0xe8, 0xe8 ], #grey
+% },
+% #'grey_background' => 'false',
+% 'legend_labels' => $data->{'item_labels'},
+% 'brush_size' => 4,
+% #'pt_size' => 12,
+% );
+%
+% #my @data = map { $data->{$_} } ( 'label', @items );
+% my @data = @{ $data->{data} };
+% unshift @data, $data->{'label'};
+%
+% http_header('Content-Type' => 'image/png' );
+%
+% $chart->_set_colors();
+%
+%
+<% $chart->scalar_png(\@data) %>
+%
+%
+% } else {
+%
+% my @mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
+%
+%
+<% include('/elements/header.html', $opt{'title'} ) %>
+% $cgi->param('_type', 'png');
+
+<IMG SRC="<% $cgi->self_url %>" WIDTH="976" HEIGHT="384">
+<BR>
+
+<% table('e8e8e8') %>
+
+<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>
+% }
+% unless ( $opt{'nototal'} ) {
+
+ <TH>Total</TH>
+% }
+
+
+</TR>
+% my @bottom_total = ();
+% foreach my $row ( @{ $data->{'items'} } ) {
+%
+% #my $color = shift( @{ $opt{'colors'} } );
+% my $color = shift( @{ $data->{'colors'} } );
+% my $link = shift( @{ $data->{'links'} } );
+% $link = $link ? qq(<A HREF="$link) : '';
+%
+
+
+ <TR>
+
+ <TH><FONT COLOR="#<% $color %>"><% shift( @{ $data->{'item_labels'} } ) %></FONT></TH>
+% #my $link = exists($opt{'links'}{$row})
+% # ? qq(<A HREF="$opt{'links'}{$row})
+% # : '';
+% my @speriod = @{$data->{speriod}};
+% my @eperiod = @{$data->{eperiod}};
+% my $total = 0;
+%
+% my $col = 0;
+% foreach my $column ( @{ shift( @{$data->{data}} ) } ) { # ( @{$data->{$row}} ) {
+%
+
+
+ <TD ALIGN="right" BGCOLOR="#ffffff">
+ <% $link ? $link. 'begin='. shift(@speriod). ';end='. shift(@eperiod). '">' : '' %><FONT COLOR="#<% $color %>">$<% sprintf("%.2f", $column) %></FONT><% $link ? '</A>' : '' %>
+ </TD>
+%
+% $total += $column;
+% $bottom_total[$col++] += $column;
+%
+% }
+% unless ( $opt{'nototal'} ) {
+
+
+ <TD ALIGN="right" BGCOLOR="#f5f6be">
+ <% $link ? $link. 'begin='. ${$data->{speriod}}[0]. ';end='. ${$data->{eperiod}}[-1]. '">' : '' %><FONT COLOR="#<% $color %>">$<% sprintf("%.2f", $total) %></FONT><% $link ? '</A>' : '' %>
+ </TD>
+% $bottom_total[$col++] += $total;
+% }
+
+
+ </TR>
+% }
+% if ( $opt{'bottom_total'} ) {
+% my @speriod = ( @{$data->{speriod}}, ${$data->{speriod}}[0] );
+% my @eperiod = ( @{$data->{eperiod}}, ${$data->{eperiod}}[-1] );
+%
+
+
+ <TR>
+ <TH>Total</TH>
+% foreach my $total ( @bottom_total ) {
+
+
+ <TD ALIGN="right" BGCOLOR="#f5f6be">
+ <% $opt{'bottom_link'}
+ ? '<A HREF="'. $opt{'bottom_link'}.
+ 'begin='. shift(@speriod).
+ ';end='. shift(@eperiod). '">'
+ : ''
+ %>$<% sprintf("%.2f", $total) %><% $opt{'bottom_link'} ? '</A>' : '' %>
+
+ </TD>
+% }
+
+
+ </TR>
+% }
+
+
+</TABLE>
+
+<% include('/elements/footer.html') %>
+% }
+
diff --git a/httemplate/graph/money_time.cgi b/httemplate/graph/money_time.cgi
new file mode 100644
index 000000000..2b98af838
--- /dev/null
+++ b/httemplate/graph/money_time.cgi
@@ -0,0 +1,85 @@
+<% include('elements/monthly.html',
+ 'title' => $agentname.
+ 'Sales, Credits and Receipts Summary',
+ 'items' => \@items,
+ 'labels' => \%label,
+ 'graph_labels' => \%graph_label,
+ 'colors' => \%color,
+ 'links' => \%link,
+ 'start_month' => $smonth,
+ 'start_year' => $syear,
+ 'end_month' => $emonth,
+ 'end_year' => $eyear,
+ 'agentnum' => $agentnum,
+ 'nototal' => scalar($cgi->param('12mo')),
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+#find first month
+my $syear = $cgi->param('start_year'); # || 1899+$curyear;
+my $smonth = $cgi->param('start_month'); # || $curmon+1;
+
+#find last month
+my $eyear = $cgi->param('end_year'); # || 1900+$curyear;
+my $emonth = $cgi->param('end_month'); # || $curmon+1;
+
+#XXX or virtual
+my( $agentnum, $agent ) = ('', '');
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+ $agent = qsearchs('agent', { 'agentnum' => $agentnum } );
+ die "agentnum $agentnum not found!" unless $agent;
+}
+
+my $agentname = $agent ? $agent->agent.' ' : '';
+
+my @items = qw( invoiced netsales credits payments receipts );
+if ( $cgi->param('12mo') == 1 ) {
+ @items = map $_.'_12mo', @items;
+}
+
+my %label = (
+ 'invoiced' => 'Gross Sales',
+ 'netsales' => 'Net Sales',
+ 'credits' => 'Credits',
+ 'payments' => 'Gross Receipts',
+ 'receipts' => 'Net Receipts',
+);
+
+my %graph_suffix = (
+ 'invoiced' => ' (invoiced)',
+ 'netsales' => ' (invoiced - applied credits)',
+ 'credits' => '',
+ 'payments' => ' (payments)',
+ 'receipts' => '/Cashflow (payments - refunds)',
+);
+my %graph_label = map { $_ => $label{$_}.$graph_suffix{$_} } keys %label;
+
+$label{$_.'_12mo'} = $label{$_}. " (previous 12 months)"
+ foreach keys %label;
+
+$graph_label{$_.'_12mo'} = $graph_label{$_}. " (previous 12 months)"
+ foreach keys %graph_label;
+
+my %color = (
+ 'invoiced' => '9999ff', #light blue
+ 'netsales' => '0000cc', #blue
+ 'credits' => 'cc0000', #red
+ 'payments' => '99cc99', #light green
+ 'receipts' => '00cc00', #green
+);
+$color{$_.'_12mo'} = $color{$_}
+ foreach keys %color;
+
+my %link = (
+ 'invoiced' => "${p}search/cust_bill.html?agentnum=$agentnum;",
+ 'credits' => "${p}search/cust_credit.html?agentnum=$agentnum;",
+ 'payments' => "${p}search/cust_pay.cgi?magic=_date;agentnum=$agentnum;",
+);
+# XXX link 12mo?
+
+</%init>
diff --git a/httemplate/graph/report_cust_bill_pkg.html b/httemplate/graph/report_cust_bill_pkg.html
new file mode 100644
index 000000000..c18e94d5d
--- /dev/null
+++ b/httemplate/graph/report_cust_bill_pkg.html
@@ -0,0 +1,35 @@
+<% include('/elements/header.html', 'Sales Report' ) %>
+
+<FORM ACTION="cust_bill_pkg.cgi" METHOD="GET">
+
+<TABLE>
+
+<% include('/elements/tr-select-from_to.html' ) %>
+
+<% include('/elements/tr-select-agent.html', '', 'label' => 'For agent: ' ) %>
+
+<% include('/elements/tr-select-pkg_class.html', '',
+ 'pre_options' => [ '0' => 'all' ],
+ 'empty_label' => '(empty class)',
+ )
+%>
+
+<!--
+<TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="separate_0freq" VALUE="1"></TD>
+ <TD>Separate one-time vs. recurring sales</TD>
+</TR>
+-->
+
+</TABLE>
+
+<BR><INPUT TYPE="submit" VALUE="Display">
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/graph/report_money_time.html b/httemplate/graph/report_money_time.html
new file mode 100644
index 000000000..652d7152e
--- /dev/null
+++ b/httemplate/graph/report_money_time.html
@@ -0,0 +1,39 @@
+<% include('/elements/header.html', 'Sales, Credits and Receipts Summary' ) %>
+
+<FORM ACTION="money_time.cgi" METHOD="GET">
+
+<!--
+<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>
+-->
+
+<TABLE>
+
+<% include('/elements/tr-select-from_to.html' ) %>
+
+<% include('/elements/tr-select-agent.html', '', 'label' => 'For agent: ' ) %>
+
+<TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="12mo" VALUE="1"></TD>
+ <TD>Show 12 month totals instead of monthly values</TD>
+</TR>
+
+</TABLE>
+
+<BR><INPUT TYPE="submit" VALUE="Display">
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/images/32clear.gif b/httemplate/images/32clear.gif
new file mode 100644
index 000000000..5fdcea204
--- /dev/null
+++ b/httemplate/images/32clear.gif
Binary files differ
diff --git a/httemplate/images/ach.png b/httemplate/images/ach.png
new file mode 100644
index 000000000..fdcd5e6ed
--- /dev/null
+++ b/httemplate/images/ach.png
Binary files differ
diff --git a/httemplate/images/arrow.down.png b/httemplate/images/arrow.down.png
new file mode 100644
index 000000000..34cb0286a
--- /dev/null
+++ b/httemplate/images/arrow.down.png
Binary files differ
diff --git a/httemplate/images/arrow.right.black.png b/httemplate/images/arrow.right.black.png
new file mode 100644
index 000000000..933c25894
--- /dev/null
+++ b/httemplate/images/arrow.right.black.png
Binary files differ
diff --git a/httemplate/images/arrow.right.png b/httemplate/images/arrow.right.png
new file mode 100644
index 000000000..60bcb76ab
--- /dev/null
+++ b/httemplate/images/arrow.right.png
Binary files differ
diff --git a/httemplate/images/background-cheat.png b/httemplate/images/background-cheat.png
new file mode 100644
index 000000000..ad332f675
--- /dev/null
+++ b/httemplate/images/background-cheat.png
Binary files differ
diff --git a/httemplate/images/black-gradient.png b/httemplate/images/black-gradient.png
new file mode 100644
index 000000000..225732d16
--- /dev/null
+++ b/httemplate/images/black-gradient.png
Binary files differ
diff --git a/httemplate/images/black-gray-corner.png b/httemplate/images/black-gray-corner.png
new file mode 100644
index 000000000..17954cdd7
--- /dev/null
+++ b/httemplate/images/black-gray-corner.png
Binary files differ
diff --git a/httemplate/images/black-gray-gradient.png b/httemplate/images/black-gray-gradient.png
new file mode 100644
index 000000000..f5c318fe7
--- /dev/null
+++ b/httemplate/images/black-gray-gradient.png
Binary files differ
diff --git a/httemplate/images/black-gray-side.png b/httemplate/images/black-gray-side.png
new file mode 100644
index 000000000..f7a98a43d
--- /dev/null
+++ b/httemplate/images/black-gray-side.png
Binary files differ
diff --git a/httemplate/images/black-gray-top.png b/httemplate/images/black-gray-top.png
new file mode 100644
index 000000000..ed0707573
--- /dev/null
+++ b/httemplate/images/black-gray-top.png
Binary files differ
diff --git a/httemplate/images/calendar-disabled.png b/httemplate/images/calendar-disabled.png
new file mode 100644
index 000000000..81816bcd6
--- /dev/null
+++ b/httemplate/images/calendar-disabled.png
Binary files differ
diff --git a/httemplate/images/calendar.png b/httemplate/images/calendar.png
new file mode 100644
index 000000000..163266174
--- /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 000000000..48c58d561
--- /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 000000000..82d1f4715
--- /dev/null
+++ b/httemplate/images/cvv2_amex.png
Binary files differ
diff --git a/httemplate/images/menu-left-example.png b/httemplate/images/menu-left-example.png
new file mode 100644
index 000000000..375725cb9
--- /dev/null
+++ b/httemplate/images/menu-left-example.png
Binary files differ
diff --git a/httemplate/images/menu-top-example.png b/httemplate/images/menu-top-example.png
new file mode 100644
index 000000000..bd9bea883
--- /dev/null
+++ b/httemplate/images/menu-top-example.png
Binary files differ
diff --git a/httemplate/images/progressbar-empty.png b/httemplate/images/progressbar-empty.png
new file mode 100644
index 000000000..318219c77
--- /dev/null
+++ b/httemplate/images/progressbar-empty.png
Binary files differ
diff --git a/httemplate/images/progressbar-full.png b/httemplate/images/progressbar-full.png
new file mode 100644
index 000000000..863d8e1ee
--- /dev/null
+++ b/httemplate/images/progressbar-full.png
Binary files differ
diff --git a/httemplate/images/red_telephone_mimooh_01.png b/httemplate/images/red_telephone_mimooh_01.png
new file mode 100644
index 000000000..2212ff0e8
--- /dev/null
+++ b/httemplate/images/red_telephone_mimooh_01.png
Binary files differ
diff --git a/httemplate/images/small-logo.png b/httemplate/images/small-logo.png
new file mode 100644
index 000000000..1e415e6d8
--- /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 000000000..60ab26f86
--- /dev/null
+++ b/httemplate/index.html
@@ -0,0 +1,54 @@
+% my $conf = new FS::Conf;
+
+<% include('/elements/header.html', 'Billing Main' ) %>
+
+<% include('/elements/dashboard-toplist.html') %>
+
+% my $sth = dbh->prepare(
+% #"SELECT DISTINCT custnum FROM h_cust_main JOIN cust_main USING ( custnum )
+% "SELECT custnum FROM h_cust_main JOIN cust_main USING ( custnum )
+% WHERE ( history_action = 'insert' OR history_action = 'replace_new' )
+% AND history_user = ?
+% ORDER BY history_date desc" # LIMIT 10
+% ) or die dbh->errstr;
+%
+% $sth->execute( getotaker() ) or die $sth->errstr;
+%
+% my %saw = ();
+% my @custnums = grep { !$saw{$_}++ } map $_->[0], @{ $sth->fetchall_arrayref };
+%
+% @custnums = splice(@custnums, 0, 10);
+%
+% if ( @custnums ) {
+
+ <% include('/elements/table-grid.html') %>
+
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor = $bgcolor2;
+
+ <TR>
+ <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=1>Customers I recently added or modified</TH>
+ </TR>
+
+% foreach my $custnum ( @custnums ) {
+% my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+% next unless $cust_main;
+
+ <TR>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="view/cust_main.cgi?<% $custnum %>"><% $custnum %>: <% $cust_main->name %></A></TD>
+ </TR>
+
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+% }
+
+ </TABLE>
+
+% }
+
+<% include('/elements/footer.html') %>
diff --git a/httemplate/misc/batch-cust_pay.html b/httemplate/misc/batch-cust_pay.html
new file mode 100644
index 000000000..d85f3b6c3
--- /dev/null
+++ b/httemplate/misc/batch-cust_pay.html
@@ -0,0 +1,395 @@
+<% include("/elements/header.html", 'Quick payment entry',
+ menubar(
+ 'Main Menu' => $p, #popurl(1),
+ ),
+ ( $cgi->param('error') ? '' : 'onload="addRow()"' ),
+ )
+%>
+% if ( $cgi->param('error') ) {
+
+ <FONT SIZE="+1" COLOR="#ff0000"><% $cgi->param('error') %></FONT><BR><BR>
+% }
+
+
+
+<FORM ACTION="process/batch-cust_pay.cgi" NAME="OneTrueForm" METHOD="POST" onsubmit="document.OneTrueForm.submit.disabled=true;">
+
+<!-- <B>Batch</B> <INPUT TYPE="text" NAME="paybatch"><BR><BR> -->
+
+<SCRIPT TYPE="text/javascript">
+
+ function clearhint_custnum() {
+
+ //this.style.color = '#000000';
+
+ if ( this.value == 'Not found' || this.value == 'Multiple' ) {
+ this.value = '';
+ this.style.color = '#000000';
+ }
+
+ }
+
+ function clearhint_customer() {
+
+ this.style.color = '#000000';
+
+ if ( this.value == '(last name or company)' || this.value == 'Not found' )
+ this.value = '';
+
+ }
+
+ function search_custnum() {
+
+ this.style.color = '#000000'
+
+ var custnum_obj = this;
+ var searchrow = this.getAttribute('rownum');
+ var custnum = this.value;
+
+ if ( custnum == 'searching...' || custnum == 'Not found' || custnum == '' )
+ return;
+
+ if ( this.getAttribute('magic') == 'nosearch' ) {
+ this.setAttribute('magic', '');
+ return;
+ }
+
+ if ( ( rownum - searchrow ) == 1 ) {
+ addRow();
+ }
+ var customer = document.getElementById('customer'+searchrow);
+ customer.value = 'searching...';
+ customer.disabled = true;
+ customer.style.color = '#000000';
+ customer.style.backgroundColor = '#dddddd';
+
+ var customer_select = document.getElementById('cust_select'+searchrow);
+
+ //alert('search for custnum ' + custnum + ', row#' + searchrow );
+
+ customer.style.display = '';
+ customer_select.style.display = 'none';
+
+ function search_custnum_update(name) {
+
+ var name = eval('(' + name + ')' );
+
+ customer.disabled = false;
+ customer.style.backgroundColor = '#ffffff';
+
+ if ( name.length > 0 ) {
+ //alert('custnum found: ' + name);
+ customer.value = name;
+ customer.setAttribute('magic', 'nosearch');
+ } else {
+ customer.value = 'Not found';
+ customer.style.color = '#ff0000';
+ custnum_obj.style.color = '#ff0000';
+
+ }
+
+ }
+
+ custnum_search( custnum, search_custnum_update );
+
+ }
+
+ function search_customer() {
+
+ var customer_obj = this;
+ var searchrow = this.getAttribute('rownum');
+ var customer = this.value;
+
+ if ( customer == 'searching...' || customer == 'Not found' || customer == '' )
+ return;
+
+ if ( this.getAttribute('magic') == 'nosearch' ) {
+ this.setAttribute('magic', '');
+ return;
+ }
+
+ if ( ( rownum - searchrow ) == 1 ) {
+ addRow();
+ }
+
+ var custnum_obj = document.getElementById('custnum'+searchrow);
+ custnum_obj.value = 'searching...';
+ custnum_obj.disabled = true;
+ custnum_obj.style.color = '#000000';
+ custnum_obj.style.backgroundColor = '#dddddd';
+
+ var customer_select = document.getElementById('cust_select'+searchrow);
+
+ //alert('search for customer ' + customer + ', row#' + searchrow );
+
+ function search_customer_update(customers) {
+
+ //alert('customers returned: ' + customers);
+
+ var customerArray = eval('(' + customers + ')');
+
+ custnum_obj.disabled = false;
+ custnum_obj.style.backgroundColor = '#ffffff';
+
+ if ( customerArray.length == 0 ) {
+
+ custnum_obj.value = 'Not found';
+ custnum_obj.style.color = '#ff0000';
+ customer_obj.style.color = '#ff0000';
+
+ customer_obj.style.display = '';
+ customer_select.style.display = 'none';
+
+
+ } else if ( customerArray.length == 1 ) {
+
+ //alert('one customer found: ' + customerArray[0]);
+
+ custnum_obj.value = customerArray[0][0];
+ customer_obj.value = customerArray[0][1];
+
+ customer_obj.style.display = '';
+ customer_select.style.display = 'none';
+
+
+ } else {
+
+ custnum_obj.value = 'Multiple'; // or something
+ custnum_obj.style.color = '#ff0000';
+
+ //alert('multiple customers found, have to create select dropdown');
+
+ //blank the current list
+ for ( var i = customer_select.length; i >= 0; i-- )
+ customer_select.options[i] = null;
+
+ opt(customer_select, '', 'Multiple customers match "' + customer + '" - select one', '#ff0000');
+
+ //add the multiple customers
+ for ( var s = 0; s < customerArray.length; s++ )
+ opt(customer_select, customerArray[s][0], customerArray[s][1], '#000000');
+
+ opt(customer_select, 'cancel', '(Edit search string)', '#000000');
+
+ customer_obj.style.display = 'none';
+
+ customer_select.style.display = '';
+
+ }
+
+ }
+
+ smart_search( customer, search_customer_update );
+
+ }
+
+ function select_customer() {
+
+ var custnum = this.options[this.selectedIndex].value;
+ var customer = this.options[this.selectedIndex].text;
+
+ var searchrow = this.getAttribute('rownum');
+ var custnum_obj = document.getElementById('custnum'+searchrow);
+ var customer_obj = document.getElementById('customer'+searchrow);
+
+ if ( custnum == '' ) {
+ //this.style.color = '#ff0000';
+
+ } else if ( custnum == 'cancel' ) {
+
+ custnum_obj.value = '';
+ custnum_obj.style.color = '#000000';
+
+ this.style.display = 'none';
+ customer_obj.style.display = '';
+ customer_obj.focus();
+
+ } else {
+
+
+ custnum_obj.value = custnum;
+ custnum_obj.style.color = '#000000';
+
+ customer_obj.value = customer;
+ customer_obj.style.color = '#000000';
+
+ this.style.display = 'none';
+ customer_obj.style.display = '';
+
+ }
+
+ }
+
+ function opt(what,value,text,color) {
+ var optionName = new Option(text, value, false, false);
+ optionName.style.color = color;
+ var length = what.length;
+ what.options[length] = optionName;
+ }
+
+</SCRIPT>
+
+<TABLE ID="OneTrueTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
+
+<TR>
+ <TH>Cust #</TH>
+ <TH>Customer</TH>
+ <TH>Amount</TH>
+ <TH>Check #</TH>
+ <TH BGCOLOR="#e8e8e8"></TH>
+</TR>
+% my $row = 0;
+% if ( $cgi->param('error') ) {
+% my $param = $cgi->Vars;
+%
+% for ( $row = 0; exists($param->{"custnum$row"}); $row++ ) {
+
+
+ <TR>
+
+ <TD>
+ <INPUT TYPE="text" NAME="custnum<% $row %>" ID="custnum<% $row %>" SIZE=8 MAXLENGTH=12 VALUE="<% $param->{"custnum$row"} %>" rownum="<% $row %>">
+ <SCRIPT TYPE="text/javascript">
+ var custnum_input<% $row %> = document.getElementById("custnum<% $row %>");
+ custnum_input<% $row %>.onfocus = clearhint_custnum;
+ custnum_input<% $row %>.onchange = search_custnum;
+ </SCRIPT>
+ </TD>
+
+ <TD>
+ <INPUT TYPE="text" NAME="customer<% $row %>" ID="customer<% $row %>" SIZE=64 VALUE="<% $param->{"customer$row"} %>" rownum="<% $row %>">
+ <SCRIPT TYPE="text/javascript">
+ var customer_input<% $row %> = document.getElementById("customer<% $row %>");
+ customer_input<% $row %>.onfocus = clearhint_customer;
+ customer_input<% $row %>.onclick = clearhint_customer;
+ customer_input<% $row %>.onchange = search_customer;
+ </SCRIPT>
+ <SELECT NAME="cust_select<% $row %>" ID="cust_select<% $row %>" rownum="<% $row %>" STYLE="color:#ff0000; display:none">
+ </SELECT>
+ <SCRIPT TYPE="text/javascript">
+ var customer_select<% $row %> = document.getElementById("cust_select<% $row %>");
+ customer_select<% $row %>.onchange = select_customer;
+ </SCRIPT>
+ </TD>
+
+ <TD>
+ $<INPUT TYPE="text" NAME="paid<% $row %>" SIZE=8 MAXLENGTH=8 VALUE="<% $param->{"paid$row"} %>" >
+ </TD>
+
+ <TD>
+ <INPUT TYPE="text" NAME="payinfo<% $row %>" SIZE=10 VALUE="<% $param->{"payinfo$row"} %>" >
+ </TD>
+
+ <TD BGCOLOR="#e8e8e8">
+% if ( $param->{"error$row"} ) {
+
+ <FONT SIZE="-1" COLOR="#ff0000">Error: <% $param->{"error$row"} %></FONT>
+% }
+
+ </TD>
+
+ </TR>
+% }
+% }
+
+
+</TABLE>
+
+<!-- <BR>
+<INPUT TYPE="button" VALUE="TEST addrow" onclick="addRow()"> -->
+
+<BR>
+<INPUT TYPE="submit" NAME="submit" VALUE="Post payment batch">
+
+</FORM>
+
+
+<% include('/elements/xmlhttp.html',
+ 'url' => $p. 'misc/xmlhttp-cust_main-search.cgi',
+ 'subs' => [qw( custnum_search smart_search )],
+ )
+%>
+
+<SCRIPT TYPE="text/javascript">
+
+ var rownum = <% $row %>;
+
+ function addRow() {
+
+ var table = document.getElementById('OneTrueTable');
+ var tablebody = table.getElementsByTagName('tbody').item(0);
+
+ var row = document.createElement('TR');
+
+ var custnum_cell = document.createElement('TD');
+
+ var custnum_input = document.createElement('INPUT');
+ custnum_input.setAttribute('name', 'custnum'+rownum);
+ custnum_input.setAttribute('id', 'custnum'+rownum);
+ custnum_input.setAttribute('size', 8);
+ custnum_input.setAttribute('maxlength', 12);
+ custnum_input.setAttribute('rownum', rownum);
+ custnum_input.onfocus = clearhint_custnum;
+ custnum_input.onchange = search_custnum;
+ custnum_cell.appendChild(custnum_input);
+
+ row.appendChild(custnum_cell);
+
+ var customer_cell = document.createElement('TD');
+
+ var customer_input = document.createElement('INPUT');
+ customer_input.setAttribute('name', 'customer'+rownum);
+ customer_input.setAttribute('id', 'customer'+rownum);
+ customer_input.setAttribute('size', 64);
+ customer_input.setAttribute('value', '(last name or company)' );
+ customer_input.setAttribute('rownum', rownum);
+ customer_input.onfocus = clearhint_customer;
+ customer_input.onclick = clearhint_customer;
+ customer_input.onchange = search_customer;
+ customer_cell.appendChild(customer_input);
+
+ var customer_select = document.createElement('SELECT');
+ customer_select.setAttribute('name', 'cust_select'+rownum);
+ customer_select.setAttribute('id', 'cust_select'+rownum);
+ customer_select.setAttribute('rownum', rownum);
+ customer_select.style.color = '#ff0000';
+ customer_select.style.display = 'none';
+ customer_select.onchange = select_customer;
+ customer_cell.appendChild(customer_select);
+
+ row.appendChild(customer_cell);
+
+ var paid_cell = document.createElement('TD');
+
+ var paid_text = document.createTextNode('$');
+ paid_cell.appendChild(paid_text);
+
+ var paid_input = document.createElement('INPUT');
+ paid_input.setAttribute('name', 'paid'+rownum);
+ paid_input.setAttribute('size', 8);
+ paid_input.setAttribute('maxlength', 8);
+ paid_cell.appendChild(paid_input);
+
+ row.appendChild(paid_cell);
+
+ var payinfo_cell = document.createElement('TD');
+ var payinfo_input = document.createElement('INPUT');
+ payinfo_input.setAttribute('name', 'payinfo'+rownum);
+ payinfo_input.setAttribute('size', 10);
+ payinfo_cell.appendChild(payinfo_input);
+ row.appendChild(payinfo_cell);
+
+ var error_cell = document.createElement('TD');
+ error_cell.style.backgroundColor = '#e8e8e8';
+ row.appendChild(error_cell);
+
+ tablebody.appendChild(row);
+
+ rownum++;
+
+ }
+
+</SCRIPT>
+
+</BODY>
+</HTML>
diff --git a/httemplate/misc/bill.cgi b/httemplate/misc/bill.cgi
new file mode 100755
index 000000000..8d4b4ac65
--- /dev/null
+++ b/httemplate/misc/bill.cgi
@@ -0,0 +1,41 @@
+%
+%#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 $conf = new FS::Conf;
+%
+%my $error = $cust_main->bill(
+%# 'time'=>$time
+% );
+%#&eidiot($error) if $error;
+%
+%unless ( $error ) {
+% $cust_main->apply_payments_and_credits;
+%
+% $error = $cust_main->collect(
+% # 'invoice-time'=>$time,
+% #'batch_card'=> 'yes',
+% #'batch_card'=> 'no',
+% #'report_badcard'=> 'yes',
+% #'retry_card' => 'yes',
+% 'retry' => 'yes',
+% 'realtime' => $conf->exists('realtime-backend'),
+% );
+%}
+%#&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 000000000..6f070a444
--- /dev/null
+++ b/httemplate/misc/cancel-unaudited.cgi
@@ -0,0 +1,36 @@
+%
+%
+%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.html b/httemplate/misc/cancel_pkg.html
new file mode 100755
index 000000000..e61000618
--- /dev/null
+++ b/httemplate/misc/cancel_pkg.html
@@ -0,0 +1,95 @@
+%# if ( $link eq 'popup' ) {
+ <% include('/elements/header-popup.html', $title ) %>
+%# } else {
+%# <% include("/elements/header.html", $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>
+
+% if ( $cgi->param('error') ) {
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+
+<FORM NAME="sc_popup" ACTION="<% popurl(1) %>process/cancel_pkg.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
+<INPUT TYPE="hidden" NAME="method" VALUE="<% $method %>">
+
+
+<BR><BR>
+<% ucfirst($method) . " $pkgnum: " .$part_pkg->pkg. ' - ' .$part_pkg->comment %>
+<% ntable("#cccccc", 2) %>
+
+% if ($method eq 'expire') {
+<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>
+<SCRIPT TYPE="text/javascript">
+ Calendar.setup({
+ inputField: "expire_date",
+ ifFormat: "%m/%d/%Y",
+ button: "expire_button",
+ align: "BR"
+ });
+</SCRIPT>
+%}
+%
+
+<% include('/elements/tr-select-reason.html', 'reasonnum', $class, '', '', '', 'document.sc_popup.submit' ) %>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" NAME="submit" VALUE="<% $submit %>" disabled='true'>
+
+</FORM>
+</BODY>
+</HTML>
+
+<%init>
+my($method, $pkgnum, $reasonnum, $submit, $cust_pkg, $part_pkg,
+ $date, $curuser, $class);
+$date = time2str("%m/%d/%Y", time);
+if ( $cgi->param('error') ) {
+ $method = $cgi->param('method');
+ $pkgnum = $cgi->param('pkgnum');
+ $reasonnum = $cgi->param('reasonnum');
+ $date = $cgi->param('date');
+} elsif ( $cgi->param('pkgnum') =~ /^(\d+)$/ ) {
+ $pkgnum = $1;
+} else {
+ die "illegal query ". $cgi->keywords;
+}
+
+$method = $cgi->param('method');
+if ($method eq 'cancel') {
+ $class = 'C';
+ $submit = "Cancel Now";
+}elsif ($method eq 'expire') {
+ $class = 'C';
+ $submit = "Cancel Later";
+}elsif ($method eq 'suspend') {
+ $class = 'S';
+ $submit = "Suspend";
+}else{
+ die "illegal query ". $cgi->keywords;
+}
+
+my $title = ucfirst($method) . ' Package';
+
+$cust_pkg = qsearchs('cust_pkg', {'pkgnum' => $pkgnum});
+die "No such package: $pkgnum" unless $cust_pkg;
+
+$part_pkg = $cust_pkg->part_pkg;
+
+$curuser = $FS::CurrentUser::CurrentUser;
+
+</%init>
+
diff --git a/httemplate/misc/catchall.cgi b/httemplate/misc/catchall.cgi
new file mode 100755
index 000000000..8881746d1
--- /dev/null
+++ b/httemplate/misc/catchall.cgi
@@ -0,0 +1,134 @@
+<!-- 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/cdr-import.html b/httemplate/misc/cdr-import.html
new file mode 100644
index 000000000..5e9e2690d
--- /dev/null
+++ b/httemplate/misc/cdr-import.html
@@ -0,0 +1,16 @@
+<% include("/elements/header.html",'Call Detail Record Import') %>
+<FORM ACTION="process/cdr-import.html" METHOD="POST" ENCTYPE="multipart/form-data">
+Import a CSV file containing Call Detail Records (CDRs).<BR><BR>
+CDR Format: <SELECT NAME="format">
+<OPTION VALUE="asterisk">Asterisk (untested)</OPTION>
+<OPTION VALUE="unitel">Unitel/RSLCOM</OPTION>
+<OPTION VALUE="ams">AMS</OPTION>
+</SELECT><BR><BR>
+
+Filename: <INPUT TYPE="file" NAME="csvfile"><BR><BR>
+
+<INPUT TYPE="submit" VALUE="Upload">
+</FORM>
+
+<% include('/elements/footer.html') %>
+
diff --git a/httemplate/misc/change_pkg.cgi b/httemplate/misc/change_pkg.cgi
new file mode 100755
index 000000000..655799fc1
--- /dev/null
+++ b/httemplate/misc/change_pkg.cgi
@@ -0,0 +1,69 @@
+<% include('/elements/header.html', "Change Package") %>
+
+% if ( $cgi->param('error') ) {
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+
+<% small_custview( $cust_main, $conf->config('countrydefault') || '' , '',
+ "${p}view/cust_main.cgi")
+%>
+
+<FORM ACTION="<% $p %>edit/process/cust_pkg.cgi" METHOD=POST>
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
+<INPUT TYPE="hidden" NAME="remove_pkg" VALUE="<% $pkgnum %>">
+
+<BR>
+Current package: <% $part_pkg->pkg %> - <% $part_pkg->comment %>
+
+<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;
+
+ <OPTION VALUE="<% $pkgpart %>" <% ( $cgi->param('error') && $cgi->param('new_pkgpart') == $pkgpart ) ? ' SELECTED' : '' %>>
+ <% $pkgpart %>: <% $part_pkg->pkg %> - <% $part_pkg->comment %>
+ </OPTION>
+
+%}
+
+</SELECT>
+<BR><BR><INPUT TYPE="submit" VALUE="Change package">
+ </FORM>
+ </BODY>
+</HTML>
+<%init>
+
+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;
+
+my $part_pkg = $cust_pkg->part_pkg;
+
+</%init>
diff --git a/httemplate/misc/counties.cgi b/httemplate/misc/counties.cgi
new file mode 100644
index 000000000..c022a27d9
--- /dev/null
+++ b/httemplate/misc/counties.cgi
@@ -0,0 +1,7 @@
+[ <% join(', ', map { qq("$_") } @counties) %> ]
+<%init>
+
+my( $state, $country ) = $cgi->param('arg');
+my @counties = counties($state, $country);
+
+</%init>
diff --git a/httemplate/misc/cust_main-cancel.cgi b/httemplate/misc/cust_main-cancel.cgi
new file mode 100755
index 000000000..d29e4f5fc
--- /dev/null
+++ b/httemplate/misc/cust_main-cancel.cgi
@@ -0,0 +1,23 @@
+%
+%
+%my $custnum;
+%my $ban = '';
+%if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+% $custnum = $1;
+% $ban = $cgi->param('ban');
+%} else {
+% my($query) = $cgi->keywords;
+% $query =~ /^(\d+)$/ || die "Illegal custnum";
+% $custnum = $1;
+%}
+%
+%my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+%
+%my @errors = $cust_main->cancel( 'ban' => $ban );
+%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 000000000..73efd3705
--- /dev/null
+++ b/httemplate/misc/cust_main-import.cgi
@@ -0,0 +1,76 @@
+<% include("/elements/header.html",'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>
+
+<!-- Simple 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> -->
+
+Extended file format is CSV, with the following field order: <i>agent_custid, refnum[1]<%$req%>, last<%$req%>, first<%$req%>, address1<%$req%>, address2, city<%$req%>, state<%$req%>, zip<%$req%>, country, daytime, night, ship_last, ship_first, ship_address1, ship_address2, ship_city, ship_state, ship_zip, ship_country, payinfo<%$req%>, paycvv, paydate<%$req%>, invoicing_list, pkgpart, username[2], _password[2]</i>
+<BR><BR>
+
+<%$req%> Required fields
+<BR><BR>
+
+[1] This field has special treatment upon import: If a string is passed instead
+of an integer, the string is searched for and if necessary auto-created in the
+target table.
+<BR><BR>
+
+[2] <i>username</i> and <i>_password</i> are required if <i>pkgpart</i> is specified.
+<BR><BR>
+
+<% &ntable("#cccccc") %>
+
+<% include('/elements/tr-select-agent.html', '', #$agentnum,
+ 'label' => "<B>Agent</B>",
+ 'empty_label' => 'Select agent',
+ )
+%>
+
+<TR>
+ <TH ALIGN="right">Format</TH>
+ <TD>
+ <SELECT NAME="format">
+<!-- <OPTION VALUE="simple">Simple -->
+ <OPTION VALUE="extended" SELECTED>Extended
+ </SELECT>
+ </TD>
+</TR>
+
+<TR>
+ <TH ALIGN="right">CSV filename</TH>
+ <TD><INPUT TYPE="file" NAME="csvfile"></TD>
+</TR>
+% #include('/elements/tr-select-part_referral.html')
+%
+
+
+<!--
+<TR>
+ <TH>First package</TH>
+ <TD>
+ <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>
+ </TD>
+</TR>
+-->
+
+</TABLE>
+<BR><BR>
+
+<INPUT TYPE="submit" VALUE="Import">
+</FORM>
+
+<% include('/elements/footer.html') %>
+
+<%once>
+my $req = qq!<font color="#ff0000">*</font>!;
+</%once>
diff --git a/httemplate/misc/cust_main-import_charges.cgi b/httemplate/misc/cust_main-import_charges.cgi
new file mode 100644
index 000000000..cd4441e0b
--- /dev/null
+++ b/httemplate/misc/cust_main-import_charges.cgi
@@ -0,0 +1,14 @@
+<!-- mason kludge -->
+<% include("/elements/header.html",'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-agent_payment_gateway.cgi b/httemplate/misc/delete-agent_payment_gateway.cgi
new file mode 100644
index 000000000..fb0f1e4b8
--- /dev/null
+++ b/httemplate/misc/delete-agent_payment_gateway.cgi
@@ -0,0 +1,15 @@
+% die "you don't have the 'Configuration' access right"
+% unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+%
+% my($query) = $cgi->keywords;
+% $query =~ /^(\d+)$/ || die "Illegal agentgatewaynum";
+% my $agentgatewaynum = $1;
+%
+% my $agent_payment_gateway = qsearchs('agent_payment_gateway', {
+% 'agentgatewaynum' => $agentgatewaynum,
+% });
+%
+% my $error = $agent_payment_gateway->delete;
+% eidiot($error) if $error;
+%
+% print $cgi->redirect($p. "browse/agent.cgi");
diff --git a/httemplate/misc/delete-cust_credit.cgi b/httemplate/misc/delete-cust_credit.cgi
new file mode 100755
index 000000000..e4756a922
--- /dev/null
+++ b/httemplate/misc/delete-cust_credit.cgi
@@ -0,0 +1,17 @@
+%
+%
+%#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 000000000..1fda82e2a
--- /dev/null
+++ b/httemplate/misc/delete-cust_pay.cgi
@@ -0,0 +1,17 @@
+%
+%
+%#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-cust_refund.cgi b/httemplate/misc/delete-cust_refund.cgi
new file mode 100755
index 000000000..3e44560d0
--- /dev/null
+++ b/httemplate/misc/delete-cust_refund.cgi
@@ -0,0 +1,17 @@
+%
+%
+%#untaint refundnum
+%my($query) = $cgi->keywords;
+%$query =~ /^(\d+)$/ || die "Illegal refundnum";
+%my $refundnum = $1;
+%
+%my $cust_refund = qsearchs('cust_refund',{'refundnum'=>$refundnum});
+%my $custnum = $cust_refund->custnum;
+%
+%my $error = $cust_refund->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 000000000..378f69e61
--- /dev/null
+++ b/httemplate/misc/delete-customer.cgi
@@ -0,0 +1,61 @@
+<!-- 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 000000000..cccce357e
--- /dev/null
+++ b/httemplate/misc/delete-domain_record.cgi
@@ -0,0 +1,16 @@
+%
+%
+%#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 000000000..16389a90c
--- /dev/null
+++ b/httemplate/misc/delete-part_export.cgi
@@ -0,0 +1,16 @@
+%
+%
+%#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 000000000..8d6c94969
--- /dev/null
+++ b/httemplate/misc/download-batch.cgi
@@ -0,0 +1,171 @@
+%if ($format eq "BoM") {
+%
+% my($origid,$datacenter,$typecode,$shortname,$longname,$mybank,$myacct) =
+% $conf->config("batchconfig-$format");
+%
+<% sprintf( "A%10s%04u%06u%05u%54s\n",$origid,$pay_batch->batchnum,$jdate,$datacenter,"").
+ sprintf( "XD%03u%06u%-15s%-30s%09u%-12s \n",$typecode,$jdate,$shortname,$longname,$mybank,$myacct )
+ %>
+%
+%}elsif ($format eq "PAP"){
+%
+% my($origid,$datacenter,$typecode,$shortname,$longname,$mybank,$myacct) =
+% $conf->config("batchconfig-$format");
+%
+<% sprintf( "H%10sD%3s%06u%-15s%09u%-12s%04u%19s\n",$origid,$typecode,$cdate,$shortname,$mybank,$myacct,$pay_batch->batchnum,"") %>
+%
+%
+%}elsif ($format eq "csv-td_canada_trust-merchant_pc_batch"){
+%# 1;
+%}elsif ($format eq "csv-chase_canada-E-xactBatch"){
+%
+% my($origid) = $conf->config("batchconfig-$format");
+<% sprintf( '$$E-xactBatchFileV1.0$$%s:%03u$$%s',$sdate,$pay_batch->batchnum, $origid)
+ %>
+%
+%}else{
+% die "Unknown format for batch in batchconfig. \n";
+%}
+%
+%
+%for my $cust_pay_batch ( sort { $a->paybatchnum <=> $b->paybatchnum }
+% qsearch('cust_pay_batch',
+% {'batchnum'=>$pay_batch->batchnum} )
+%) {
+%
+% $cust_pay_batch->exp =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+% my( $mon, $y ) = ( $2, $1 );
+% if ( $conf->exists('batch-increment_expiration') ) {
+% my( $curmon, $curyear ) = (localtime(time))[4,5];
+% $curmon++; $curyear-=100;
+% $y++ while $y < $curyear || ( $y == $curyear && $mon < $curmon );
+% }
+% $mon = "0$mon" if $mon =~ /^\d$/;
+% $y = "0$y" if $y =~ /^\d$/;
+% my $exp = "$mon$y";
+%
+% if ( $first_download ) {
+% my $balance = $cust_pay_batch->cust_main->balance;
+% if ( $balance <= 0 ) {
+% my $error = $cust_pay_batch->delete;
+% if ( $error ) {
+% $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+% die $error;
+% }
+% next;
+% } elsif ( $balance < $cust_pay_batch->amount ) {
+% $cust_pay_batch->amount($balance);
+% my $error = $cust_pay_batch->replace;
+% if ( $error ) {
+% $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+% die $error;
+% }
+% #} elsif ( $balance > $cust_pay_batch->amount ) {
+% }
+% }
+%
+% $batchcount++;
+% $batchtotal += $cust_pay_batch->amount;
+%
+% if ($format eq "BoM") {
+%
+% my( $account, $aba ) = split( '@', $cust_pay_batch->payinfo );
+%
+<% sprintf( "D%010.0f%09u%-12s%-29s%-19s\n",$cust_pay_batch->amount*100,$aba,$account,$cust_pay_batch->payname,$cust_pay_batch->paybatchnum) %>
+%
+%
+% } elsif ($format eq "PAP"){
+%
+% my( $account, $aba ) = split( '@', $cust_pay_batch->payinfo );
+%
+<% sprintf( "D%-23s%06u%-19s%09u%-12s%010.0f\n",$cust_pay_batch->payname,$cdate,$cust_pay_batch->paybatchnum,$aba,$account,$cust_pay_batch->amount*100) %>
+%
+%
+% } elsif ($format eq "csv-td_canada_trust-merchant_pc_batch") {
+%
+%
+,,,,<% $cust_pay_batch->payinfo %>,<% $exp %>,<% $cust_pay_batch->amount %>,<% $cust_pay_batch->paybatchnum %>
+%
+%
+% } elsif ($format eq "csv-chase_canada-E-xactBatch"){
+%
+% my $payname=$cust_pay_batch->payname; $payname =~ tr/",/ /; #payinfo too? :P
+<% $cust_pay_batch->paybatchnum %>,<% $cust_pay_batch->custnum %>,<% $cust_pay_batch->invnum %>,"<% $payname %>",00,<% $cust_pay_batch->payinfo %>,<% $cust_pay_batch->amount %>,<% $exp %>,,
+%
+%
+% } else {
+% die "I'm already dead, but you did not know that.\n";
+% }
+%
+%}
+%
+%if ($format eq "BoM") {
+%
+%
+<% sprintf( "YD%08u%014.0f%56s\n",$batchcount,$batchtotal*100,"" ).
+ sprintf( "Z%014u%05u%014u%05u%41s\n",$batchtotal*100,$batchcount,"0","0","" ) %>
+%
+%
+%} elsif ($format eq "PAP"){
+%
+%
+<% sprintf( "T%08u%014.0f%57s\n",$batchcount,$batchtotal*100,"" ) %>
+%
+%
+%} elsif ($format eq "csv-td_canada_trust-merchant_pc_batch"){
+% #1;
+%} elsif ($format eq "csv-chase_canada-E-xactBatch"){
+% #1;
+%} else {
+% die "I'm already dead (again), but you did not know that.\n";
+%}
+%
+%$dbh->commit or die $dbh->errstr if $oldAutoCommit;
+<%init>
+
+my $conf=new FS::Conf;
+
+#http_header('Content-Type' => 'text/comma-separated-values' ); #IE chokes
+http_header('Content-Type' => 'text/plain' );
+
+my $batchnum;
+if ( $cgi->param('batchnum') =~ /^(\d+)$/ ) {
+ $batchnum = $1;
+} else {
+ die "No batch number (bad URL) \n";
+}
+
+my $format;
+if ( $cgi->param('format') =~ /^([\w\- ]+)$/ ) {
+ $format = $1;
+} else {
+ $format = $conf->config('batch-default_format');
+}
+
+my $oldAutoCommit = $FS::UID::AutoCommit;
+local $FS::UID::AutoCommit = 0;
+my $dbh = dbh;
+
+my $pay_batch = qsearchs('pay_batch', {'batchnum'=>$batchnum, 'status'=>'O'} );
+my $first_download = 1;
+unless ($pay_batch) {
+ $pay_batch = qsearchs('pay_batch', {'batchnum'=>$batchnum, 'status'=>'I'} )
+ if $FS::CurrentUser::CurrentUser->access_right('Reprocess batches');
+ $first_download = 0;
+}
+die "No pending batch. \n" unless $pay_batch;
+
+my $error = $pay_batch->set_status('I');
+die "error updating batch status: $error\n" if $error;
+
+my $batchtotal=0;
+my $batchcount=0;
+
+my (@date)=localtime($pay_batch->download);
+my $jdate = sprintf("%03d", $date[5] % 100).sprintf("%03d", $date[7] + 1);
+my $cdate = sprintf("%02d", $date[3]).sprintf("%02d", $date[4] + 1).
+ sprintf("%02d", $date[5] % 100);
+my $sdate = sprintf("%02d", $date[5] % 100).'/'.sprintf("%02d", $date[4] + 1).
+ '/'.sprintf("%02d", $date[3]);
+
+</%init>
diff --git a/httemplate/misc/dump.cgi b/httemplate/misc/dump.cgi
new file mode 100644
index 000000000..e8f4b6f38
--- /dev/null
+++ b/httemplate/misc/dump.cgi
@@ -0,0 +1,20 @@
+%
+% 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 000000000..8a3dd90b1
--- /dev/null
+++ b/httemplate/misc/email-invoice.cgi
@@ -0,0 +1,18 @@
+%
+%
+%#untaint invnum
+%my($query) = $cgi->keywords;
+%$query =~ /^((.+)-)?(\d+)$/;
+%my $template = $2;
+%my $invnum = $3;
+%my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+%die "Can't find invoice!\n" unless $cust_bill;
+%
+%$cust_bill->email($template);
+%
+%my $custnum = $cust_bill->getfield('custnum');
+%
+%print $cgi->redirect("${p}view/cust_main.cgi?$custnum");
+%
+%
+
diff --git a/httemplate/misc/email_invoice_events.cgi b/httemplate/misc/email_invoice_events.cgi
new file mode 100644
index 000000000..ba6e72c1a
--- /dev/null
+++ b/httemplate/misc/email_invoice_events.cgi
@@ -0,0 +1,4 @@
+%
+%my $server = new FS::UI::Web::JSRPC 'FS::cust_bill_event::process_reemail', $cgi;
+%
+<% $server->process %>
diff --git a/httemplate/misc/email_invoices.cgi b/httemplate/misc/email_invoices.cgi
new file mode 100644
index 000000000..6c2103f7b
--- /dev/null
+++ b/httemplate/misc/email_invoices.cgi
@@ -0,0 +1,4 @@
+%
+%my $server = new FS::UI::Web::JSRPC 'FS::cust_bill::process_reemail', $cgi;
+%
+<% $server->process %>
diff --git a/httemplate/misc/fax-invoice.cgi b/httemplate/misc/fax-invoice.cgi
new file mode 100755
index 000000000..1ddc23ece
--- /dev/null
+++ b/httemplate/misc/fax-invoice.cgi
@@ -0,0 +1,18 @@
+%
+%
+%#untaint invnum
+%my($query) = $cgi->keywords;
+%$query =~ /^((.+)-)?(\d+)$/;
+%my $template = $2;
+%my $invnum = $3;
+%my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+%die "Can't find invoice!\n" unless $cust_bill;
+%
+%$cust_bill->fax($template);
+%
+%my $custnum = $cust_bill->getfield('custnum');
+%
+%print $cgi->redirect("${p}view/cust_main.cgi?$custnum");
+%
+%
+
diff --git a/httemplate/misc/fax_invoice_events.cgi b/httemplate/misc/fax_invoice_events.cgi
new file mode 100644
index 000000000..deb78d456
--- /dev/null
+++ b/httemplate/misc/fax_invoice_events.cgi
@@ -0,0 +1,4 @@
+%
+%my $server = new FS::UI::Web::JSRPC 'FS::cust_bill_event::process_refax', $cgi;
+%
+<% $server->process %>
diff --git a/httemplate/misc/fax_invoices.cgi b/httemplate/misc/fax_invoices.cgi
new file mode 100644
index 000000000..4bdac970c
--- /dev/null
+++ b/httemplate/misc/fax_invoices.cgi
@@ -0,0 +1,4 @@
+%
+%my $server = new FS::UI::Web::JSRPC 'FS::cust_bill::process_refax', $cgi;
+%
+<% $server->process %>
diff --git a/httemplate/misc/inventory_item-import.html b/httemplate/misc/inventory_item-import.html
new file mode 100644
index 000000000..87c6af34c
--- /dev/null
+++ b/httemplate/misc/inventory_item-import.html
@@ -0,0 +1,21 @@
+%
+%
+%my $classnum = $cgi->param('classnum');
+%$classnum =~ /^(\d+)$/ or eidiot "illegal classnum $classnum";
+%$classnum = $1;
+%my $inventory_class = qsearchs('inventory_class', { 'classnum' => $classnum } );
+%
+%
+<% include("/elements/header.html", $inventory_class->classname. 's') %>
+
+<FORM ACTION="process/inventory_item-import.html" METHOD="POST" ENCTYPE="multipart/form-data">
+<INPUT TYPE="hidden" NAME="classnum" VALUE="<% $classnum %>">
+Import a file containing <% $inventory_class->classname %>s, one per line.<BR><BR>
+
+Filename: <INPUT TYPE="file" NAME="filename"><BR><BR>
+
+<INPUT TYPE="submit" VALUE="Upload">
+</FORM>
+
+<% include('/elements/footer.html') %>
+
diff --git a/httemplate/misc/link.cgi b/httemplate/misc/link.cgi
new file mode 100755
index 000000000..ef72b4a5c
--- /dev/null
+++ b/httemplate/misc/link.cgi
@@ -0,0 +1,77 @@
+%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'
+% },
+%);
+%
+%$cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+%my $pkgnum = $1;
+%$cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+%my $svcpart = $1;
+%
+%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};
+%
+
+<% include("/elements/header.html","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 000000000..fc249a2ab
--- /dev/null
+++ b/httemplate/misc/meta-import.cgi
@@ -0,0 +1,73 @@
+<!-- mason kludge -->
+<% include("/elements/header.html",'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 000000000..728eba7b9
--- /dev/null
+++ b/httemplate/misc/payment.cgi
@@ -0,0 +1,207 @@
+%
+% 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('countrydefault') || '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, '', '', popurl(2) . "view/cust_main.cgi" ) %>
+<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 TYPE="text/javascript" SRC="../elements/overlibmws.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="../elements/overlibmws_iframe.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="../elements/overlibmws_draggable.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript">
+function OLiframeContent(src, width, height, name) {
+ return ('<iframe src="'+src+'" width="'+width+'" height="'+height+'"'
+ +(name?' name="'+name+'" id="'+name+'"':'')+' scrolling="auto">'
+ +'<div>[iframe not supported]</div></iframe>');
+}
+</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->paymask;
+% $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:void(0);" onClick="overlib( OLiframeContent('../docs/cvv2.html', 480, 352, 'cvv2_popup' ), CAPTION, 'CVV2 Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;">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->paymask =~ /^([\dx]+)\@([\dx]+)$/i
+% 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:void(0);" onClick="overlib( OLiframeContent('../docs/ach.html', 380, 240, 'ach_popup' ), CAPTION, 'ACH Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;">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&nbsp;ID&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>
+
+<% include('/elements/footer.html') %>
diff --git a/httemplate/misc/print-invoice.cgi b/httemplate/misc/print-invoice.cgi
new file mode 100755
index 000000000..511bdce19
--- /dev/null
+++ b/httemplate/misc/print-invoice.cgi
@@ -0,0 +1,18 @@
+%
+%
+%#untaint invnum
+%my($query) = $cgi->keywords;
+%$query =~ /^((.+)-)?(\d+)$/;
+%my $template = $2;
+%my $invnum = $3;
+%my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+%die "Can't find invoice!\n" unless $cust_bill;
+%
+%$cust_bill->print($template);
+%
+%my $custnum = $cust_bill->getfield('custnum');
+%
+%print $cgi->redirect("${p}view/cust_main.cgi?$custnum");
+%
+%
+
diff --git a/httemplate/misc/print_invoice_events.cgi b/httemplate/misc/print_invoice_events.cgi
new file mode 100644
index 000000000..913e2683f
--- /dev/null
+++ b/httemplate/misc/print_invoice_events.cgi
@@ -0,0 +1,4 @@
+%
+%my $server = new FS::UI::Web::JSRPC 'FS::cust_bill_event::process_reprint', $cgi;
+
+<% $server->process %>
diff --git a/httemplate/misc/print_invoices.cgi b/httemplate/misc/print_invoices.cgi
new file mode 100644
index 000000000..826a081fd
--- /dev/null
+++ b/httemplate/misc/print_invoices.cgi
@@ -0,0 +1,4 @@
+%
+%my $server = new FS::UI::Web::JSRPC 'FS::cust_bill::process_reprint', $cgi;
+%
+<% $server->process %>
diff --git a/httemplate/misc/process/batch-cust_pay.cgi b/httemplate/misc/process/batch-cust_pay.cgi
new file mode 100644
index 000000000..e4d1bbff5
--- /dev/null
+++ b/httemplate/misc/process/batch-cust_pay.cgi
@@ -0,0 +1,45 @@
+%
+% my $param = $cgi->Vars;
+%
+% #my $paybatch = $param->{'paybatch'};
+% my $paybatch = time2str('webbatch-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time);
+%
+% my @cust_pay = ();
+% #my $row = 0;
+% #while ( exists($param->{"custnum$row"}) ) {
+% for ( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
+% push @cust_pay, new FS::cust_pay {
+% 'custnum' => $param->{"custnum$row"},
+% 'paid' => $param->{"paid$row"},
+% 'payby' => 'BILL',
+% 'payinfo' => $param->{"payinfo$row"},
+% 'paybatch' => $paybatch,
+% }
+% if $param->{"custnum$row"}
+% || $param->{"paid$row"}
+% || $param->{"payinfo$row"};
+% #$row++;
+% }
+%
+% my @errors = FS::cust_pay->batch_insert(@cust_pay);
+% my $num_errors = scalar(grep $_, @errors);
+%
+% if ( $num_errors ) {
+%
+% $cgi->param('error', "$num_errors error". ($num_errors>1 ? 's' : '').
+% ' - Batch not processed, correct and resubmit'
+% );
+%
+% my $erow=0;
+% $cgi->param('error'. $erow++, shift @errors) while @errors;
+%
+%
+<% $cgi->redirect($p.'batch-cust_pay.html?'. $cgi->query_string)
+
+ %>
+% } else {
+%
+%
+<% $cgi->redirect(popurl(3). "search/cust_pay.cgi?magic=paybatch;paybatch=$paybatch") %>
+% }
+
diff --git a/httemplate/misc/process/cancel_pkg.html b/httemplate/misc/process/cancel_pkg.html
new file mode 100755
index 000000000..cd533be10
--- /dev/null
+++ b/httemplate/misc/process/cancel_pkg.html
@@ -0,0 +1,78 @@
+<%init>
+#untaint method
+my $method = $cgi->param('method');
+$method =~ /^(cancel|expire|suspend)$/ || die "Illegal method";
+$method = $1;
+
+#untaint pkgnum
+my $pkgnum = $cgi->param('pkgnum');
+$pkgnum =~ /^(\d+)$/ || die "Illegal pkgnum";
+$pkgnum = $1;
+
+#untaint reasonnum
+my $reasonnum = $cgi->param('reasonnum');
+$reasonnum =~ /^(-?\d+)$/ || die "Illegal reasonnum";
+$reasonnum = $1;
+
+my $date = time;
+if ($method eq 'expire'){
+ #untaint date
+ $date = $cgi->param('date');
+ str2time($cgi->param('date')) =~ /^(\d+)$/ || die "Illegal date";
+ $date = $1;
+}
+
+my $cust_pkg = qsearchs( 'cust_pkg', {'pkgnum'=>$pkgnum} );
+
+my $oldAutoCommit = $FS::UID::AutoCommit;
+local $FS::UID::AutoCommit = 0;
+my $dbh = dbh;
+
+my $otaker = $FS::CurrentUser::CurrentUser->name;
+$otaker = $FS::CurrentUser::CurrentUser->username
+ if ($otaker eq "User, Legacy");
+
+my $error = '';
+if ($reasonnum == -1) {
+
+ $error = 'Enter a new reason (or select an existing one)'
+ unless $cgi->param('newreasonnum') !~ /^\s*$/;
+
+ my $reason = new FS::reason({ 'reason_type' => $cgi->param('newreasonnumT'),
+ 'reason' => $cgi->param('newreasonnum'),
+ });
+ $error ||= $reason->insert;
+ $reasonnum = $reason->reasonnum
+ unless $error;
+}
+
+unless ($error) {
+ if ($method eq 'expire'){
+ my %hash = $cust_pkg->hash;
+ $hash{'expire'}=$date;
+ my $new = new FS::cust_pkg (\%hash);
+ $error = $new->replace($cust_pkg, 'reason' => $reasonnum);
+ }else{
+ $error = $cust_pkg->$method( 'reason' => $reasonnum );
+ }
+}
+
+if ($error) {
+ $cgi->param('error', $error);
+ $dbh->rollback if $oldAutoCommit;
+ print $cgi->redirect(popurl(2). "cancel_pkg.html?". $cgi->query_string );
+}
+
+$dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ my %past = ( 'cancel' => 'cancelled',
+ 'expire' => 'expired',
+ 'suspend' => 'suspended',
+ );
+</%init>
+<% header("Package $past{$method}") %>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+ </BODY></HTML>
+
diff --git a/httemplate/misc/process/catchall.cgi b/httemplate/misc/process/catchall.cgi
new file mode 100755
index 000000000..f2899c720
--- /dev/null
+++ b/httemplate/misc/process/catchall.cgi
@@ -0,0 +1,34 @@
+%
+%
+%$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/cdr-import.html b/httemplate/misc/process/cdr-import.html
new file mode 100644
index 000000000..68edaa21c
--- /dev/null
+++ b/httemplate/misc/process/cdr-import.html
@@ -0,0 +1,30 @@
+%
+%
+% my $fh = $cgi->upload('csvfile');
+%
+% my $error = defined($fh)
+% ? FS::cdr::batch_import( {
+% 'filehandle' => $fh,
+% 'format' => $cgi->param('format'),
+% } )
+% : 'No file';
+%
+% if ( $error ) {
+%
+
+ <!-- mason kludge -->
+%
+% eidiot($error);
+%# $cgi->param('error', $error);
+%# print $cgi->redirect( "${p}cust_main-import.cgi
+% } else {
+%
+
+ <!-- mason kludge -->
+ <% include("/elements/header.html",'Import successful') %>
+ <!-- XXX redirect to batch search like the payment entry... -->
+ <% include("/elements/footer.html",'Import successful') %>
+%
+% }
+%
+
diff --git a/httemplate/misc/process/cust_main-import.cgi b/httemplate/misc/process/cust_main-import.cgi
new file mode 100644
index 000000000..a5ede99a1
--- /dev/null
+++ b/httemplate/misc/process/cust_main-import.cgi
@@ -0,0 +1,35 @@
+%
+%
+% 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 )],
+% 'format' => scalar($cgi->param('format')),
+% } )
+% : 'No file';
+%
+% if ( $error ) {
+%
+
+ <!-- mason kludge -->
+%
+% eidiot($error);
+%# $cgi->param('error', $error);
+%# print $cgi->redirect( "${p}cust_main-import.cgi
+% } else {
+%
+
+ <!-- mason kludge -->
+ <% include("/elements/header.html",'Import successful') %>
+%
+% }
+%
+
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 000000000..e0ede576b
--- /dev/null
+++ b/httemplate/misc/process/cust_main-import_charges.cgi
@@ -0,0 +1,30 @@
+%
+%
+% 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 -->
+ <% include("/elements/header.html",'Import successful') %>
+%
+% }
+%
+
diff --git a/httemplate/misc/process/delete-customer.cgi b/httemplate/misc/process/delete-customer.cgi
new file mode 100755
index 000000000..d0d237ee8
--- /dev/null
+++ b/httemplate/misc/process/delete-customer.cgi
@@ -0,0 +1,30 @@
+%
+%
+%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/inventory_item-import.html b/httemplate/misc/process/inventory_item-import.html
new file mode 100644
index 000000000..f6159dc99
--- /dev/null
+++ b/httemplate/misc/process/inventory_item-import.html
@@ -0,0 +1,31 @@
+%
+%
+% my $fh = $cgi->upload('filename');
+%
+% my $error = defined($fh)
+% ? FS::inventory_item::batch_import( {
+% 'filehandle' => $fh,
+% 'classnum' => $cgi->param('classnum'),
+% } )
+% : 'No file';
+%
+% if ( $error ) {
+%
+
+ <!-- mason kludge -->
+%
+% eidiot($error);
+%# $cgi->param('error', $error);
+%# print $cgi->redirect( "${p}cust_main-import.cgi
+% } else {
+%
+
+ <!-- mason kludge -->
+ <% include("/elements/header.html",'Import successful') %>
+ <!-- XXX redirect to batch search like the payment entry... -->
+ <% include("/elements/footer.html",'Import successful') %>
+%
+% }
+%
+
+
diff --git a/httemplate/misc/process/link.cgi b/httemplate/misc/process/link.cgi
new file mode 100755
index 000000000..fd3d8bb13
--- /dev/null
+++ b/httemplate/misc/process/link.cgi
@@ -0,0 +1,78 @@
+%
+%
+%my $DEBUG = 0;
+%
+%$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 { ($a->cust_svc->pkgnum > 0) <=> ($b->cust_svc->pkgnum > 0)
+% or ($b->cust_svc->svcpart == $svcpart)
+% <=> ($a->cust_svc->svcpart == $svcpart)
+% }
+% qsearch( $svcdb, \%search )
+% );
+%
+% if ( $DEBUG ) {
+% warn scalar(@svc_x). " candidate accounts found for linking ".
+% "(svcpart $svcpart):\n";
+% foreach my $svc_x ( @svc_x ) {
+% warn " ". $svc_x->email.
+% " (svcnum ". $svc_x->svcnum. ",".
+% " pkgnum ". $svc_x->cust_svc->pkgnum. ",".
+% " svcpart ". $svc_x->cust_svc->svcpart. ")\n";
+% }
+% }
+%
+% my $svc_x = $svc_x[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 000000000..5a97d1160
--- /dev/null
+++ b/httemplate/misc/process/meta-import.cgi
@@ -0,0 +1,185 @@
+<!-- mason kludge -->
+<% include("/elements/header.html",'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 000000000..a5f4d4208
--- /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 %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' ) {
+%
+% if ($cgi->param('payinfo1') =~ /xx/i || $cgi->param('payinfo2') =~ /xx/i ) {
+% $payinfo = $cust_main->payinfo;
+% } else {
+% $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');
+% if ($payinfo eq $cust_main->paymask) {
+% $payinfo = $cust_main->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( $FS::payby::payby2bop{$payby}, $amount,
+% 'quiet' => 1,
+% 'manual' => 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 successfully, but error saving info: $error"
+% if $error;
+% $cust_main = $new;
+%}
+%
+%#success!
+%
+%
+
+<% include( '/elements/header.html', ucfirst($type{$payby}). ' processing successful',
+ include('/elements/menubar.html'),
+
+ )
+%>
+<% include( '/elements/small_custview.html', $cust_main, '', '', popurl(3). "view/cust_main.cgi" ) %>
+</BODY>
+</HTML>
diff --git a/httemplate/misc/process/recharge_svc.html b/httemplate/misc/process/recharge_svc.html
new file mode 100755
index 000000000..d9fa2070e
--- /dev/null
+++ b/httemplate/misc/process/recharge_svc.html
@@ -0,0 +1,46 @@
+%
+%
+%#untaint svcnum
+%my $svcnum = $cgi->param('svcnum');
+%$svcnum =~ /^(\d+)$/ || die "Illegal svcnum";
+%$svcnum = $1;
+%
+%#untaint prepaid
+%my $prepaid = $cgi->param('prepaid');
+%$prepaid =~ /^(\w*)$/;
+%$prepaid = $1;
+%
+%my $error = '';
+%my $svc_acct = qsearchs( 'svc_acct', {'svcnum'=>$svcnum} );
+%$error = "Can't recharge service $svcnum. " unless $svc_acct;
+%
+%my $cust_main = $svc_acct->cust_svc->cust_pkg->cust_main;
+%
+%my $oldAutoCommit = $FS::UID::AutoCommit;
+%local $FS::UID::AutoCommit = 0;
+%my $dbh = dbh;
+%
+%
+%unless ($error) {
+%
+%my ($amount, $seconds, $up, $down, $total) = (0, 0, 0, 0, 0);
+%$error = $cust_main->get_prepay($prepaid, \$amount, \$seconds, \$up, \$down, \$total)
+% || $svc_acct->increment_seconds($seconds)
+% || $svc_acct->increment_upbytes($up)
+% || $svc_acct->increment_downbytes($down)
+% || $svc_acct->increment_totalbytes($total)
+% || $cust_main->insert_cust_pay_prepay( $amount, $prepaid );
+%}
+%
+%if ($error) {
+% $cgi->param('error', $error);
+% $dbh->rollback if $oldAutoCommit;
+% print $cgi->redirect(popurl(2). "recharge_svc.html?". $cgi->query_string );
+%}
+%
+<% header("Package recharged") %>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+ </BODY></HTML>
+
diff --git a/httemplate/misc/queue.cgi b/httemplate/misc/queue.cgi
new file mode 100644
index 000000000..7370aabe1
--- /dev/null
+++ b/httemplate/misc/queue.cgi
@@ -0,0 +1,48 @@
+%
+%
+%$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). "search/queue.html");
+%
+%
+
diff --git a/httemplate/misc/recharge_svc.html b/httemplate/misc/recharge_svc.html
new file mode 100755
index 000000000..61f738455
--- /dev/null
+++ b/httemplate/misc/recharge_svc.html
@@ -0,0 +1,48 @@
+<% include('/elements/header-popup.html', 'Recharge Service' ) %>
+
+% if ( $cgi->param('error') ) {
+ <FONT SIZE="+1" COLOR="#ff0000">Error: <% $cgi->param('error') %></FONT>
+ <BR><BR>
+% }
+
+<FORM NAME="recharge_popup" ACTION="<% popurl(1) %>process/recharge_svc.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="svcnum" VALUE="<% $svcnum %>">
+
+<BR><BR>
+<% "Recharge $svcnum: $label - $value" %>
+<% ntable("#cccccc", 2) %>
+
+<TR>
+ <TD>Enter prepaid card: </TD>
+ <TD><INPUT TYPE="text" NAME="prepaid" VALUE="<% $prepaid %>"></TD>
+</TR>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" NAME="submit" VALUE="Recharge">
+
+</FORM>
+</BODY>
+</HTML>
+
+<%init>
+my($svcnum, $cust_svc, $label, $value, $prepaid);
+if ( $cgi->param('error') ) {
+ $svcnum = $cgi->param('svcnum');
+ $prepaid = $cgi->param('prepaid');
+} elsif ( $cgi->param('svcnum') =~ /^(\d+)$/ ) {
+ $svcnum = $1;
+} else {
+ die "illegal query ". $cgi->keywords;
+}
+
+my $title = 'Recharge Service';
+
+$cust_svc = qsearchs('cust_svc', {'svcnum' => $svcnum});
+die "No such service: $svcnum" unless $cust_svc;
+
+($label, $value) = $cust_svc->label;
+
+</%init>
+
diff --git a/httemplate/misc/states.cgi b/httemplate/misc/states.cgi
new file mode 100644
index 000000000..cf2b46ee2
--- /dev/null
+++ b/httemplate/misc/states.cgi
@@ -0,0 +1,7 @@
+%
+%
+% my $country = $cgi->param('arg');
+% my @output = states_hash($country);
+%
+%
+[ <% join(', ', map { qq("$_") } @output) %> ]
diff --git a/httemplate/misc/svc_acct-domains.cgi b/httemplate/misc/svc_acct-domains.cgi
new file mode 100644
index 000000000..3f9599e30
--- /dev/null
+++ b/httemplate/misc/svc_acct-domains.cgi
@@ -0,0 +1,31 @@
+%
+%
+% my $pkgpart_svcpart = $cgi->param('arg');
+% $pkgpart_svcpart =~ /^\d+_(\d+)$/;
+% my $part_svc = qsearchs('part_svc', { 'svcpart' => $1 }) if $1;
+% my $part_svc_column = $part_svc->part_svc_column('domsvc');
+%
+% my @output = split /,/, $part_svc_column->columnvalue;
+% my @svc_domain = ();
+% my %seen = ();
+%
+% foreach (@output) {
+% my $svc_domain = qsearchs('svc_domain', { 'svcnum' => $_ })
+% or warn "unknown svc_domain.svcnum $_ for part_svc_column domsvc; ".
+% "svcpart = " . $part_svc->svcpart;
+% push @svc_domain, [ $_ => $svc_domain->domain ];
+% $seen{$_}++;
+% }
+% if ($conf->exists('svc_acct-alldomains')
+% && ( $part_svc_column->columnflag eq 'D'
+% || $part_svc_column->columnflag eq '' )
+% ) {
+% foreach (grep { $_->svcnum ne $output[0] } qsearch('svc_domain', {}) ){
+% push @svc_domain, [ $_->svcnum => $_->domain ];
+% }
+% }
+%
+[ <% join(', ', map { qq("$_->[0]", "$_->[1]") } @svc_domain) %> ]
+<%init>
+my $conf = new FS::Conf;
+</%init>
diff --git a/httemplate/misc/unapply-cust_credit.cgi b/httemplate/misc/unapply-cust_credit.cgi
new file mode 100755
index 000000000..56a3ff854
--- /dev/null
+++ b/httemplate/misc/unapply-cust_credit.cgi
@@ -0,0 +1,19 @@
+%
+%
+%#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 000000000..b28f61b0f
--- /dev/null
+++ b/httemplate/misc/unapply-cust_pay.cgi
@@ -0,0 +1,19 @@
+%
+%
+%#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 000000000..e42feda8a
--- /dev/null
+++ b/httemplate/misc/unprovision.cgi
@@ -0,0 +1,31 @@
+%
+%
+%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 000000000..79c07a72a
--- /dev/null
+++ b/httemplate/misc/unsusp_pkg.cgi
@@ -0,0 +1,16 @@
+%
+%
+%#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/unvoid-cust_pay_void.cgi b/httemplate/misc/unvoid-cust_pay_void.cgi
new file mode 100755
index 000000000..75c3edc06
--- /dev/null
+++ b/httemplate/misc/unvoid-cust_pay_void.cgi
@@ -0,0 +1,17 @@
+%
+%
+%#untaint paynum
+%my($query) = $cgi->keywords;
+%$query =~ /^(\d+)$/ || die "Illegal paynum";
+%my $paynum = $1;
+%
+%my $cust_pay_void = qsearchs('cust_pay_void', { 'paynum' => $paynum } );
+%my $custnum = $cust_pay_void->custnum;
+%
+%my $error = $cust_pay_void->unvoid;
+%eidiot($error) if $error;
+%
+%print $cgi->redirect($p. "view/cust_main.cgi?". $custnum);
+%
+%
+
diff --git a/httemplate/misc/upload-batch.cgi b/httemplate/misc/upload-batch.cgi
new file mode 100644
index 000000000..69d3ca6b1
--- /dev/null
+++ b/httemplate/misc/upload-batch.cgi
@@ -0,0 +1,39 @@
+% if ( $error ) {
+
+ <!-- mason kludge -->
+
+% eidiot($error);
+%# $cgi->param('error', $error);
+%# print $cgi->redirect( "${p}cust_main-import.cgi
+% } else {
+
+ <% include("/elements/header.html",'Batch results upload successful') %>
+
+% }
+<%init>
+
+my $error;
+
+my $fh = $cgi->upload('batch_results');
+$error = 'No file uploaded' unless defined($fh);
+
+unless ( $error ) {
+
+ $cgi->param('batchnum') =~ /^(\d+)$/;
+ my $batchnum = $1;
+
+ my $pay_batch = qsearchs( 'pay_batch', { 'batchnum' => $batchnum } );
+ if ( ! $pay_batch ) {
+ $error = "batchnum $batchnum not found";
+ } elsif ( $pay_batch->status ne 'I' ) {
+ $error = "batch $batchnum is not in transit";
+ } else {
+ $error = $pay_batch->import_results(
+ 'filehandle' => $fh,
+ 'format' => $cgi->param('format'),
+ );
+ }
+
+}
+
+</%init>
diff --git a/httemplate/misc/void-cust_pay.cgi b/httemplate/misc/void-cust_pay.cgi
new file mode 100755
index 000000000..b55d22c41
--- /dev/null
+++ b/httemplate/misc/void-cust_pay.cgi
@@ -0,0 +1,17 @@
+%
+%
+%#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 000000000..d3d9649fd
--- /dev/null
+++ b/httemplate/misc/whois.cgi
@@ -0,0 +1,27 @@
+%
+% my $svcnum = $cgi->param('svcnum');
+% my $custnum = $cgi->param('custnum');
+% my $domain = $cgi->param('domain');
+%
+%
+
+<% include("/elements/header.html","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/misc/xmlhttp-cust_main-search.cgi b/httemplate/misc/xmlhttp-cust_main-search.cgi
new file mode 100644
index 000000000..67512fad9
--- /dev/null
+++ b/httemplate/misc/xmlhttp-cust_main-search.cgi
@@ -0,0 +1,22 @@
+%
+% my $sub = $cgi->param('sub');
+%
+% if ( $sub eq 'custnum_search' ) {
+%
+% my $custnum = $cgi->param('arg');
+% my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+%
+%
+"<% $cust_main ? $cust_main->name : '' %>"
+% } elsif ( $sub eq 'smart_search' ) {
+%
+% my $string = $cgi->param('arg');
+% my @cust_main = smart_search( 'search' => $string );
+% my $return = [ map [ $_->custnum, $_->name ], @cust_main ];
+%
+%
+<% objToJson($return) %>
+% }
+
+
+
diff --git a/httemplate/misc/xmlrpc.cgi b/httemplate/misc/xmlrpc.cgi
new file mode 100644
index 000000000..1d0383f2a
--- /dev/null
+++ b/httemplate/misc/xmlrpc.cgi
@@ -0,0 +1,18 @@
+%
+%
+% my $request_xml = $cgi->param('POSTDATA');
+%
+% #$r->log_error($request_xml);
+%
+% my $fsxmlrpc = new FS::XMLRPC;
+% my ($error, $response_xml) = $fsxmlrpc->serve($request_xml);
+%
+% #$r->log_error($error) if $error;
+%
+% http_header('Content-Type' => 'text/xml',
+% 'Content-Length' => length($response_xml));
+%
+% print $response_xml;
+%
+%
+
diff --git a/httemplate/pref/pref-process.html b/httemplate/pref/pref-process.html
new file mode 100644
index 000000000..ed3350971
--- /dev/null
+++ b/httemplate/pref/pref-process.html
@@ -0,0 +1,44 @@
+% my $error = '';
+%
+% my $access_user;
+% if ( grep { $cgi->param($_) !~ /^\s*$/ }
+% qw(_password new_password new_password2)
+% ) {
+%
+% $access_user = qsearchs( 'access_user', {
+% 'username' => getotaker,
+% '_password' => $cgi->param('_password'),
+% } );
+%
+% $error = 'Current password incorrect; password not changed'
+% unless $access_user;
+%
+% $error ||= "New passwords don't match"
+% unless $cgi->param('new_password') eq $cgi->param('new_password2');
+%
+% $error ||= "No new password entered"
+% unless length($cgi->param('new_password'));
+%
+% $access_user->_password($cgi->param('new_password')) unless $error;
+%
+% } else {
+%
+% $access_user = $FS::CurrentUser::CurrentUser;
+%
+% }
+%
+% $error ||= $access_user->replace( {
+% map { $_ => scalar($cgi->param($_)) }
+% #XXX autogen
+% qw( menu_position
+% height width availHeight availWidth colorDepth
+% )
+% } );
+%
+% if ( $error ) {
+% $cgi->param('error', $error);
+% print $cgi->redirect(popurl(1). "pref.html?". $cgi->query_string );
+% } else {
+<% include('/elements/header.html', 'Preferences updated') %>
+<% include('/elements/footer.html') %>
+% }
diff --git a/httemplate/pref/pref.html b/httemplate/pref/pref.html
new file mode 100644
index 000000000..507a897d7
--- /dev/null
+++ b/httemplate/pref/pref.html
@@ -0,0 +1,61 @@
+<% include('/elements/header.html', 'Preferences for '. getotaker ) %>
+
+<FORM METHOD="POST" NAME="pref_form" ACTION="pref-process.html">
+
+<% include('/elements/error.html') %>
+
+
+Change password (leave blank for no change)
+<% ntable("#cccccc",2) %>
+
+<TR>
+ <TD ALIGN="right">Current password: </TD>
+ <TD><INPUT TYPE="password" NAME="_password"></TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right">New password: </TD>
+ <TD><INPUT TYPE="password" NAME="new_password"></TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right">Re-enter new password: </TD>
+ <TD><INPUT TYPE="password" NAME="new_password2"></TD>
+</TR>
+
+</TABLE>
+<BR>
+
+Interface
+<% ntable("#cccccc",2) %>
+
+<TR>
+ <TD>Menu location: </TD>
+ <TD>
+ <INPUT TYPE="radio" NAME="menu_position" VALUE="left" onClick="document.images['menu_example'].src='../images/menu-left-example.png';" <% $menu_position eq 'left' ? ' CHECKED' : ''%>> Left<BR>
+ <INPUT TYPE="radio" NAME="menu_position" VALUE="top"onClick="document.images['menu_example'].src='../images/menu-top-example.png';" <% $menu_position eq 'top' ? ' CHECKED' : ''%>> Top <BR>
+ </TD>
+ <TD><IMG NAME="menu_example" SRC="../images/menu-<% $menu_position %>-example.png"></TD>
+</TR>
+
+</TABLE>
+<BR>
+
+% foreach my $prop (qw( height width availHeight availWidth colorDepth )) {
+ <INPUT TYPE="hidden" NAME="<% $prop %>" VALUE="">
+ <SCRIPT TYPE="text/javascript">
+ document.pref_form.<% $prop %>.value = screen.<% $prop %>;
+ </script>
+% }
+
+<INPUT TYPE="submit" VALUE="Update preferences">
+
+<% include('/elements/footer.html') %>
+<%init>
+
+# XSS via your own preferences? seems unlikely, but nice try anyway...
+( $FS::CurrentUser::CurrentUser->option('menu_position') || 'left' )
+ =~ /^(\w+)$/ or die "illegal menu_position";
+my $menu_position = $1;
+
+</%init>
diff --git a/httemplate/search/cdr.html b/httemplate/search/cdr.html
new file mode 100644
index 000000000..54c804c1a
--- /dev/null
+++ b/httemplate/search/cdr.html
@@ -0,0 +1,41 @@
+<% include( 'elements/search.html',
+ 'title' => $title,
+ 'name' => 'call detail records',
+ 'query' => { 'table' => 'cdr',
+ 'hashref' => $hashref
+ },
+ 'count_query' => $count_query,
+ 'header' => [ fields('cdr') ], #XXX fill in some nice names
+ 'fields' => [ fields('cdr') ], #XXX fill in some pretty-print
+ # processing, etc.
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List rating data');
+
+my $title = 'Call Detail Records';
+my $hashref = {};
+my $count_query = 'SELECT COUNT(*) FROM cdr';
+
+#process params for CDR search, populate $hashref...
+# and fixup $count_query
+
+if ( $cgi->param('freesidestatus') eq 'NULL' ) {
+
+ my $title = "Unprocessed $title";
+ $hashref->{'freesidestatus'} = ''; # Record.pm will take care of it
+ #$count_query .= " AND ( freesidestatus IS NULL OR freesidestatus = '' )";
+ $count_query .= " WHERE ( freesidestatus IS NULL OR freesidestatus = '' )";
+
+} elsif ( $cgi->param('freesidestatus') =~ /^([\w ]+)$/ ) {
+
+ my $title = "Processed $title";
+ $hashref->{'freesidestatus'} = $1;
+ #$count_query .= " AND freesidestatus = '$1'";
+ $count_query .= " WHERE freesidestatus = '$1'";
+
+}
+
+</%init>
diff --git a/httemplate/search/cust_bill.html b/httemplate/search/cust_bill.html
new file mode 100755
index 000000000..b65608eab
--- /dev/null
+++ b/httemplate/search/cust_bill.html
@@ -0,0 +1,216 @@
+<% include( 'elements/search.html',
+ 'title' => 'Invoice Search Results',
+ 'html_init' => $html_init,
+ 'menubar' => $menubar,
+ 'name' => 'invoices',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'count_addl' => $count_addl,
+ 'redirect' => $link,
+ 'header' => [ 'Invoice #',
+ 'Balance',
+ 'Amount',
+ 'Date',
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [
+ 'invnum',
+ sub { sprintf($money_char.'%.2f', shift->get('owed') ) },
+ sub { sprintf($money_char.'%.2f', shift->charged ) },
+ sub { time2str('%b %d %Y', shift->_date ) },
+ \&FS::UI::Web::cust_fields,
+ ],
+ 'align' => 'rrrr'.FS::UI::Web::cust_aligns(),
+ 'links' => [
+ $link,
+ $link,
+ $link,
+ $link,
+ ( map { $_ ne 'Cust. Status' ? $clink : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'color' => [
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
+
+
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List invoices');
+
+my $join_cust_main = 'LEFT JOIN cust_main USING ( custnum )';
+#here is the agent virtualization
+my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my( $count_query, $sql_query );
+my( $count_addl ) = ( '' );
+my( $distinct ) = ( '' );
+my($begin, $end) = ( '', '' );
+my($agentnum) = ( '' );
+my($open, $days) = ( '', '' );
+if ( $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/ ) {
+ $count_query =
+ "SELECT COUNT(*) FROM cust_bill $join_cust_main".
+ " WHERE invnum = $2 AND $agentnums_sql"; #agent virtualization
+ $sql_query = {
+ 'table' => 'cust_bill',
+ 'addl_from' => $join_cust_main,
+ 'hashref' => { 'invnum' => $2 },
+ #'select' => '*',
+ 'extra_sql' => " AND $agentnums_sql", #agent virtualization
+ };
+} else {
+#if ( $cgi->param('begin') || $cgi->param('end')
+# || $cgi->param('beginning') || $cgi->param('ending')
+# || $cgi->keywords
+# )
+#{
+
+ #some false laziness w/cust_bill::re_X
+ my @where;
+ my $orderby = 'ORDER BY cust_bill._date';
+
+ if ( $cgi->param('beginning')
+ && $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+ $begin = str2time($1);
+ push @where, "cust_bill._date >= $begin";
+ }
+ if ( $cgi->param('ending')
+ && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+ $end = str2time($1) + 86399;
+ push @where, "cust_bill._date < $end";
+ }
+
+ if ( $cgi->param('begin') =~ /^(\d+)$/ ) {
+ $begin = $1;
+ push @where, "cust_bill._date >= $begin";
+ }
+ if ( $cgi->param('end') =~ /^(\d+)$/ ) {
+ $end = $1;
+ push @where, "cust_bill._date < $end";
+ }
+
+ if ( $cgi->param('invnum_min') =~ /^\s*(\d+)\s*$/ ) {
+ push @where, "cust_bill.invnum >= $1";
+ }
+ if ( $cgi->param('invnum_max') =~ /^\s*(\d+)\s*$/ ) {
+ push @where, "cust_bill.invnum <= $1";
+ }
+
+ if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+ push @where, "cust_main.agentnum = $agentnum";
+ }
+
+ 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 )";
+
+ if ( $cgi->param('open') ) {
+ push @where, "0 != $owed";
+ $open = 1;
+ }
+
+ my($query) = $cgi->keywords;
+ if ( $query =~ /^(OPEN(\d*)_)?(invnum|date|custnum)$/ ) {
+ ($open, $days, my $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;
+ }
+
+ #here is the agent virtualization
+ push @where, $agentnums_sql;
+ my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
+
+ if ( $cgi->param('newest_percust') ) {
+ $distinct = 'DISTINCT ON ( cust_bill.custnum )';
+ $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
+ #$count_query = "SELECT 'N/A', 'N/A', 'N/A'"; #XXXXXXX fix
+ $count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
+ }
+
+ unless ( $count_query ) {
+ $count_query = "SELECT COUNT(*), sum(charged), sum($owed)";
+ $count_addl = [ '$%.2f total invoiced',
+ '$%.2f total outstanding balance',
+ ];
+ }
+ $count_query .= " FROM cust_bill $join_cust_main $extra_sql";
+
+ $sql_query = {
+ 'table' => 'cust_bill',
+ 'addl_from' => $join_cust_main,
+ 'hashref' => {},
+ 'select' => "$distinct ". join(', ',
+ 'cust_bill.*',
+ #( map "cust_main.$_", qw(custnum last first company) ),
+ 'cust_main.custnum as cust_main_custnum',
+ FS::UI::Web::cust_sql_fields(),
+ "$owed as owed",
+ ),
+ 'extra_sql' => "$extra_sql $orderby"
+ };
+
+}
+
+my $link = [ "${p}view/cust_bill.cgi?", 'invnum', ];
+my $clink = sub {
+ my $cust_bill = shift;
+ $cust_bill->cust_main_custnum
+ ? [ "${p}view/cust_main.cgi?", 'custnum' ]
+ : '';
+};
+
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
+
+my $html_init = join("\n", map {
+ ( my $action = $_ ) =~ s/_$//;
+ include('/elements/progress-init.html',
+ $_.'form',
+ [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
+ "../misc/${_}invoices.cgi",
+ { 'message' => "Invoices re-${action}ed" }, #would be nice to show the number of them, but...
+ $_, #key
+ ),
+ qq!<FORM NAME="${_}form">!,
+ qq!<INPUT TYPE="hidden" NAME="begin" VALUE="$begin">!,
+ qq!<INPUT TYPE="hidden" NAME="end" VALUE="$end">!,
+ qq!<INPUT TYPE="hidden" NAME="agentnum" VALUE="$agentnum">!,
+ qq!<INPUT TYPE="hidden" NAME="open" VALUE="$open">!,
+ qq!<INPUT TYPE="hidden" NAME="days" VALUE="$days">!,
+ qq!</FORM>!
+} qw( print_ email_ fax_ ) );
+
+my $menubar = [
+ 'Main menu' => $p,
+ 'Print these invoices' =>
+ "javascript:print_process()",
+ 'Email these invoices' =>
+ "javascript:email_process()",
+ ];
+
+push @$menubar, 'Fax these invoices' =>
+ "javascript:fax_process()"
+ if $conf->exists('hylafax');
+
+</%init>
diff --git a/httemplate/search/cust_bill_event.cgi b/httemplate/search/cust_bill_event.cgi
new file mode 100644
index 000000000..ada7e4362
--- /dev/null
+++ b/httemplate/search/cust_bill_event.cgi
@@ -0,0 +1,161 @@
+<% include( 'elements/search.html',
+ 'title' => $title,
+ 'html_init' => $html_init,
+ 'menubar' => $menubar,
+ 'name' => 'billing events',
+ 'query' => $sql_query,
+ 'count_query' => $count_sql,
+ 'header' => [ 'Event',
+ 'Date',
+ 'Status',
+ #'Inv #', 'Inv Date', 'Cust #',
+ 'Invoice',
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [
+ 'event',
+ sub { time2str("%b %d %Y %T", $_[0]->_date) },
+ sub {
+ #my $cust_bill_event = shift;
+ my $status = $_[0]->status;
+ $status .= ': '.$_[0]->statustext
+ if $_[0]->statustext;
+ $status;
+ },
+ sub {
+ #my $cust_bill_event = shift;
+ 'Invoice #'. $_[0]->invnum.
+ ' ('.
+ time2str("%D", $_[0]->cust_bill_date).
+ ')';
+ },
+ \&FS::UI::Web::cust_fields,
+ ],
+ 'align' => 'lrlr'.FS::UI::Web::cust_aligns(),
+ 'links' => [
+ '',
+ '',
+ '',
+ sub {
+ my $part_bill_event = shift;
+ my $template = $part_bill_event->templatename;
+ $template .= '-' if $template;
+ [ "${p}view/cust_bill.cgi?$template", 'invnum'];
+ },
+ ( map { $_ ne 'Cust. Status' ? $link_cust : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'color' => [
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Billing event reports');
+
+my $title = $cgi->param('failed')
+ ? 'Failed invoice events'
+ : 'Invoice events';
+
+my @search = ();
+
+if ( $cgi->param('agentnum') && $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ push @search, "agentnum = $1";
+ #my $agent = qsearchs('agent', { 'agentnum' => $1 } );
+ #die "unknown agentnum $1" unless $agent;
+}
+
+my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+push @search, "cust_bill_event._date >= $beginning",
+ "cust_bill_event._date <= $ending";
+
+if ( $cgi->param('failed') ) {
+ push @search, "statustext != ''",
+ "statustext IS NOT NULL",
+ "statustext != 'N/A'";
+}
+
+if ( $cgi->param('part_bill_event.payby') =~ /^(\w+)$/ ) {
+ push @search, "part_bill_event.payby = '$1'";
+}
+
+#here is the agent virtualization
+push @search, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my $where = 'WHERE '. join(' AND ', @search );
+
+my $join = 'LEFT JOIN part_bill_event USING ( eventpart ) '.
+ 'LEFT JOIN cust_bill USING ( invnum ) '.
+ 'LEFT JOIN cust_main USING ( custnum ) ';
+
+my $sql_query = {
+ 'table' => 'cust_bill_event',
+ 'select' => join(', ',
+ 'cust_bill_event.*',
+ 'part_bill_event.event',
+ 'cust_bill.custnum',
+ 'cust_bill._date AS cust_bill_date',
+ 'cust_main.custnum AS cust_main_custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'hashref' => {},
+ 'extra_sql' => "$where ORDER BY _date ASC",
+ 'addl_from' => $join,
+};
+
+my $count_sql = "SELECT COUNT(*) FROM cust_bill_event $join $where";
+
+my $conf = new FS::Conf;
+
+my $failed = $cgi->param('failed');
+
+my $html_init = join("\n", map {
+ ( my $action = $_ ) =~ s/_$//;
+ include('/elements/progress-init.html',
+ $_.'form',
+ [ 'action', 'beginning', 'ending', 'failed' ],
+ "../misc/${_}invoice_events.cgi",
+ { 'message' => "Invoices re-${action}ed" }, #would be nice to show the number of them, but...
+ $_, #key
+ ),
+ qq!<FORM NAME="${_}form">!,
+ qq!<INPUT TYPE="hidden" NAME="action" VALUE="$_">!, #not used though
+ qq!<INPUT TYPE="hidden" NAME="beginning" VALUE="$beginning">!,
+ qq!<INPUT TYPE="hidden" NAME="ending" VALUE="$ending">!,
+ qq!<INPUT TYPE="hidden" NAME="failed" VALUE="$failed">!,
+ qq!</FORM>!
+} qw( print_ email_ fax_ ) );
+
+my $menubar = [
+ 'Re-print these events' =>
+ "javascript:print_process()",
+ 'Re-email these events' =>
+ "javascript:email_process()",
+ ];
+
+push @$menubar, 'Re-fax these events' =>
+ "javascript:fax_process()"
+ if $conf->exists('hylafax');
+
+my $link_cust = sub {
+ my $cust_bill_event = shift;
+ $cust_bill_event->cust_main_custnum
+ ? [ "${p}view/cust_main.cgi?", 'custnum' ]
+ : '';
+};
+
+</%init>
diff --git a/httemplate/search/cust_bill_event.html b/httemplate/search/cust_bill_event.html
new file mode 100755
index 000000000..334bda3d3
--- /dev/null
+++ b/httemplate/search/cust_bill_event.html
@@ -0,0 +1,64 @@
+<% include(
+ '/elements/header.html',
+ ( $cgi->param('failed') ? 'Failed invoice events' : 'Invoice events' ),
+ )
+%>
+
+ <FORM ACTION="cust_bill_event.cgi" METHOD="GET">
+ <INPUT TYPE="hidden" NAME="failed" VALUE="<% $cgi->param('failed') %>">
+ <TABLE>
+
+ <% include( '/elements/tr-select-agent.html' ) %>
+
+ <!--<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>
+ -->
+ <% include( '/elements/tr-input-beginning_ending.html' ) %>
+ <!--
+ <TR>
+ <TD ALIGN="right">Events: </TD>
+ <TD>
+ <SELECT NAME="eventpart">
+ <OPTION SELECTED VALUE=""><% $cgi->param('failed') ? '(all failed events)' : '(all events)' %>
+% #foreach my $part_bill_event ( qsearch( 'part_bill_event', {} ) ) {
+% #}
+
+ </SELECT>
+ </TD>
+ </TR>
+ -->
+ <TR>
+ <TD ALIGN="right">Events for payment type: </TD>
+ <TD>
+ <SELECT NAME="part_bill_event.payby">
+ <OPTION SELECTED VALUE="">(all)
+ <OPTION VALUE="CARD">Credit card (automatic)
+ <OPTION VALUE="BILL">Billing
+ <OPTION VALUE="CHEK">Electronic check (automatic)
+ <OPTION VALUE="DCRD">Credit card (on-demand)
+ <OPTION VALUE="DCHK">Electronic check (on-demand)
+ <OPTION VALUE="LECB">Phone bill billing
+ <OPTION VALUE="COMP">Complimentary
+ </SELECT>
+ </TD>
+ </TR>
+ </TABLE>
+ <BR><INPUT TYPE="submit" VALUE="Get Report">
+ </FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Billing event reports');
+
+</%init>
diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi
new file mode 100644
index 000000000..17b4bc240
--- /dev/null
+++ b/httemplate/search/cust_bill_pkg.cgi
@@ -0,0 +1,191 @@
+<% include( 'elements/search.html',
+ 'title' => 'Line items',
+ 'name' => 'line items',
+ 'query' => $query,
+ 'count_query' => $count_query,
+ 'count_addl' => [ $money_char. '%.2f total', ],
+ 'header' => [
+ '#',
+ 'Description',
+ 'Setup charge',
+ 'Recurring charge',
+ 'Invoice',
+ 'Date',
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [
+ 'billpkgnum',
+ sub { $_[0]->pkgnum > 0
+ ? $_[0]->get('pkg')
+ : $_[0]->get('itemdesc')
+ },
+ #strikethrough or "N/A ($amount)" or something these when
+ # they're not applicable to pkg_tax search
+ sub { sprintf($money_char.'%.2f', shift->setup ) },
+ sub { sprintf($money_char.'%.2f', shift->recur ) },
+ 'invnum',
+ sub { time2str('%b %d %Y', shift->_date ) },
+ \&FS::UI::Web::cust_fields,
+ ],
+ 'links' => [
+ '',
+ '',
+ '',
+ '',
+ $ilink,
+ $ilink,
+ ( map { $_ ne 'Cust. Status' ? $clink : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'align' => 'rlrrrc'.FS::UI::Web::cust_aligns(),
+ 'color' => [
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+
+my $join_cust = "
+ JOIN cust_bill USING ( invnum )
+ LEFT JOIN cust_main USING ( custnum )
+";
+
+my $join_pkg = "
+ LEFT JOIN cust_pkg USING ( pkgnum )
+ LEFT JOIN part_pkg USING ( pkgpart )
+";
+
+my $where = " WHERE _date >= $beginning AND _date <= $ending ";
+
+$where .= " AND payby != 'COMP' "
+ unless $cgi->param('include_comp_cust');
+
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $where .= " AND agentnum = $1 ";
+}
+
+if ( $cgi->param('classnum') =~ /^(\d+)$/ ) {
+ if ( $1 == 0 ) {
+ $where .= " AND classnum IS NULL ";
+ } else {
+ $where .= " AND classnum = $1 ";
+ }
+}
+
+if ( $cgi->param('out') ) {
+
+ $where .= "
+ AND 0 = (
+ SELECT COUNT(*) FROM cust_main_county
+ WHERE ( cust_main_county.county = cust_main.county
+ OR ( cust_main_county.county IS NULL AND cust_main.county = '' )
+ OR ( cust_main_county.county = '' AND cust_main.county IS NULL)
+ OR ( cust_main_county.county IS NULL AND cust_main.county IS NULL)
+ )
+ AND ( cust_main_county.state = cust_main.state
+ OR ( cust_main_county.state IS NULL AND cust_main.state = '' )
+ OR ( cust_main_county.state = '' AND cust_main.state IS NULL )
+ OR ( cust_main_county.state IS NULL AND cust_main.state IS NULL )
+ )
+ AND cust_main_county.country = cust_main.country
+ AND cust_main_county.tax > 0
+ )
+ ";
+
+} elsif ( $cgi->param('country' ) ) {
+
+ my $county = dbh->quote( $cgi->param('county') );
+ my $state = dbh->quote( $cgi->param('state') );
+ my $country = dbh->quote( $cgi->param('country') );
+ $where .= "
+ AND ( county = $county OR $county = '' )
+ AND ( state = $state OR $state = '' )
+ AND country = $country
+ ";
+ $where .= ' AND taxclass = '. dbh->quote( $cgi->param('taxclass') )
+ if $cgi->param('taxclass');
+
+}
+
+$where .= ' AND pkgnum != 0' if $cgi->param('nottax');
+
+$where .= ' AND pkgnum = 0' if $cgi->param('istax');
+
+$where .= " AND tax = 'Y'" if $cgi->param('cust_tax');
+
+my $count_query;
+if ( $cgi->param('pkg_tax') ) {
+
+ $count_query =
+ "SELECT COUNT(*), SUM(
+ ( CASE WHEN part_pkg.setuptax = 'Y'
+ THEN cust_bill_pkg.setup
+ ELSE 0
+ END
+ )
+ +
+ ( CASE WHEN part_pkg.recurtax = 'Y'
+ THEN cust_bill_pkg.recur
+ ELSE 0
+ END
+ )
+ )
+ ";
+
+ $where .= " AND (
+ ( part_pkg.setuptax = 'Y' AND cust_bill_pkg.setup > 0 )
+ OR ( part_pkg.recurtax = 'Y' AND cust_bill_pkg.recur > 0 )
+ )
+ AND ( tax != 'Y' OR tax IS NULL )
+ ";
+
+} else {
+
+ $count_query =
+ "SELECT COUNT(*), SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)";
+
+}
+$count_query .= " FROM cust_bill_pkg $join_cust $join_pkg $where";
+
+my $query = {
+ 'table' => 'cust_bill_pkg',
+ 'addl_from' => "$join_cust $join_pkg",
+ 'hashref' => {},
+ 'select' => join(', ',
+ 'cust_bill_pkg.*',
+ 'cust_bill._date',
+ 'part_pkg.pkg',
+ 'cust_main.custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'extra_sql' => $where,
+};
+
+my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ];
+my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
+
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
+
+</%init>
diff --git a/httemplate/search/cust_credit.html b/httemplate/search/cust_credit.html
new file mode 100755
index 000000000..e4975c8de
--- /dev/null
+++ b/httemplate/search/cust_credit.html
@@ -0,0 +1,104 @@
+<% include( 'elements/search.html',
+ 'title' => $title,
+ 'name' => 'credits',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'count_addl' => [ '$%.2f total credited', ],
+ #'redirect' => $link,
+ 'header' => [ 'Amount',
+ 'Date',
+ FS::UI::Web::cust_header(),
+ 'By',
+ 'Reason'
+ ],
+ 'fields' => [
+ #'crednum',
+ sub { sprintf('$%.2f', shift->amount ) },
+ sub { time2str('%b %d %Y', shift->_date ) },
+ \&FS::UI::Web::cust_fields,
+ 'otaker',
+ 'reason',
+ ],
+ #'align' => 'rrrllll',
+ 'align' => 'rr'.FS::UI::Web::cust_aligns().'ll',
+ 'links' => [
+ '',
+ '',
+ ( map { $_ ne 'Cust. Status' ? $clink : '' }
+ FS::UI::Web::cust_header()
+ ),
+ '',
+ '',
+ ],
+ 'color' => [
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ '',
+ '',
+ ],
+ 'style' => [
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ '',
+ '',
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+my $title = 'Credit Search Results';
+#my( $count_query, $sql_query );
+
+my @search = ();
+
+if ( $cgi->param('otaker') && $cgi->param('otaker') =~ /^([\w\.\-]+)$/ ) {
+ push @search, "cust_credit.otaker = '$1'";
+}
+
+if ( $cgi->param('agentnum') && $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ push @search, "agentnum = $1";
+ my $agent = qsearchs('agent', { 'agentnum' => $1 } );
+ die "unknown agentnum $1" unless $agent;
+ $title = $agent->agent. " $title";
+}
+
+my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+push @search, "_date >= $beginning ",
+ "_date <= $ending";
+
+push @search, FS::UI::Web::parse_lt_gt($cgi, 'amount' );
+
+#here is the agent virtualization
+push @search, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my $where = 'WHERE '. join(' AND ', @search);
+
+my $count_query = 'SELECT COUNT(*), SUM(amount) '.
+ 'FROM cust_credit LEFT JOIN cust_main USING ( custnum ) '.
+ $where;
+
+my $sql_query = {
+ 'table' => 'cust_credit',
+ 'select' => join(', ',
+ 'cust_credit.*',
+ 'cust_main.custnum as cust_main_custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'hashref' => {},
+ 'extra_sql' => $where,
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+};
+
+ my $clink = sub {
+ my $cust_bill = shift;
+ $cust_bill->cust_main_custnum
+ ? [ "${p}view/cust_main.cgi?", 'custnum' ]
+ : '';
+ };
+
+</%init>
diff --git a/httemplate/search/cust_main-otaker.cgi b/httemplate/search/cust_main-otaker.cgi
new file mode 100755
index 000000000..0c252e44b
--- /dev/null
+++ b/httemplate/search/cust_main-otaker.cgi
@@ -0,0 +1,31 @@
+<% include('/elements/header.html', 'Customer Search' ) %>
+
+<FORM ACTION="cust_main.cgi" METHOD="GET">
+
+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->fetchall_arrayref};
+%
+
+<SELECT NAME="otaker">
+% my $otaker; while ( $otaker = $sth->fetchrow_arrayref ) {
+
+ <OPTION><% $otaker->[0] %>
+% }
+
+</SELECT>
+
+<P><INPUT TYPE="submit" VALUE="Search">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/search/cust_main-zip.html b/httemplate/search/cust_main-zip.html
new file mode 100644
index 000000000..56df924bc
--- /dev/null
+++ b/httemplate/search/cust_main-zip.html
@@ -0,0 +1,99 @@
+<% include( 'elements/search.html',
+ 'title' => 'Zip code Search Results',
+ 'name' => 'zip codes',
+ 'query' => $sql_query,
+ 'count_query' => $count_sql,
+ 'header' => [ 'Zip code', 'Customers', ],
+ #'fields' => [ 'zip', 'num_cust', ],
+ 'links' => [ '', sub { 'somewhere'; } ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List zip codes');
+
+# XXX link to customers
+
+my @where = ();
+
+# select status
+
+if ( $cgi->param('status') =~ /^(prospect|uncancel|active|susp|cancel)$/ ) {
+ my $method = $1.'_sql';
+ push @where, FS::cust_main->$method();
+}
+
+# select agent
+# XXX this needs to be virtualized by agent too (like lots of stuff)
+
+my $agentnum = '';
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+ push @where, "cust_main.agentnum = $agentnum";
+}
+my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
+
+# bill zip vs ship zip
+
+sub fieldorempty {
+ my $field = shift;
+ "CASE WHEN $field IS NULL THEN '' ELSE $field END";
+}
+
+sub strip_plus4 {
+ my $field = shift;
+ "CASE WHEN $field is NULL
+ THEN ''
+ ELSE CASE WHEN $field LIKE '_____-____'
+ THEN SUBSTRING($field FROM 1 FOR 5)
+ ELSE $field
+ END
+ END";
+}
+
+my( $zip, $czip);
+if ( $cgi->param('column') eq 'ship_zip' ) {
+
+ my $casewhen_noship =
+ "CASE WHEN ( ship_last IS NULL OR ship_last = '' ) THEN ";
+
+ $czip = "$casewhen_noship zip ELSE ship_zip END";
+
+ if ( $cgi->param('ignore_plus4') ) {
+ $zip = $casewhen_noship. strip_plus4('zip').
+ " ELSE ". strip_plus4('ship_zip'). ' END';
+
+ } else {
+ $zip = $casewhen_noship. fieldorempty('zip').
+ " ELSE ". fieldorempty('ship_zip'). ' END';
+ }
+
+} else {
+
+ $czip = 'zip';
+
+ if ( $cgi->param('ignore_plus4') ) {
+ $zip = strip_plus4('zip');
+ } else {
+ $zip = fieldorempty('zip');
+ }
+
+}
+
+# construct the queries and send 'em off
+
+my $sql_query =
+ "SELECT $zip AS zipcode,
+ COUNT(*) AS num_cust
+ FROM cust_main
+ $where
+ GROUP BY zipcode
+ ORDER BY num_cust DESC
+ ";
+
+my $count_sql = "select count(distinct $czip) from cust_main $where";
+
+# XXX should link...
+
+</%init>
diff --git a/httemplate/search/cust_main.cgi b/httemplate/search/cust_main.cgi
new file mode 100755
index 000000000..e87fe36d7
--- /dev/null
+++ b/httemplate/search/cust_main.cgi
@@ -0,0 +1,728 @@
+%die "access denied"
+% unless $FS::CurrentUser::CurrentUser->access_right('List customers');
+%
+%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);
+%my @select = ();
+%my @addl_headers = ();
+%my @addl_cols = ();
+%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 )";
+% } elsif ( $query eq 'tickets' ) {
+% $sortby = \*tickets_sort;
+% $orderby = "ORDER BY tickets DESC";
+% push @select, FS::TicketSystem->sql_num_customer_tickets. " as tickets";
+% push @addl_headers, 'Tickets';
+% push @addl_cols, 'tickets';
+% } else {
+% die "unknown browse field $query";
+% }
+% } else {
+% $sortby = \*last_sort; #??
+% $orderby = "ORDER BY LOWER(last || ' ' || first)"; #??
+% }
+%
+% if ( $cgi->param('otaker_on') ) {
+% die "access denied"
+% unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+% $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, FS::cust_main->uncancel_sql;
+%
+% }
+%
+% 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->inactive_sql if $cgi->param('inactive');
+% 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);
+%
+% #here is the agent virtualization
+% $addl_qual .= ( $addl_qual ? ' AND ' : '' ).
+% $FS::CurrentUser::CurrentUser->agentnums_sql;
+%
+% 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";
+% }
+% }
+%
+% my $select;
+% if ( @select ) {
+% $select = 'cust_main.*, '. join (', ', @select);
+% } else {
+% $select = '*';
+% }
+%
+% @cust_main = qsearch('cust_main', \%search, $select,
+% "$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};
+% }
+%
+% if ( $cgi->param('search_cust') ) {
+% $sortby = \*company_sort;
+% $orderby = "ORDER BY LOWER(company || ' ' || last || ' ' || first )";
+% push @cust_main, smart_search( 'search' => $cgi->param('search_cust') );
+% }
+%
+% @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 {
+%
+
+<% include('/elements/header.html', "Customer Search Results", '' ) %>
+% $total ||= scalar(@cust_main);
+
+
+ <% $total %> matching customers found
+
+% my $pager = include( '/elements/pager.html',
+% 'offset' => $offset,
+% 'num_rows' => scalar(@cust_main),
+% 'total' => $total,
+% 'maxrecords' => $maxrecords,
+% );
+%
+% 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="GET">'.
+% 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>';
+% }
+%
+% my @custom_priorities = ();
+% if ( $conf->config('ticket_system-custom_priority_field')
+% && @{[ $conf->config('ticket_system-custom_priority_field-values') ]} ) {
+% @custom_priorities =
+% $conf->config('ticket_system-custom_priority_field-values');
+% }
+%
+% print "<BR><BR>". $pager. include('/elements/table-grid.html'). <<END;
+% <TR>
+% <TH CLASS="grid" BGCOLOR="#cccccc">#</TH>
+% <TH CLASS="grid" BGCOLOR="#cccccc">Status</TH>
+% <TH CLASS="grid" BGCOLOR="#cccccc">(bill) name</TH>
+% <TH CLASS="grid" BGCOLOR="#cccccc">company</TH>
+%END
+%
+%if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+% print <<END;
+% <TH CLASS="grid" BGCOLOR="#cccccc">(service) name</TH>
+% <TH CLASS="grid" BGCOLOR="#cccccc">company</TH>
+%END
+%}
+%
+%foreach my $addl_header ( @addl_headers ) {
+% print '<TH CLASS="grid" BGCOLOR="#cccccc">'. "$addl_header</TH>";
+%}
+%
+%print <<END;
+% <TH CLASS="grid" BGCOLOR="#cccccc">Packages</TH>
+% <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=2>Services</TH>
+% </TR>
+%END
+%
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor;
+%
+% my(%saw,$cust_main);
+% foreach $cust_main (
+% sort $sortby grep(!$saw{$_->custnum}++, @cust_main)
+% ) {
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+% 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>';
+%
+% my $status = $cust_main->status;
+% my $statuscol = $FS::cust_main::statuscolor{$status};
+
+ <TR>
+ <TD CLASS="grid" ALIGN="right" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %>><A HREF="<% $view %>"><FONT SIZE=-1><% $custnum %></FONT></A></TD>
+ <TD CLASS="grid" ALIGN="center" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %>><FONT SIZE=-1 COLOR=<% $statuscol %>><B><% ucfirst($status) %></B></FONT></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %>><A HREF="<% $view %>"><FONT SIZE=-1><% "$last, $first" %></FONT></A></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %>><% $pcompany %></TD>
+%
+% 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>';
+%
+
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %>><A HREF="<% $view %>"><FONT SIZE=-1><% "$ship_last, $ship_first" %></FONT></A></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %>><% $pship_company %></A></TD>
+% }
+%
+% foreach my $addl_col ( @addl_cols ) {
+% if ( $addl_col eq 'tickets' ) {
+% if ( @custom_priorities ) {
+
+
+ <TD CLASS="inv" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %> ALIGN=right><FONT SIZE=-1>
+
+ <TABLE CLASS="inv" CELLSPACING=0 CELLPADDING=0>
+% foreach my $priority ( @custom_priorities, '' ) {
+%
+% my $num =
+% FS::TicketSystem->num_customer_tickets($custnum,$priority);
+% my $ahref = '';
+% $ahref= '<A HREF="'.
+% FS::TicketSystem->href_customer_tickets($custnum,$priority).
+% '">'
+% if $num;
+%
+
+
+ <TR>
+ <TD ALIGN=right>
+ <FONT SIZE=-1><% $ahref.$num %></A></FONT>
+ </TD>
+ <TD ALIGN=left>
+ <FONT SIZE=-1><% $ahref %><% $priority || '<i>(none)</i>' %></A></FONT>
+ </TD>
+ </TR>
+% }
+
+
+ <TR>
+ <TH ALIGN=right STYLE="border-top: dashed 1px black">
+ <FONT SIZE=-1>
+% } else {
+
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %> ALIGN=right><FONT SIZE=-1>
+% }
+%
+% my $ahref = '';
+% $ahref = '<A HREF="'.
+% FS::TicketSystem->href_customer_tickets($custnum).
+% '">'
+% if $cust_main->get($addl_col);
+%
+
+
+ <% $ahref %><% $cust_main->get($addl_col) %></A>
+% if ( @custom_priorities ) {
+
+
+ </FONT></TH>
+ <TH ALIGN=left STYLE="border-top: dashed 1px black">
+ <FONT SIZE=-1><% ${ahref} %>Total</A><FONT>
+ </TH>
+ </TR>
+ </TABLE>
+% }
+
+
+ </FONT></TD>
+% } else {
+
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan || 1 %> ALIGN=right><FONT SIZE=-1>
+ <% $cust_main->get($addl_col) %>
+ </FONT></TD>
+%
+% }
+% }
+%
+% 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 CLASS="grid" BGCOLOR="$bgcolor" 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 CLASS="grid" BGCOLOR="$bgcolor" >!. FS::UI::Web::svc_link($m, $cust_svc->part_svc, $cust_svc) . qq!</TD> !.
+% qq!<TD CLASS="grid" BGCOLOR="$bgcolor" >!. FS::UI::Web::svc_label_link($m, $cust_svc->part_svc, $cust_svc) . qq!</TD> !;
+% $n2="</TR><TR>";
+% }
+%
+% unless ( @cust_svc ) {
+% print qq!<TD CLASS="grid" BGCOLOR="$bgcolor" COLSPAN=2>&nbsp;</TD>!;
+% }
+%
+% #print qq!</TR><TR>\n!;
+% $n1="</TR><TR>";
+% }
+%
+% unless ( @{$all_pkgs{$custnum}} ) {
+% print qq!<TD CLASS="grid" BGCOLOR="$bgcolor" COLSPAN=3>&nbsp;</TD>!;
+% }
+%
+% print "</TR>";
+% }
+%
+%
+
+
+ </TABLE><% $pager %>
+
+ <% include('/elements/footer.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 tickets_sort {
+% $b->getfield('tickets') <=> $a->getfield('tickets');
+%}
+%
+%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 000000000..8ef1ecb9a
--- /dev/null
+++ b/httemplate/search/cust_main.html
@@ -0,0 +1,47 @@
+<HTML>
+ <HEAD>
+ <TITLE>Customer Search</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="#e8e8e8">
+ <FONT SIZE=7>
+ Customer Search
+ </FONT>
+ <BR><BR>
+ <FORM ACTION="cust_main.cgi" METHOD="GET">
+ <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>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List customers');
+
+</%init>
diff --git a/httemplate/search/cust_pay.cgi b/httemplate/search/cust_pay.cgi
new file mode 100755
index 000000000..6215b4b2f
--- /dev/null
+++ b/httemplate/search/cust_pay.cgi
@@ -0,0 +1,228 @@
+<% include( 'elements/search.html',
+ 'title' => $title,
+ 'name' => 'payments',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'count_addl' => [ '$%.2f total paid', ],
+ 'header' => [ 'Payment',
+ 'Amount',
+ 'Date',
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [
+ sub {
+ my $cust_pay = shift;
+ if ( $cust_pay->payby eq 'CARD' ) {
+ 'Card #'. $cust_pay->paymask;
+ } elsif ( $cust_pay->payby eq 'CHEK' ) {
+ 'E-check acct#'. $cust_pay->payinfo;
+ } elsif ( $cust_pay->payby eq 'BILL' ) {
+ 'Check #'. $cust_pay->payinfo;
+ } elsif ( $cust_pay->payby eq 'PREP' ) {
+ 'Prepaid card #'. $cust_pay->payinfo;
+ } elsif ( $cust_pay->payby eq 'CASH' ) {
+ 'Cash '. $cust_pay->payinfo;
+ } elsif ( $cust_pay->payby eq 'WEST' ) {
+ 'Western Union'; #. $cust_pay->payinfo;
+ } elsif ( $cust_pay->payby eq 'MCRD' ) {
+ 'Manual credit card'; #. $cust_pay->payinfo;
+ } else {
+ $cust_pay->payby. ' '. $cust_pay->payinfo;
+ }
+ },
+ sub { sprintf('$%.2f', shift->paid ) },
+ sub { time2str('%b %d %Y', shift->_date ) },
+ \&FS::UI::Web::cust_fields,
+ ],
+ #'align' => 'lrrrll',
+ 'align' => 'rrr'.FS::UI::Web::cust_aligns(),
+ 'links' => [
+ '',
+ '',
+ '',
+ ( map { $_ ne 'Cust. Status' ? $link : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'color' => [
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+my $title = 'Payment Search Results';
+my( $count_query, $sql_query );
+if ( $cgi->param('magic') ) {
+
+ my @search = ();
+ my $orderby;
+ if ( $cgi->param('magic') eq '_date' ) {
+
+
+ if ( $cgi->param('agentnum') && $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ push @search, "agentnum = $1"; # $search{'agentnum'} = $1;
+ my $agent = qsearchs('agent', { 'agentnum' => $1 } );
+ die "unknown agentnum $1" unless $agent;
+ $title = $agent->agent. " $title";
+ }
+
+ if ( $cgi->param('payby') ) {
+ $cgi->param('payby') =~
+ /^(CARD|CHEK|BILL|PREP|CASH|WEST|MCRD)(-(VisaMC|Amex|Discover|Maestro))?$/
+ or die "illegal payby ". $cgi->param('payby');
+ push @search, "cust_pay.payby = '$1'";
+ if ( $3 ) {
+
+ my $cardtype = $3;
+
+ my $search;
+ if ( $cardtype eq 'VisaMC' ) {
+ #avoid posix regexes for portability
+ $search =
+ " ( ( substring(cust_pay.payinfo from 1 for 1) = '4' ".
+ " AND substring(cust_pay.payinfo from 1 for 4) != '4936' ".
+ " AND substring(cust_pay.payinfo from 1 for 6) ".
+ " NOT SIMILAR TO '49030[2-9]' ".
+ " AND substring(cust_pay.payinfo from 1 for 6) ".
+ " NOT SIMILAR TO '49033[5-9]' ".
+ " AND substring(cust_pay.payinfo from 1 for 6) ".
+ " NOT SIMILAR TO '49110[1-2]' ".
+ " AND substring(cust_pay.payinfo from 1 for 6) ".
+ " NOT SIMILAR TO '49117[4-9]' ".
+ " AND substring(cust_pay.payinfo from 1 for 6) ".
+ " NOT SIMILAR TO '49118[1-2]' ".
+ " )".
+ " OR substring(cust_pay.payinfo from 1 for 2) = '51' ".
+ " OR substring(cust_pay.payinfo from 1 for 2) = '52' ".
+ " OR substring(cust_pay.payinfo from 1 for 2) = '53' ".
+ " OR substring(cust_pay.payinfo from 1 for 2) = '54' ".
+ " OR substring(cust_pay.payinfo from 1 for 2) = '54' ".
+ " OR substring(cust_pay.payinfo from 1 for 2) = '55' ".
+ " OR substring(cust_pay.payinfo from 1 for 2) = '36' ". #Diner's int'l processed as Visa/MC inside US
+ " ) ";
+ } elsif ( $cardtype eq 'Amex' ) {
+ $search =
+ " ( substring(cust_pay.payinfo from 1 for 2 ) = '34' ".
+ " OR substring(cust_pay.payinfo from 1 for 2 ) = '37' ".
+ " ) ";
+ } elsif ( $cardtype eq 'Discover' ) {
+ $search =
+ " ( substring(cust_pay.payinfo from 1 for 4 ) = '6011' ".
+ " OR substring(cust_pay.payinfo from 1 for 2 ) = '65' ".
+ " OR substring(cust_pay.payinfo from 1 for 3 ) = '622' ". #China Union Pay processed as Discover outside CN
+ " ) ";
+ } elsif ( $cardtype eq 'Maestro' ) {
+ $search =
+ " ( substring(cust_pay.payinfo from 1 for 2 ) = '63' ".
+ " OR substring(cust_pay.payinfo from 1 for 2 ) = '67' ".
+ " OR substring(cust_pay.payinfo from 1 for 6 ) = '564182' ".
+ " OR substring(cust_pay.payinfo from 1 for 4 ) = '4936' ".
+ " OR substring(cust_pay.payinfo from 1 for 6 ) ".
+ " SIMILAR TO '49030[2-9]' ".
+ " OR substring(cust_pay.payinfo from 1 for 6 ) ".
+ " SIMILAR TO '49033[5-9]' ".
+ " OR substring(cust_pay.payinfo from 1 for 6 ) ".
+ " SIMILAR TO '49110[1-2]' ".
+ " OR substring(cust_pay.payinfo from 1 for 6 ) ".
+ " SIMILAR TO '49117[4-9]' ".
+ " OR substring(cust_pay.payinfo from 1 for 6 ) ".
+ " SIMILAR TO '49118[1-2]' ".
+ " ) ";
+ } else {
+ die "unknown card type $cardtype";
+ }
+
+ my $masksearch = $search;
+ $masksearch =~ s/cust_pay\.payinfo/cust_pay.paymask/gi;
+
+ push @search,
+ "( $search OR ( cust_pay.paymask IS NOT NULL AND $masksearch ) )";
+
+ }
+ }
+
+ my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+ push @search, "_date >= $beginning ",
+ "_date <= $ending";
+
+ push @search, FS::UI::Web::parse_lt_gt($cgi, 'paid' );
+
+ $orderby = '_date';
+
+ } elsif ( $cgi->param('magic') eq 'paybatch' ) {
+
+ $cgi->param('paybatch') =~ /^([\w\/\:\-\.]+)$/
+ or die "illegal paybatch: ". $cgi->param('paybatch');
+
+ push @search, "paybatch = '$1'";
+
+ $orderby = "LOWER(company || ' ' || last || ' ' || first )";
+
+ } else {
+ die "unknown search magic: ". $cgi->param('magic');
+ }
+
+ #here is the agent virtualization
+ push @search, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+ my $search = ' WHERE '. join(' AND ', @search);
+
+ $count_query = "SELECT COUNT(*), SUM(paid) ".
+ "FROM cust_pay LEFT JOIN cust_main USING ( custnum )".
+ $search;
+
+ $sql_query = {
+ 'table' => 'cust_pay',
+ 'select' => join(', ',
+ 'cust_pay.*',
+ 'cust_main.custnum as cust_main_custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'hashref' => {},
+ 'extra_sql' => "$search ORDER BY $orderby",
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ };
+
+} 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'".
+ " AND ". $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+ $sql_query = {
+ 'table' => 'cust_pay',
+ 'hashref' => { 'payinfo' => $payinfo,
+ 'payby' => $payby },
+ 'extra_sql' => $FS::CurrentUser::CurrentUser->agentnums_sql.
+ " ORDER BY _date",
+ };
+
+}
+
+my $link = sub {
+ my $cust_pay = shift;
+ $cust_pay->cust_main_custnum
+ ? [ "${p}view/cust_main.cgi?", 'custnum' ]
+ : '';
+};
+
+</%init>
diff --git a/httemplate/search/cust_pay_batch.cgi b/httemplate/search/cust_pay_batch.cgi
new file mode 100755
index 000000000..e378ffae7
--- /dev/null
+++ b/httemplate/search/cust_pay_batch.cgi
@@ -0,0 +1,190 @@
+<% include('elements/search.html',
+ 'title' => 'Batch payment details',
+ 'name' => 'batch details',
+ 'menubar' => ['Main Menu' => $p,],
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'html_init' => $pay_batch ? $html_init : '',
+ 'header' => [ '#',
+ 'Inv #',
+ 'Customer',
+ 'Customer',
+ 'Card Name',
+ 'Card',
+ 'Exp',
+ 'Amount',
+ 'Status',
+ ],
+ 'fields' => [ sub {
+ shift->[0];
+ },
+ sub {
+ shift->[1];
+ },
+ sub {
+ shift->[2];
+ },
+ sub {
+ my $cpb = shift;
+ $cpb->[3] . ', ' . $cpb->[4];
+ },
+ sub {
+ shift->[5];
+ },
+ sub {
+ my $cardnum = shift->[6];
+ 'x'x(length($cardnum)-4). substr($cardnum,(length($cardnum)-4));
+ },
+ sub {
+ shift->[7] =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+ my( $mon, $year ) = ( $2, $1 );
+ $mon = "0$mon" if length($mon) == 1;
+ "$mon/$year";
+ },
+ sub {
+ shift->[8];
+ },
+ sub {
+ shift->[9];
+ },
+ ],
+ 'align' => 'lllllllrl',
+ 'links' => [ ['', sub{'#';}],
+ ["${p}view/cust_bill.cgi?", sub{shift->[1];},],
+ ["${p}view/cust_main.cgi?", sub{shift->[2];},],
+ ["${p}view/cust_main.cgi?", sub{shift->[2];},],
+ ],
+ )
+%>
+<%init>
+
+my $conf = new FS::Conf;
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports')
+ || $FS::CurrentUser::CurrentUser->access_right('Process batches')
+ || ( $cgi->param('custnum')
+ && $conf->exists('batch-enable')
+ #&& $FS::CurrentUser::CurrentUser->access_right('View customer batched payments')
+ );
+
+my( $count_query, $sql_query );
+my $hashref = {};
+my @search = ();
+my $orderby = 'paybatchnum';
+
+my( $pay_batch, $batchnum ) = ( '', '');
+if ( $cgi->param('batchnum') && $cgi->param('batchnum') =~ /^(\d+)$/ ) {
+ push @search, "batchnum = $1";
+ $pay_batch = qsearchs('pay_batch', { 'batchnum' => $1 } );
+ die "Batch $1 not found!" unless $pay_batch;
+ $batchnum = $pay_batch->batchnum;
+}
+
+if ( $cgi->param('custnum') && $cgi->param('custnum') =~ /^(\d+)$/ ) {
+ push @search, "custnum = $1";
+}
+
+if ( $cgi->param('status') && $cgi->param('status') =~ /^(\w)$/ ) {
+ push @search, "pay_batch.status = '$1'";
+}
+
+if ( $cgi->param('payby') ) {
+ $cgi->param('payby') =~ /^(CARD|CHEK)$/
+ or die "illegal payby " . $cgi->param('payby');
+
+ push @search, "cust_pay_batch.payby = '$1'";
+}
+
+if ( not $cgi->param('dcln') ) {
+ push @search, "cpb.status IS DISTINCT FROM 'Approved'";
+}
+
+my ($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+unless ($pay_batch){
+ push @search, "pay_batch.upload >= $beginning" if ($beginning);
+ push @search, "pay_batch.upload <= $ending" if ($ending < 4294967295);#2^32-1
+ $orderby = "pay_batch.download,paybatchnum";
+}
+
+push @search, $FS::CurrentUser::CurrentUser->agentnums_sql;
+my $search = ' WHERE ' . join(' AND ', @search);
+
+$count_query = 'SELECT COUNT(*) FROM cust_pay_batch AS cpb ' .
+ 'LEFT JOIN cust_main USING ( custnum ) ' .
+ 'LEFT JOIN pay_batch USING ( batchnum )' .
+ $search;
+
+#grr
+$sql_query = "SELECT paybatchnum,invnum,custnum,cpb.last,cpb.first," .
+ "cpb.payname,cpb.payinfo,cpb.exp,amount,cpb.status " .
+ "FROM cust_pay_batch AS cpb " .
+ 'LEFT JOIN cust_main USING ( custnum ) ' .
+ 'LEFT JOIN pay_batch USING ( batchnum ) ' .
+ "$search ORDER BY $orderby";
+
+my $html_init = '';
+if ( $pay_batch ) {
+ my $fixed = $conf->config('batch-fixed_format-'. $pay_batch->payby);
+ if (
+ $pay_batch->status eq 'O'
+ || ( $pay_batch->status eq 'I'
+ && $FS::CurrentUser::CurrentUser->access_right('Reprocess batches')
+ )
+ ) {
+ $html_init .= qq!<FORM ACTION="$p/misc/download-batch.cgi" METHOD="POST">!;
+ if ( $fixed ) {
+ $html_init .= qq!<INPUT TYPE="hidden" NAME="format" VALUE="$fixed">!;
+ } else {
+ $html_init .= qq!Download batch in format <SELECT NAME="format">!.
+ qq!<OPTION VALUE="">Default batch mode</OPTION>!.
+ qq!<OPTION VALUE="csv-td_canada_trust-merchant_pc_batch">CSV file for TD Canada Trust Merchant PC Batch</OPTION>!.
+ qq!<OPTION VALUE="csv-chase_canada-E-xactBatch">CSV file for Chase Canada E-xactBatch</OPTION>!.
+ qq!<OPTION VALUE="PAP">80 byte file for TD Canada Trust PAP Batch</OPTION>!.
+ qq!<OPTION VALUE="BoM">Bank of Montreal ECA batch</OPTION>!.
+ qq!</SELECT>!;
+ }
+ $html_init .= qq!<INPUT TYPE="hidden" NAME="batchnum" VALUE="$batchnum"><INPUT TYPE="submit" VALUE="Download"></FORM><BR>!;
+ }
+
+ if (
+ $pay_batch->status eq 'I'
+ || ( $pay_batch->status eq 'R'
+ && $FS::CurrentUser::CurrentUser->access_right('Reprocess batches')
+ )
+ ) {
+ $html_init .= qq!<FORM ACTION="$p/misc/upload-batch.cgi" METHOD="POST" ENCTYPE="multipart/form-data">!.
+ qq!Upload results<BR>!.
+ qq!Filename <INPUT TYPE="file" NAME="batch_results"><BR>!;
+ if ( $fixed ) {
+ $html_init .= qq!<INPUT TYPE="hidden" NAME="format" VALUE="$fixed">!;
+ } else {
+ $html_init .= qq!Format <SELECT NAME="format">!.
+ qq!<OPTION VALUE="">Default batch mode</OPTION>!.
+ qq!<OPTION VALUE="csv-td_canada_trust-merchant_pc_batch">CSV results from TD Canada Trust Merchant PC Batch</OPTION>!.
+ qq!<OPTION VALUE="csv-chase_canada-E-xactBatch">CSV file for Chase Canada E-xactBatch</OPTION>!.
+ qq!<OPTION VALUE="PAP">264 byte results for TD Canada Trust PAP Batch</OPTION>!.
+ qq!<OPTION VALUE="BoM">Bank of Montreal ECA results</OPTION>!.
+ qq!</SELECT><BR>!;
+ }
+ $html_init .= qq!<INPUT TYPE="hidden" NAME="batchnum" VALUE="$batchnum">!;
+ $html_init .= '<INPUT TYPE="submit" VALUE="Upload"></FORM><BR>';
+ }
+
+}
+
+if ($pay_batch) {
+ my $sth = dbh->prepare($count_query) or die dbh->errstr. "doing $count_query";
+ $sth->execute or die "Error executing \"$count_query\": ". $sth->errstr;
+ my $cards = $sth->fetchrow_arrayref->[0];
+
+ my $st = "SELECT SUM(amount) from cust_pay_batch WHERE batchnum=". $batchnum;
+ $sth = dbh->prepare($st) or die dbh->errstr. "doing $st";
+ $sth->execute or die "Error executing \"$st\": ". $sth->errstr;
+ my $total = $sth->fetchrow_arrayref->[0];
+
+ $html_init .= "$cards credit card payments batched<BR>\$" .
+ sprintf("%.2f", $total) ." total in batch<BR>";
+}
+
+</%init>
diff --git a/httemplate/search/cust_pkg.cgi b/httemplate/search/cust_pkg.cgi
new file mode 100755
index 000000000..84b083a3d
--- /dev/null
+++ b/httemplate/search/cust_pkg.cgi
@@ -0,0 +1,339 @@
+<% include( 'elements/search.html',
+ 'title' => 'Package Search Results',
+ 'name' => 'packages',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ #'redirect' => $link,
+ 'header' => [ '#',
+ 'Package',
+ 'Class',
+ 'Status',
+ 'Freq.',
+ 'Setup',
+ 'Last bill',
+ 'Next bill',
+ 'Susp.',
+ 'Expire',
+ 'Cancel',
+ FS::UI::Web::cust_header(
+ $cgi->param('cust_fields')
+ ),
+ 'Services',
+ ],
+ 'fields' => [
+ 'pkgnum',
+ sub { #my $part_pkg = $part_pkg{shift->pkgpart};
+ #$part_pkg->pkg; # ' - '. $part_pkg->comment;
+ $_[0]->pkg; # ' - '. $_[0]->comment;
+ },
+ 'classname',
+ sub { ucfirst(shift->status); },
+ sub { #shift->part_pkg->freq_pretty;
+
+ #my $part_pkg = $part_pkg{shift->pkgpart};
+ #$part_pkg->freq_pretty;
+
+ FS::part_pkg::freq_pretty(shift);
+ },
+
+ #sub { time2str('%b %d %Y', shift->setup); },
+ #sub { time2str('%b %d %Y', shift->last_bill); },
+ #sub { time2str('%b %d %Y', shift->bill); },
+ #sub { time2str('%b %d %Y', shift->susp); },
+ #sub { time2str('%b %d %Y', shift->expire); },
+ #sub { time2str('%b %d %Y', shift->get('cancel')); },
+ ( map { time_or_blank($_) }
+ qw( setup last_bill bill susp expire cancel ) ),
+
+ \&FS::UI::Web::cust_fields,
+ #sub { '<table border=0 cellspacing=0 cellpadding=0 STYLE="border:none">'.
+ # join('', map { '<tr><td align="right" style="border:none">'. $_->[0].
+ # ':</td><td style="border:none">'. $_->[1]. '</td></tr>' }
+ # shift->labels
+ # ).
+ # '</table>';
+ # },
+ sub {
+ [ map {
+ [
+ { 'data' => $_->[0]. ':',
+ 'align'=> 'right',
+ },
+ { 'data' => $_->[1],
+ 'align'=> 'left',
+ 'link' => $p. 'view/' .
+ $_->[2]. '.cgi?'. $_->[3],
+ },
+ ];
+ } shift->labels
+ ];
+ },
+ ],
+ 'color' => [
+ '',
+ '',
+ '',
+ sub { shift->statuscolor; },
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ '',
+ ],
+ 'style' => [ '', '', '', 'b', '', '', '', '', '', '', '',
+ FS::UI::Web::cust_styles() ],
+ 'size' => [ '', '', '', '-1', ],
+ 'align' => 'rllclrrrrrr'. FS::UI::Web::cust_aligns(). 'r',
+ 'links' => [
+ $link,
+ $link,
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ ( map { $_ ne 'Cust. Status' ? $clink : '' }
+ FS::UI::Web::cust_header(
+ $cgi->param('cust_fields')
+ )
+ ),
+ '',
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List packages');
+
+# my %part_pkg = map { $_->pkgpart => $_ } qsearch('part_pkg', {});
+
+my($query) = $cgi->keywords;
+
+my @where = ();
+
+##
+# parse agent
+##
+
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ and $1 ) {
+ push @where,
+ "agentnum = $1";
+}
+
+##
+# parse status
+##
+
+if ( $cgi->param('magic') eq 'active'
+ || $cgi->param('status') eq 'active' ) {
+
+ push @where, FS::cust_pkg->active_sql();
+
+} elsif ( $cgi->param('magic') eq 'inactive'
+ || $cgi->param('status') eq 'inactive' ) {
+
+ push @where, FS::cust_pkg->inactive_sql();
+
+
+} elsif ( $cgi->param('magic') eq 'suspended'
+ || $cgi->param('status') eq 'suspended' ) {
+
+ push @where, FS::cust_pkg->suspended_sql();
+
+} elsif ( $cgi->param('magic') =~ /^cancell?ed$/
+ || $cgi->param('status') =~ /^cancell?ed$/ ) {
+
+ push @where, FS::cust_pkg->cancelled_sql();
+
+} elsif ( $cgi->param('status') =~ /^(one-time charge|inactive)$/ ) {
+
+ push @where, FS::cust_pkg->inactive_sql();
+
+}
+
+###
+# parse package class
+###
+
+#false lazinessish w/graph/cust_bill_pkg.cgi
+my $classnum = 0;
+my @pkg_class = ();
+if ( exists($cgi->Vars->{'classnum'})
+ && $cgi->param('classnum') =~ /^(\d*)$/
+ )
+{
+ $classnum = $1;
+ if ( $classnum ) { #a specific class
+ push @where, "classnum = $classnum";
+
+ #@pkg_class = ( qsearchs('pkg_class', { 'classnum' => $classnum } ) );
+ #die "classnum $classnum not found!" unless $pkg_class[0];
+ #$title .= $pkg_class[0]->classname.' ';
+
+ } elsif ( $classnum eq '' ) { #the empty class
+
+ push @where, "classnum IS NULL";
+ #$title .= 'Empty class ';
+ #@pkg_class = ( '(empty class)' );
+ } elsif ( $classnum eq '0' ) {
+ #@pkg_class = qsearch('pkg_class', {} ); # { 'disabled' => '' } );
+ #push @pkg_class, '(empty class)';
+ } else {
+ die "illegal classnum";
+ }
+}
+#eslaf
+
+###
+# parse part_pkg
+###
+
+my $pkgpart = join (' OR pkgpart=',
+ grep {$_} map { /^(\d+)$/; } ($cgi->param('pkgpart')));
+push @where, '(pkgpart=' . $pkgpart . ')' if $pkgpart;
+
+###
+# parse dates
+###
+
+my $orderby = '';
+
+#false laziness w/report_cust_pkg.html
+my %disable = (
+ 'all' => {},
+ 'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, },
+ 'active' => { 'susp'=>1, 'cancel'=>1 },
+ 'suspended' => { 'cancel' => 1 },
+ 'cancelled' => {},
+ '' => {},
+);
+
+foreach my $field (qw( setup last_bill bill susp expire cancel )) {
+
+ my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field);
+
+ next if $beginning == 0 && $ending == 4294967295
+ or $disable{$cgi->param('status')}->{$field};
+
+ push @where,
+ "$field IS NOT NULL",
+ "$field >= $beginning",
+ "$field <= $ending";
+
+ $orderby ||= "ORDER BY $field";
+
+}
+
+$orderby ||= 'ORDER BY bill';
+
+###
+# parse magic, legacy, etc.
+###
+
+if ( $cgi->param('magic') &&
+ $cgi->param('magic') =~ /^(active|inactive|suspended|cancell?ed)$/
+) {
+
+ $orderby = 'ORDER BY pkgnum';
+
+ if ( $cgi->param('pkgpart') =~ /^(\d+)$/ ) {
+ push @where, "pkgpart = $1";
+ }
+
+} elsif ( $query eq 'pkgnum' ) {
+
+ $orderby = 'ORDER BY pkgnum';
+
+} elsif ( $query eq 'APKG_pkgnum' ) {
+
+ $orderby = 'ORDER BY pkgnum';
+
+ push @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
+ )
+ )';
+
+}
+
+##
+# setup queries, links, subs, etc. for the search
+##
+
+# here is the agent virtualization
+push @where, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
+
+my $addl_from = 'LEFT JOIN cust_main USING ( custnum ) '.
+ 'LEFT JOIN part_pkg USING ( pkgpart ) '.
+ 'LEFT JOIN pkg_class USING ( classnum ) ';
+
+my $count_query = "SELECT COUNT(*) FROM cust_pkg $addl_from $extra_sql";
+
+my $sql_query = {
+ 'table' => 'cust_pkg',
+ 'hashref' => {},
+ 'select' => join(', ',
+ 'cust_pkg.*',
+ ( map "part_pkg.$_", qw( pkg freq ) ),
+ 'pkg_class.classname',
+ 'cust_main.custnum as cust_main_custnum',
+ FS::UI::Web::cust_sql_fields(
+ $cgi->param('cust_fields')
+ ),
+ ),
+ 'extra_sql' => "$extra_sql $orderby",
+ 'addl_from' => $addl_from,
+};
+
+my $link = sub {
+ [ "${p}view/cust_main.cgi?".shift->custnum.'#cust_pkg', 'pkgnum' ];
+};
+
+my $clink = sub {
+ my $cust_pkg = shift;
+ $cust_pkg->cust_main_custnum
+ ? [ "${p}view/cust_main.cgi?", 'custnum' ]
+ : '';
+};
+
+#if ( scalar(@cust_pkg) == 1 ) {
+# print $cgi->redirect("${p}view/cust_main.cgi?". $cust_pkg[0]->custnum.
+# "#cust_pkg". $cust_pkg[0]->pkgnum );
+
+# my @cust_svc = qsearch( 'cust_svc', { 'pkgnum' => $pkgnum } );
+# my $rowspan = scalar(@cust_svc) || 1;
+
+# 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>";
+# }
+
+sub time_or_blank {
+ my $column = shift;
+ return sub {
+ my $record = shift;
+ my $value = $record->get($column); #mmm closures
+ $value ? time2str('%b %d %Y', $value ) : '';
+ };
+}
+
+</%init>
diff --git a/httemplate/search/cust_svc.html b/httemplate/search/cust_svc.html
new file mode 100644
index 000000000..6369b202e
--- /dev/null
+++ b/httemplate/search/cust_svc.html
@@ -0,0 +1,138 @@
+<% include( 'elements/search.html',
+ 'title' => 'Service search results',
+ 'name' => 'services',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'redirect' => $link,
+ 'header' => [ '#',
+ 'Service',
+ # package?
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [ 'svcnum',
+ sub {
+ #$_[0]->svc. ': '. $_[0]->label;
+ my($label, $value, $svcdb) = $_[0]->label;
+ "$label: $value";
+ },
+ # package?
+ \&FS::UI::Web::cust_fields,
+ ],
+ 'links' => [ $link,
+ $link,
+ # package?
+ ( map { $_ ne 'Cust. Status' ? $link_cust : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'align' => 'rl'. FS::UI::Web::cust_aligns(),
+ 'color' => [
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List services');
+
+my $addl_from = ' LEFT JOIN part_svc USING ( svcpart ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ';
+
+my @extra_sql = ();
+my $orderby = 'ORDER BY svcnum'; #has to be ordered by something
+ #for pagination to work
+if ( length( $cgi->param('search_svc') ) ) {
+
+ my $string = $cgi->param('search_svc');
+ $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
+
+ # implement fuzzy searching in subclasses too at some point?
+ # service searching maybe shouldn't be fuzzy...
+
+ push @extra_sql,
+ ' ( '. join(' OR ',
+ map { my $table = $_;
+ my $search_sql = "FS::$table"->search_sql($string);
+ " ( svcdb = '$table'
+ AND 0 < ( SELECT COUNT(*) FROM $table
+ WHERE $table.svcnum = cust_svc.svcnum
+ AND $search_sql
+ )
+ ) ";
+ }
+ FS::part_svc->svc_tables
+ ). ' ) ';
+
+} elsif ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+
+ $cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unknown svcdb";
+ push @extra_sql, "svcdb = '$1'";
+
+ push @extra_sql, 'pkgnum IS NULL'
+ if $cgi->param('magic') eq 'unlinked';
+
+ if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+ my $sortby = $1;
+ $orderby = "ORDER BY $sortby";
+ }
+
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+
+ push @extra_sql, "svcpart = $1";
+
+} else {
+ eidiot("No search term specified");
+}
+
+#here is the agent virtualization
+push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my $extra_sql = ' WHERE '. join(' AND ', @extra_sql );
+
+my $sql_query = {
+ 'select' => join(', ',
+ 'cust_svc.*',
+ 'part_svc.*',
+ 'cust_main.custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'table' => 'cust_svc',
+ 'addl_from' => $addl_from,
+ 'hashref' => {},
+ 'extra_sql' => "$extra_sql $orderby",
+};
+
+my $count_query = "SELECT COUNT(*) FROM cust_svc $addl_from $extra_sql";
+
+my $link = sub {
+ my $cust_svc = shift;
+ my $url = FS::UI::Web::svc_url(
+ 'm' => $m,
+ 'action' => 'view',
+ #'part_svc' => $cust_svc->part_svc,
+ 'svcdb' => $cust_svc->svcdb, #we have it from the joined search
+ #'svc' => $cust_svc, #redundant
+ 'query' => '',
+ );
+ [ $url, 'svcnum' ];
+};
+
+my $link_cust = sub {
+ my $cust_svc = shift;
+ if ( $cust_svc->custnum ) {
+ [ "${p}view/cust_main.cgi?", 'custnum' ];
+ } else {
+ '';
+ }
+};
+
+</%init>
diff --git a/httemplate/search/cust_tax_exempt_pkg.cgi b/httemplate/search/cust_tax_exempt_pkg.cgi
new file mode 100644
index 000000000..604502d6f
--- /dev/null
+++ b/httemplate/search/cust_tax_exempt_pkg.cgi
@@ -0,0 +1,182 @@
+<% include( 'elements/search.html',
+ 'title' => 'Tax exemptions',
+ 'name' => 'tax exemptions',
+ 'query' => $query,
+ 'count_query' => $count_query,
+ 'count_addl' => [ $money_char. '%.2f total', ],
+ 'header' => [
+ '#',
+ 'Month',
+ 'Amount',
+ 'Line item',
+ 'Invoice',
+ 'Date',
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [
+ 'exemptpkgnum',
+ sub { $_[0]->month. '/'. $_[0]->year; },
+ sub { $money_char. $_[0]->amount; },
+
+ sub {
+ $_[0]->billpkgnum. ': '.
+ ( $_[0]->pkgnum > 0
+ ? $_[0]->get('pkg')
+ : $_[0]->get('itemdesc')
+ ).
+ ' ('.
+ ( $_[0]->setup > 0
+ ? $money_char. $_[0]->setup. ' setup'
+ : ''
+ ).
+ ( $_[0]->setup > 0 && $_[0]->recur > 0
+ ? ' / '
+ : ''
+ ).
+ ( $_[0]->recur > 0
+ ? $money_char. $_[0]->recur. ' recur'
+ : ''
+ ).
+ ')';
+ },
+
+ 'invnum',
+ sub { time2str('%b %d %Y', shift->_date ) },
+
+ \&FS::UI::Web::cust_fields,
+ ],
+ 'links' => [
+ '',
+ '',
+ '',
+
+ '',
+ $ilink,
+ $ilink,
+
+ ( map { $_ ne 'Cust. Status' ? $clink : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'align' => 'rrrlrc'.FS::UI::Web::cust_aligns(), # 'rlrrrc',
+ 'color' => [
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
+ )
+%>
+<%once>
+
+my $join_cust = "
+ JOIN cust_bill USING ( invnum )
+ LEFT JOIN cust_main USING ( custnum )
+";
+
+my $join_pkg = "
+ LEFT JOIN cust_pkg USING ( pkgnum )
+ LEFT JOIN part_pkg USING ( pkgpart )
+";
+
+my $join = "
+ JOIN cust_bill_pkg USING ( billpkgnum )
+ $join_cust
+ $join_pkg
+";
+
+</%once>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('View customer tax exemptions');
+
+my @where = ();
+
+my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+if ( $beginning || $ending ) {
+ push @where, "_date >= $beginning",
+ "_date <= $ending";
+ #"payby != 'COMP';
+}
+
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ push @where, "agentnum = $1";
+}
+
+if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+ push @where, "cust_main.custnum = $1";
+}
+
+if ( $cgi->param('out') ) {
+
+ push @where, "
+ 0 = (
+ SELECT COUNT(*) FROM cust_main_county AS county_out
+ WHERE ( county_out.county = cust_main.county
+ OR ( county_out.county IS NULL AND cust_main.county = '' )
+ OR ( county_out.county = '' AND cust_main.county IS NULL)
+ OR ( county_out.county IS NULL AND cust_main.county IS NULL)
+ )
+ AND ( county_out.state = cust_main.state
+ OR ( county_out.state IS NULL AND cust_main.state = '' )
+ OR ( county_out.state = '' AND cust_main.state IS NULL )
+ OR ( county_out.state IS NULL AND cust_main.state IS NULL )
+ )
+ AND county_out.country = cust_main.country
+ AND county_out.tax > 0
+ )
+ ";
+
+} elsif ( $cgi->param('country' ) ) {
+
+ my $county = dbh->quote( $cgi->param('county') );
+ my $state = dbh->quote( $cgi->param('state') );
+ my $country = dbh->quote( $cgi->param('country') );
+ push @where, "( county = $county OR $county = '' )",
+ "( state = $state OR $state = '' )",
+ " country = $country";
+ push @where, 'taxclass = '. dbh->quote( $cgi->param('taxclass') )
+ if $cgi->param('taxclass');
+
+}
+
+my $where = scalar(@where) ? 'WHERE '.join(' AND ', @where) : '';
+
+my $count_query = "SELECT COUNT(*), SUM(amount)".
+ " FROM cust_tax_exempt_pkg $join $where";
+
+my $query = {
+ 'table' => 'cust_tax_exempt_pkg',
+ 'addl_from' => $join,
+ 'hashref' => {},
+ 'select' => join(', ',
+ 'cust_tax_exempt_pkg.*',
+ 'cust_bill_pkg.*',
+ 'cust_bill.*',
+ 'part_pkg.pkg',
+ 'cust_main.custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'extra_sql' => $where,
+};
+
+my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ];
+my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
+
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
+
+</%init>
diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html
new file mode 100644
index 000000000..7d5e58a3b
--- /dev/null
+++ b/httemplate/search/elements/search.html
@@ -0,0 +1,658 @@
+% # options example...
+% # (everything not commented required is optional)
+% #
+% # # basic options, required
+% # 'title' => 'Page title',
+% #
+% # 'name_singular' => 'item', #singular name for the records returned
+% # #OR# # (preferred, will be pluralized automatically)
+% # 'name' => 'items', #plural name for the records returned
+% # # (deprecated, will be singularlized
+% # # simplisticly)
+% #
+% # # some HTML callbacks...
+% # 'menubar' => '', #menubar arrayref
+% # 'html_init' => '', #after the header/menubar and before the pager
+% # 'html_form' => '', #after the pager, right before the results
+% # # (only shown if there are results)
+% # # (use this for any form-opening tag rather than
+% # # html_init, to avoid a nested form)
+% # 'html_foot' => '', #at the bottom
+% # 'html_posttotal' => '', #at the bottom
+% # # (these three can be strings or coderefs)
+% #
+% #
+% # #literal SQL query string or qsearch hashref, required
+% # 'query' => {
+% # 'table' => 'tablename',
+% # #everything else is optional...
+% # 'hashref' => { 'field' => 'value',
+% # 'field' => { 'op' => '<',
+% # 'value' => '54',
+% # },
+% # },
+% # 'select' => '*',
+% # 'addl_from' => '', #'LEFT JOIN othertable USING ( key )',
+% # 'extra_sql' => '', #'AND otherstuff', #'WHERE onlystuff',
+% #
+% #
+% # },
+% # # "select * from tablename";
+% #
+% # #required unless 'query' is an SQL query string (shouldn't be...)
+% # 'count_query' => 'SELECT COUNT(*) FROM tablename',
+% #
+% # 'count_addl' => [], #additional count fields listref of sprintf strings
+% # # [ $money_char.'%.2f total paid', ],
+% #
+% # #listref of column labels, <TH>
+% # #required unless 'query' is an SQL query string
+% # # (if not specified the database column names will be used)
+% # 'header' => [ '#', 'Item' ],
+% #
+% # 'disable_download' => '', # set true to hide the CSV/Excel download links
+% # 'disable_nonefound' => '', # set true to disable the "No matching Xs found"
+% # # message
+% #
+% # #listref - each item is a literal column name (or method) or coderef
+% # #if not specified all columns will be shown
+% # 'fields' => [
+% # 'column',
+% # sub { my $row = shift; $row->column; },
+% # ],
+% #
+% # #listref of column footers
+% # 'footer' => [],
+% #
+% # #listref - each item is the empty string, or a listref of ...
+% # 'links' =>
+% #
+% #
+% # 'align' => 'lrc.', #one letter for each column, left/right/center/none
+% # # can also pass a listref with full values:
+% # # [ 'left', 'right', 'center', '' ]
+% #
+% # #listrefs...
+% # #currently only HTML, maybe eventually Excel too
+% # 'color' => [],
+% # 'size' => [],
+% # 'style' => [],
+% #
+% # #redirect if there's only one item...
+% # # listref of URL base and column name (or method)
+% # # or a coderef that returns the same
+% # 'redirect' =>
+% #
+% # #set to 1 (or column position for "disabled" status col) to enable
+% # #"show disabled/hide disabled" links
+% # #(can't be used with a literal query)
+% # 'disableable' => 1,
+%
+% my $DEBUG = 0;
+%
+% my(%opt) = @_;
+% #warn join(' / ', map { "$_ => $opt{$_}" } keys %opt ). "\n";
+%
+% my %align = (
+% 'l' => 'left',
+% 'r' => 'right',
+% 'c' => 'center',
+% ' ' => '',
+% '.' => '',
+% );
+% $opt{align} = [ map $align{$_}, split(//, $opt{align}) ],
+% unless !$opt{align} || ref($opt{align});
+%
+% my $type = '';
+% my $limit = '';
+% my($confmax, $maxrecords, $total, $offset, $count_arrayref);
+%
+% if ( $cgi->param('_type') =~ /^(csv|\w*\.xls)$/ ) {
+%
+% $type = $1;
+%
+% } else { #setup some pagination things if we're in html mode
+%
+% 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; #silly vim:/
+% }
+%
+% if ( $opt{disableable} && ! $cgi->param('showdisabled') ) {
+% $opt{count_query} .=
+% ( ( $opt{count_query} =~ /WHERE/i ) ? ' AND ' : ' WHERE ' ).
+% "( disabled = '' OR disabled IS NULL )";
+% }
+%
+% my $conf = new FS::Conf;
+% $confmax = $conf->config('maxsearchrecordsperpage');
+% if ( $cgi->param('maxrecords') =~ /^(\d+)$/ ) {
+% $maxrecords = $1;
+% } else {
+% $maxrecords ||= $confmax;
+% }
+%
+% $limit = $maxrecords ? "LIMIT $maxrecords" : '';
+%
+% $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;
+% $count_arrayref = $count_sth->fetchrow_arrayref;
+% $total = $count_arrayref->[0];
+%
+% }
+%
+% #disableable handling
+% my $posttotal = '';
+% if ( $opt{disableable} ) {
+%
+% my $name= $opt{'name_singular'} ? PL($opt{'name_singular'}) : $opt{'name'};
+%
+% if ( $cgi->param('showdisabled') ) {
+% $cgi->param('showdisabled', 0);
+% $posttotal= '( <a href="'. $cgi->self_url. '">'.
+% "hide disabled $name</a> )";
+% $cgi->param('showdisabled', 1);
+% } else {
+% $cgi->param('showdisabled', 1);
+% $posttotal= '( <a href="'. $cgi->self_url. '">'.
+% "show disabled $name</a> )";
+% $cgi->param('showdisabled', 0);
+% }
+%
+% if ( $cgi->param('showdisabled') ) {
+%
+% my $offset = $opt{disableable};
+%
+% splice @{ $opt{header} }, $offset, 0, 'Status';
+%
+% splice @{ $opt{fields} }, $offset, 0,
+% sub { shift->disabled ? 'DISABLED' : 'Active' };
+%
+% if ( $opt{links} && scalar( @{ $opt{links} } ) ) {
+% splice @{ $opt{links} }, $offset, 0, '';
+% }
+%
+% if ( $opt{align} && scalar( @{ $opt{align} } ) ) {
+% splice @{ $opt{align} }, $offset, 0, 'center';
+% }
+%
+% unless ( $opt{color} && scalar( @{ $opt{color} } ) ) {
+% #$opt{color} = [ map { '000000'; } @{$opt{header}} ];
+% $opt{color} = [ map { ''; } @{$opt{header}} ];
+% }
+% splice @{ $opt{color} }, $offset, 0,
+% sub { shift->disabled ? 'FF0000' : '00CC00'; };
+%
+% if ( $opt{size} && scalar( @{ $opt{size} } ) ) {
+% splice @{ $opt{size} }, $offset, 0, '';
+% }
+%
+% unless ( $opt{style} && scalar( @{ $opt{style} } ) ) {
+% $opt{style} = [ map { ''; } @{$opt{header}} ];
+% }
+% splice @{ $opt{style} }, $offset, 0, 'b';
+%
+% }
+%
+% }
+%
+% # run the query
+%
+% my $header = $opt{header};
+% my $rows;
+% if ( ref($opt{query}) ) {
+%
+% if ( $opt{disableable} && ! $cgi->param('showdisabled') ) {
+% #%search = ( 'disabled' => '' );
+% $opt{'query'}->{'hashref'}->{'disabled'} = '';
+% $opt{'query'}->{'extra_sql'} =~ s/^\s*WHERE/ AND/i;
+% }
+%
+% #eval "use FS::$opt{'query'};";
+% $rows = [ qsearch(
+% $opt{'query'}->{'table'},
+% $opt{'query'}->{'hashref'} || {},
+% $opt{'query'}->{'select'},
+% $opt{'query'}->{'extra_sql'}. " $limit",
+% '',
+% (exists($opt{'query'}->{'addl_from'}) ? $opt{'query'}->{'addl_from'} : '')
+% ) ];
+%
+% } 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};
+%
+% }
+%
+% warn scalar(@$rows). ' rows returned from '.
+% ( ref($opt{'query'}) ? 'qsearch query' : 'literal SQL query' )
+% if $DEBUG || $opt{'debug'};
+%
+% # display the results - csv, xls or html
+%
+% if ( $type eq 'csv' ) {
+%
+% #http_header('Content-Type' => 'text/comma-separated-values' ); #IE chokes
+% http_header('Content-Type' => 'text/plain' );
+%
+% my $csv = new Text::CSV_XS { 'always_quote' => 1,
+% 'eol' => "\n", #"\015\012", #"\012"
+% };
+%
+% $csv->combine(@$header); #or die $csv->status;
+%
+<% $csv->string %>
+%
+%
+% foreach my $row ( @$rows ) {
+%
+% if ( $opt{'fields'} ) {
+%
+% my @line = ();
+%
+% foreach my $field ( @{$opt{'fields'}} ) {
+% if ( ref($field) eq 'CODE' ) {
+% push @line, map {
+% ref($_) eq 'ARRAY'
+% ? '(N/A)' #unimplemented
+% : $_;
+% }
+% &{$field}($row);
+% } else {
+% push @line, $row->$field();
+% }
+% }
+%
+% $csv->combine(@line); #or die $csv->status;
+%
+% } else {
+% $csv->combine(@$row); #or die $csv->status;
+% }
+%
+%
+<% $csv->string %>
+%
+%
+% }
+%
+% #} elsif ( $type eq 'excel' ) {
+% } elsif ( $type =~ /\.xls$/ ) {
+%
+% #http_header('Content-Type' => 'application/excel' ); #eww
+% http_header('Content-Type' => 'application/vnd.ms-excel' );
+% #http_header('Content-Type' => 'application/msexcel' ); #alas
+%
+% my $data = '';
+% my $XLS = new IO::Scalar \$data;
+% my $workbook = Spreadsheet::WriteExcel->new($XLS)
+% or die "Error opening .xls file: $!";
+%
+% my $worksheet = $workbook->add_worksheet(substr($opt{'title'},0,31));
+%
+% my($r,$c) = (0,0);
+%
+% $worksheet->write($r, $c++, $_) foreach @$header;
+%
+% foreach my $row ( @$rows ) {
+% $r++;
+% $c = 0;
+%
+% 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' ) {
+% foreach my $value ( &{$field}($row) ) {
+% if ( ref($value) eq 'ARRAY' ) {
+% $worksheet->write($r, $c++, '(N/A)' ); #unimplemented
+% } else {
+% $worksheet->write($r, $c++, $value );
+% }
+% }
+% } else {
+% $worksheet->write($r, $c++, $row->$field() );
+% }
+% }
+%
+% } else {
+% $worksheet->write($r, $c++, $_) foreach @$row;
+% }
+%
+% }
+%
+% $workbook->close();# or die "Error creating .xls file: $!";
+%
+% http_header('Content-Length' => length($data) );
+%
+<% $data %>
+%
+%
+% } else { # regular HTML
+%
+% if ( exists($opt{'redirect'}) && scalar(@$rows) == 1 && $total == 1 ) {
+% my $redirect = $opt{'redirect'};
+% $redirect = &{$redirect}($rows->[0]) if ref($redirect) eq 'CODE';
+% my( $url, $method ) = @$redirect;
+% redirect( $url. $rows->[0]->$method() );
+% } else {
+% if ( $opt{'name_singular'} ) {
+% $opt{'name'} = PL($opt{'name_singular'});
+% }
+% ( my $xlsname = $opt{'name'} ) =~ s/\W//g;
+% if ( $total == 1 ) {
+% if ( $opt{'name_singular'} ) {
+% $opt{'name'} = $opt{'name_singular'}
+% } else {
+% #$opt{'name'} =~ s/s$// if $total == 1;
+% $opt{'name'} =~ s/((s)e)?s$/$2/ if $total == 1;
+% }
+% }
+%
+% my @menubar = ();
+% if ( $opt{'menubar'} ) {
+% @menubar = @{ $opt{'menubar'} };
+% #} else {
+% # @menubar = ( 'Main menu' => $p );
+% }
+
+ <% include( '/elements/header.html', $opt{'title'},
+ include( '/elements/menubar.html', @menubar )
+ )
+ %>
+ <% defined($opt{'html_init'})
+ ? ( ref($opt{'html_init'})
+ ? &{$opt{'html_init'}}()
+ : $opt{'html_init'}
+ )
+ : ''
+ %>
+%
+% unless ( $total ) {
+% unless ( $opt{'disable_nonefound'} ) {
+
+ No matching <% $opt{'name'} %> found.<BR>
+% }
+% } else {
+
+ <TABLE>
+ <TR>
+
+ <TD VALIGN="bottom">
+
+ <FORM>
+
+ <% $total %> total <% $opt{'name'} %>
+
+% if ( $confmax && $total > $confmax ) {
+% $cgi->delete('maxrecords');
+% $cgi->param('_dummy', 1);
+
+%# ( show <SELECT NAME="maxrecords" onChange="this.form.submit();">
+ ( show <SELECT NAME="maxrecords" onChange="window.location = '<% $cgi->self_url %>;maxrecords=' + this.options[this.selectedIndex].value;">
+
+% foreach my $max ( map { $_ * $confmax } qw( 1 5 10 25 ) ) {
+ <OPTION VALUE="<% $max %>" <% ( $maxrecords == $max ) ? 'SELECTED' : '' %>><% $max %></OPTION>
+% }
+
+ </SELECT> per page )
+
+% $cgi->param('maxrecords', $maxrecords);
+% }
+
+ <% $posttotal %>
+
+ <% defined($opt{'html_posttotal'})
+ ? ( ref($opt{'html_posttotal'})
+ ? &{$opt{'html_posttotal'}}()
+ : $opt{'html_posttotal'}
+ )
+ : ''
+ %>
+ <BR>
+
+% if ( $opt{'count_addl'} ) {
+% my $n=0; foreach my $count ( @{$opt{'count_addl'}} ) {
+
+ <% sprintf( $count, $count_arrayref->[++$n] ) %><BR>
+
+% }
+% }
+ </FORM>
+
+ </TD>
+
+% unless ( $opt{'disable_download'} ) {
+
+ <TD ALIGN="right">
+% $cgi->param('_type', "$xlsname.xls" );
+
+ Download full results<BR>
+ as <A HREF="<% $cgi->self_url %>">Excel spreadsheet</A><BR>
+% $cgi->param('_type', 'csv');
+
+ as <A HREF="<% $cgi->self_url %>">CSV file</A>
+ </TD>
+% $cgi->param('_type', "html" );
+% }
+
+ </TR>
+ <TR>
+ <TD COLSPAN=2>
+
+ <% my $pager = include ( '/elements/pager.html',
+ 'offset' => $offset,
+ 'num_rows' => scalar(@$rows),
+ 'total' => $total,
+ 'maxrecords' => $maxrecords,
+ ) %>
+
+ <% defined($opt{'html_form'})
+ ? ( ref($opt{'html_form'})
+ ? &{$opt{'html_form'}}()
+ : $opt{'html_form'}
+ )
+ : ''
+ %>
+
+ <% include('/elements/table-grid.html') %>
+
+ <TR>
+%
+% foreach my $header ( @$header ) {
+
+ <TH CLASS="grid" BGCOLOR="#cccccc"><% $header %></TH>
+% }
+
+ </TR>
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor;
+% foreach my $row ( @$rows ) {
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+
+ <TR>
+% if ( $opt{'fields'} ) {
+%
+% my $links = $opt{'links'} ? [ @{$opt{'links'}} ] : '';
+% my $aligns = $opt{'align'} ? [ @{$opt{'align'}} ] : '';
+% my $colors = $opt{'color'} ? [ @{$opt{'color'}} ] : [];
+% my $sizes = $opt{'size'} ? [ @{$opt{'size'}} ] : [];
+% my $styles = $opt{'style'} ? [ @{$opt{'style'}} ] : [];
+%
+% foreach my $field (
+%
+% map {
+% if ( ref($_) eq 'ARRAY' ) {
+%
+% my $tableref = $_;
+%
+% '<TABLE CLASS="inv" CELLSPACING=0 CELLPADDING=0>'.
+%
+% join('', map {
+%
+% my $rowref = $_;
+%
+% '<tr>'.
+%
+% join('', map {
+%
+% my $element = $_;
+%
+% '<TD'.
+% ( $element->{'align'}
+% ? ' ALIGN="'. $element->{'align'}. '"'
+% : ''
+% ). '>'.
+% ( $element->{'link'}
+% ? '<A HREF="'. $element->{'link'}.'">'
+% : ''
+% ).
+% $element->{'data'}.
+% ( $element->{'link'}
+% ? '</A>'
+% : ''
+% ).
+% '</td>';
+%
+% } @$rowref ).
+%
+% '</tr>';
+% } @$tableref ).
+%
+% '</table>';
+%
+% } else {
+% $_;
+% }
+% }
+%
+% map {
+% if ( ref($_) eq 'CODE' ) {
+% &{$_}($row);
+% } else {
+% $row->$_();
+% }
+% }
+% @{$opt{'fields'}}
+%
+% ) {
+%
+% my $class = ( $field =~ /^<TABLE/i ) ? 'inv' : 'grid';
+%
+% 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">);
+% }
+% }
+%
+% my $font = '';
+% my $color = shift @$colors;
+% $color = &{$color}($row) if ref($color) eq 'CODE';
+% my $size = shift @$sizes;
+% $size = &{$size}($row) if ref($size) eq 'CODE';
+% if ( $color || $size ) {
+% $font = '<FONT '.
+% ( $color ? "COLOR=#$color " : '' ).
+% ( $size ? qq(SIZE="$size" ) : '' ).
+% '>';
+% }
+%
+% my($s, $es) = ( '', '' );
+% my $style = shift @$styles;
+% $style = &{$style}($row) if ref($style) eq 'CODE';
+% if ( $style ) {
+% $s = join( '', map "<$_>", split('', $style) );
+% $es = join( '', map "</$_>", split('', $style) );
+% }
+%
+%
+
+ <TD CLASS="<% $class %>" BGCOLOR="<% $bgcolor %>"<% $align %>><% $font %><% $a %><% $s %><% $field %><% $es %><% $a ? '</A>' : '' %><% $font ? '</FONT>' : '' %></TD>
+% }
+% } else {
+% foreach ( @$row ) {
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $_ %></TD>
+% }
+% }
+
+ </TR>
+% }
+% if ( $opt{'footer'} ) {
+
+ <TR>
+% foreach my $footer ( @{ $opt{'footer'} } ) {
+
+ <TD CLASS="grid" BGCOLOR="#dddddd" STYLE="border-top: dashed 1px black;"><i><% $footer %></i></TH>
+% }
+
+ </TR>
+% }
+
+
+ </TABLE>
+ <% $pager %>
+
+ </TD>
+ </TR>
+ </TABLE>
+% }
+
+ <% defined($opt{'html_foot'})
+ ? ( ref($opt{'html_foot'})
+ ? &{$opt{'html_foot'}}()
+ : $opt{'html_foot'}
+ )
+ : ''
+ %>
+ <% include( '/elements/footer.html' ) %>
+% }
+% }
diff --git a/httemplate/search/inventory_item.html b/httemplate/search/inventory_item.html
new file mode 100644
index 000000000..1e7bdd91c
--- /dev/null
+++ b/httemplate/search/inventory_item.html
@@ -0,0 +1,125 @@
+<% include( 'elements/search.html',
+ 'title' => $title,
+
+ #less lame to use Lingua:: something to pluralize
+ 'name' => $inventory_class->classname. 's',
+
+ 'query' => {
+ 'table' => 'inventory_item',
+ 'hashref' => { 'classnum' => $classnum },
+ 'select' => join(', ',
+ 'inventory_item.*',
+ 'cust_main.custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'extra_sql' => $extra_sql,
+ 'addl_from' => $addl_from,
+ },
+
+ 'count_query' => $count_query,
+
+ 'header' => [
+ '#',
+ $inventory_class->classname,
+ 'Service',
+ FS::UI::Web::cust_header(),
+ ],
+
+ 'fields' => [
+ 'itemnum',
+ 'item',
+ #'svcnum', #XXX proper full service customer link ala svc_acct
+ # "unallocated" ? "available" ?
+ sub {
+ #this could be way more efficient with a mixin
+ # like cust_main_Mixin that let us all all the methods
+ # on data we already have...
+ my $inventory_item = shift;
+ my $cust_svc = $inventory_item->cust_svc;
+ if ( $cust_svc ) {
+ my($label, $value) = $cust_svc->label;
+ "$label: $value";
+ } else {
+ '(available)';
+ }
+ },
+
+ \&FS::UI::Web::cust_fields,
+
+ ],
+ 'align' => 'rll'.FS::UI::Web::cust_aligns(),
+ 'links' => [
+ '',
+ '',
+ $link,
+ ( map { $_ ne 'Cust. Status' ? $link_cust : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'color' => [
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
+
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $classnum = $cgi->param('classnum');
+$classnum =~ /^(\d+)$/ or eidiot "illegal classnum $classnum";
+$classnum = $1;
+
+my $inventory_class = qsearchs( {
+ 'table' => 'inventory_class',
+ 'hashref' => { 'classnum' => $classnum },
+} );
+
+my $title = $inventory_class->classname. ' Inventory';
+
+#little false laziness with SQL fragments in inventory_class.pm
+my $extra_sql = '';
+if ( $cgi->param('avail') ) {
+ $extra_sql = 'AND ( svcnum IS NULL OR svcnum = 0 )';
+ $title .= ' - Available';
+} elsif ( $cgi->param('used') ) {
+ $extra_sql = 'AND svcnum IS NOT NULL AND svcnum > 0';
+ $title .= ' - In use';
+}
+
+my $count_query =
+ "SELECT COUNT(*) FROM inventory_item WHERE classnum = $classnum $extra_sql";
+
+my $link = sub {
+ my $inventory_item = shift;
+ if ( $inventory_item->svcnum ) {
+ [ "${p}view/svc_acct.cgi?", 'svcnum' ];
+ } else {
+ '';
+ }
+};
+my $link_cust = sub {
+ my $inventory_item = shift;
+ if ( $inventory_item->custnum ) {
+ [ "${p}view/cust_main.cgi?", 'custnum' ];
+ } else {
+ '';
+ }
+};
+
+my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN part_svc USING ( svcpart ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ';
+
+</%init>
diff --git a/httemplate/search/pay_batch.cgi b/httemplate/search/pay_batch.cgi
new file mode 100755
index 000000000..cb2171799
--- /dev/null
+++ b/httemplate/search/pay_batch.cgi
@@ -0,0 +1,130 @@
+<% include( 'elements/search.html',
+ 'title' => 'Payment Batches',
+ 'name_singular' => 'batch',
+ 'query' => { 'table' => 'pay_batch',
+ 'hashref' => $hashref,
+ 'extra_sql' => "$extra_sql ORDER BY batchnum DESC",
+ },
+ 'count_query' => "$count_query $extra_sql",
+ 'header' => [ 'Batch',
+ 'Type',
+ 'First Download',
+ 'Last Upload',
+ 'Item Count',
+ 'Amount',
+ 'Status',
+ ],
+ 'align' => 'rcllrrc',
+ 'fields' => [ 'batchnum',
+ sub {
+ FS::payby->shortname(shift->payby);
+ },
+ sub {
+ my $self = shift;
+ my $_date = $self->download;
+ if ( $_date ) {
+ time2str("%a %b %e %T %Y", $_date);
+ } elsif ( $self->status eq 'O' ) {
+ 'Download batch';
+ } else {
+ '';
+ }
+ },
+ sub {
+ my $self = shift;
+ my $_date = $self->upload;
+ if ( $_date ) {
+ time2str("%a %b %e %T %Y", $_date);
+ } elsif ( $self->status eq 'I' ) {
+ 'Upload results';
+ } else {
+ '';
+ }
+ },
+ sub {
+ my $st = "SELECT COUNT(*) from cust_pay_batch WHERE batchnum=" . shift->batchnum;
+ my $sth = dbh->prepare($st)
+ or die dbh->errstr. "doing $st";
+ $sth->execute
+ or die "Error executing \"$st\": ". $sth->errstr;
+ $sth->fetchrow_arrayref->[0];
+ },
+ sub {
+ my $st = "SELECT SUM(amount) from cust_pay_batch WHERE batchnum=" . shift->batchnum;
+ my $sth = dbh->prepare($st)
+ or die dbh->errstr. "doing $st";
+ $sth->execute
+ or die "Error executing \"$st\": ". $sth->errstr;
+ $sth->fetchrow_arrayref->[0];
+ },
+ sub {
+ $statusmap{shift->status};
+ },
+ ],
+ 'links' => [
+ $link,
+ '',
+ sub { shift->status eq 'O' ? $link : '' },
+ sub { shift->status eq 'I' ? $link : '' },
+ ],
+ 'size' => [
+ '',
+ '',
+ sub { shift->status eq 'O' ? "+1" : '' },
+ sub { shift->status eq 'I' ? "+1" : '' },
+ ],
+ 'style' => [
+ '',
+ '',
+ sub { shift->status eq 'O' ? "b" : '' },
+ sub { shift->status eq 'I' ? "b" : '' },
+ ],
+ )
+
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports')
+ || $FS::CurrentUser::CurrentUser->access_right('Process batches');
+
+my %statusmap = ('I'=>'In Transit', 'O'=>'Open', 'R'=>'Resolved');
+my $hashref = {};
+my $count_query = 'SELECT COUNT(*) FROM pay_batch';
+
+my($begin, $end) = ( '', '' );
+
+my @where;
+if ( $cgi->param('beginning')
+ && $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+ $begin = str2time($1);
+ push @where, "download >= $begin";
+}
+if ( $cgi->param('ending')
+ && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+ $end = str2time($1) + 86399;
+ push @where, "download < $end";
+}
+
+my @status;
+if ( $cgi->param('open') ) {
+ push @status, "O";
+}
+
+if ( $cgi->param('intransit') ) {
+ push @status, "I";
+}
+
+if ( $cgi->param('resolved') ) {
+ push @status, "R";
+}
+
+push @where,
+ scalar(@status) ? q!(status='! . join(q!' OR status='!, @status) . q!')!
+ : q!status='X'!; # kludgy, X is unused at present
+
+my $extra_sql = scalar(@where) ? 'WHERE ' . join(' AND ', @where) : '';
+
+my $link = [ "${p}search/cust_pay_batch.cgi?batchnum=", 'batchnum' ];
+
+</%init>
diff --git a/httemplate/search/pay_batch.html b/httemplate/search/pay_batch.html
new file mode 100644
index 000000000..5907169d8
--- /dev/null
+++ b/httemplate/search/pay_batch.html
@@ -0,0 +1,33 @@
+<% include('/elements/header.html', 'Batch criteria' ) %>
+
+<FORM ACTION="pay_batch.cgi" METHOD="GET">
+<INPUT TYPE="hidden" NAME="magic" VALUE="_date">
+
+<TABLE>
+ <% include( '/elements/tr-input-beginning_ending.html' ) %>
+ <TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="open" VALUE="1" CHECKED></TD>
+ <TD>Show open batches</TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="intransit" VALUE="1" CHECKED></TD>
+ <TD>Show in-transit batches</TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="resolved" VALUE="1" CHECKED></TD>
+ <TD>Show resolved batches</TD>
+ </TR>
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Get Batches">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/search/prepay_credit.html b/httemplate/search/prepay_credit.html
new file mode 100644
index 000000000..ab6490d33
--- /dev/null
+++ b/httemplate/search/prepay_credit.html
@@ -0,0 +1,68 @@
+<% include( 'elements/search.html',
+ 'title' => 'Unused Prepaid Cards'.
+ ($agent ? ' for '. $agent->agent : ''),
+ 'menubar' => [
+ 'Main menu' => $p,
+ 'Generate cards' => $p.'edit/prepay_credit.cgi',
+ ],
+ 'name' => 'prepaid cards',
+ 'query' => { 'table' => 'prepay_credit',
+ 'hashref' => $hashref,
+ },
+ 'count_query' => $count_query,
+ #'redirect' => $link,
+ 'header' => [ '#', qw(Amount Time Upload Download Total Agent) ],
+ 'fields' => [
+ 'identifier',
+ sub { sprintf('$%.2f', shift->amount ) },
+ sub { my $c = shift;
+ $c->seconds ? duration_exact($c->seconds) : ''
+ },
+ sub { my $c = shift;
+ $c->upbytes
+ ? FS::UI::Web::bytecount_unexact($c->upbytes)
+ : ''
+ },
+ sub { my $c = shift;
+ $c->downbytes
+ ? FS::UI::Web::bytecount_unexact($c->downbytes)
+ : ''
+ },
+ sub { my $c = shift;
+ $c->totalbytes
+ ? FS::UI::Web::bytecount_unexact($c->totalbytes)
+ : ''
+ },
+ sub { my $agent = shift->agent;
+ $agent ? $agent->agent : '';
+ },
+ ],
+ 'links' => [
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ sub { my $agent = shift->agent;
+ $agent ? [ "${p}view/agent.cgi?", 'agentnum' ] : '';
+ },
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $agent = '';
+my $hashref = {};
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+$hashref->{agentnum} = $1;
+$agent = qsearchs('agent', { 'agentnum' => $1 } );
+}
+
+my $count_query = 'SELECT COUNT(*) FROM prepay_credit';
+$count_query .= ' WHERE agentnum = '. $agent->agentnum if $agent;
+
+</%init>
diff --git a/httemplate/search/queue.html b/httemplate/search/queue.html
new file mode 100644
index 000000000..c343014cc
--- /dev/null
+++ b/httemplate/search/queue.html
@@ -0,0 +1,139 @@
+<% include( 'elements/search.html',
+ 'title' => 'Job Queue',
+ 'menubar' => [ 'Main menu' => $p, ],
+ 'name' => 'jobs',
+ 'html_form' => qq!<FORM NAME="jobForm" ACTION="$p/misc/queue.cgi" METHOD="POST">!,
+ 'query' => { 'table' => 'queue',
+ 'hashref' => $hashref,
+ 'extra_sql' => 'ORDER BY jobnum',
+ },
+ 'count_query' => $count_query,
+ 'header' => [ '#',
+ 'Job',
+ 'Args',
+ 'Date',
+ 'Status',
+ 'Account', # unless $hashref->{'svcnum'}
+ '', # checkbox column
+ ],
+ 'fields' => [
+ 'jobnum',
+ 'job',
+ sub {
+ my $queue = shift;
+ if ( $dangerous
+ || $queue->job !~ /^FS::part_export::/
+ || !$noactions
+ )
+ {
+ encode_entities( join(' ', $queue->args) );
+ } else {
+ '';
+ }
+ },
+ sub {
+ time2str( "%a %b %e %T %Y", shift->_date );
+ },
+ sub {
+ my $queue = shift;
+ my $jobnum = $queue->jobnum;
+ my $status = $queue->status;
+ $status .= ': '. $queue->statustext
+ if $queue->statustext;
+ my @queue_depend = $queue->queue_depend;
+ $status .= ' (waiting for '.
+ join(', ', map { $_->depend_jobnum }
+ @queue_depend
+ ).
+ ')'
+ if @queue_depend;
+ my $changable = $dangerous
+ || ( ! $noactions
+ && $status =~ /^failed/
+ || $status =~ /^locked/
+ );
+ if ( $changable ) {
+ $status .=
+ qq! (&nbsp;<A HREF="$p/misc/queue.cgi?jobnum=$jobnum&action=new">retry</A>&nbsp;|!.
+ qq!&nbsp;<A HREF="$p/misc/queue.cgi?jobnum=$jobnum&action=del">remove</A>&nbsp;)!;
+ }
+ $status;
+ },
+ sub {
+ my $queue = shift;
+ # return '' if $hashref->{'svcnum'}
+ my $cust_svc = $queue->cust_svc;
+ my $account;
+ if ( $cust_svc ) {
+ my $table = $cust_svc->part_svc->svcdb;
+ my $label = ( $cust_svc->label )[1];
+ qq!<A HREF="../view/$table.cgi?!. $queue->svcnum.
+ qq!">$label</A>!;
+ } else {
+ '';
+ }
+ },
+ sub {
+ my $queue = shift;
+ my $jobnum = $queue->jobnum;
+ my $status = $queue->status;
+ my $changable = $dangerous
+ || ( ! $noactions
+ && $status eq 'failed'
+ || $status eq 'locked'
+ );
+ if ( $changable ) {
+ $areboxes = 1;
+ qq!<INPUT NAME="jobnum$jobnum" TYPE="checkbox" VALUE="1">!;
+ } else {
+ '';
+ }
+ },
+ ],
+ #'links' => [
+ # '',
+ # '',
+ # '',
+ # '',
+ # '',
+ # '', #$acct_link,
+ # '',
+ # ],
+ 'html_foot' => sub {
+ if ( $areboxes ) {
+ '<BR><INPUT TYPE="button" VALUE="select all" onClick="setAll(true)">'.
+ '<INPUT TYPE="button" VALUE="unselect all" onClick="setAll(false)">'.
+ '<BR><INPUT TYPE="submit" NAME="action" VALUE="retry selected">'.
+ '<INPUT TYPE="submit" NAME="action" VALUE="remove selected"><BR>'.
+ '<SCRIPT TYPE="text/javascript">'.
+ ' function setAll(setTo) { '.
+ ' theForm = document.jobForm;'.
+ ' for (i=0,n=theForm.elements.length;i<n;i++)'.
+ ' if (theForm.elements[i].name.indexOf("jobnum") != -1)'.
+ ' theForm.elements[i].checked = setTo;'.
+ ' }'.
+ '</SCRIPT>';
+ } else {
+ '';
+ }
+ },
+ )
+
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Job queue');
+
+my $hashref = {};
+
+my $conf = new FS::Conf;
+my $dangerous = $conf->exists('queue_dangerous_controls');
+
+my $noactions = 0;
+
+my $count_query = 'SELECT COUNT(*) FROM queue'; # + $hashref
+
+my $areboxes = 0;
+
+</%init>
diff --git a/httemplate/search/reg_code.html b/httemplate/search/reg_code.html
new file mode 100644
index 000000000..87e0fcdd5
--- /dev/null
+++ b/httemplate/search/reg_code.html
@@ -0,0 +1,40 @@
+<% include( 'elements/search.html',
+ 'title' => 'Unused Registration Codes for '.
+ $agent->agent,
+ 'name' => 'registration codes',
+ 'query' => { 'table' => 'reg_code',
+ 'hashref' => { 'agentnum' => $agentnum, },
+ },
+ 'count_query' => $count_query,
+ #'redirect' => $link,
+ 'header' => [ qw(Code Packages) ],
+ 'fields' => [
+ 'code',
+ sub {
+ map {
+ qq!<A HREF="${p}edit/part_pkg.cgi?!. $_->pkgpart. '">'.
+ $_->pkg. ' - '. $_->comment.
+ '</A><BR>'
+ } $_[0]->part_pkg
+ },
+ ],
+ 'links' => [
+ '',
+ #$plink,
+ '',
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $agentnum = $cgi->param('agentnum');
+$agentnum =~ /^(\d+)$/ or eidiot "illegal agentnum $agentnum";
+$agentnum = $1;
+my $agent = qsearchs('agent', { 'agentnum' => $agentnum } );
+
+my $count_query = "SELECT COUNT(*) FROM reg_code WHERE agentnum = $agentnum";
+
+<%init>
diff --git a/httemplate/search/report_cdr.html b/httemplate/search/report_cdr.html
new file mode 100644
index 000000000..819ba2195
--- /dev/null
+++ b/httemplate/search/report_cdr.html
@@ -0,0 +1,17 @@
+<% include('/elements/header.html', 'Call Detail Record Search' ) %>
+
+<FORM ACTION="cdr.html" METHOD="GET">
+Status: <SELECT NAME="freesidestatus">
+ <OPTION VALUE="">(all)
+ <OPTION VALUE="NULL">unprocessed
+ <OPTION VALUE="done">processed
+</SELECT><BR>
+<INPUT TYPE="submit" VALUE="Search Call Detail Records">
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List rating data');
+
+</%init>
diff --git a/httemplate/search/report_cust_bill.html b/httemplate/search/report_cust_bill.html
new file mode 100644
index 000000000..4fa09f96c
--- /dev/null
+++ b/httemplate/search/report_cust_bill.html
@@ -0,0 +1,34 @@
+<% include('/elements/header.html', 'Invoice report criteria' ) %>
+
+<FORM ACTION="cust_bill.html" METHOD="GET">
+<INPUT TYPE="hidden" NAME="magic" VALUE="_date">
+
+<TABLE>
+ <% include( '/elements/tr-select-agent.html',
+ $cgi->param('agentnum'),
+ 'label' => 'Invoices for agent: ',
+ )
+ %>
+ <% include( '/elements/tr-input-beginning_ending.html' ) %>
+ <TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="open" VALUE="1" CHECKED></TD>
+ <TD>Show only open invoices</TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="newest_percust" VALUE="1"></TD>
+ <TD>Show only the single most recent invoice per-customer</TD>
+ </TR>
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Get Report">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List invoices');
+
+</%init>
diff --git a/httemplate/search/report_cust_credit.html b/httemplate/search/report_cust_credit.html
new file mode 100644
index 000000000..993209763
--- /dev/null
+++ b/httemplate/search/report_cust_credit.html
@@ -0,0 +1,53 @@
+<% include('/elements/header.html', 'Credit report' ) %>
+
+<FORM ACTION="cust_credit.html" METHOD="GET">
+<INPUT TYPE="hidden" NAME="magic" VALUE="_date">
+
+<TABLE>
+ <TR>
+ <TD ALIGN="right">Credits by employee: </TD>
+
+ <TD>
+ <SELECT NAME="otaker">
+ <OPTION VALUE="">all</OPTION>
+% foreach my $otaker ( @otakers ) {
+ <OPTION VALUE="<% $otaker %>"><% $otaker %></OPTION>
+% }
+ </SELECT>
+ </TD>
+ </TR>
+
+ <% include( '/elements/tr-select-agent.html',
+ $cgi->param('agentnum'),
+ 'label' => 'for agent: ',
+ )
+ %>
+
+ <% include( '/elements/tr-input-beginning_ending.html' ) %>
+
+ <% include( '/elements/tr-input-lessthan_greaterthan.html',
+ 'label' => 'Amount',
+ 'field' => 'amount',
+ )
+ %>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Get Report">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+my $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};
+
+</%init>
diff --git a/httemplate/search/report_cust_main-zip.html b/httemplate/search/report_cust_main-zip.html
new file mode 100644
index 000000000..1cd07ef76
--- /dev/null
+++ b/httemplate/search/report_cust_main-zip.html
@@ -0,0 +1,52 @@
+<% include('/elements/header.html', 'Zip code report') %>
+
+ <FORM ACTION="cust_main-zip.html" METHOD="GET">
+
+ <TABLE>
+
+ <TR>
+ <TD ALIGN="right">Billing or service zip</TD>
+ <TD>
+ <SELECT NAME="column">
+ <OPTION VALUE="zip">Billing zip
+ <OPTION VALUE="ship_zip">Service zip
+ </SELECT>
+ </TD>
+ </TR>
+
+ <TR>
+ <TD ALIGN="right">Ignore +4 for US zip codes</TD>
+ <TD><INPUT TYPE="checkbox" NAME="ignore_plus4" VALUE="yes" CHECKED> </TD>
+ </TR>
+
+ <TR>
+ <TD ALIGN="right">Show customers with status:</TD>
+ <TD>
+ <SELECT NAME="status">
+ <OPTION VALUE="">all
+ <OPTION VALUE="prospect">prospect (no packages ever)
+ <OPTION SELECTED VALUE="uncancel">all except cancelled
+ <OPTION VALUE="active">active recurring packages
+ <OPTION VALUE="susp">suspended
+ <OPTION VALUE="cancel">cancelled
+ </SELECT>
+ </TD>
+ </TR>
+
+ <% include( '/elements/tr-select-agent.html',
+ $cgi->param('agentnum'),
+ 'label' => 'for agent: ',
+ )
+ %>
+
+ </TABLE>
+ <BR><INPUT TYPE="submit" VALUE="Get Report">
+ </FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List zip codes');
+
+</%init>
diff --git a/httemplate/search/report_cust_pay.html b/httemplate/search/report_cust_pay.html
new file mode 100644
index 000000000..0327e042e
--- /dev/null
+++ b/httemplate/search/report_cust_pay.html
@@ -0,0 +1,78 @@
+<% include('/elements/header.html', 'Payment report' ) %>
+
+<FORM ACTION="cust_pay.cgi" METHOD="GET">
+<INPUT TYPE="hidden" NAME="magic" VALUE="_date">
+
+<TABLE>
+
+ <TR>
+ <TD ALIGN="right">Payments of type: </TD>
+ <TD>
+ <SELECT NAME="payby" onChange="payby_changed(this)">
+ <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="CARD-Maestro">credit card (Maestro/Switch/Solo)</OPTION>
+ <OPTION VALUE="CHEK">electronic check / ACH</OPTION>
+ <OPTION VALUE="BILL">check</OPTION>
+ <OPTION VALUE="PREP">prepaid card</OPTION>
+ <OPTION VALUE="CASH">cash</OPTION>
+ <OPTION VALUE="WEST">Western Union</OPTION>
+ <OPTION VALUE="MCRD">manual credit card</OPTION>
+ </SELECT>
+ </TD>
+ </TR>
+
+ <SCRIPT TYPE="text/javascript">
+
+ function payby_changed(what) {
+ if ( what.options[what.selectedIndex].value == 'BILL' ) {
+ document.getElementById('checkno_caption').style.color = '#000000';
+ what.form.payinfo.disabled = false;
+ what.form.payinfo.style.backgroundColor = '#ffffff';
+ } else {
+ document.getElementById('checkno_caption').style.color = '#bbbbbb';
+ what.form.payinfo.disabled = true;
+ what.form.payinfo.style.backgroundColor = '#dddddd';
+ }
+ }
+
+ </SCRIPT>
+
+ <TR>
+ <TD ALIGN="right"><FONT ID="checkno_caption" COLOR="#bbbbbb">Check #: </FONT></TD>
+ <TD>
+ <INPUT TYPE="text" NAME="payinfo" DISABLED STYLE="background-color: #dddddd">
+ </TD>
+ </TR>
+
+ <% include( '/elements/tr-select-agent.html',
+ $cgi->param('agentnum'),
+ 'label' => 'for agent: ',
+ )
+ %>
+
+ <% include( '/elements/tr-input-beginning_ending.html' ) %>
+
+ <% include( '/elements/tr-input-lessthan_greaterthan.html',
+ 'label' => 'Amount',
+ 'field' => 'paid',
+ )
+ %>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Get Report">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/search/report_cust_pay_batch.html b/httemplate/search/report_cust_pay_batch.html
new file mode 100644
index 000000000..f57a9557e
--- /dev/null
+++ b/httemplate/search/report_cust_pay_batch.html
@@ -0,0 +1,43 @@
+<% include('/elements/header.html', 'Batch payment report' ) %>
+
+<FORM ACTION="cust_pay_batch.cgi" METHOD="GET">
+
+<TABLE>
+
+ <TR>
+ <TD ALIGN="right">Payments of type: </TD>
+ <TD>
+ <SELECT NAME="payby">
+ <OPTION VALUE="">all</OPTION>
+ <OPTION VALUE="CARD">credit card</OPTION>
+ <OPTION VALUE="CHEK">electronic check / ACH</OPTION>
+ </SELECT>
+ </TD>
+ </TR>
+
+ <% include( '/elements/tr-select-agent.html',
+ $cgi->param('agentnum'),
+ 'label' => 'for agent: ',
+ )
+ %>
+
+ <% include( '/elements/tr-input-beginning_ending.html' ) %>
+
+ <TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="dcln" VALUE="1" CHECKED></TD>
+ <TD>Include approved items</TD>
+ </TR>
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Get Report">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/search/report_cust_pkg.html b/httemplate/search/report_cust_pkg.html
new file mode 100755
index 000000000..5b2efa2ca
--- /dev/null
+++ b/httemplate/search/report_cust_pkg.html
@@ -0,0 +1,144 @@
+<% include('/elements/header.html', 'Package Report' ) %>
+
+<FORM ACTION="cust_pkg.cgi" METHOD="GET">
+<INPUT TYPE="hidden" NAME="magic" VALUE="bill">
+
+ <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
+
+ <TR>
+ <TH BGCOLOR="#e8e8e8" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Search options</FONT></TH>
+ </TR>
+ <% include( '/elements/tr-select-agent.html',
+ $cgi->param('agentnum'),
+ )
+ %>
+
+ <% include( '/elements/tr-select-cust_pkg-status.html', '',
+ 'onchange' => 'status_changed(this);',
+ )
+ %>
+
+ <SCRIPT TYPE="text/javascript">
+
+ function status_changed(what) {
+
+% foreach my $status ( '', FS::cust_pkg->statuses() ) {
+
+ if ( what.options[what.selectedIndex].value == '<% $status %>' ) {
+
+% foreach my $field (qw( setup last_bill bill susp expire cancel )) {
+% if ( $disable{$status}->{$field} ) {
+
+ what.form.<% $field %>_beginning_text.disabled = true;
+ what.form.<% $field %>_ending_text.disabled = true;
+ what.form.<% $field %>_beginning_text.style.backgroundColor = '#dddddd';
+ what.form.<% $field %>_ending_text.style.backgroundColor = '#dddddd';
+
+ what.form.<% $field %>_beginning_button.style.display = 'none';
+ what.form.<% $field %>_ending_button.style.display = 'none';
+ what.form.<% $field %>_beginning_disabled.style.display = '';
+ what.form.<% $field %>_ending_disabled.style.display = '';
+
+% } else {
+
+ what.form.<% $field %>_beginning_text.disabled = false;
+ what.form.<% $field %>_ending_text.disabled = false;
+ what.form.<% $field %>_beginning_text.style.backgroundColor = '#ffffff';
+ what.form.<% $field %>_ending_text.style.backgroundColor = '#ffffff';
+
+ what.form.<% $field %>_beginning_button.style.display = '';
+ what.form.<% $field %>_ending_button.style.display = '';
+ what.form.<% $field %>_beginning_disabled.style.display = 'none';
+ what.form.<% $field %>_ending_disabled.style.display = 'none';
+
+% }
+% }
+
+ }
+
+% }
+
+ }
+
+ </SCRIPT>
+
+ <% include( '/elements/tr-select-pkg_class.html', '',
+ 'pre_options' => [ '0' => 'all' ],
+ 'empty_label' => '(empty class)',
+ )
+ %>
+
+% foreach my $field (qw( setup last_bill bill susp expire cancel )) {
+
+ <TR>
+ <TD ALIGN="right" VALIGN="center"><% $label{$field} %></TD>
+ <TD>
+ <TABLE>
+ <% include( '/elements/tr-input-beginning_ending.html',
+ prefix => $field,
+ layout => 'horiz',
+ )
+ %>
+ </TABLE>
+ </TD>
+ </TR>
+
+% }
+
+ <% include( '/elements/tr-selectmultiple-part_pkg.html' ) %>
+
+ <TR>
+ <TH BGCOLOR="#e8e8e8" COLSPAN=2>&nbsp;</TH>
+ </TR>
+
+ <TR>
+ <TH BGCOLOR="#e8e8e8" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Display options</FONT></TH>
+ </TR>
+ <% include( '/elements/tr-select-cust-fields.html' ) %>
+
+ </TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Get Report">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List packages');
+
+</%init>
+<%once>
+
+my %label = (
+ 'setup' => 'Setup',
+ 'last_bill' => 'Last bill',
+ 'bill' => 'Next bill',
+ 'susp' => 'Suspended',
+ 'expire' => 'Expires',
+ 'cancel' => 'Cancelled',
+);
+
+#false laziness w/cust_pkg.cgi
+my %disable = (
+ 'all' => {},
+ 'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, },
+ 'active' => { 'susp'=>1, 'cancel'=>1 },
+ 'suspended' => { 'cancel' => 1 },
+ 'cancelled' => {},
+ '' => {},
+);
+
+#hmm?
+my %checkbox = (
+ 'setup' => 0,
+ 'last_bill' => 0,
+ 'bill' => 0,
+ 'susp' => 1,
+ 'expire' => 1,
+ 'cancel' => 1,
+);
+
+</%once>
diff --git a/httemplate/search/report_prepaid_income.cgi b/httemplate/search/report_prepaid_income.cgi
new file mode 100644
index 000000000..fd9b01ec1
--- /dev/null
+++ b/httemplate/search/report_prepaid_income.cgi
@@ -0,0 +1,87 @@
+<% include("/elements/header.html", '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>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+#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);
+
+</%init>
diff --git a/httemplate/search/report_prepaid_income.html b/httemplate/search/report_prepaid_income.html
new file mode 100644
index 000000000..81adb64ad
--- /dev/null
+++ b/httemplate/search/report_prepaid_income.html
@@ -0,0 +1,43 @@
+<% include('/elements/header.html', 'Prepaid Income (Unearned Revenue) Report',
+ '',
+ '',
+ '<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 ACTION="report_prepaid_income.cgi" METHOD="GET">
+ <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">
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/search/report_receivables.cgi b/httemplate/search/report_receivables.cgi
new file mode 100755
index 000000000..6e5893870
--- /dev/null
+++ b/httemplate/search/report_receivables.cgi
@@ -0,0 +1,229 @@
+<% include( 'elements/search.html',
+ 'title' => 'Accounts Receivable Aging Summary',
+ 'name' => 'customers',
+ 'query' => $sql_query,
+ 'count_query' => $count_sql,
+ 'header' => [
+ FS::UI::Web::cust_header(),
+ #'Status', # (me)',
+ #'Status', # (cust_main)',
+ '0-30',
+ '30-60',
+ '60-90',
+ '90+',
+ 'Total',
+ ],
+ 'footer' => [
+ 'Total',
+ ( map '',
+ ( 1 ..
+ scalar(FS::UI::Web::cust_header()-1)
+ )
+ ),
+ #'',
+ #'',
+ sprintf( $money_char.'%.2f',
+ $row->{'owed_0_30'} ),
+ sprintf( $money_char.'%.2f',
+ $row->{'owed_30_60'} ),
+ sprintf( $money_char.'%.2f',
+ $row->{'owed_60_90'} ),
+ sprintf( $money_char.'%.2f',
+ $row->{'owed_90_0'} ),
+ sprintf( '<b>'. $money_char.'%.2f'. '</b>',
+ $row->{'owed_0_0'} ),
+ ],
+ 'fields' => [
+ \&FS::UI::Web::cust_fields,
+ #sub { ( &{$status_statuscol}(shift) )[0] },
+ #sub { ucfirst(shift->status) },
+ sub { sprintf( $money_char.'%.2f',
+ shift->get('owed_0_30') ) },
+ sub { sprintf( $money_char.'%.2f',
+ shift->get('owed_30_60') ) },
+ sub { sprintf( $money_char.'%.2f',
+ shift->get('owed_60_90') ) },
+ sub { sprintf( $money_char.'%.2f',
+ shift->get('owed_90_0') ) },
+ sub { sprintf( $money_char.'%.2f',
+ shift->get('owed_0_0') ) },
+ ],
+ 'links' => [
+ ( map { $_ ne 'Cust. Status' ? $clink : '' }
+ FS::UI::Web::cust_header()
+ ),
+ #'',
+ #'',
+ '',
+ '',
+ '',
+ '',
+ '',
+ ],
+ #'align' => 'rlccrrrrr',
+ 'align' => FS::UI::Web::cust_aligns(). 'rrrrr',
+ #'size' => [ '', '', '-1', '-1', '', '', '', '', '', ],
+ #'style' => [ '', '', 'b', 'b', '', '', '', '', 'b', ],
+ 'size' => [ ( map '', FS::UI::Web::cust_header() ),
+ #'-1', '', '', '', '', '', ],
+ '', '', '', '', '', ],
+ 'style' => [ FS::UI::Web::cust_styles(),
+ #'b', '', '', '', '', 'b', ],
+ '', '', '', '', 'b', ],
+ 'color' => [
+ FS::UI::Web::cust_colors(),
+ #sub { ( &{$status_statuscol}(shift) )[1] },
+ #sub { shift->statuscolor; },
+ '',
+ '',
+ '',
+ '',
+ '',
+ ],
+
+ )
+%>
+<%once>
+
+sub owed {
+ my($start, $end, %opt) = @_;
+
+ my @where = ();
+
+ #handle start and end ranges
+
+ #24h * 60m * 60s
+ push @where, "cust_bill._date <= extract(epoch from now())-".
+ ($start * 86400)
+ if $start;
+
+ push @where, "cust_bill._date > extract(epoch from now()) - ".
+ ($end * 86400)
+ if $end;
+
+ #handle 'cust' option
+
+ push @where, "cust_main.custnum = cust_bill.custnum"
+ if $opt{'cust'};
+
+ #handle 'agentnum' option
+ my $join = '';
+ if ( $opt{'agentnum'} ) {
+ $join = 'LEFT JOIN cust_main USING ( custnum )';
+ push @where, "agentnum = '$opt{'agentnum'}'";
+ }
+
+ my $where = scalar(@where) ? 'WHERE '.join(' AND ', @where) : '';
+
+ my $as = $opt{'noas'} ? '' : "as owed_${start}_$end";
+
+ 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
+
+ "coalesce( ( select $charged from cust_bill $join $where ) ,0 ) $as";
+
+}
+
+</%once>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+my @ranges = (
+ [ 0, 30 ],
+ [ 30, 60 ],
+ [ 60, 90 ],
+ [ 90, 0 ],
+ [ 0, 0 ],
+);
+
+my $owed_cols = join(',', map owed( @$_, 'cust'=>1 ), @ranges );
+
+my $select_count_pkgs = FS::cust_main->select_count_pkgs_sql;
+
+my $active_sql = FS::cust_pkg->active_sql;
+my $inactive_sql = FS::cust_pkg->inactive_sql;
+my $suspended_sql = FS::cust_pkg->suspended_sql;
+my $cancelled_sql = FS::cust_pkg->cancelled_sql;
+
+my $packages_cols = <<END;
+ ( $select_count_pkgs ) AS num_pkgs_sql,
+ ( $select_count_pkgs AND $active_sql ) AS active_pkgs,
+ ( $select_count_pkgs AND $inactive_sql ) AS inactive_pkgs,
+ ( $select_count_pkgs AND $suspended_sql ) AS suspended_pkgs,
+ ( $select_count_pkgs AND $cancelled_sql ) AS cancelled_pkgs
+END
+
+my $days = 0;
+if ( $cgi->param('days') =~ /^\s*(\d+)\s*$/ ) {
+ $days = $1;
+}
+
+#my $where = "where ". owed(0, 0, 'cust'=>1, 'noas'=>1). " > 0";
+my $where = "where ". owed($days, 0, 'cust'=>1, 'noas'=>1). " > 0";
+
+my $agentnum = '';
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+ $where .= " AND agentnum = '$agentnum' ";
+}
+
+#here is the agent virtualization
+$where .= ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my $count_sql = "select count(*) from cust_main $where";
+
+my $sql_query = {
+ 'table' => 'cust_main',
+ 'hashref' => {},
+ 'select' => "*, $owed_cols, $packages_cols",
+ 'extra_sql' => "$where order by coalesce(lower(company), ''), lower(last)",
+};
+
+my $total_sql = "select ".
+ join(',', map owed( @$_, 'agentnum'=>$agentnum ), @ranges );
+
+my $total_sth = dbh->prepare($total_sql) or die dbh->errstr;
+$total_sth->execute or die "error executing $total_sql: ". $total_sth->errstr;
+my $row = $total_sth->fetchrow_hashref();
+
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
+
+my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
+
+my $status_statuscol = sub {
+ #conceptual false laziness with cust_main::status...
+ my $row = shift;
+
+ my $status = 'unknown';
+ if ( $row->num_pkgs_sql == 0 ) {
+ $status = 'prospect';
+ } elsif ( $row->active_pkgs > 0 ) {
+ $status = 'active';
+ } elsif ( $row->inactive_pkgs > 0 ) {
+ $status = 'inactive';
+ } elsif ( $row->suspended_pkgs > 0 ) {
+ $status = 'suspended';
+ } elsif ( $row->cancelled_pkgs > 0 ) {
+ $status = 'cancelled'
+ }
+
+ ( ucfirst($status), $FS::cust_main::statuscolor{$status} );
+};
+
+</%init>
diff --git a/httemplate/search/report_receivables.html b/httemplate/search/report_receivables.html
new file mode 100755
index 000000000..bb23f1f87
--- /dev/null
+++ b/httemplate/search/report_receivables.html
@@ -0,0 +1,26 @@
+<% include('/elements/header.html', 'Accounts Receivable Aging Summary' ) %>
+
+ <FORM ACTION="report_receivables.cgi" METHOD="GET">
+
+ <TABLE>
+
+ <% include( '/elements/tr-select-agent.html' ) %>
+
+ <TR>
+ <TD ALIGN="right">Over </TD>
+ <TD><INPUT NAME="days" TYPE="text" SIZE=4 MAXLENGTH=3> days</TD>
+ </TR>
+
+ </TABLE>
+
+ <BR><INPUT TYPE="submit" VALUE="Get Report">
+ </FORM>
+
+ </BODY>
+</HTML>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi
new file mode 100755
index 000000000..918383b67
--- /dev/null
+++ b/httemplate/search/report_tax.cgi
@@ -0,0 +1,539 @@
+<% include("/elements/header.html", "$agentname Sales Tax Report - ".
+ ( $beginning
+ ? time2str('%h %o %Y ', $beginning )
+ : ''
+ ).
+ 'through '.
+ ( $ending == 4294967295
+ ? 'now'
+ : time2str('%h %o %Y', $ending )
+ ),
+ menubar( 'Main Menu'=>$p, )
+ )
+%>
+
+<% include('/elements/table-grid.html') %>
+
+ <TR>
+ <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=9>Sales</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Rate</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Tax owed</TH>
+% unless ( $cgi->param('show_taxclasses') ) {
+
+ <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Tax invoiced</TH>
+% }
+
+ </TR>
+ <TR>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Total</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Non-taxable<BR><FONT SIZE=-1>(tax-exempt customer)</FONT></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Non-taxable<BR><FONT SIZE=-1>(tax-exempt package)</FONT></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Non-taxable<BR><FONT SIZE=-1>(monthly exemption)</FONT></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Taxable</TH>
+ </TR>
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor;
+%
+% foreach my $region ( @regions ) {
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+% my $link = '';
+% if ( $region->{'label'} ne 'Total' ) {
+% if ( $region->{'label'} eq $out ) {
+% $link = ';out=1';
+% } else {
+% $link = ';'. $region->{'url_param'};
+% }
+% }
+%
+%
+%
+%
+%
+
+
+ <TR>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $region->{'label'} %></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
+ <A HREF="<% $baselink. $link %>;nottax=1"><% $money_char %><% sprintf('%.2f', $region->{'total'} ) %></A>
+ </TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><FONT SIZE="+1"><B> - </B></FONT></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
+ <A HREF="<% $baselink. $link %>;nottax=1;cust_tax=Y"><% $money_char %><% sprintf('%.2f', $region->{'exempt_cust'} ) %></A>
+ </TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><FONT SIZE="+1"><B> - </B></FONT></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
+ <A HREF="<% $baselink. $link %>;nottax=1;pkg_tax=Y"><% $money_char %><% sprintf('%.2f', $region->{'exempt_pkg'} ) %></A>
+ </TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><FONT SIZE="+1"><B> - </B></FONT></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
+ <A HREF="<% $exemptlink. $link %>"><% $money_char %><% sprintf('%.2f', $region->{'exempt_monthly'} ) %></A>
+ </TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><FONT SIZE="+1"><B> = </B></FONT></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
+ <% $money_char %><% sprintf('%.2f', $region->{'taxable'} ) %></A>
+ </TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $region->{'label'} eq 'Total' ? '' : '<FONT FACE="sans-serif" SIZE="+1"><B> X </B></FONT>' %></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right"><% $region->{'rate'} %></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $region->{'label'} eq 'Total' ? '' : '<FONT FACE="sans-serif" SIZE="+1"><B> = </B></FONT>' %></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
+ <% $money_char %><% sprintf('%.2f', $region->{'owed'} ) %>
+ </TD>
+% unless ( $cgi->param('show_taxclasses') ) {
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
+ <A HREF="<% $baselink. $link %>;istax=1"><% $money_char %><% sprintf('%.2f', $region->{'tax'} ) %></A>
+ </TD>
+% }
+
+ </TR>
+% }
+
+
+</TABLE>
+% if ( $cgi->param('show_taxclasses') ) {
+
+
+ <BR>
+ <% include('/elements/table-grid.html') %>
+ <TR>
+ <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Tax invoiced</TH>
+ </TR>
+% #some false laziness w/above
+% $bgcolor1 = '#eeeeee';
+% $bgcolor2 = '#ffffff';
+% foreach my $region ( @base_regions ) {
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+% my $link = '';
+% #if ( $region->{'label'} ne 'Total' ) {
+% if ( $region->{'label'} eq $out ) {
+% $link = ';out=1';
+% } else {
+% $link = ';'. $region->{'url_param'};
+% }
+% #}
+%
+
+
+ <TR>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $region->{'label'} %></TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
+ <A HREF="<% $baselink. $link %>;istax=1"><% $money_char %><% sprintf('%.2f', $region->{'tax'} ) %></A>
+ </TD>
+ </TR>
+% }
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+
+
+ <TR>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">Total</TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
+ <A HREF="<% $baselink %>;istax=1"><% $money_char %><% sprintf('%.2f', $tax ) %></A>
+ </TD>
+ </TR>
+
+ </TABLE>
+% }
+
+
+</BODY>
+</HTML>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
+
+my $user = getotaker;
+
+my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+
+my $join_cust = "
+ JOIN cust_bill USING ( invnum )
+ LEFT JOIN cust_main USING ( custnum )
+";
+my $from_join_cust = "
+ FROM cust_bill_pkg
+ $join_cust
+";
+my $join_pkg = "
+ LEFT JOIN cust_pkg USING ( pkgnum )
+ LEFT JOIN part_pkg USING ( pkgpart )
+";
+
+my $where = "WHERE _date >= $beginning AND _date <= $ending ";
+my @base_param = qw( county county state state country );
+if ( $conf->exists('tax-ship_address') ) {
+
+ $where .= "
+ AND ( ( ( ship_last IS NULL OR ship_last = '' )
+ AND ( county = ? OR ? = '' )
+ AND ( state = ? OR ? = '' )
+ AND country = ?
+ )
+ OR ( ship_last IS NOT NULL AND ship_last != ''
+ AND ( ship_county = ? OR ? = '' )
+ AND ( ship_state = ? OR ? = '' )
+ AND ship_country = ?
+ )
+ )
+ ";
+ # AND payby != 'COMP'
+
+ push @base_param, @base_param;
+
+} else {
+
+ $where .= "
+ AND ( county = ? OR ? = '' )
+ AND ( state = ? OR ? = '' )
+ AND country = ?
+ ";
+ # AND payby != 'COMP'
+
+}
+
+my $agentname = '';
+if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ my $agent = qsearchs('agent', { 'agentnum' => $1 } );
+ die "agent not found" unless $agent;
+ $agentname = $agent->agent;
+ $where .= ' AND agentnum = '. $agent->agentnum;
+}
+
+my $gotcust = "
+ WHERE 0 < ( SELECT COUNT(*) FROM cust_main
+";
+if ( $conf->exists('tax-ship_address') ) {
+
+ $gotcust .= "
+ WHERE
+
+ ( cust_main_county.country = cust_main.country
+ OR cust_main_county.country = cust_main.ship_country
+ )
+
+ AND
+
+ (
+
+ ( ( ship_last IS NULL OR ship_last = '' )
+ AND ( cust_main_county.country = cust_main.country )
+ AND ( cust_main_county.state = cust_main.state
+ OR cust_main_county.state = ''
+ OR cust_main_county.state IS NULL )
+ AND ( cust_main_county.county = cust_main.county
+ OR cust_main_county.county = ''
+ OR cust_main_county.county IS NULL )
+ )
+
+ OR
+
+ ( ship_last IS NOT NULL AND ship_last != ''
+ AND ( cust_main_county.country = cust_main.ship_country )
+ AND ( cust_main_county.state = cust_main.ship_state
+ OR cust_main_county.state = ''
+ OR cust_main_county.state IS NULL )
+ AND ( cust_main_county.county = cust_main.ship_county
+ OR cust_main_county.county = ''
+ OR cust_main_county.county IS NULL )
+ )
+
+ )
+
+ LIMIT 1
+ )
+ ";
+
+} else {
+
+ $gotcust .= "
+ 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
+ )
+ ";
+
+}
+
+my($total, $tot_taxable, $owed, $tax) = ( 0, 0, 0, 0, 0 );
+my( $exempt_cust, $exempt_pkg, $exempt_monthly ) = ( 0, 0 );
+my $out = 'Out of taxable region(s)';
+my %regions = ();
+foreach my $r (qsearch('cust_main_county', {}, '', $gotcust) ) {
+ #warn $r->county. ' '. $r->state. ' '. $r->country. "\n";
+
+ my $label = getlabel($r);
+ $regions{$label}->{'label'} = $label;
+ $regions{$label}->{'url_param'} = join(';', map "$_=".$r->$_(), qw( county state country ) );
+
+ my @param = @base_param;
+ my $mywhere = $where;
+
+ if ( $r->taxclass ) {
+ $mywhere .= " AND taxclass = ? ";
+ push @param, 'taxclass';
+ $regions{$label}->{'url_param'} .= ';taxclass='. $r->taxclass
+ if $cgi->param('show_taxclasses');
+ }
+
+ my $fromwhere = $from_join_cust. $join_pkg. $mywhere. " AND payby != 'COMP' ";
+
+# my $label = getlabel($r);
+# $regions{$label}->{'label'} = $label;
+
+ my $nottax = 'pkgnum != 0';
+
+ ## calculate total for this region
+
+ my $t = scalar_sql($r, \@param,
+ "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) $fromwhere AND $nottax"
+ );
+ $total += $t;
+ $regions{$label}->{'total'} += $t;
+
+ ## calculate customer-exemption for this region
+
+## my $taxable = $t;
+
+# my($taxable, $x_cust) = (0, 0);
+# foreach my $e ( grep { $r->get($_.'tax') !~ /^Y/i }
+# qw( cust_bill_pkg.setup cust_bill_pkg.recur ) ) {
+# $taxable += scalar_sql($r, \@param,
+# "SELECT SUM($e) $fromwhere AND $nottax AND ( tax != 'Y' OR tax IS NULL )"
+# );
+#
+# $x_cust += scalar_sql($r, \@param,
+# "SELECT SUM($e) $fromwhere AND $nottax AND tax = 'Y'"
+# );
+# }
+
+ my $x_cust = scalar_sql($r, \@param,
+ "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur)
+ $fromwhere AND $nottax AND tax = 'Y' "
+ );
+
+ $exempt_cust += $x_cust;
+ $regions{$label}->{'exempt_cust'} += $x_cust;
+
+ ## calculate package-exemption for this region
+
+ my $x_pkg = scalar_sql($r, \@param,
+ "SELECT SUM(
+ ( CASE WHEN part_pkg.setuptax = 'Y'
+ THEN cust_bill_pkg.setup
+ ELSE 0
+ END
+ )
+ +
+ ( CASE WHEN part_pkg.recurtax = 'Y'
+ THEN cust_bill_pkg.recur
+ ELSE 0
+ END
+ )
+ )
+ $fromwhere
+ AND $nottax
+ AND (
+ ( part_pkg.setuptax = 'Y' AND cust_bill_pkg.setup > 0 )
+ OR ( part_pkg.recurtax = 'Y' AND cust_bill_pkg.recur > 0 )
+ )
+ AND ( tax != 'Y' OR tax IS NULL )
+ "
+ );
+ $exempt_pkg += $x_pkg;
+ $regions{$label}->{'exempt_pkg'} += $x_pkg;
+
+ ## calculate monthly exemption (texas tax) for this region
+
+ # count up all the cust_tax_exempt_pkg records associated with
+ # the actual line items.
+
+ my $x_monthly = scalar_sql($r, \@param,
+ "SELECT SUM(amount)
+ FROM cust_tax_exempt_pkg
+ JOIN cust_bill_pkg USING ( billpkgnum )
+ $join_cust $join_pkg
+ $mywhere"
+ );
+# if ( $x_monthly ) {
+# #warn $r->taxnum(). ": $x_monthly\n";
+# $taxable -= $x_monthly;
+# }
+
+ $exempt_monthly += $x_monthly;
+ $regions{$label}->{'exempt_monthly'} += $x_monthly;
+
+ my $taxable = $t - $x_cust - $x_pkg - $x_monthly;
+
+ $tot_taxable += $taxable;
+ $regions{$label}->{'taxable'} += $taxable;
+
+ $owed += $taxable * ($r->tax/100);
+ $regions{$label}->{'owed'} += $taxable * ($r->tax/100);
+
+ if ( defined($regions{$label}->{'rate'})
+ && $regions{$label}->{'rate'} != $r->tax.'%' ) {
+ $regions{$label}->{'rate'} = 'variable';
+ } else {
+ $regions{$label}->{'rate'} = $r->tax.'%';
+ }
+
+}
+
+my $taxwhere = "$from_join_cust $where AND payby != 'COMP' ";
+my @taxparam = @base_param;
+my %base_regions = ();
+#foreach my $label ( keys %regions ) {
+foreach my $r (
+ qsearch( 'cust_main_county',
+ {},
+ 'DISTINCT ON (country, state, county, taxname) *',
+ $gotcust
+ )
+) {
+
+ #warn join('-', map { $r->$_() } qw( country state county taxname ) )."\n";
+
+ my $label = getlabel($r);
+
+ #my $fromwhere = $join_pkg. $where. " AND payby != 'COMP' ";
+ #my @param = @base_param;
+
+ #match itemdesc if necessary!
+ my $named_tax =
+ $r->taxname
+ ? 'AND itemdesc = '. dbh->quote($r->taxname)
+ : "AND ( itemdesc IS NULL OR itemdesc = '' OR itemdesc = 'Tax' )";
+ my $x = scalar_sql($r, \@taxparam,
+ "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) $taxwhere ".
+ "AND pkgnum = 0 $named_tax",
+ );
+ $tax += $x;
+ $regions{$label}->{'tax'} += $x;
+
+ if ( $cgi->param('show_taxclasses') ) {
+ my $base_label = getlabel($r, 'no_taxclass'=>1 );
+ $base_regions{$base_label}->{'label'} = $base_label;
+ $base_regions{$base_label}->{'url_param'} =
+ join(';', map "$_=".$r->$_(), qw( county state country ) );
+ $base_regions{$base_label}->{'tax'} += $x;
+ }
+
+}
+
+#ordering
+my @regions =
+ map $regions{$_},
+ sort { ( ($a eq $out) cmp ($b eq $out) ) || ($b cmp $a) }
+ keys %regions;
+
+my @base_regions =
+ map $base_regions{$_},
+ sort { ( ($a eq $out) cmp ($b eq $out) ) || ($b cmp $a) }
+ keys %base_regions;
+
+push @regions, {
+ 'label' => 'Total',
+ 'url_param' => '',
+ 'total' => $total,
+ 'exempt_cust' => $exempt_cust,
+ 'exempt_pkg' => $exempt_pkg,
+ 'exempt_monthly' => $exempt_monthly,
+ 'taxable' => $tot_taxable,
+ 'rate' => '',
+ 'owed' => $owed,
+ 'tax' => $tax,
+};
+
+#--
+
+sub getlabel {
+ my $r = shift;
+ my %opt = @_;
+
+ my $label;
+ if (
+ $r->tax == 0
+ && ! scalar( qsearch('cust_main_county', { 'state' => $r->state,
+ 'county' => $r->county,
+ 'country' => $r->country,
+ 'tax' => { op=>'>', value=>0 },
+ }
+ )
+ )
+
+ ) {
+ #kludge to avoid "will not stay shared" warning
+ my $out = 'Out of taxable region(s)';
+ $label = $out;
+ } elsif ( $r->taxname ) {
+ $label = $r->taxname;
+# $regions{$label}->{'taxname'} = $label;
+# push @{$regions{$label}->{$_}}, $r->$_() foreach qw( county state country );
+ } else {
+ $label = $r->country;
+ $label = $r->state.", $label" if $r->state;
+ $label = $r->county." county, $label" if $r->county;
+ $label = "$label (". $r->taxclass. ")"
+ if $r->taxclass
+ && $cgi->param('show_taxclasses')
+ && ! $opt{'no_taxclass'};
+ #$label = $r->taxname. " ($label)" if $r->taxname;
+ }
+ return $label;
+}
+
+#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;
+}
+
+
+
+my $dateagentlink = "begin=$beginning;end=$ending";
+$dateagentlink .= ';agentnum='. $cgi->param('agentnum')
+ if length($agentname);
+my $baselink = $p. "search/cust_bill_pkg.cgi?$dateagentlink";
+my $exemptlink = $p. "search/cust_tax_exempt_pkg.cgi?$dateagentlink";
+
+</%init>
diff --git a/httemplate/search/report_tax.html b/httemplate/search/report_tax.html
new file mode 100755
index 000000000..35b290c19
--- /dev/null
+++ b/httemplate/search/report_tax.html
@@ -0,0 +1,42 @@
+<% include('/elements/header.html', 'Tax Report' ) %>
+
+<FORM ACTION="report_tax.cgi" METHOD="GET">
+
+<TABLE>
+
+ <% include( '/elements/tr-select-agent.html' ) %>
+
+ <% include( '/elements/tr-input-beginning_ending.html' ) %>
+% my $conf = new FS::Conf;
+% if ( $conf->exists('enable_taxclasses') ) {
+%
+
+ <TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_taxclasses" VALUE="1"></TD>
+ <TD>Show tax classes</TD>
+ </TR>
+% }
+% my @pkg_class = qsearch('pkg_class', {});
+% if ( @pkg_class ) {
+%
+
+ <TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_pkgclasses" VALUE="1"></TD>
+ <TD>Show package classes</TD>
+ </TR>
+% }
+
+
+</TABLE>
+
+<BR><INPUT TYPE="submit" VALUE="Get Report">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
diff --git a/httemplate/search/sql.html b/httemplate/search/sql.html
new file mode 100644
index 000000000..5f64ebc28
--- /dev/null
+++ b/httemplate/search/sql.html
@@ -0,0 +1,13 @@
+<% include( 'elements/search.html',
+ 'title' => 'Query Results',
+ 'name' => 'rows',
+ 'query' => 'SELECT '. ( $cgi->param('sql')
+ || eidiot('Empty query') ),
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Raw SQL');
+
+</%init>
diff --git a/httemplate/search/sqlradius.cgi b/httemplate/search/sqlradius.cgi
new file mode 100644
index 000000000..324729b6a
--- /dev/null
+++ b/httemplate/search/sqlradius.cgi
@@ -0,0 +1,311 @@
+<% include( '/elements/header.html', 'RADIUS Sessions',
+ include('/elements/menubar.html',
+ 'Main menu' => $p, # popurl(2),
+ ),
+
+ )
+%>
+
+% ###
+% # and finally, display the thing
+% ###
+%
+% foreach my $part_export (
+% #grep $_->can('usage_sessions'), qsearch( 'part_export' )
+% qsearch( 'part_export', { 'exporttype' => 'sqlradius' } ),
+% qsearch( 'part_export', { 'exporttype' => 'sqlradius_withdomain' } )
+% ) {
+% %user2svc_acct = ();
+%
+% my $efields = tie my %efields, 'Tie::IxHash', %fields;
+% delete $efields{'framedipaddress'} if $part_export->option('hide_ip');
+% if ( $part_export->option('hide_data') ) {
+% delete $efields{$_} foreach qw(acctinputoctets acctoutputoctets);
+% }
+% if ( $part_export->option('show_called_station') ) {
+% $efields->Splice(1, 0,
+% 'calledstationid' => {
+% 'name' => 'Destination',
+% 'attrib' => 'Called-Station-ID',
+% 'fmt' =>
+% sub { length($_[0]) ? shift : '&nbsp'; },
+% 'align' => 'left',
+% },
+% );
+% }
+%
+%
+
+ <% $part_export->exporttype %> to <% $part_export->machine %><BR>
+ <% include( '/elements/table-grid.html' ) %>
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor;
+
+ <TR>
+% foreach my $field ( keys %efields ) {
+
+ <TH CLASS="grid" BGCOLOR="#cccccc">
+ <% $efields{$field}->{name} %><BR>
+ <FONT SIZE=-2><% $efields{$field}->{attrib} %></FONT>
+ </TH>
+
+% }
+ </TR>
+
+% foreach my $session (
+% @{ $part_export->usage_sessions(
+% $beginning, $ending, $cgi_svc_acct, $ip, $prefix, ) }
+% ) {
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+
+ <TR>
+% foreach my $field ( keys %efields ) {
+% my $html = &{ $efields{$field}->{fmt} }( $session->{$field},
+% $session,
+% $part_export,
+% );
+% my $class = ( $html =~ /<TABLE/ ? 'inv' : 'grid' );
+
+ <TD CLASS="<%$class%>" BGCOLOR="<% $bgcolor %>" ALIGN="<% $efields{$field}->{align} %>">
+ <% $html %>
+ </TD>
+% }
+ </TR>
+
+% }
+
+</TABLE>
+<BR><BR>
+
+% }
+
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List rating data');
+
+###
+# parse cgi params
+###
+
+#sort of false laziness w/cust_pay.cgi
+my $beginning = '';
+my $ending = '';
+if ( $cgi->param('beginning')
+ && $cgi->param('beginning') =~ /^([ 0-9\-\/\:\w]{0,54})$/ ) {
+ $beginning = str2time($1);
+}
+if ( $cgi->param('ending')
+ && $cgi->param('ending') =~ /^([ 0-9\-\/\:\w]{0,54})$/ ) {
+ $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;
+}
+
+my $prefix = $cgi->param('prefix');
+$prefix =~ s/\D//g;
+if ( $prefix =~ /^(\d+)$/ ) {
+ $prefix = $1;
+ $prefix = "011$prefix" unless $prefix =~ /^1/;
+} else {
+ $prefix = '';
+}
+
+###
+# 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 CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0>'.
+ '<TR><TD CLASS="inv" ALIGN="right">'.
+ ( $hour ? "<B>$hour</B>h" : '&nbsp;' ).
+ '</TD><TD CLASS="inv" ALIGN="right">'.
+ ( ( $hour || $min ) ? "<B>$min</B>m" : '&nbsp;' ).
+ '</TD><TD CLASS="inv" 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;
+
+</%init>
diff --git a/httemplate/search/sqlradius.html b/httemplate/search/sqlradius.html
new file mode 100644
index 000000000..660a54f3c
--- /dev/null
+++ b/httemplate/search/sqlradius.html
@@ -0,0 +1,59 @@
+<% include( '/elements/header.html', 'Search RADIUS sessions' ) %>
+
+<FORM NAME="OneTrueForm" ACTION="sqlradius.cgi" METHOD="GET">
+% #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>
+% my @part_export = qsearch( 'part_export', { 'exporttype' => 'sqlradius' } );
+% push @part_export,
+% qsearch( 'part_export', { 'exporttype' => 'sqlradius_withdomain' } );
+%
+% if ( grep { ! $_->option('hide_ip') } @part_export ) {
+
+ <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>
+% }
+% if ( grep { $_->option('show_called_station') } @part_export ) {
+
+ <TR>
+ <TD ALIGN="right">Destination prefix:</TD>
+ <TD><INPUT TYPE="text" NAME="prefix"></TD>
+ </TR>
+ <TR>
+ <TD></TD>
+ <TD><FONT SIZE="-1"><I>(country code or country code and prefix)</I></FONT></TD>
+ </TR>
+ <TR>
+ <TD></TD>
+ <TD><FONT SIZE="-1"><I>(leave blank to show all destinations)</I></FONT></TD>
+ </TR>
+% }
+
+
+<% include( '/elements/tr-input-beginning_ending.html', 'input_time'=>1 ) %>
+
+</TABLE>
+<BR><INPUT TYPE="submit" VALUE="View sessions">
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List rating data');
+
+</%init>
diff --git a/httemplate/search/svc_acct.cgi b/httemplate/search/svc_acct.cgi
new file mode 100755
index 000000000..a702d604b
--- /dev/null
+++ b/httemplate/search/svc_acct.cgi
@@ -0,0 +1,174 @@
+<% include( 'elements/search.html',
+ 'title' => 'Account Search Results',
+ 'name' => 'accounts',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'redirect' => $link,
+ 'header' => [ '#',
+ 'Service',
+ 'Account',
+ 'UID',
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [ 'svcnum',
+ 'svc',
+ 'email',
+ 'uid',
+ \&FS::UI::Web::cust_fields,
+ ],
+ 'links' => [ $link,
+ $link,
+ $link,
+ $link,
+ ( map { $_ ne 'Cust. Status' ? $link_cust : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'align' => 'rlll'. FS::UI::Web::cust_aligns(),
+ 'color' => [
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List services');
+
+my @extra_sql = ();
+
+ if ( $cgi->param('domain') ) {
+ my $svc_domain =
+ qsearchs('svc_domain', { 'domain' => $cgi->param('domain') } );
+ unless ( $svc_domain ) {
+ #it would be nice if this looked more like the other "not found"
+ #errors, but this will do for now.
+ eidiot "Domain ". $cgi->param('domain'). " not found at all";
+ } else {
+ push @extra_sql, 'domsvc = '. $svc_domain->svcnum;
+ }
+ }
+
+my $orderby = 'ORDER BY svcnum';
+if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+
+ push @extra_sql, 'pkgnum IS NULL'
+ if $cgi->param('magic') eq 'unlinked';
+
+ if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+ my $sortby = $1;
+ $sortby = "LOWER($sortby)"
+ if $sortby eq 'username';
+ push @extra_sql, "$sortby IS NOT NULL"
+ if $sortby eq 'uid';
+ $orderby = "ORDER BY $sortby";
+ }
+
+} elsif ( $cgi->param('popnum') =~ /^(\d+)$/ ) {
+ push @extra_sql, "popnum = $1";
+ $orderby = "ORDER BY LOWER(username)";
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+ push @extra_sql, "svcpart = $1";
+ $orderby = "ORDER BY uid";
+ #$orderby = "ORDER BY svcnum";
+} else {
+ $orderby = "ORDER BY uid";
+
+ my @username_sql;
+
+ my %username_type;
+ foreach ( $cgi->param('username_type') ) {
+ $username_type{$_}++;
+ }
+
+ $cgi->param('username') =~ /^([\w\-\.\&]+)$/; #untaint username_text
+ my $username = $1;
+
+ push @username_sql, "username ILIKE '$username'"
+ if $username_type{'Exact'}
+ || $username_type{'Fuzzy'};
+
+ push @username_sql, "username ILIKE '\%$username\%'"
+ if $username_type{'Substring'}
+ || $username_type{'All'};
+
+ 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'}) {
+ #}
+
+ push @username_sql, "username = '$_'"
+ foreach (keys %username);
+
+ }
+
+ push @extra_sql, '( '. join( ' OR ', @username_sql). ' )';
+
+}
+
+my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN part_svc USING ( svcpart ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ';
+
+#here is the agent virtualization
+push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my $extra_sql =
+ scalar(@extra_sql)
+ ? ' WHERE '. join(' AND ', @extra_sql )
+ : '';
+
+my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql";
+#if ( keys %svc_acct ) {
+# $count_query .= ' WHERE '.
+# join(' AND ', map "$_ = ". dbh->quote($svc_acct{$_}),
+# keys %svc_acct
+# );
+#}
+
+my $sql_query = {
+ 'table' => 'svc_acct',
+ 'hashref' => {}, # \%svc_acct,
+ 'select' => join(', ',
+ 'svc_acct.*',
+ 'part_svc.svc',
+ 'cust_main.custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'extra_sql' => "$extra_sql $orderby",
+ 'addl_from' => $addl_from,
+};
+
+my $link = [ "${p}view/svc_acct.cgi?", 'svcnum' ];
+my $link_cust = sub {
+ my $svc_acct = shift;
+ if ( $svc_acct->custnum ) {
+ [ "${p}view/cust_main.cgi?", 'custnum' ];
+ } else {
+ '';
+ }
+};
+
+</%init>
+
diff --git a/httemplate/search/svc_broadband.cgi b/httemplate/search/svc_broadband.cgi
new file mode 100755
index 000000000..1bbdbfcdb
--- /dev/null
+++ b/httemplate/search/svc_broadband.cgi
@@ -0,0 +1,121 @@
+%die "access denied"
+% unless $FS::CurrentUser::CurrentUser->access_right('List services');
+%
+%my $conf = new FS::Conf;
+%
+%my @svc_broadband = ();
+%my $sortby=\*svcnum_sort;
+%if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+%
+% @svc_broadband=qsearch('svc_broadband',{});
+%
+% if ( $cgi->param('magic') eq 'unlinked' ) {
+% @svc_broadband = grep { qsearchs('cust_svc', {
+% 'svcnum' => $_->svcnum,
+% 'pkgnum' => '',
+% }
+% )
+% }
+% @svc_broadband;
+% }
+%
+% if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+% my $sortby = $1;
+% if ( $sortby eq 'blocknum' ) {
+% $sortby = \*blocknum_sort;
+% }
+% }
+%
+%} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+%
+% @svc_broadband =
+% qsearch( 'svc_broadband', {}, '',
+% " WHERE $1 = ( SELECT svcpart FROM cust_svc ".
+% " WHERE cust_svc.svcnum = svc_external.svcnum ) "
+% );
+%
+%} elsif ( $cgi->param('ip_addr') =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
+% my $ip_addr = $1;
+% @svc_broadband = qsearchs('svc_broadband',{'ip_addr'=>$ip_addr});
+%}
+%
+%my %routerbyblock = ();
+%foreach my $router (qsearch('router', {})) {
+% foreach ($router->addr_block) {
+% $routerbyblock{$_->blocknum} = $router;
+% }
+%}
+%
+%if ( scalar(@svc_broadband) == 1 ) {
+% print $cgi->redirect(popurl(2). "view/svc_broadband.cgi?". $svc_broadband[0]->svcnum);
+% #exit;
+%} elsif ( scalar(@svc_broadband) == 0 ) {
+%
+
+<!-- mason kludge -->
+%
+% eidiot "No matching ip address found!\n";
+%} else {
+%
+
+<!-- mason kludge -->
+%
+% my($total)=scalar(@svc_broadband);
+% print header("IP Address Search Results",''), <<END;
+%
+% $total matching broadband services found
+% <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
+% <TR>
+% <TH>Service #</TH>
+% <TH>Router</TH>
+% <TH>IP Address</TH>
+% </TR>
+%END
+%
+% foreach my $svc_broadband (
+% sort $sortby (@svc_broadband)
+% ) {
+% my($svcnum,$ip_addr,$routername,$routernum)=(
+% $svc_broadband->svcnum,
+% $svc_broadband->ip_addr,
+% $routerbyblock{$svc_broadband->blocknum}->routername,
+% $routerbyblock{$svc_broadband->blocknum}->routernum,
+% );
+%
+% my $rowspan = 1;
+%
+% print <<END;
+% <TR>
+% <TD ROWSPAN=$rowspan><A HREF="${p}view/svc_broadband.cgi?$svcnum">$svcnum</A></TD>
+% <TD ROWSPAN=$rowspan><A HREF="${p}view/router.cgi?$routernum">$routername</A></TD>
+% <TD ROWSPAN=$rowspan><A HREF="${p}view/svc_broadband.cgi?$svcnum">$ip_addr</A></TD>
+%END
+%
+% #print @rows;
+% print "</TR>";
+%
+% }
+%
+% print <<END;
+% </TABLE>
+% </BODY>
+%</HTML>
+%END
+%
+%}
+%
+%sub svcnum_sort {
+% $a->getfield('svcnum') <=> $b->getfield('svcnum');
+%}
+%
+%sub blocknum_sort {
+% if ($a->getfield('blocknum') == $b->getfield('blocknum')) {
+% $a->getfield('ip_addr') cmp $b->getfield('ip_addr');
+% } else {
+% $a->getfield('blocknum') cmp $b->getfield('blocknum');
+% }
+%}
+%
+%
+%
+
diff --git a/httemplate/search/svc_domain.cgi b/httemplate/search/svc_domain.cgi
new file mode 100755
index 000000000..b14a1cc4f
--- /dev/null
+++ b/httemplate/search/svc_domain.cgi
@@ -0,0 +1,110 @@
+<% include( 'elements/search.html',
+ 'title' => "Domain Search Results",
+ 'name' => 'domains',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'redirect' => $link,
+ 'header' => [ '#',
+ 'Service',
+ 'Domain',
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [ 'svcnum',
+ 'svc',
+ 'domain',
+ \&FS::UI::Web::cust_fields,
+ ],
+ 'links' => [ $link,
+ $link,
+ $link,
+ ( map { $_ ne 'Cust. Status' ? $link_cust : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'align' => 'rll'. FS::UI::Web::cust_aligns(),
+ 'color' => [
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List services');
+
+my $conf = new FS::Conf;
+
+my $orderby = 'ORDER BY svcnum';
+my %svc_domain = ();
+my @extra_sql = ();
+if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+
+ push @extra_sql, 'pkgnum IS NULL'
+ if $cgi->param('magic') eq 'unlinked';
+
+ if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+ my $sortby = $1;
+ $orderby = "ORDER BY $sortby";
+ }
+
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+ push @extra_sql, "svcpart = $1";
+} else {
+ $cgi->param('domain') =~ /^([\w\-\.]+)$/;
+ $svc_domain{'domain'} = $1;
+}
+
+my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN part_svc USING ( svcpart ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ';
+
+#here is the agent virtualization
+push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my $extra_sql = '';
+if ( @extra_sql ) {
+ $extra_sql = ( keys(%svc_domain) ? ' AND ' : ' WHERE ' ).
+ join(' AND ', @extra_sql );
+}
+
+my $count_query = "SELECT COUNT(*) FROM svc_domain $addl_from ";
+if ( keys %svc_domain ) {
+ $count_query .= ' WHERE '.
+ join(' AND ', map "$_ = ". dbh->quote($svc_domain{$_}),
+ keys %svc_domain
+ );
+}
+$count_query .= $extra_sql;
+
+my $sql_query = {
+ 'table' => 'svc_domain',
+ 'hashref' => \%svc_domain,
+ 'select' => join(', ',
+ 'svc_domain.*',
+ 'part_svc.svc',
+ 'cust_main.custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'extra_sql' => "$extra_sql $orderby",
+ 'addl_from' => $addl_from,
+};
+
+my $link = [ "${p}view/svc_domain.cgi?", 'svcnum' ];
+
+#smaller false laziness w/svc_*.cgi here
+my $link_cust = sub {
+ my $svc_x = shift;
+ $svc_x->custnum ? [ "${p}view/cust_main.cgi?", 'custnum' ] : '';
+};
+
+</%init>
diff --git a/httemplate/search/svc_external.cgi b/httemplate/search/svc_external.cgi
new file mode 100755
index 000000000..2710d75bc
--- /dev/null
+++ b/httemplate/search/svc_external.cgi
@@ -0,0 +1,153 @@
+%die "access denied"
+% unless $FS::CurrentUser::CurrentUser->access_right('List services');
+%
+%my $conf = new FS::Conf;
+%
+%my @svc_external = ();
+%my @h_svc_external = ();
+%my $sortby=\*svcnum_sort;
+%if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+%
+% @svc_external=qsearch('svc_external',{});
+%
+% if ( $cgi->param('magic') eq 'unlinked' ) {
+% @svc_external = grep { qsearchs('cust_svc', {
+% 'svcnum' => $_->svcnum,
+% 'pkgnum' => '',
+% }
+% )
+% }
+% @svc_external;
+% }
+%
+% if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+% my $sortby = $1;
+% if ( $sortby eq 'id' ) {
+% $sortby = \*id_sort;
+% }
+% }
+%
+%} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+%
+% @svc_external =
+% qsearch( 'svc_external', {}, '',
+% " WHERE $1 = ( SELECT svcpart FROM cust_svc ".
+% " WHERE cust_svc.svcnum = svc_external.svcnum ) "
+% );
+%
+%} elsif ( $cgi->param('title') =~ /^(.*)$/ ) {
+% $sortby=\*id_sort;
+% @svc_external=qsearch('svc_external',{ title => $1 });
+% if( $cgi->param('history') == 1 ) {
+% @h_svc_external=qsearch('h_svc_external',{ title => $1 });
+% }
+%} elsif ( $cgi->param('id') =~ /^([\w\-\.]+)$/ ) {
+% my $id = $1;
+% @svc_external = qsearchs('svc_external',{'id'=>$id});
+%}
+%
+%if ( scalar(@svc_external) == 1 ) {
+%
+%
+<% $cgi->redirect(popurl(2). "view/svc_external.cgi?". $svc_external[0]->svcnum) %>
+%
+%
+%} elsif ( scalar(@svc_external) == 0 ) {
+%
+%
+<% include('/elements/header.html', 'External Search Results' ) %>
+
+ No matching external services found
+% } else {
+%
+%
+<% include('/elements/header.html', '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>";
+%
+% }
+% if( scalar(@h_svc_external) > 0 ) {
+% print <<HTML;
+% </TABLE>
+% <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
+% <TR>
+% <TH>Freeside ID</TH>
+% <TH>Service #</TH>
+% <TH>Title</TH>
+% <TH>Date</TH>
+% </TR>
+%HTML
+%
+% foreach my $h_svc ( @h_svc_external ) {
+% my($svcnum, $id, $title, $user, $date)=(
+% $h_svc->svcnum,
+% $h_svc->id,
+% $h_svc->title,
+% $h_svc->history_user,
+% $h_svc->history_date,
+% );
+% my $rowspan = 1;
+% my ($h_cust_svc) = qsearchs( 'h_cust_svc', {
+% svcnum => $svcnum,
+% });
+% my $cust_pkg = qsearchs( 'cust_pkg', {
+% pkgnum => $h_cust_svc->pkgnum,
+% });
+% my $custnum = $cust_pkg->custnum;
+%
+% print <<END;
+% <TR>
+% <TD ROWSPAN=$rowspan><A HREF="${p}view/cust_main.cgi?$custnum">$custnum</A></TD>
+% <TD ROWSPAN=$rowspan><A HREF="${p}view/cust_main.cgi?$custnum">$svcnum</A></TD>
+% <TD ROWSPAN=$rowspan><A HREF="${p}view/cust_main.cgi?$custnum">$title</A></TD>
+% <TD ROWSPAN=$rowspan><A HREF="${p}view/cust_main.cgi?$custnum">$date</A></TD>
+% </TR>
+%END
+% }
+% }
+%
+% 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 000000000..eeb4c1075
--- /dev/null
+++ b/httemplate/search/svc_forward.cgi
@@ -0,0 +1,145 @@
+<% include( 'elements/search.html',
+ 'title' => "Mail forward Search Results",
+ 'name' => 'mail forwards',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'redirect' => $link,
+ 'header' => [ '#',
+ 'Service',
+ 'Mail to',
+ 'Forwards to',
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [ 'svcnum',
+ 'svc',
+ $format_src,
+ $format_dst,
+ \&FS::UI::Web::cust_fields,
+ ],
+ 'links' => [ $link,
+ $link,
+ $link_src,
+ $link_dst,
+ ( map { $_ ne 'Cust. Status' ? $link_cust : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'align' => 'rlll'. FS::UI::Web::cust_aligns(),
+ 'color' => [
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List services');
+
+
+my $conf = new FS::Conf;
+
+my $orderby = 'ORDER BY svcnum';
+my @extra_sql = ();
+if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+
+ push @extra_sql, 'pkgnum IS NULL'
+ if $cgi->param('magic') eq 'unlinked';
+
+ if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+ my $sortby = $1;
+ $orderby = "ORDER BY $sortby";
+ }
+
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+ push @extra_sql, "svcpart = $1";
+}
+
+my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN part_svc USING ( svcpart ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ';
+
+#here is the agent virtualization
+push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my $extra_sql =
+ scalar(@extra_sql)
+ ? ' WHERE '. join(' AND ', @extra_sql )
+ : '';
+
+my $count_query = "SELECT COUNT(*) FROM svc_forward $addl_from $extra_sql";
+my $sql_query = {
+ 'table' => 'svc_forward',
+ 'hashref' => {},
+ 'select' => join(', ',
+ 'svc_forward.*',
+ 'part_svc.svc',
+ 'cust_main.custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'extra_sql' => "$extra_sql $orderby",
+ 'addl_from' => $addl_from,
+};
+
+# <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>
+
+my $link = [ "${p}view/svc_forward.cgi?", 'svcnum' ];
+
+my $format_src = sub {
+ my $svc_forward = shift;
+ if ( $svc_forward->srcsvc_acct ) {
+ $svc_forward->srcsvc_acct->email;
+ } else {
+ my $src = $svc_forward->src;
+ $src = "<I>(anything)</I>$src" if $src =~ /^@/;
+ $src;
+ }
+};
+
+my $link_src = sub {
+ my $svc_forward = shift;
+ if ( $svc_forward->srcsvc_acct ) {
+ [ "${p}view/svc_acct.cgi?", 'srcsvc' ];
+ } else {
+ '';
+ }
+};
+
+my $format_dst = sub {
+ my $svc_forward = shift;
+ if ( $svc_forward->dstsvc_acct ) {
+ $svc_forward->dstsvc_acct->email;
+ } else {
+ $svc_forward->dst;
+ }
+};
+
+my $link_dst = sub {
+ my $svc_forward = shift;
+ if ( $svc_forward->dstsvc_acct ) {
+ [ "${p}view/svc_acct.cgi?", 'dstsvc' ];
+ } else {
+ '';
+ }
+};
+
+#smaller false laziness w/svc_*.cgi here
+my $link_cust = sub {
+ my $svc_x = shift;
+ $svc_x->custnum ? [ "${p}view/cust_main.cgi?", 'custnum' ] : '';
+};
+
+</%init>
diff --git a/httemplate/search/svc_phone.cgi b/httemplate/search/svc_phone.cgi
new file mode 100644
index 000000000..0c1d57887
--- /dev/null
+++ b/httemplate/search/svc_phone.cgi
@@ -0,0 +1,115 @@
+<% include( 'elements/search.html',
+ 'title' => "Phone number search results",
+ 'name' => 'phone numbers',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'redirect' => $link,
+ 'header' => [ '#',
+ 'Service',
+ 'Country code',
+ 'Phone number',
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [ 'svcnum',
+ 'svc',
+ 'countrycode',
+ 'phonenum',
+ \&FS::UI::Web::cust_fields,
+ ],
+ 'links' => [ $link,
+ $link,
+ $link,
+ $link,
+ ( map { $_ ne 'Cust. Status' ? $link_cust : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'align' => 'rlrr'. FS::UI::Web::cust_aligns(),
+ 'color' => [
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List services');
+
+my $conf = new FS::Conf;
+
+my $orderby = 'ORDER BY svcnum';
+my %svc_phone = ();
+my @extra_sql = ();
+if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+
+ push @extra_sql, 'pkgnum IS NULL'
+ if $cgi->param('magic') eq 'unlinked';
+
+ if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+ my $sortby = $1;
+ $orderby = "ORDER BY $sortby";
+ }
+
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+ push @extra_sql, "svcpart = $1";
+} else {
+ $cgi->param('phonenum') =~ /^([\d\- ]+)$/;
+ ( $svc_phone{'phonenum'} = $1 ) =~ s/\D//g;
+}
+
+my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN part_svc USING ( svcpart ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ';
+
+#here is the agent virtualization
+push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my $extra_sql = '';
+if ( @extra_sql ) {
+ $extra_sql = ( keys(%svc_phone) ? ' AND ' : ' WHERE ' ).
+ join(' AND ', @extra_sql );
+}
+
+my $count_query = "SELECT COUNT(*) FROM svc_phone $addl_from ";
+if ( keys %svc_phone ) {
+ $count_query .= ' WHERE '.
+ join(' AND ', map "$_ = ". dbh->quote($svc_phone{$_}),
+ keys %svc_phone
+ );
+}
+$count_query .= $extra_sql;
+
+my $sql_query = {
+ 'table' => 'svc_phone',
+ 'hashref' => \%svc_phone,
+ 'select' => join(', ',
+ 'svc_phone.*',
+ 'part_svc.svc',
+ 'cust_main.custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'extra_sql' => "$extra_sql $orderby",
+ 'addl_from' => $addl_from,
+};
+
+my $link = [ "${p}view/svc_phone.cgi?", 'svcnum' ];
+
+#smaller false laziness w/svc_*.cgi here
+my $link_cust = sub {
+ my $svc_x = shift;
+ $svc_x->custnum ? [ "${p}view/cust_main.cgi?", 'custnum' ] : '';
+};
+
+</%init>
diff --git a/httemplate/search/svc_www.cgi b/httemplate/search/svc_www.cgi
new file mode 100755
index 000000000..d9332e95d
--- /dev/null
+++ b/httemplate/search/svc_www.cgi
@@ -0,0 +1,111 @@
+<% include( 'elements/search.html',
+ 'title' => 'Virtual Host Search Results',
+ 'name' => 'virtual hosts',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'redirect' => $link,
+ 'header' => [ '#',
+ 'Service',
+ 'Zone',
+ 'User',
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [ 'svcnum',
+ 'svc',
+ sub { $_[0]->domain_record->zone },
+ sub {
+ my $svc_www = shift;
+ my $svc_acct = $svc_www->svc_acct;
+ $svc_acct
+ ? $svc_acct->email
+ : '';
+ },
+ \&FS::UI::Web::cust_fields,
+ ],
+ 'links' => [ $link,
+ $link,
+ '',
+ $ulink,
+ ( map { $_ ne 'Cust. Status' ? $link_cust : '' }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ 'align' => 'rlll'. FS::UI::Web::cust_aligns(),
+ 'color' => [
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_colors(),
+ ],
+ 'style' => [
+ '',
+ '',
+ '',
+ '',
+ FS::UI::Web::cust_styles(),
+ ],
+ )
+%>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List services');
+
+#my $conf = new FS::Conf;
+
+my $orderby = 'ORDER BY svcnum';
+my @extra_sql = ();
+if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+
+ push @extra_sql, 'pkgnum IS NULL'
+ if $cgi->param('magic') eq 'unlinked';
+
+ if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+ my $sortby = $1;
+ $orderby = "ORDER BY $sortby";
+ }
+
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+ push @extra_sql, "svcpart = $1";
+}
+
+my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN part_svc USING ( svcpart ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ';
+
+#here is the agent virtualization
+push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my $extra_sql =
+ scalar(@extra_sql)
+ ? ' WHERE '. join(' AND ', @extra_sql )
+ : '';
+
+
+my $count_query = "SELECT COUNT(*) FROM svc_www $addl_from $extra_sql";
+my $sql_query = {
+ 'table' => 'svc_www',
+ 'hashref' => {},
+ 'select' => join(', ',
+ 'svc_www.*',
+ 'part_svc.svc',
+ 'cust_main.custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'extra_sql' => "$extra_sql $orderby",
+ 'addl_from' => $addl_from,
+};
+
+my $link = [ "${p}view/svc_www.cgi?", 'svcnum', ];
+#my $dlink = [ "${p}view/svc_www.cgi?", 'svcnum', ];
+my $ulink = [ "${p}view/svc_acct.cgi?", 'usersvc', ];
+
+#smaller false laziness w/svc_*.cgi here
+my $link_cust = sub {
+ my $svc_x = shift;
+ $svc_x->custnum ? [ "${p}view/cust_main.cgi?", 'custnum' ] : '';
+};
+
+</%init>
diff --git a/httemplate/view/cust_bill-logo.cgi b/httemplate/view/cust_bill-logo.cgi
new file mode 100755
index 000000000..e2f810c3f
--- /dev/null
+++ b/httemplate/view/cust_bill-logo.cgi
@@ -0,0 +1,20 @@
+<% $conf->config_binary("logo$templatename.png") %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('View invoices');
+
+my $conf = new FS::Conf;
+
+my($query) = $cgi->keywords;
+$query =~ /^([^\.\/]*)$/;
+my $templatename = $1;
+if ( $templatename && $conf->exists("logo_$templatename.png") ) {
+ $templatename = "_$templatename";
+} else {
+ $templatename = '';
+}
+
+http_header('Content-Type' => 'image/png' );
+
+</%init>
diff --git a/httemplate/view/cust_bill-pdf.cgi b/httemplate/view/cust_bill-pdf.cgi
new file mode 100755
index 000000000..f09e1b74d
--- /dev/null
+++ b/httemplate/view/cust_bill-pdf.cgi
@@ -0,0 +1,28 @@
+<% $pdf %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('View invoices');
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^((.+)-)?(\d+)(.pdf)?$/;
+my $templatename = $2;
+my $invnum = $3;
+
+my $cust_bill = qsearchs({
+ 'select' => 'cust_bill.*',
+ 'table' => 'cust_bill',
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => { 'invnum' => $invnum },
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+});
+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' );
+
+</%init>
diff --git a/httemplate/view/cust_bill-ps.cgi b/httemplate/view/cust_bill-ps.cgi
new file mode 100755
index 000000000..5313dbf02
--- /dev/null
+++ b/httemplate/view/cust_bill-ps.cgi
@@ -0,0 +1,24 @@
+<% $cust_bill->print_ps( '', $templatename) %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('View invoices');
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^((.+)-)?(\d+)$/;
+my $templatename = $2;
+my $invnum = $3;
+
+my $cust_bill = qsearchs({
+ 'select' => 'cust_bill.*',
+ 'table' => 'cust_bill',
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => { 'invnum' => $invnum },
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+});
+die "Invoice #$invnum not found!" unless $cust_bill;
+
+http_header('Content-Type' => 'application/postscript' );
+
+</%init>
diff --git a/httemplate/view/cust_bill.cgi b/httemplate/view/cust_bill.cgi
new file mode 100755
index 000000000..42e1e6177
--- /dev/null
+++ b/httemplate/view/cust_bill.cgi
@@ -0,0 +1,165 @@
+<% include("/elements/header.html",'Invoice View', menubar(
+ "Main Menu" => $p,
+ "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+)) %>
+
+
+% if ( $cust_bill->owed > 0
+% && ( $payby{'BILL'} || $payby{'CASH'} || $payby{'WEST'} || $payby{'MCRD'} )
+% )
+% {
+% my $s = 0;
+
+ Post
+% if ( $payby{'BILL'} ) {
+
+
+ <% $s++ ? ' | ' : '' %>
+ <A HREF="<% $p %>edit/cust_pay.cgi?payby=BILL;invnum=<% $invnum %>">check</A>
+% }
+% if ( $payby{'CASH'} ) {
+
+
+ <% $s++ ? ' | ' : '' %>
+ <A HREF="<% $p %>edit/cust_pay.cgi?payby=CASH;invnum=<% $invnum %>">cash</A>
+% }
+% if ( $payby{'WEST'} ) {
+
+
+ <% $s++ ? ' | ' : '' %>
+ <A HREF="<% $p %>edit/cust_pay.cgi?payby=WEST;invnum=<% $invnum %>">Western Union</A>
+% }
+% if ( $payby{'MCRD'} ) {
+
+
+ <% $s++ ? ' | ' : '' %>
+ <A HREF="<% $p %>edit/cust_pay.cgi?payby=MCRD;invnum=<% $invnum %>">manual credit card</A>
+% }
+
+
+ payment against this invoice<BR>
+% }
+
+
+<A HREF="<% $p %>misc/print-invoice.cgi?<% $link %>">Re-print this invoice</A>
+% if ( grep { $_ ne 'POST' } $cust_bill->cust_main->invoicing_list ) {
+
+ | <A HREF="<% $p %>misc/email-invoice.cgi?<% $link %>">Re-email
+ this invoice</A>
+% }
+% if ( $conf->exists('hylafax') && length($cust_bill->cust_main->fax) ) {
+
+ | <A HREF="<% $p %>misc/fax-invoice.cgi?<% $link %>">Re-fax
+ this invoice</A>
+% }
+
+
+<BR><BR>
+% if ( $conf->exists('invoice_latex') ) {
+
+ <A HREF="<% $p %>view/cust_bill-pdf.cgi?<% $link %>.pdf">View typeset invoice</A>
+ <BR><BR>
+% }
+% #false laziness with search/cust_bill_event.cgi
+% unless ( $templatename ) {
+
+
+ <% 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 .= ': '. encode_entities($cust_bill_event->statustext)
+% if $cust_bill_event->statustext;
+% my $part_bill_event = $cust_bill_event->part_bill_event;
+%
+
+ <TR>
+ <TD><% $part_bill_event->event %>
+% if ( $part_bill_event->templatename ) {
+% my $alt_templatename = $part_bill_event->templatename;
+% my $alt_link = "$alt_templatename-$invnum";
+%
+
+ ( <A HREF="<% $p %>view/cust_bill.cgi?<% $alt_link %>">view</A>
+ | <A HREF="<% $p %>view/cust_bill-pdf.cgi?<% $alt_link %>.pdf">view
+ typeset</A>
+ | <A HREF="<% $p %>misc/print-invoice.cgi?<% $alt_link %>">re-print</A>
+% if ( grep { $_ ne 'POST' }
+% $cust_bill->cust_main->invoicing_list ) {
+
+ | <A HREF="<% $p %>misc/email-invoice.cgi?<% $alt_link %>">re-email</A>
+% }
+% if ( $conf->exists('hylafax')
+% && length($cust_bill->cust_main->fax) ) {
+
+ | <A HREF="<% $p %>misc/fax-invoice.cgi?<% $alt_link %>">re-fax</A>
+% }
+
+
+ )
+% }
+
+
+ </TD>
+ <TD><% time2str("%a %b %e %T %Y", $cust_bill_event->_date) %></TD>
+ <TD><% $status %></TD>
+ </TR>
+% }
+
+
+ </TABLE>
+ <BR>
+% }
+% if ( $conf->exists('invoice_html') ) {
+
+ <% join('', $cust_bill->print_html('', $templatename) ) %>
+% } else {
+
+ <PRE><% join('', $cust_bill->print_text('', $templatename) ) %></PRE>
+% }
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('View invoices');
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^((.+)-)?(\d+)$/;
+my $templatename = $2;
+my $invnum = $3;
+
+my $conf = new FS::Conf;
+
+my @payby = grep /\w/, $conf->config('payby');
+#@payby = (qw( CARD DCRD CHEK DCHK LECB BILL CASH WEST COMP ))
+@payby = (qw( CARD DCRD CHEK DCHK LECB BILL CASH COMP ))
+ unless @payby;
+my %payby = map { $_=>1 } @payby;
+
+my $cust_bill = qsearchs({
+ 'select' => 'cust_bill.*',
+ 'table' => 'cust_bill',
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => { 'invnum' => $invnum },
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+});
+die "Invoice #$invnum not found!" unless $cust_bill;
+
+my $custnum = $cust_bill->custnum;
+
+#my $printed = $cust_bill->printed;
+
+my $link = $templatename ? "$templatename-$invnum" : $invnum;
+
+</%init>
+
+
diff --git a/httemplate/view/cust_main.cgi b/httemplate/view/cust_main.cgi
new file mode 100755
index 000000000..850b48b27
--- /dev/null
+++ b/httemplate/view/cust_main.cgi
@@ -0,0 +1,171 @@
+<% include("/elements/header.html","Customer View: ". $cust_main->name ) %>
+
+% if ( $curuser->access_right('Edit customer') ) {
+ <A HREF="<% $p %>edit/cust_main.cgi?<% $custnum %>">Edit this customer</A> |
+% }
+
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws_iframe.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws_draggable.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/iframecontentmws.js"></SCRIPT>
+
+<SCRIPT TYPE="text/javascript">
+function areyousure(href, message) {
+ if (confirm(message) == true)
+ window.location.href = href;
+}
+</SCRIPT>
+
+<SCRIPT TYPE="text/javascript">
+%
+%my $ban = '';
+%if ( $cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
+% $ban = '<BR><P ALIGN="center">'.
+% '<INPUT TYPE="checkbox" NAME="ban" VALUE="1"> Ban this customer\\\'s ';
+% if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
+% $ban .= 'credit card';
+% } elsif ( $cust_main->payby =~ /^(CHEK|DCHK)$/ ) {
+% $ban .= 'ACH account';
+% }
+%}
+%
+
+
+var confirm_cancel = '<FORM METHOD="POST" ACTION="<% $p %>misc/cust_main-cancel.cgi"> <INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>"> <BR><P ALIGN="center"><B>Permanently delete all services and cancel this customer?</B> <% $ban%><BR><P ALIGN="CENTER"> <INPUT TYPE="submit" VALUE="Cancel customer">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<INPUT TYPE="BUTTON" VALUE="Don\'t cancel" onClick="cClick()"> </FORM> ';
+
+</SCRIPT>
+% if ( $curuser->access_right('Cancel customer')
+% && $cust_main->ncancelled_pkgs
+% ) {
+%
+
+ <A HREF="javascript:void(0);" onClick="overlib(confirm_cancel, CAPTION, 'Confirm cancellation', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH, 576, HEIGHT, 128, TEXTSIZE, 3, BGCOLOR, '#ff0000', CGCOLOR, '#ff0000' ); return false; ">Cancel this customer</A> |
+% }
+% if ( $conf->exists('deletecustomers')
+% && $curuser->access_right('Delete customer')
+% ) {
+%
+
+ <A HREF="<% $p %>misc/delete-customer.cgi?<% $custnum%>">Delete this customer</A> |
+% }
+% unless ( $conf->exists('disable_customer_referrals') ) {
+
+ <A HREF="<% popurl(2) %>edit/cust_main.cgi?referral_custnum=<% $custnum %>">Refer a new customer</A> |
+ <A HREF="<% popurl(2) %>search/cust_main.cgi?referral_custnum=<% $custnum %>">View this customer's referrals</A>
+% }
+
+
+
+<BR><BR>
+%
+%my $signupurl = $conf->config('signupurl');
+%if ( $signupurl ) {
+%
+
+ This customer's signup URL: <A HREF="<% $signupurl %>?ref=<% $custnum %>"><% $signupurl %>?ref=<% $custnum %></A><BR><BR>
+% }
+
+
+<A NAME="cust_main"></A>
+<TABLE BORDER=0>
+<TR>
+ <TD VALIGN="top">
+ <% include('cust_main/contacts.html', $cust_main ) %>
+ </TD>
+ <TD VALIGN="top" STYLE="padding-left: 54px">
+ <% include('cust_main/misc.html', $cust_main ) %>
+% if ( $conf->config('payby-default') ne 'HIDE' ) {
+
+ <BR>
+ <% include('cust_main/billing.html', $cust_main ) %>
+% }
+
+ </TD>
+</TR>
+</TABLE>
+%
+%if ( $cust_main->comments =~ /[^\s\n\r]/ ) {
+%
+
+<BR>
+Comments
+<% ntable("#cccccc") %><TR><TD><% ntable("#cccccc",2) %>
+<TR>
+ <TD BGCOLOR="#ffffff">
+ <PRE><% encode_entities($cust_main->comments) %></PRE>
+ </TD>
+</TR>
+</TABLE></TABLE>
+% }
+<BR><BR>
+% my $notecount = scalar($cust_main->notes());
+% if ( ! $conf->exists('cust_main-disable_notes') || $notecount) {
+
+<A NAME="cust_main_note"><FONT SIZE="+2">Notes</FONT></A><BR>
+% if ( $curuser->access_right('Add customer note') &&
+% ! $conf->exists('cust_main-disable_notes')
+% ) {
+
+ <A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('<% $p %>edit/cust_main_note.cgi?custnum=<% $cust_main->custnum %>', 616, 386, 'cust_main_note_popup' ), CAPTION, 'Enter customer note', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK); return false;">Add customer note</A>
+
+% }
+
+<BR>
+
+% if ($notecount) {
+
+<iframe src="<% $p %>view/cust_main/notes.html?custnum=<% $cust_main->custnum %>" height="186" width="616" name="cust_main_notes" frameborder="0" marginborder="0" marginheight="0" scrolling="auto">
+ <div><br>[iframe not supported]<br><br></div>
+</iframe>
+
+% }else{ # make firefox happy wrt POSTDATA
+
+<iframe src="<% $p %>view/cust_main/notes.html?custnum=<% $cust_main->custnum %>" height="24" width="616" name="cust_main_notes" frameborder="0" marginborder="0" marginheight="0" scrolling="auto">
+ <div><br>[iframe not supported]<br><br></div>
+</iframe>
+
+% }
+
+% }
+
+
+% if ( $conf->config('ticket_system') ) {
+
+ <BR><BR>
+ <% include('cust_main/tickets.html', $cust_main ) %>
+% }
+
+
+<BR><BR>
+
+% #XXX enable me# if ( $curuser->access_right('View customer packages') {
+<% include('cust_main/packages.html', $cust_main ) %>
+% #}
+
+% if ( $conf->config('payby-default') ne 'HIDE' ) {
+ <% include('cust_main/payment_history.html', $cust_main ) %>
+% }
+
+
+<% include('/elements/footer.html') %>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('View customer');
+
+my $conf = new FS::Conf;
+
+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({
+ 'table' => 'cust_main',
+ 'hashref' => {'custnum'=>$custnum},
+ 'extra_sql' => ' AND '. $curuser->agentnums_sql,
+});
+die "Customer not found!" unless $cust_main;
+
+</%init>
diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html
new file mode 100644
index 000000000..1f80dc5bc
--- /dev/null
+++ b/httemplate/view/cust_main/billing.html
@@ -0,0 +1,192 @@
+%
+% my( $cust_main ) = @_;
+% my @invoicing_list = $cust_main->invoicing_list;
+% my $conf = new FS::Conf;
+% my $money_char = $conf->config('money_char') || '$';
+%
+
+
+Billing information
+% # If we can't see the unencrypted card, then bill now is an exercise in frustration
+%if ( ! $cust_main->is_encrypted($cust_main->payinfo) ) {
+ (<A HREF="<% $p %>misc/bill.cgi?<% $cust_main->custnum %>">Bill now</A>)
+% }
+
+<% ntable("#cccccc") %><TR><TD><% ntable("#cccccc",2) %>
+%
+%( my $balance = $cust_main->balance )
+% =~ s/^(\-?)(.*)$/<FONT SIZE=+1>$1<\/FONT>$money_char$2/;
+%
+
+
+<TR>
+ <TD ALIGN="right">Balance due</TD>
+ <TD BGCOLOR="#ffffff"><B><% $balance %></B></TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right">Billing&nbsp;type</TD>
+ <TD BGCOLOR="#ffffff">
+% if ( $cust_main->payby eq 'CARD' || $cust_main->payby eq 'DCRD' ) {
+
+
+ Credit&nbsp;card&nbsp;<% $cust_main->payby eq 'CARD' ? '(automatic)' : '(on-demand)' %>
+ </TD>
+</TR>
+<TR>
+ <TD ALIGN="right">Card number</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->paymask %></TD>
+</TR>
+%
+%#false laziness w/elements/select-month_year.html & edit/cust_main/billing.html
+%my( $mon, $year );
+%my $date = $cust_main->paydate || '12-2037';
+%if ( $date =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #PostgreSQL date format
+% ( $mon, $year ) = ( $2, $1 );
+%} elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
+% ( $mon, $year ) = ( $1, $3 );
+%} else {
+% warn "unrecognized expiration date format: $date";
+% ( $mon, $year ) = ( '', '' );
+%}
+%
+
+<TR>
+ <TD ALIGN="right">Expiration</TD>
+ <TD BGCOLOR="#ffffff"><% "$mon/$year" %></TD>
+</TR>
+% if ( $cust_main->paystart_month ) {
+
+ <TR>
+ <TD ALIGN="right">Start date</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->paystart_month. '/'. $cust_main->paystart_year %>
+ </TR>
+% } elsif ( $cust_main->payissue ) {
+
+ <TR>
+ <TD ALIGN="right">Issue #</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->payissue %>
+ </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 );
+%
+
+
+ Electronic&nbsp;check&nbsp;<% $cust_main->payby eq 'CHEK' ? '(automatic)' : '(on-demand)' %>
+ </TD>
+</TR>
+<TR>
+ <TD ALIGN="right">ABA/Routing code</TD>
+ <TD BGCOLOR="#ffffff"><% $aba %></TD>
+</TR>
+<TR>
+ <TD ALIGN="right">Account number</TD>
+ <TD BGCOLOR="#ffffff"><% 'x'x(length($account)-2). substr($account,(length($account)-2)) %></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";
+%
+
+
+ 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' ) {
+
+
+ Billing
+ </TD>
+</TR>
+% if ( $cust_main->payinfo ) {
+
+<TR>
+ <TD ALIGN="right">P.O. </TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->payinfo %></TD>
+</TR>
+% }
+
+
+<TR>
+ <TD ALIGN="right">Attention</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->payname %></TD>
+</TR>
+% } elsif ( $cust_main->payby eq 'COMP' ) {
+
+
+ Complimentary
+ </TD>
+</TR>
+<TR>
+ <TD ALIGN="right">Authorized&nbsp;by</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->payinfo %></TD>
+</TR>
+%
+%#false laziness w/above etc.
+%my( $mon, $year );
+%my $date = $cust_main->paydate || '12-2037';
+%if ( $date =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #PostgreSQL date format
+% ( $mon, $year ) = ( $2, $1 );
+%} elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
+% ( $mon, $year ) = ( $1, $3 );
+%} else {
+% warn "unrecognized expiration date format: $date";
+% ( $mon, $year ) = ( '', '' );
+%}
+%
+
+<TR>
+ <TD ALIGN="right">Expiration</TD>
+ <TD BGCOLOR="#ffffff"><% "$mon/$year" %></TD>
+</TR>
+% }
+
+
+<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">FAX&nbsp;invoices</TD>
+ <TD BGCOLOR="#ffffff">
+ <% ( grep { $_ eq 'FAX' } @invoicing_list ) ? 'yes' : 'no' %>
+ </TD>
+</TR>
+<TR>
+ <TD ALIGN="right">Email&nbsp;invoices</TD>
+ <TD BGCOLOR="#ffffff">
+ <% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) || 'no' %>
+ </TD>
+</TR>
+% if ( $conf->exists('voip-cust_cdr_spools') ) {
+
+ <TR>
+ <TD ALIGN="right">Spool&nbsp;CDRs</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->spool_cdr ? 'yes' : 'no' %></TD>
+ </TR>
+% }
+
+
+</TABLE></TD></TR></TABLE>
+
diff --git a/httemplate/view/cust_main/contacts.html b/httemplate/view/cust_main/contacts.html
new file mode 100644
index 000000000..d5788c9a4
--- /dev/null
+++ b/httemplate/view/cust_main/contacts.html
@@ -0,0 +1,100 @@
+% my %which = (
+% '' => 'Billing',
+% 'ship_' => 'Service',
+% );
+% foreach my $which ( '', 'ship_' ) {
+% my $pre = $cust_main->get("${which}last") ? $which : '';
+
+<% $which{$which} %> 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>
+% if ( $which eq '' && $conf->exists('show_ss') ) {
+ <TD ALIGN="right">SS#</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->ss || '&nbsp' %></TD>
+% }
+</TR>
+<TR>
+ <TD ALIGN="right">Company</TD>
+ <TD COLSPAN=7 BGCOLOR="#ffffff"><% $cust_main->get("${pre}company") %></TD>
+</TR>
+<TR>
+ <TD ALIGN="right">Address</TD>
+ <TD COLSPAN=7 BGCOLOR="#ffffff"><% $cust_main->get("${pre}address1") %></TD>
+</TR>
+% if ( $cust_main->get("${pre}address2") ) {
+
+<TR>
+ <TD ALIGN="right">&nbsp;</TD>
+ <TD COLSPAN=7 BGCOLOR="#ffffff"><% $cust_main->get("${pre}address2") %></TD>
+</TR>
+% }
+
+<TR>
+ <TD ALIGN="right">City</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->get("${pre}city") %></TD>
+% if ( $cust_main->get("${pre}county") ) {
+ <TD ALIGN="right">County</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->get("${pre}county") %></TD>
+% }
+ <TD ALIGN="right">State</TD>
+ <TD BGCOLOR="#ffffff"><% state_label( $cust_main->get("${pre}state"), $cust_main->get("${pre}country") ) %></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"><% code2country( $cust_main->get("${pre}country") ) %></TD>
+</TR>
+<TR>
+ <TD ALIGN="right"><% $daytime_label %></TD>
+ <TD COLSPAN=3 BGCOLOR="#ffffff">
+ <% include('/elements/phonenumber.html',
+ $cust_main->get("${pre}daytime"),
+ 'callable'=>1
+ )
+ %>
+ </TD>
+</TR>
+<TR>
+ <TD ALIGN="right"><% $night_label %></TD>
+ <TD COLSPAN=3 BGCOLOR="#ffffff">
+ <% include('/elements/phonenumber.html',
+ $cust_main->get("${pre}night"),
+ 'callable'=>1
+ )
+ %>
+ </TD>
+</TR>
+<TR>
+ <TD ALIGN="right">Fax</TD>
+ <TD COLSPAN=3 BGCOLOR="#ffffff">
+ <% $cust_main->get("${pre}fax") || '&nbsp' %>
+ </TD>
+</TR>
+</TABLE></TD></TR></TABLE>
+% if ( $which ne 'ship_' ) {
+<BR>
+% }
+% }
+
+<%once>
+
+my $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/
+ ? 'Day&nbsp;Phone'
+ : FS::Msgcat::_gettext('daytime');
+my $night_label = FS::Msgcat::_gettext('night') =~ /^(night)?$/
+ ? 'Night&nbsp;Phone'
+ : FS::Msgcat::_gettext('night');
+
+</%once>
+<%init>
+
+my( $cust_main ) = @_;
+my $conf = new FS::Conf;
+
+</%init>
+
diff --git a/httemplate/view/cust_main/misc.html b/httemplate/view/cust_main/misc.html
new file mode 100644
index 000000000..9528db243
--- /dev/null
+++ b/httemplate/view/cust_main/misc.html
@@ -0,0 +1,112 @@
+%
+% my( $cust_main ) = @_;
+% my $conf = new FS::Conf;
+% my $date_format = ($conf->config('date_format') || "%m/%d/%Y");
+%
+
+
+<% ntable("#cccccc") %><TR><TD><% &ntable("#cccccc",2) %>
+
+<TR>
+ <TD ALIGN="right">Customer&nbsp;number</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->custnum %></TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right">Status</TD>
+ <TD BGCOLOR="#ffffff"><FONT COLOR="#<% $cust_main->statuscolor %>"><B><% ucfirst($cust_main->status) %></B></FONT></TD>
+</TR>
+%
+% my @agents = qsearch( 'agent', {} );
+% my $agent;
+% unless ( scalar(@agents) == 1 ) {
+% $agent = qsearchs('agent',{ 'agentnum' => $cust_main->agentnum } );
+%
+
+
+<TR>
+ <TD ALIGN="right">Agent</TD>
+ <TD BGCOLOR="#ffffff"><% $agent->agentnum %>: <% $agent->agent %></TD>
+</TR>
+%
+% } else {
+% $agent = $agents[0];
+% }
+%
+% if ( $cust_main->agent_custid ) {
+%
+
+
+<TR>
+ <TD ALIGN="right">Agent customer ref#</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->agent_custid %></TD>
+</TR>
+%
+% }
+%
+% unless ( FS::part_referral->num_part_referral == 1 ) {
+% my $referral = qsearchs('part_referral', {
+% 'refnum' => $cust_main->refnum
+% } );
+%
+
+
+<TR>
+ <TD ALIGN="right">Advertising&nbsp;source</TD>
+ <TD BGCOLOR="#ffffff"><% $referral->refnum %>: <% $referral->referral%></TD>
+</TR>
+% }
+
+
+<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 } )
+% )
+% ) {
+%
+
+
+<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>
+% }
+
+
+ </TD>
+</TR>
+
+<TR>
+ <TD ALIGN="right">Order taker</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->otaker %></TD>
+</TR>
+
+ <TR>
+ <TD ALIGN="right">Signup Date</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->signupdate ? time2str($date_format, $cust_main->signupdate) : '' %></TD>
+ </TR>
+
+% if ( $conf->exists('cust_main-enable_birthdate') ) {
+% my $dt = DateTime->from_epoch(epoch => $cust_main->birthdate,
+% time_zone=>'floating',
+% );
+
+ <TR>
+ <TD ALIGN="right">Date of Birth</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->birthdate ne '' ? $dt->strftime($date_format) : '' %></TD>
+ </TR>
+
+% }
+
+</TABLE></TD></TR></TABLE>
+
diff --git a/httemplate/view/cust_main/notes.html b/httemplate/view/cust_main/notes.html
new file mode 100755
index 000000000..f2d116930
--- /dev/null
+++ b/httemplate/view/cust_main/notes.html
@@ -0,0 +1,93 @@
+%
+% my $conf = new FS::Conf;
+% my $curuser = $FS::CurrentUser::CurrentUser;
+%
+% $cgi->param('custnum') =~ /^(\d+)$/
+% or die "No customer specified (bad URL)!";
+% my $custnum = $1;
+%
+% my $cust_main = qsearchs('cust_main', {'custnum' => $custnum} );
+% die "Custimer not found!" unless $cust_main;
+%
+
+<STYLE TYPE="text/css">
+
+body { background: #e8e8e8 }
+.inv table { border: none }
+.inv TH { border: none }
+.inv TD { border: none }
+
+</STYLE>
+
+% my (@notes) = $cust_main->notes();
+% if ( scalar(@notes) ) {
+
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws_iframe.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws_crossframe.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/iframecontentmws.js"></SCRIPT>
+
+<TABLE CLASS="inv" CELLSPACING=0 CELLPADDING=0 BORDER=0 >
+
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor = '';
+%
+% foreach my $note (@notes) {
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+% my $pop = popurl(3);
+% my $notenum = $note->notenum;
+% my $clickjs = qq!onclick="overlib( OLiframeContent('${pop}edit/! .
+% qq!cust_main_note.cgi?custnum=$custnum&! .
+% qq!notenum=$notenum', 616, ! .
+% qq!386, 'cust_main_note_popup' ), CAPTION, 'Edit customer ! .
+% qq!note', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, ! .
+% qq!CLOSECLICK, FRAME, top); return false;"!;
+%
+% my ($el, $eel);
+% if ($curuser->access_right('Edit customer note') ) {
+% $el = qq!<A HREF="javascript:void(0);" $clickjs>!;
+% $eel = qq!</A>!;
+% }else{
+% $el = $eel = '';
+% }
+
+<TR>
+ <% note_datestr($note,$conf,$bgcolor, $el, $eel) %>
+ <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+ <% $el %> &nbsp;<%$note->otaker%>&nbsp; <% $eel %>
+ </TD>
+ <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+ &nbsp;<%$note->comments%>
+ </TD>
+</TR>
+
+% } #end display notes
+
+</TABLE>
+
+% }
+%
+%#subroutines
+%
+%sub note_datestr {
+% my($note, $conf, $bgcolor, $el, $eel) = @_ or return '';
+% my $format=qq{<TD class="inv" bgcolor="$bgcolor" align="left">$el<B>%b</B>$eel</TD>}.
+% qq{<TD class="inv" bgcolor="$bgcolor" align="right">$el<B>&nbsp;%o,</B>$eel</TD>}.
+% qq{<TD class="inv" bgcolor="$bgcolor" align="right">$el<B>&nbsp;%Y&nbsp;</B>$eel</TD>};
+% $format .= qq{<TD class="inv" bgcolor="$bgcolor" ALIGN="right">$el<B>&nbsp;%l$eel</TD>}.
+% qq{<TD class="inv" bgcolor="$bgcolor" ALIGN="center">$el<B>:</B>$eel</TD>}.
+% qq{<TD class="inv" bgcolor="$bgcolor" ALIGN="left">$el<B>%M</B>$eel</TD>}.
+% qq{<TD class="inv" bgcolor="$bgcolor" ALIGN="left">$el<B>&nbsp;%P&nbsp;</B>$eel</TD>}
+% if $conf->exists('cust_main_note-display_times');
+% ( my $strip = time2str($format, $note->_date) ) =~ s/ (\d)/$1/g;
+% $strip;
+% }
+%
+
diff --git a/httemplate/view/cust_main/order_pkg.html b/httemplate/view/cust_main/order_pkg.html
new file mode 100644
index 000000000..f48bf0929
--- /dev/null
+++ b/httemplate/view/cust_main/order_pkg.html
@@ -0,0 +1,41 @@
+%
+% my( $cust_main ) = @_;
+%
+
+
+<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="<% $cust_main->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 = '. $cust_main->agent->typenum.
+% ' AND type_pkgs.pkgpart = part_pkg.pkgpart )'.
+% ' ORDER BY pkg' # case ?
+% )
+%) {
+%
+
+
+ <OPTION VALUE="<% $part_pkg->pkgpart %>"><% $part_pkg->pkg %> - <% $part_pkg->comment %>
+% }
+
+
+</SELECT>
+
+<INPUT NAME="submit" TYPE="submit" VALUE="Order Package" disabled>
+
+</FORM>
diff --git a/httemplate/view/cust_main/packages.html b/httemplate/view/cust_main/packages.html
new file mode 100755
index 000000000..b54208b79
--- /dev/null
+++ b/httemplate/view/cust_main/packages.html
@@ -0,0 +1,545 @@
+% my( $cust_main ) = @_;
+% my $conf = new FS::Conf;
+%
+% my $curuser = $FS::CurrentUser::CurrentUser;
+%
+% my $packages = get_packages($cust_main, $conf);
+
+
+<A NAME="cust_pkg"><FONT SIZE="+2">Packages</FONT></A>
+% if ( $curuser->access_right('Order customer package') ) {
+
+ <% include('order_pkg.html', $cust_main ) %>
+% }
+% if ( $curuser->access_right('One-time charge')
+% && $conf->config('payby-default') ne 'HIDE'
+% ) {
+%
+
+ <% popup_link('edit/quick-charge.html?custnum='. $cust_main->custnum, 'One-time charge', 'One-time charge', 545) %>
+ <BR>
+% }
+% if ( $curuser->access_right('Bulk change customer packages') ) {
+
+ <A HREF="<% $p %>edit/cust_pkg.cgi?<% $cust_main->custnum %>">Bulk order and cancel packages</A> (preserves services)
+% }
+
+
+<BR><BR>
+% if ( @$packages ) {
+
+Current packages
+% }
+% if ( $cust_main->num_cancelled_pkgs ) {
+% if ( $cgi->param('showcancelledpackages') eq '0' #see if it was set by me
+% || ( $conf->exists('hidecancelledpackages')
+% && ! $cgi->param('showcancelledpackages')
+% )
+% )
+% {
+% $cgi->param('showcancelledpackages', 1);
+%
+
+ ( <a href="<% $cgi->self_url %>">show
+% } else {
+% $cgi->param('showcancelledpackages', 0);
+%
+
+ ( <a href="<% $cgi->self_url %>">hide
+% }
+
+ cancelled packages</a> )
+% }
+% if ( @$packages ) {
+
+
+<% include('/elements/table-grid.html') %>
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor = '';
+
+<TR>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Package</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Status</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Services</TH>
+</TR>
+
+%foreach my $cust_pkg (@$packages) {
+%
+% my $part_pkg = $cust_pkg->part_pkg;
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+
+
+<!--pkgnum: <% $cust_pkg->pkgnum %>-->
+<TR>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <A NAME="cust_pkg<% $cust_pkg->pkgnum %>"><% $cust_pkg->pkgnum %></A>:
+ <% $part_pkg->pkg %> - <% $part_pkg->comment %><BR>
+ <FONT SIZE=-1>
+% unless ( $cust_pkg->get('cancel') ) {
+% if ( $curuser->access_right('Change customer package') ) {
+
+ (&nbsp;<%pkg_change_link($cust_pkg)%>&nbsp;)
+% }
+% if ( $curuser->access_right('Edit customer package dates') ) {
+
+ (&nbsp;<%pkg_dates_link($cust_pkg)%>&nbsp;)
+% }
+% if ( $curuser->access_right('Customize customer package') ) {
+
+ (&nbsp;<%pkg_customize_link($cust_pkg,$cust_main->custnum)%>&nbsp;)
+% }
+% }
+
+ </FONT>
+ </TD>
+ <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+ <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
+%
+% sub myfreq {
+% my $part_pkg = shift;
+% my $freq = $part_pkg->freq_pretty;
+% $freq =~ s/ /&nbsp;/g;
+% $freq;
+% }
+%
+% #this should use cust_pkg->status and cust_pkg->statuscolor eventually
+%
+% my $colspan = $conf->exists('cust_pkg-display_times') ? 8 : 4;
+% my $width = $conf->exists('cust_pkg-display_times') ? '38%' : '56%';
+%
+% #false laziness w/edit/REAL_cust_pkg.cgi
+% my( $billed_or_prepaid, $last_bill_or_renewed, $next_bill_or_prepaid_until );
+% unless ( $part_pkg->is_prepaid ) {
+% $billed_or_prepaid = 'billed';
+% $last_bill_or_renewed = 'Last&nbsp;bill';
+% $next_bill_or_prepaid_until = 'Next&nbsp;bill';
+% } else {
+% $billed_or_prepaid = 'prepaid';
+% $last_bill_or_renewed = 'Renewed';
+% $next_bill_or_prepaid_until = 'Prepaid&nbsp;until';
+% }
+%
+%
+% if ( $cust_pkg->get('cancel') ) {
+ <!-- #status: cancelled -->
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right"><FONT COLOR="#ff0000"><B>Cancelled&nbsp;</B></FONT></TD>
+ <% pkg_datestr($cust_pkg, 'cancel', $conf) %>
+ </TR>
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right"><FONT COLOR="#ff0000" SIZE="-2">
+ <% $cust_pkg->last_reason ? $cust_pkg->last_reason->reason : '' %>
+ </FONT></TD>
+ </TR>
+% unless ( $cust_pkg->get('setup') ) {
+
+
+ <TR>
+ <TD COLSPAN=<%$colspan%>>Never billed</TD>
+ </TR>
+% } else {
+
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right">Setup&nbsp;</TD>
+ <% pkg_datestr($cust_pkg, 'setup', $conf) %>
+ </TR>
+% if ( $cust_pkg->get('last_bill') ) {
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right"><% $last_bill_or_renewed %>&nbsp;</TD>
+ <% pkg_datestr($cust_pkg, 'last_bill',$conf) %>
+ </TR>
+% }
+% if ( $cust_pkg->get('susp') ) {
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right">Suspended&nbsp;</TD>
+ <% pkg_datestr($cust_pkg, 'susp', $conf) %>
+ </TR>
+% }
+% }
+% } else {
+% if ( $cust_pkg->get('susp') ) {
+ <!-- #status: suspended -->
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right"><FONT COLOR="#FF9900"><B>Suspended</B>&nbsp;</FONT></TD>
+ <% pkg_datestr($cust_pkg, 'susp', $conf) %>
+ </TR>
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right"><FONT COLOR="#FF9900" SIZE="-2">
+ <% $cust_pkg->last_reason ? $cust_pkg->last_reason->reason : '' %>
+ </FONT></TD>
+ </TR>
+% unless ( $cust_pkg->get('setup') ) {
+
+
+ <TR>
+ <TD COLSPAN=<%$colspan%>>Never billed</TD>
+ </TR>
+% } else {
+
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right">Setup&nbsp;</TD>
+ <% pkg_datestr($cust_pkg, 'setup', $conf) %>
+ </TR>
+% }
+% if ( $cust_pkg->get('last_bill') ) {
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right"><% $last_bill_or_renewed %>&nbsp;</TD>
+ <% pkg_datestr($cust_pkg, 'last_bill', $conf) %>
+ </TR>
+% }
+
+
+ <!-- # next bill ?? -->
+% if ( $cust_pkg->get('expire') ) {
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right">Expires&nbsp;</TD>
+ <% pkg_datestr($cust_pkg, 'expire', $conf) %>
+ </TR>
+% }
+
+
+ <TR>
+ <TD COLSPAN=<%$colspan%>>
+ <FONT SIZE=-1>
+% if ( $curuser->access_right('Unsuspend customer package') ) {
+
+ (&nbsp;<% pkg_unsuspend_link($cust_pkg) %>&nbsp;)
+% }
+% if ( $curuser->access_right('Cancel customer package immediately') ) {
+
+ (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
+% }
+
+ </FONT>
+ </TD>
+ </TR>
+% } else {
+ <!-- #status: active -->
+% unless ( $cust_pkg->get('setup') ) {
+ <!-- #not setup -->
+% unless ( $part_pkg->freq ) {
+
+
+ <TR>
+ <TD COLSPAN=<%$colspan%>>Not&nbsp;yet&nbsp;billed&nbsp;(one-time&nbsp;charge)</TD>
+ </TR>
+
+ <TR>
+ <TD COLSPAN=<%$colspan%>>
+ <FONT SIZE=-1>
+% if ( $curuser->access_right('Cancel customer package immediately') ) {
+
+ (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
+% }
+
+ </FONT>
+ </TD>
+ </TR>
+% } else {
+
+
+ <TR>
+ <TD COLSPAN=<%$colspan%>>Not&nbsp;yet&nbsp;billed&nbsp;(<% $billed_or_prepaid %>&nbsp;<% myfreq($part_pkg) %>)</TD>
+ </TR>
+% }
+% } else {
+ <!-- #setup -->
+% unless ( $part_pkg->freq ) {
+
+
+ <TR>
+ <TD COLSPAN=<%$colspan%>>One-time&nbsp;charge</TD>
+ </TR>
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right">Billed&nbsp;</TD>
+ <% pkg_datestr($cust_pkg, 'setup', $conf) %>
+ </TR>
+% } else {
+
+
+ <TR>
+ <TD COLSPAN=<%$colspan%>><FONT COLOR="#00CC00"><B>Active</B></FONT>,&nbsp;<% $billed_or_prepaid %>&nbsp;<% myfreq($part_pkg) %></TD>
+ </TR>
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right">Setup&nbsp;</TD>
+ <% pkg_datestr($cust_pkg, 'setup', $conf) %>
+ </TR>
+% }
+% }
+% if ( $cust_pkg->get('last_bill') ) {
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right"><% $last_bill_or_renewed %>&nbsp;</TD>
+ <% pkg_datestr($cust_pkg, 'last_bill', $conf) %>
+ </TR>
+% }
+% if ( $cust_pkg->get('bill') ) { #next bill
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right"><% $next_bill_or_prepaid_until %>&nbsp;</TD>
+ <% pkg_datestr($cust_pkg, 'bill', $conf) %>
+ </TR>
+% }
+% if ( $cust_pkg->get('expire') ) {
+
+ <TR>
+ <TD WIDTH="<%$width%>" ALIGN="right">Expires&nbsp;</TD>
+ <% pkg_datestr($cust_pkg, 'expire', $conf) %>
+ </TR>
+% }
+% if ( $part_pkg->freq ) {
+
+ <TR>
+ <TD COLSPAN=<%$colspan%>>
+ <FONT SIZE=-1>
+% if ( $curuser->access_right('Suspend customer package') ) {
+
+ (&nbsp;<% pkg_suspend_link($cust_pkg) %>&nbsp;)
+% }
+% if ( $curuser->access_right('Cancel customer package immediately') ) {
+
+ (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
+% }
+% if ( $curuser->access_right('Cancel customer package later') ) {
+
+ (&nbsp;<% pkg_expire_link($cust_pkg) %>&nbsp;)
+% }
+
+ <FONT>
+ </TD>
+ </TR>
+% }
+% }
+% }
+
+
+</TABLE>
+</TD>
+
+<TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+ <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
+
+% #foreach my $svcpart (sort {$a->{svcpart} <=> $b->{svcpart}} @{$pkg->{svcparts}}) {
+% foreach my $part_svc ( $cust_pkg->part_svc ) {
+
+% #foreach my $service (@{$svcpart->{services}}) {
+% foreach my $cust_svc ( @{ $part_svc->cust_pkg_svc } ) {
+
+ <TR>
+ <TD ALIGN="right" VALIGN="top"><% FS::UI::Web::svc_link($m, $part_svc, $cust_svc) %></TD>
+ <TD STYLE="padding-bottom:0px"><B><% FS::UI::Web::svc_label_link($m, $part_svc, $cust_svc) %></B></TD>
+ </TR>
+
+ <TR>
+ <TD ALIGN="right" VALIGN="top" STYLE="padding-bottom:5px;padding-top:0px"><FONT SIZE="-2">
+
+% if ( $curuser->access_right('Recharge customer service')
+% && $part_svc->svcdb eq 'svc_acct'
+% && ( $cust_svc->svc_x->seconds ne ''
+% || $cust_svc->svc_x->upbytes ne ''
+% || $cust_svc->svc_x->downbytes ne ''
+% || $cust_svc->svc_x->totalbytes ne ''
+% )
+% ) {
+ (&nbsp;<%svc_recharge_link($cust_svc)%>&nbsp;)
+% }
+ </FONT></TD>
+
+ <TD ALIGN="right" VALIGN="top" STYLE="padding-bottom:5px;padding-top:0px"><FONT SIZE="-2">
+
+% if ( $curuser->access_right('Unprovision customer service') ) {
+ (&nbsp;<%svc_unprovision_link($cust_svc)%>&nbsp;)
+% }
+ </FONT></TD>
+ </TR>
+% }
+
+% if ( ! $cust_pkg->get('cancel')
+% && $curuser->access_right('Provision customer service')
+% && $part_svc->num_avail
+% ) {
+
+ <TR>
+ <TD COLSPAN=2 ALIGN="center" STYLE="padding-bottom:4px;padding-top:0px">
+ <B><% svc_provision_link($cust_pkg, $part_svc, $conf, $curuser) %></B>
+ </TD>
+ </TR>
+
+% }
+
+% }
+
+</TABLE>
+</TD>
+% } #end display packages
+%
+
+
+</TABLE>
+% } else {
+
+<BR>
+% }
+%
+%#subroutines
+%
+%sub get_packages {
+% my $cust_main = shift or return undef;
+% my $conf = shift;
+%
+% my @packages = ();
+% my $method;
+% if ( $cgi->param('showcancelledpackages') eq '0' #see if it was set by me
+% || ( $conf->exists('hidecancelledpackages')
+% && ! $cgi->param('showcancelledpackages') )
+% )
+% {
+% $method = 'ncancelled_pkgs';
+% } else {
+% $method = 'all_pkgs';
+% }
+%
+% [ $cust_main->$method() ];
+%}
+%
+%sub svc_provision_link {
+% my ($cust_pkg, $part_svc, $conf, $curuser) = @_;
+% ( my $svc_nbsp = $part_svc->svc ) =~ s/\s+/&nbsp;/g;
+% my $num_avail = $part_svc->num_avail;
+% my $pkgnum_svcpart = "pkgnum=". $cust_pkg->pkgnum. ';'.
+% "svcpart=". $part_svc->svcpart;
+% my $url;
+% if ( $part_svc->svcdb eq 'svc_external' #could be generalized
+% && $conf->exists('svc_external-skip_manual')
+% ) {
+% $url = "${p}edit/process/". $part_svc->svcdb. ".cgi?$pkgnum_svcpart";
+% } else {
+% $url = FS::UI::Web::svc_url(
+% 'm' => $m,
+% 'action' => 'edit',
+% 'part_svc' => $part_svc,
+% 'query' => $pkgnum_svcpart,
+% );
+% #$url = "${p}edit/$svcpart->{svcdb}.cgi?$pkgnum_svcpart";
+% }
+%
+% my $link = qq!<A CLASS="provision" HREF="$url">!.
+% "Provision&nbsp;$svc_nbsp&nbsp;($num_avail)</A>";
+% if ( $conf->exists('legacy_link')
+% && $curuser->access_right('View/link unlinked services')
+% )
+% {
+% $link .= '<BR>'.
+% qq!<A CLASS="provision" HREF="${p}misc/link.cgi?!.
+% qq!$pkgnum_svcpart">!.
+% "Link&nbsp;to&nbsp;legacy&nbsp;$svc_nbsp&nbsp;($num_avail)</A>";
+% }
+% $link;
+%}
+%
+%sub svc_unprovision_link {
+% my $cust_svc = shift or return '';
+% qq!<A HREF="javascript:areyousure('${p}misc/unprovision.cgi?!. $cust_svc->svcnum.
+% qq!', 'Permanently unprovision and delete this service?')">Unprovision</A>!;
+%}
+%
+%sub pkg_datestr {
+% my($cust_pkg, $field, $conf) = @_ or return '';
+% return '&nbsp;' unless $cust_pkg->get($field);
+% my $format = '<TD align="left"><B>%b</B></TD>'.
+% '<TD align="right"><B>&nbsp;%o,</B></TD>'.
+% '<TD align="right"><B>&nbsp;%Y</B></TD>';
+% #$format .= '&nbsp;<FONT SIZE=-3>%l:%M:%S%P&nbsp;%z</FONT>'
+% $format .= '<TD ALIGN="right"><B>&nbsp;%l</TD>'.
+% '<TD ALIGN="center"><B>:</B></TD>'.
+% '<TD ALIGN="left"><B>%M</B></TD>'.
+% '<TD ALIGN="left"><B>&nbsp;%P</B></TD>'
+% if $conf->exists('cust_pkg-display_times');
+% my $strip = time2str($format, $cust_pkg->get($field) );
+% $strip =~ s/ (\d)/$1/g;
+% $strip;
+%}
+%
+%sub pkg_change_link { pkg_link('misc/change_pkg', 'Change&nbsp;package', @_ ); }
+%
+%sub pkg_suspend_link { pkg_popup_link( 'misc/cancel_pkg.html?method=suspend',
+% 'Suspend',
+% 'Suspend',
+% @_
+% );
+% }
+%
+%sub pkg_unsuspend_link { pkg_link('misc/unsusp_pkg', 'Unsuspend', @_ ); }
+%sub pkg_expire_link { pkg_link('misc/expire_pkg', 'Cancel&nbsp;later', @_ ); }
+%sub pkg_dates_link { pkg_link('edit/REAL_cust_pkg', 'Edit&nbsp;dates', @_ ); }
+%
+%sub pkg_cancel_link { pkg_popup_link( 'misc/cancel_pkg.html?method=cancel',
+% 'Cancel&nbsp;now',
+% 'Cancel',
+% @_
+% );
+% }
+%sub pkg_expire_link { pkg_popup_link( 'misc/cancel_pkg.html?method=expire',
+% 'Cancel&nbsp;later',
+% 'Expire', #"Cancel package $num later"
+% @_
+% );
+% }
+%
+%sub svc_recharge_link { svc_popup_link( 'misc/recharge_svc.html',
+% 'Recharge',
+% 'Recharge',
+% @_
+% );
+% }
+%
+%sub pkg_link {
+% my($action, $label, $cust_pkg) = @_;
+% return '' unless $cust_pkg;
+% qq!<a href="${p}$action.cgi?!. $cust_pkg->pkgnum. qq!">$label</a>!;
+%}
+%
+%sub pkg_popup_link {
+% my($action, $label, $actionlabel, $cust_pkg) = @_;
+% $action .= '&pkgnum='. $cust_pkg->pkgnum;
+% $actionlabel .= ' package '. $cust_pkg->pkgnum;
+% popup_link($action, $label, $actionlabel, 392);
+%}
+%
+%sub svc_popup_link {
+% my($action, $label, $actionlabel, $cust_svc) = @_;
+% $action .= '?svcnum='. $cust_svc->svcnum;
+% $actionlabel .= ' service '. $cust_svc->svcnum;
+% popup_link($action, $label, $actionlabel, 392);
+%}
+%
+%sub popup_link {
+% my($action, $label, $actionlabel, $width) = @_;
+% qq!<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('$p$action', $width, 336, 'pkg_or_svc_action_popup' ), CAPTION, '$actionlabel', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">$label</A>!;
+%}
+%
+%sub pkg_customize_link {
+% my $cust_pkg = shift or return '';
+% my $custnum = $cust_pkg->custnum;
+% qq!<A HREF="${p}edit/part_pkg.cgi?!.
+% "keywords=$custnum;".
+% "clone=". $cust_pkg->part_pkg->pkgpart. ';'.
+% "pkgnum=". $cust_pkg->pkgnum.
+% qq!">Customize</A>!;
+%}
diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html
new file mode 100644
index 000000000..b2d98cc55
--- /dev/null
+++ b/httemplate/view/cust_main/payment_history.html
@@ -0,0 +1,605 @@
+<BR><BR><A NAME="history"><FONT SIZE="+2">Payment History</FONT></A><BR>
+
+% my $s = 0;
+% if ( $payby{'BILL'} && $curuser->access_right('Post payment') ) {
+ <% $s++ ? ' | ' : '' %>
+ <A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('<% $p %>edit/cust_pay.cgi?popup=1;payby=BILL;custnum=<% $custnum %>', 392, 336, 'cust_pay_popup' ), CAPTION, 'Enter check payment', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">Enter check payment</A>
+% }
+
+% if ( $payby{'CASH'} && $curuser->access_right('Post payment') ) {
+ <% $s++ ? ' | ' : '' %>
+ <A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('<% $p %>edit/cust_pay.cgi?popup=1;payby=CASH;custnum=<% $custnum %>', 392, 336, 'cust_pay_popup' ), CAPTION, 'Enter cash payment', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">Enter cash payment</A>
+% }
+
+% if ( $payby{'WEST'} && $curuser->access_right('Post payment') ) {
+ <% $s++ ? ' | ' : '' %>
+ <A HREF="<% $p %>edit/cust_pay.cgi?payby=WEST;custnum=<% $custnum %>">Enter Western Union payment</A>
+% }
+
+% if ( ( $payby{'CARD'} || $payby{'DCRD'} )
+% && $curuser->access_right('Process payment')
+% && ! $cust_main->is_encrypted($cust_main->payinfo)
+% ) {
+ <% $s++ ? ' | ' : '' %>
+ <A HREF="<% $p %>misc/payment.cgi?payby=CARD;custnum=<% $custnum %>">Process credit card payment</A>
+% }
+
+% if ( ( $payby{'CHEK'} || $payby{'DCHK'} )
+% && $curuser->access_right('Process payment')
+% && ! $cust_main->is_encrypted($cust_main->payinfo)
+% ) {
+ <% $s++ ? ' | ' : '' %>
+ <A HREF="<% $p %>misc/payment.cgi?payby=CHEK;custnum=<% $custnum %>">Process electronic check (ACH) payment</A>
+% }
+
+% if ( $payby{'MCRD'} && $curuser->access_right('Post payment') ) {
+ <% $s++ ? ' | ' : '' %>
+ <A HREF="<% $p %>edit/cust_pay.cgi?payby=MCRD;custnum=<% $custnum %>">Post manual (offline) credit card payment</A>
+% }
+
+<BR>
+
+% if ( $curuser->access_right('Post credit') ) {
+ <A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('<% $p %>edit/cust_credit.cgi?<% $custnum %>', 392, 336, 'cust_credit_popup' ), CAPTION, 'Enter credit', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">Enter credit</A>
+ <BR>
+% }
+
+% if ( $curuser->access_right('View customer tax exemptions') ) {
+ <A HREF="<% $p %>search/cust_tax_exempt_pkg.cgi?custnum=<% $custnum %>">View tax exemptions</A>
+ <BR>
+% }
+
+% if ( $conf->exists('batch-enable')
+% #&& $curuser->access_right('View customer tax exemptions')
+% ) {
+ View batched payments:
+% foreach my $status (qw( Queued In-transit Complete All )) {
+ <A HREF="<% $p %>search/cust_pay_batch.cgi?status=<% $status{$status} %>;custnum=<% $custnum %>"><% $status %></A>
+ <% $status ne 'All' ? '|' : '' %>
+% }
+ <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;
+% my $link = $curuser->access_right('View invoices')
+% ? qq!<A HREF="${p}view/cust_bill.cgi?$invnum">!
+% : '';
+% push @history, {
+% 'date' => $cust_bill->_date,
+% 'desc' => $link. $pre.
+% "Invoice #$invnum (Balance \$". $cust_bill->owed. ')'.
+% $post. ( $link ? '</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;
+% if ( $payby eq 'CARD' ) {
+% $payinfo = $cust_pay->paymask;
+% } elsif ( $payby eq 'CHEK' && $cust_pay->payinfo =~ /^(\d+)\@(\d+)$/ ) {
+% $payinfo = "ABA $2, Acct# $1";
+% } else {
+% $payinfo = $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/^PREP$/Prepaid card /;
+% $payby =~ s/^CARD$/Credit card #/;
+% $payby =~ s/^COMP$/Complimentary by /;
+% $payby =~ s/^CASH$/Cash/;
+% $payby =~ s/^WEST$/Western Union/;
+% $payby =~ s/^MCRD$/Manual credit card/;
+% $payby =~ s/^BILL$//;
+% 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="javascript:void(0);" onClick="overlib( OLiframeContent('${p}edit/cust_bill_pay.cgi?!.
+% $cust_pay->paynum.
+% qq!', 392, 336, 'cust_bill_pay_popup' ), CAPTION, 'Apply payment', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">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="javascript:void(0);" onClick="overlib( OLiframeContent('${p}edit/cust_bill_pay.cgi?!.
+% $cust_pay->paynum.
+% qq!', 392, 336, 'cust_bill_pay_popup' ), CAPTION, 'Apply payment', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">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
+% && $curuser->access_right('Refund payment')
+% ) {
+% $refund = qq! (<A HREF="${p}edit/cust_refund.cgi?payby=$1;!.
+% qq!paynum=!. $cust_pay->paynum. '"'.
+% qq! TITLE="Send a refund for this payment to the payment gateway"!.
+% qq!>refund</A>)!;
+% }
+%
+% my $void = '';
+% if ( $cust_pay->closed !~ /^Y/i
+% && ( ( $cust_pay->payby eq 'CARD'
+% && $curuser->access_right('Credit card void')
+% )
+% || ( $cust_pay->payby eq 'CHEK'
+% && $curuser->access_right('Echeck void')
+% )
+% || ( $cust_pay->payby !~ /^(CARD|CHEK)$/
+% && $curuser->access_right('Regular void')
+% )
+% )
+% )
+% {
+% $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! TITLE="Void this payment from the database!.
+% ( $cust_pay->payby =~ /^(CARD|CHEK)$/
+% ? ' (do not send anything to the payment gateway)'
+% : ''
+% ). '"'.
+% qq!>void</A>)!;
+% }
+%
+% my $delete = '';
+% if ( $cust_pay->closed !~ /^Y/i
+% && $conf->exists('deletepayments')
+% && $curuser->access_right('Delete payment')
+% )
+% {
+% $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! TITLE="Delete this payment from the database completely - not recommended"!.
+% qq!>delete</A>)!;
+% }
+%
+% my $unapply = '';
+% if ( $cust_pay->closed !~ /^Y/i
+% && scalar(@cust_bill_pay)
+% && $curuser->access_right('Unapply payment')
+% )
+% {
+% $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! TITLE="Keep this payment, but dissociate it from the invoices it is currently applied against"!.
+% 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->paymask
+% : $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)" : '';
+%
+% my $unvoid = '';
+% if ( $cust_pay_void->closed !~ /^Y/i
+% && $curuser->access_right('Unvoid')
+% )
+% {
+% $unvoid = qq! (<A HREF="javascript:areyousure('!.
+% qq!${p}misc/unvoid-cust_pay_void.cgi?!. $cust_pay_void->paynum.
+% qq!', 'Are you sure you want to unvoid this payment?')"!.
+% qq! TITLE="Unvoid this payment from the database!.
+% ( $cust_pay_void->payby =~ /^(CARD|CHEK)$/
+% ? ' (do not send anything to the payment gateway)'
+% : ''
+% ). '"'.
+% qq!>unvoid</A>)!;
+% }
+%
+% 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>'. $unvoid,
+% '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="javascript:void(0);" onClick="overlib( OLiframeContent('${p}edit/cust_credit_bill.cgi?!.
+% $cust_credit->crednum.
+% qq!', 392, 336, 'cust_credit_bill_popup' ), CAPTION, 'Apply credit', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">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="javascript:void(0);" onClick="overlib( OLiframeContent('${p}edit/cust_credit_bill.cgi?!.
+% $cust_credit->crednum.
+% qq!', 392, 336, 'cust_credit_bill_popup' ), CAPTION, 'Apply credit', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">apply</A>)!.
+% '<BR>';
+% }
+% }
+%#
+% my $delete = '';
+% if ( $cust_credit->closed !~ /^Y/i
+%
+% #s'pose deleting a credit isn't bad like deleting a payment
+% # and this needs to be generally available until we have credit voiding..
+% #&& $conf->exists('deletecredits')
+%
+% && $curuser->access_right('Delete credit')
+% )
+% {
+% $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
+% && scalar(@cust_credit_bill)
+% && $curuser->access_right('Unapply credit')
+% )
+% {
+% $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
+% ? ' ('. $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->paymask
+% : $cust_refund->payinfo;
+%
+% $payby =~ s/^BILL$/Check #/ if $payinfo;
+% $payby =~ s/^CHEK$/Electronic check /;
+% $payby =~ s/^(CARD|COMP)$/$1 /;
+%
+% my $delete = '';
+% if ( $cust_refund->closed !~ /^Y/i
+% && $conf->exists('deleterefunds')
+% && $curuser->access_right('Delete refund')
+% )
+% {
+% $delete = qq! (<A HREF="javascript:areyousure('!.
+% qq!${p}misc/delete-cust_refund.cgi?!. $cust_refund->refundnum.
+% qq!', 'Are you sure you want to delete this refund?')"!.
+% qq! TITLE="Delete this refund from the database completely - not recommended"!.
+% qq!>delete</A>)!;
+% }
+%
+% push @history, {
+% 'date' => $cust_refund->_date,
+% 'desc' => "Refund ($payby$payinfo) by ". $cust_refund->otaker. "<BR>".
+% $delete,
+% 'refund' => $cust_refund->refund,
+% };
+%
+%}
+%
+%
+
+
+<% include("/elements/table-grid.html") %>
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor = '';
+%
+
+
+<TR>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Date</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Description</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>Charge</FONT></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>Payment</FONT></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>In-house<BR>Credit</FONT></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>Refund</FONT></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>Balance</FONT></TH>
+</TR>
+%
+%#display payment history
+%
+%sub balance_forward_row {
+% my( $b, $date ) = @_;
+% my $conf = new FS::Conf;
+% my $money_char = $conf->config('money_char') || '$';
+% ( my $balance_forward = $money_char. $b ) =~ s/^\$\-/-&nbsp;\$/;
+
+ <TR ID="balance_forward_row">
+ <TD CLASS="grid" BGCOLOR="#dddddd">
+ <% time2str("%D",$date) %>
+ </TD>
+
+ <TD CLASS="grid" BGCOLOR="#dddddd">
+ <I>Starting balance on <% time2str("%D",$date) %></I>
+ (<A HREF="javascript:void(0);" onClick="show_history();">show prior history</A>)
+ </TD>
+
+ <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+ <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+ <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+ <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+ <TD CLASS="grid" BGCOLOR="#dddddd"><I><% $balance_forward %></I></TD>
+
+ </TR>
+%}
+%
+%my $balance = 0;
+%my %target = ();
+%my $money_char = $conf->config('money_char') || '$';
+%
+%my $years = $conf->config('payment_history-years') || 2;
+%my $older_than = time - $years * 31556736; #60*60*24*365.24
+%my $hidden = 0;
+%my $seen = 0;
+%my $old_history = 0;
+%my $lastdate = 0;
+%
+%foreach my $item ( sort { $a->{'date'} <=> $b->{'date'} } @history ) {
+%
+% $lastdate = $item->{'date'};
+%
+% my $display;
+% if ( $item->{'date'} < $older_than ) {
+% $display = ' STYLE="display:none" ';
+% $hidden = 1;
+% } else {
+%
+% $display = '';
+%
+% if ( $hidden && ! $seen++ ) {
+% balance_forward_row($balance, $item->{'date'});
+% }
+%
+% }
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+% my $charge = exists($item->{'charge'})
+% ? sprintf("$money_char\%.2f", $item->{'charge'})
+% : '';
+%
+% my $payment = exists($item->{'payment'})
+% ? sprintf("-&nbsp;$money_char\%.2f", $item->{'payment'})
+% : '';
+%
+% $payment ||= sprintf( "<DEL>-&nbsp;$money_char\%.2f</DEL>",
+% $item->{'void_payment'}
+% )
+% if exists($item->{'void_payment'});
+%
+% my $credit = exists($item->{'credit'})
+% ? sprintf("-&nbsp;$money_char\%.2f", $item->{'credit'})
+% : '';
+%
+% my $refund = exists($item->{'refund'})
+% ? sprintf("$money_char\%.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 = $money_char. $balance ) =~ s/^\$\-/-&nbsp;\$/;
+%
+%
+
+
+ <TR <% $display ? $display.' ID="old_history'.$old_history++.'"' : ''%>>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+% unless ( !$target || $target{$target}++ ) {
+
+ <A NAME="<% $target %>">
+% }
+
+ <% time2str("%D",$item->{'date'}) %>
+% if ( $target && $target{$target} == 1 ) {
+
+ </A>
+% }
+
+ </FONT>
+ </TD>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $item->{'desc'} %>
+ </TD>
+ <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $charge %>
+ </TD>
+ <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $payment %>
+ </TD>
+ <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $credit %>
+ </TD>
+ <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $refund %>
+ </TD>
+ <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <% $showbalance %>
+ </TD>
+ </TR>
+% }
+
+%if ( scalar(@history) && $hidden && ! $seen++ ) {
+% balance_forward_row($balance, $lastdate);
+%}
+
+</TABLE>
+
+<SCRIPT TYPE="text/javascript">
+
+function show_history () {
+ //alert('showing history!');
+
+ var balance_forward_row = document.getElementById('balance_forward_row');
+
+ balance_forward_row.style.display = 'none';
+ for ( var i = 0; i < <% $old_history %>; i++ ) {
+ var oldRow = document.getElementById('old_history'+i);
+ oldRow.style.display = '';
+ }
+
+}
+
+</SCRIPT>
+
+<%init>
+
+my( $cust_main ) = @_;
+my $custnum = $cust_main->custnum;
+
+my $conf = new FS::Conf;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+my @payby = grep /\w/, $conf->config('payby');
+#@payby = (qw( CARD DCRD CHEK DCHK LECB BILL CASH WEST COMP ))
+@payby = (qw( CARD DCRD CHEK DCHK LECB BILL CASH COMP ))
+ unless @payby;
+my %payby = map { $_=>1 } @payby;
+
+my %status = (
+ 'Queued' => 'O', #Open
+ 'In-transit' => 'I',
+ 'Complete' => 'R', #Resolved
+ 'All' => '',
+);
+
+</%init>
diff --git a/httemplate/view/cust_main/quick-charge.html b/httemplate/view/cust_main/quick-charge.html
new file mode 100644
index 000000000..06ffd75e6
--- /dev/null
+++ b/httemplate/view/cust_main/quick-charge.html
@@ -0,0 +1,73 @@
+<SCRIPT TYPE="text/javascript">
+
+function enable_quick_charge () {
+ //alert('enable_quick_charge ' + document.QuickChargeForm.amount.value + ' - ' + document.QuickChargeForm.pkg.value );
+ if ( document.QuickChargeForm.amount.value
+ && document.QuickChargeForm.pkg.value ) {
+ document.QuickChargeForm.submit.disabled = false;
+ } else {
+ document.QuickChargeForm.submit.disabled = true;
+ }
+}
+
+function enable_quick_charge_desc () {
+ //alert('enable_quick_charge ' + document.QuickChargeForm.amount.value + ' - ' + document.QuickChargeForm.pkg.value );
+ if ( document.QuickChargeForm.amount.value ) {
+ document.QuickChargeForm.submit.disabled = false;
+ } else {
+ document.QuickChargeForm.submit.disabled = true;
+ }
+}
+
+function enable_quick_charge_amount () {
+ //alert('enable_quick_charge ' + document.QuickChargeForm.amount.value + ' - ' + document.QuickChargeForm.pkg.value );
+ if ( document.QuickChargeForm.pkg.value ) {
+ document.QuickChargeForm.submit.disabled = false;
+ } else {
+ document.QuickChargeForm.submit.disabled = true;
+ }
+}
+
+function validate_quick_charge () {
+ //alert('validate_quick_charge');
+ var pkg = document.QuickChargeForm.pkg.value;
+ var pkg_regex = /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]]+)$/ ;
+ var amount = document.QuickChargeForm.amount.value;
+ var amount_regex = /^\s*\$?\s*(\d+(\.\d{1,2})?)\s*$/ ;
+
+ if ( amount_regex.test(amount) && pkg_regex.test(pkg) ) {
+ return true;
+ } else if ( amount_regex.test(amount) ) {
+ if ( pkg ) {
+ alert('Illegal description - spaces, letters, numbers, and the following punctuation characters are allowed: . , ! ? @ # $ % & ( ) - + ; : ' + "'" + ' " = [ ]' );
+ } else {
+ alert('Enter a description for the one-time charge');
+ }
+ return false;
+ } else {
+ alert('Illegal amount - enter an amount to charge, for example, "5" or "43" or "21.46".');
+ return false;
+ }
+}
+
+</SCRIPT>
+
+<FORM NAME="QuickChargeForm" ACTION="<%$p%>edit/process/quick-charge.cgi" METHOD="POST" onSubmit="return validate_quick_charge()">
+
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $cust_main->custnum %>">
+
+Description:<INPUT TYPE="text" NAME="pkg" onChange="enable_quick_charge()" onKeyPress="enable_quick_charge_desc()">
+
+Amount:<INPUT TYPE="text" NAME="amount" SIZE=6 onChange="enable_quick_charge()" onKeyPress="enable_quick_charge_amount()">
+
+<% include('/elements/select-taxclass.html') %>
+
+<INPUT NAME="submit" TYPE="submit" VALUE="One-time charge" DISABLED>
+
+</FORM>
+
+<%init>
+
+my( $cust_main ) = @_;
+
+</%init>
diff --git a/httemplate/view/cust_main/tickets.html b/httemplate/view/cust_main/tickets.html
new file mode 100644
index 000000000..84cc90299
--- /dev/null
+++ b/httemplate/view/cust_main/tickets.html
@@ -0,0 +1,78 @@
+%
+% my( $cust_main ) = @_;
+%
+% my $conf = new FS::Conf;
+% my $num = $conf->config('cust_main-max_tickets') || 10;
+%
+% my @tickets = ();
+% unless ( $conf->config('ticket_system-custom_priority_field') ) {
+%
+% @tickets =
+% @{ FS::TicketSystem->customer_tickets($cust_main->custnum, $num) };
+%
+% } else {
+%
+% foreach my $priority (
+% $conf->config('ticket_system-custom_priority_field-values'), ''
+% ) {
+% last if scalar(@tickets) >= $num;
+% push @tickets,
+% @{ FS::TicketSystem->customer_tickets( $cust_main->custnum,
+% $num - scalar(@tickets),
+% $priority,
+% )
+% };
+% }
+%
+% }
+%
+%
+
+<A NAME="tickets"><FONT SIZE="+2">Tickets</FONT></A>
+<BR>
+
+(<A HREF="<% FS::TicketSystem->href_customer_tickets($cust_main->custnum) %>">View all tickets for this customer</A>)
+(<A HREF="<% FS::TicketSystem->href_new_ticket($cust_main, join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list ) ) %>">New ticket for this customer</A>)
+
+<% include("/elements/table-grid.html") %>
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor = '';
+%
+
+
+<TR>
+ <TH CLASS="grid" BGCOLOR="#cccccc">#</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Subject</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Priority</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Queue</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Status</TH>
+</TR>
+% foreach my $ticket ( @tickets ) {
+% my $href = FS::TicketSystem->href_ticket($ticket->{id});
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+
+
+<TR>
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF=<%$href%>><% $ticket->{id} %></A></TD>
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF=<%$href%>><% $ticket->{subject} %></A></TD>
+
+ <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $ticket->{content} || $ticket->{priority} %></TD>
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $ticket->{name} %></TD>
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $ticket->{status} %></TD>
+
+</TR>
+% }
+
+
+</TABLE>
+
diff --git a/httemplate/view/elements/svc_Common.html b/httemplate/view/elements/svc_Common.html
new file mode 100644
index 000000000..f5b65ac49
--- /dev/null
+++ b/httemplate/view/elements/svc_Common.html
@@ -0,0 +1,137 @@
+% # options example...
+% #
+% # 'table' => 'svc_something'
+% #
+% # 'labels' => {
+% # 'column' => 'Label',
+% # },
+% #
+% # listref - each item is a literal column name (or method) or (notyet) coderef
+% # if not specified all columns (except for the primary key) will be viewable
+% # 'fields' => [
+% # ]
+% #
+% # # defaults to "edit/$table.cgi?", will have svcnum appended
+% # 'edit_url' =>
+%
+%
+% if ( $custnum ) {
+
+
+ <% include("/elements/header.html","View $label: $value") %>
+
+ <% include( '/elements/small_custview.html', $custnum, '', 1,
+ "${p}view/cust_main.cgi") %>
+ <BR>
+% } else {
+
+
+ <SCRIPT>
+ function areyousure(href) {
+ if (confirm("Permanently delete this <% $label %>?") == true)
+ window.location.href = href;
+ }
+ </SCRIPT>
+
+ <% include("/elements/header.html","View $label: $value", menubar(
+ "Cancel this (unaudited) $label" =>
+ "javascript:areyousure(\'${p}misc/cancel-unaudited.cgi?$svcnum\')"
+ )) %>
+% }
+
+
+Service #<B><% $svcnum %></B>
+% my $url = $opt{'edit_url'} || $p. 'edit/'. $opt{'table'}. '.cgi?';
+| <A HREF="<%$url%><%$svcnum%>">Edit this <% $label %></A>
+<BR>
+
+<% ntable("#cccccc") %><TR><TD><% ntable("#cccccc",2) %>
+% foreach my $f ( @$fields ) {
+%
+% my( $field, $type);
+% if ( ref($f) ) {
+% $field = $f->{'field'},
+% $type = $f->{'type'} || 'text',
+% } else {
+% $field = $f;
+% $type = 'text';
+% }
+%
+
+
+ <TR>
+ <TD ALIGN="right">
+ <% ( $opt{labels} && exists $opt{labels}->{$field} )
+ ? $opt{labels}->{$field}
+ : $field
+ %>
+ </TD>
+%
+% #eventually more options for <SELECT>, etc. fields
+%
+
+
+ <TD BGCOLOR="#ffffff"><% $svc_x->$field %><TD>
+
+ </TR>
+% }
+% foreach (sort { $a cmp $b } $svc_x->virtual_fields) {
+
+ <% $svc_x->pvf($_)->widget('HTML', 'view', $svc_x->getfield($_)) %>
+% }
+
+
+</TABLE></TD></TR></TABLE>
+
+<BR>
+<% joblisting({'svcnum'=>$svcnum}, 1) %>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('View customer services')
+ || $FS::CurrentUser::CurrentUser->access_right('View customer'); #XXX remove me
+
+my(%opt) = @_;
+
+my $table = $opt{'table'};
+
+my $fields = $opt{'fields'}
+ #|| [ grep { $_ ne 'svcnum' } dbdef->table($table)->columns ];
+ || [ grep { $_ ne 'svcnum' } fields($table) ];
+
+my $svcnum;
+if ( $cgi->param('svcnum') ) {
+ $cgi->param('svcnum') =~ /^(\d+)$/ or die "unparsable svcnum";
+ $svcnum = $1;
+} else {
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/ or die "no svcnum";
+ $svcnum = $1;
+}
+my $svc_x = qsearchs({
+ 'select' => $opt{'table'}.'.*',
+ 'table' => $opt{'table'},
+ 'addl_from' => ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ',
+ 'hashref' => { 'svcnum' => $svcnum },
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+}) or die "Unknown svcnum $svcnum in ". $opt{'table'}. " table\n";
+
+my $cust_svc = $svc_x->cust_svc;
+my($label, $value, $svcdb) = $cust_svc->label;
+
+my $pkgnum = $cust_svc->pkgnum;
+
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+ $cust_pkg = $cust_svc->cust_pkg;
+ $custnum = $cust_pkg->custnum;
+} else {
+ $cust_pkg = '';
+ $custnum = '';
+}
+
+</%init>
diff --git a/httemplate/view/svc_Common.html b/httemplate/view/svc_Common.html
new file mode 100644
index 000000000..defbee974
--- /dev/null
+++ b/httemplate/view/svc_Common.html
@@ -0,0 +1,29 @@
+<% include('elements/svc_Common.html',
+ 'table' => $table,
+ 'edit_url' => $p."edit/svc_Common.html?svcdb=$table;svcnum=",
+ %opt,
+ )
+%>
+<%init>
+
+# false laziness w/edit/svc_Common.html
+
+$cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unparsable svcdb";
+my $table = $1;
+require "FS/$table.pm";
+
+my %opt;
+if ( UNIVERSAL::can("FS::$table", 'table_info') ) {
+ $opt{'name'} = "FS::$table"->table_info->{'name'};
+
+ my $fields = "FS::$table"->table_info->{'fields'};
+ my %labels = map { $_ => ( ref($fields->{$_})
+ ? $fields->{$_}{'label'}
+ : $fields->{$_}
+ );
+ }
+ keys %$fields;
+ $opt{'labels'} = \%labels;
+}
+
+</%init>
diff --git a/httemplate/view/svc_acct.cgi b/httemplate/view/svc_acct.cgi
new file mode 100755
index 000000000..86478681c
--- /dev/null
+++ b/httemplate/view/svc_acct.cgi
@@ -0,0 +1,364 @@
+% if ( $custnum ) {
+
+ <% include("/elements/header.html","View $svc account") %>
+ <% include( '/elements/small_custview.html', $custnum, '', 1,
+ "${p}view/cust_main.cgi") %>
+ <BR>
+
+% } else {
+
+ <SCRIPT>
+ function areyousure(href) {
+ if (confirm("Permanently delete this account?") == true)
+ window.location.href = href;
+ }
+ </SCRIPT>
+
+ <% include("/elements/header.html",'Account View', menubar(
+ "Cancel this (unaudited) account" =>
+ "javascript:areyousure(\'${p}misc/cancel-unaudited.cgi?$svcnum\')",
+ )) %>
+
+% }
+
+% if ( $part_svc->part_export_usage ) {
+%
+% 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 #<B><% $svcnum %></B>
+| <A HREF="<%$p%>edit/svc_acct.cgi?<%$svcnum%>">Edit this service</A>
+% if ( @part_svc ) {
+
+| <SELECT NAME="svcpart" onChange="enable_change()">
+ <OPTION VALUE="">Change service</OPTION>
+ <OPTION VALUE="">--------------</OPTION>
+% foreach my $opt_part_svc ( @part_svc ) {
+
+ <OPTION VALUE="<% $opt_part_svc->svcpart %>"><% $opt_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</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;
+%
+
+ <I>(login disabled)</I>
+% }
+% if ( $conf->exists('showpasswords') ) {
+
+ <PRE><% encode_entities($password) %></PRE>
+% } else {
+
+ <I>(hidden)</I>
+% }
+
+
+ </TD>
+</TR>
+% $password = '';
+% if ( $conf->exists('security_phrase') ) {
+% my $sec_phrase = $svc_acct->sec_phrase;
+%
+
+ <TR>
+ <TD ALIGN="right">Security phrase</TD>
+ <TD BGCOLOR="#ffffff"><% $svc_acct->sec_phrase %></TD>
+ </TR>
+% }
+% if ( $svc_acct->popnum ) {
+% my $svc_acct_pop = qsearchs('svc_acct_pop',{'popnum'=>$svc_acct->popnum});
+%
+
+ <TR>
+ <TD ALIGN="right">Access number</TD>
+ <TD BGCOLOR="#ffffff"><% $svc_acct_pop->text %></TD>
+ </TR>
+% }
+% if ($svc_acct->uid ne '') {
+
+ <TR>
+ <TD ALIGN="right">UID</TD>
+ <TD BGCOLOR="#ffffff"><% $svc_acct->uid %></TD>
+ </TR>
+% }
+% if ($svc_acct->gid ne '') {
+
+ <TR>
+ <TD ALIGN="right">GID</TD>
+ <TD BGCOLOR="#ffffff"><% $svc_acct->gid %></TD>
+ </TR>
+% }
+% if ($svc_acct->finger ne '') {
+
+ <TR>
+ <TD ALIGN="right">GECOS</TD>
+ <TD BGCOLOR="#ffffff"><% $svc_acct->finger %></TD>
+ </TR>
+% }
+% if ($svc_acct->dir ne '') {
+
+ <TR>
+ <TD ALIGN="right">Home directory</TD>
+ <TD BGCOLOR="#ffffff"><% $svc_acct->dir %></TD>
+ </TR>
+% }
+% if ($svc_acct->shell ne '') {
+
+ <TR>
+ <TD ALIGN="right">Shell</TD>
+ <TD BGCOLOR="#ffffff"><% $svc_acct->shell %></TD>
+ </TR>
+% }
+% if ($svc_acct->quota ne '') {
+
+ <TR>
+ <TD ALIGN="right">Quota</TD>
+ <TD BGCOLOR="#ffffff"><% $svc_acct->quota %></TD>
+ </TR>
+% }
+% if ($svc_acct->slipip) {
+
+ <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>
+ </TR>
+% }
+% my %ulabel = ( seconds => 'Seconds',
+% upbytes => 'Upload bytes',
+% downbytes => 'Download bytes',
+% totalbytes => 'Total bytes',
+% );
+% foreach my $uf ( keys %ulabel ) {
+% my $tf = $uf . "_threshold";
+% if ( $svc_acct->$tf ne '' ) {
+ <TR>
+ <TD ALIGN="right"><% $ulabel{$uf} %> remaining</TD>
+ <TD BGCOLOR="#ffffff"><% $svc_acct->$uf %></TD>
+ </TR>
+
+% }
+% }
+% foreach my $attribute ( grep /^radius_/, $svc_acct->fields ) {
+% $attribute =~ /^radius_(.*)$/;
+% my $pattribute = $FS::raddb::attrib{$1};
+%
+
+ <TR>
+ <TD ALIGN="right">Radius (reply) <% $pattribute %></TD>
+ <TD BGCOLOR="#ffffff"><% $svc_acct->getfield($attribute) %></TD>
+ </TR>
+% }
+% foreach my $attribute ( grep /^rc_/, $svc_acct->fields ) {
+% $attribute =~ /^rc_(.*)$/;
+% my $pattribute = $FS::raddb::attrib{$1};
+%
+
+ <TR>
+ <TD ALIGN="right">Radius (check) <% $pattribute %></TD>
+ <TD BGCOLOR="#ffffff"><% $svc_acct->getfield($attribute) %></TD>
+ </TR>
+% }
+
+
+<TR>
+ <TD ALIGN="right">RADIUS groups</TD>
+ <TD BGCOLOR="#ffffff"><% join('<BR>', $svc_acct->radius_groups) %></TD>
+</TR>
+% if ( $svc_acct->seconds =~ /^\d+$/ ) {
+
+ <TR>
+ <TD ALIGN="right">Prepaid time</TD>
+ <TD BGCOLOR="#ffffff"><% duration_exact($svc_acct->seconds) %></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) {
+
+ <% $svc_acct->pvf($_)->widget('HTML', 'view', $svc_acct->getfield($_)) %>
+% }
+
+
+</TABLE></TD></TR></TABLE>
+</FORM>
+<BR><BR>
+
+<% join("<BR>", $conf->config('svc_acct-notes') ) %>
+<BR><BR>
+
+<% joblisting({'svcnum'=>$svcnum}, 1) %>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('View customer services')
+ || $FS::CurrentUser::CurrentUser->access_right('View customer'); #XXX remove me
+
+my $conf = new FS::Conf;
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_acct = qsearchs({
+ 'select' => 'svc_acct.*',
+ 'table' => 'svc_acct',
+ 'addl_from' => ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ',
+ 'hashref' => {'svcnum'=>$svcnum},
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+});
+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 $svc = $part_svc->svc;
+
+die 'Empty domsvc for svc_acct.svcnum '. $svc_acct->svcnum
+ unless $svc_acct->domsvc;
+my $svc_domain = qsearchs('svc_domain', { 'svcnum' => $svc_acct->domsvc } );
+die 'Unknown domain (domsvc '. $svc_acct->domsvc.
+ ' for svc_acct.svcnum '. $svc_acct->svcnum. ')'
+ unless $svc_domain;
+my $domain = $svc_domain->domain;
+
+</%init>
diff --git a/httemplate/view/svc_broadband.cgi b/httemplate/view/svc_broadband.cgi
new file mode 100644
index 000000000..a76e5a3d1
--- /dev/null
+++ b/httemplate/view/svc_broadband.cgi
@@ -0,0 +1,213 @@
+<%include("/elements/header.html",'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">Description</TD>
+ <TD BGCOLOR="#ffffff"><%$description%></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>
+ <TD ALIGN="right">IP Netmask</TD>
+ <TD BGCOLOR="#ffffff"><%$ip_netmask%></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">IP Gateway</TD>
+ <TD BGCOLOR="#ffffff"><%$ip_gateway%></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">MAC Address</TD>
+ <TD BGCOLOR="#ffffff"><%$mac_addr%></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Latitude</TD>
+ <TD BGCOLOR="#ffffff"><%$latitude%></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Longitude</TD>
+ <TD BGCOLOR="#ffffff"><%$longitude%></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Altitude</TD>
+ <TD BGCOLOR="#ffffff"><%$altitude%></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">VLAN Profile</TD>
+ <TD BGCOLOR="#ffffff"><%$vlan_profile%></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">Authentication Key</TD>
+ <TD BGCOLOR="#ffffff"><%$auth_key%></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 @sb_addr_block;
+% if (@sb_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 (@sb_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)%>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('View customer services')
+ || $FS::CurrentUser::CurrentUser->access_right('View customer'); #XXX remove me
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_broadband = qsearchs({
+ 'select' => 'svc_broadband.*',
+ 'table' => 'svc_broadband',
+ 'addl_from' => ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ',
+ 'hashref' => { 'svcnum' => $svcnum },
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+}) 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 $addr_block = $svc_broadband->addr_block;
+my $router = $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,
+ $ip_gateway,
+ $ip_netmask,
+ $mac_addr,
+ $latitude,
+ $longitude,
+ $altitude,
+ $vlan_profile,
+ $auth_key,
+ $description,
+ ) = (
+ $router->getfield('routername'),
+ $router->getfield('routernum'),
+ $svc_broadband->getfield('speed_down'),
+ $svc_broadband->getfield('speed_up'),
+ $svc_broadband->getfield('ip_addr'),
+ $addr_block->ip_gateway,
+ $addr_block->NetAddr->mask,
+ $svc_broadband->mac_addr,
+ $svc_broadband->latitude,
+ $svc_broadband->longitude,
+ $svc_broadband->altitude,
+ $svc_broadband->vlan_profile,
+ $svc_broadband->auth_key,
+ $svc_broadband->description,
+ );
+
+</%init>
diff --git a/httemplate/view/svc_domain.cgi b/httemplate/view/svc_domain.cgi
new file mode 100755
index 000000000..7fdce37df
--- /dev/null
+++ b/httemplate/view/svc_domain.cgi
@@ -0,0 +1,145 @@
+<% include("/elements/header.html",'Domain View', menubar(
+ ( ( $pkgnum || $custnum )
+ ? ( "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+ )
+ : ( "Delete this (unaudited) domain" =>
+ "javascript:areyousure('${p}misc/cancel-unaudited.cgi?$svcnum', 'Delete $domain and all records?' )" )
+ ),
+ "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, message) {
+ if ( confirm(message) == 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 ) {
+
+ <% include('/elements/table-grid.html') %>
+
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor = $bgcolor2;
+
+ <tr>
+ <th CLASS="grid" BGCOLOR="#cccccc">Zone</th>
+ <th CLASS="grid" BGCOLOR="#cccccc">Type</th>
+ <th CLASS="grid" BGCOLOR="#cccccc">Data</th>
+ </tr>
+
+% foreach my $domain_record ( @records ) {
+% my $type = $domain_record->rectype eq '_mstr'
+% ? "(slave)"
+% : $domain_record->recaf. ' '. $domain_record->rectype;
+
+
+ <tr>
+ <td CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $domain_record->reczone %></td>
+ <td CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $type %></td>
+ <td CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $domain_record->recdata %>
+
+% unless ( $domain_record->rectype eq 'SOA' ) {
+ (<A HREF="javascript:areyousure('<%$p%>misc/delete-domain_record.cgi?<%$domain_record->recnum%>', 'Delete \'<% $domain_record->reczone %> <% $type %> <% $domain_record->recdata %>\' ?' )">delete</A>)
+% }
+ </td>
+ </tr>
+
+
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+
+% }
+
+ </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 TXT) ) {
+
+ <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) %>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('View customer services')
+ || $FS::CurrentUser::CurrentUser->access_right('View customer'); #XXX remove me
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_domain = qsearchs({
+ 'select' => 'svc_domain.*',
+ 'table' => 'svc_domain',
+ 'addl_from' => ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ',
+ 'hashref' => {'svcnum'=>$svcnum},
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+});
+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;
+
+</%init>
diff --git a/httemplate/view/svc_external.cgi b/httemplate/view/svc_external.cgi
new file mode 100644
index 000000000..b87166a17
--- /dev/null
+++ b/httemplate/view/svc_external.cgi
@@ -0,0 +1,64 @@
+<% include("/elements/header.html",'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) %>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('View customer services')
+ || $FS::CurrentUser::CurrentUser->access_right('View customer'); #XXX remove me
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_external = qsearchs({
+ 'select' => 'svc_external.*',
+ 'table' => 'svc_external',
+ 'addl_from' => ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ',
+ 'hashref' => { 'svcnum' => $svcnum },
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+}) 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
+
+</%init>
diff --git a/httemplate/view/svc_forward.cgi b/httemplate/view/svc_forward.cgi
new file mode 100755
index 000000000..487ebb220
--- /dev/null
+++ b/httemplate/view/svc_forward.cgi
@@ -0,0 +1,94 @@
+% die "access denied"
+% unless $FS::CurrentUser::CurrentUser->access_right('View customer services')
+% || $FS::CurrentUser::CurrentUser->access_right('View customer'); #XXX remove me
+%
+%my $conf = new FS::Conf;
+%
+%my($query) = $cgi->keywords;
+%$query =~ /^(\d+)$/;
+%my $svcnum = $1;
+%my $svc_forward = qsearchs({
+% 'select' => 'svc_forward.*',
+% 'table' => 'svc_forward',
+% 'addl_from' => ' LEFT JOIN cust_svc USING ( svcnum ) '.
+% ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+% ' LEFT JOIN cust_main USING ( custnum ) ',
+% 'hashref' => {'svcnum'=>$svcnum},
+% 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+%});
+%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_phone.cgi b/httemplate/view/svc_phone.cgi
new file mode 100644
index 000000000..732f3cd79
--- /dev/null
+++ b/httemplate/view/svc_phone.cgi
@@ -0,0 +1,10 @@
+<% include('elements/svc_Common.html',
+ 'table' => 'svc_phone',
+ 'fields' => [qw( countrycode phonenum )], #pin
+ 'labels' => {
+ 'countrycode' => 'Country code',
+ 'phonenum' => 'Phone number',
+ 'pin' => 'PIN',
+ },
+ )
+%>
diff --git a/httemplate/view/svc_www.cgi b/httemplate/view/svc_www.cgi
new file mode 100644
index 000000000..0579a55b4
--- /dev/null
+++ b/httemplate/view/svc_www.cgi
@@ -0,0 +1,88 @@
+% die "access denied"
+% unless $FS::CurrentUser::CurrentUser->access_right('View customer services')
+% || $FS::CurrentUser::CurrentUser->access_right('View customer'); #XXX remove me
+%
+%my($query) = $cgi->keywords;
+%$query =~ /^(\d+)$/;
+%my $svcnum = $1;
+%my $svc_www = qsearchs({
+% 'select' => 'svc_www.*',
+% 'table' => 'svc_www',
+% 'addl_from' => ' LEFT JOIN cust_svc USING ( svcnum ) '.
+% ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+% ' LEFT JOIN cust_main USING ( custnum ) ',
+% 'hashref' => { 'svcnum' => $svcnum },
+% 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+%}) 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 $part_svc=qsearchs('part_svc',{'svcpart'=>$cust_svc->svcpart})
+% or die "svc_www: Unknown svcpart" . $cust_svc->svcpart;
+
+%my $usersvc = $svc_www->usersvc;
+%my $svc_acct = '';
+%my $email = '';
+%if ( $usersvc ) {
+% $svc_acct = qsearchs('svc_acct', { 'svcnum' => $usersvc } )
+% or die "svc_www: Unknown usersvc $usersvc";
+% $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>!;
+%if ( $part_svc->part_svc_column('usersvc')->columnflag ne 'F'
+% || $part_svc->part_svc_column('usersvc')->columnvalue !~ /^\s*$/) {
+% print qq!<TR><TD ALIGN="right">Account</TD>!.
+% qq!<TD BGCOLOR="#ffffff">!;
+%
+% if ( $usersvc ) {
+% print qq!<A HREF="${p}view/svc_acct.cgi?$usersvc">$email</A>!;
+% } else {
+% print '</i>(none)</i>';
+% }
+%
+% print '</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>'
+%;
+%
+