summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2012-10-04 20:25:37 -0700
committerIvan Kohler <ivan@freeside.biz>2012-10-04 20:25:37 -0700
commit0af38652da3b3be7da2d35b048285ef6f2194e1a (patch)
treec43e871e406a11ad9ddca7f5af225f8e5e507000
parenta8e1cb65cd92239721b8e81ef9fdf99f60fb3c3c (diff)
parent51b5bd15c154065a9a0f521565bd6187609c8348 (diff)
Merge branch 'master' of git.freeside.biz:/home/git/freeside
-rw-r--r--FS/FS.pm6
-rw-r--r--FS/FS/AccessRight.pm14
-rw-r--r--FS/FS/ClientAPI/MasonComponent.pm24
-rw-r--r--FS/FS/ClientAPI/MyAccount.pm39
-rw-r--r--FS/FS/Conf.pm81
-rw-r--r--FS/FS/Cron/agent_email.pm79
-rw-r--r--FS/FS/Cron/pay_batch.pm2
-rw-r--r--FS/FS/Mason.pm15
-rw-r--r--FS/FS/Mason/Request.pm18
-rw-r--r--FS/FS/Misc.pm10
-rw-r--r--FS/FS/Record.pm21
-rw-r--r--FS/FS/Report/FCC_477.pm4
-rw-r--r--FS/FS/Schema.pm275
-rw-r--r--FS/FS/TemplateItem_Mixin.pm317
-rw-r--r--FS/FS/Template_Mixin.pm22
-rw-r--r--FS/FS/TicketSystem/RT_Internal.pm3
-rw-r--r--FS/FS/Trace.pm35
-rw-r--r--FS/FS/Upgrade.pm20
-rw-r--r--FS/FS/access_right.pm13
-rwxr-xr-xFS/FS/addr_block.pm32
-rw-r--r--FS/FS/agent_pkg_class.pm117
-rw-r--r--FS/FS/cdr.pm131
-rw-r--r--FS/FS/cdr/taqua.pm2
-rw-r--r--FS/FS/cdr/taqua62.pm178
-rw-r--r--FS/FS/cust_bill.pm62
-rw-r--r--FS/FS/cust_bill_ApplicationCommon.pm5
-rw-r--r--FS/FS/cust_bill_pkg.pm899
-rw-r--r--FS/FS/cust_bill_pkg_detail_void.pm168
-rw-r--r--FS/FS/cust_bill_pkg_discount.pm4
-rw-r--r--FS/FS/cust_bill_pkg_discount_void.pm129
-rw-r--r--FS/FS/cust_bill_pkg_display_void.pm132
-rw-r--r--FS/FS/cust_bill_pkg_tax_location_void.pm139
-rw-r--r--FS/FS/cust_bill_pkg_tax_rate_location_void.pm139
-rw-r--r--FS/FS/cust_bill_pkg_void.pm272
-rw-r--r--FS/FS/cust_bill_void.pm286
-rw-r--r--FS/FS/cust_credit_bill_pkg.pm62
-rw-r--r--FS/FS/cust_main.pm39
-rw-r--r--FS/FS/cust_main/Billing.pm127
-rw-r--r--FS/FS/cust_main/Import.pm24
-rw-r--r--FS/FS/cust_main/NationalID.pm64
-rw-r--r--FS/FS/cust_main/Search.pm26
-rw-r--r--FS/FS/cust_main_county.pm241
-rw-r--r--FS/FS/cust_pay.pm2
-rw-r--r--FS/FS/cust_pkg.pm78
-rw-r--r--FS/FS/cust_pkg_discount.pm3
-rw-r--r--FS/FS/cust_svc.pm4
-rw-r--r--FS/FS/cust_tax_exempt_pkg.pm85
-rw-r--r--FS/FS/cust_tax_exempt_pkg_void.pm143
-rw-r--r--FS/FS/cust_tax_location.pm2
-rw-r--r--FS/FS/detail_format/sum_duration_prefix.pm5
-rw-r--r--FS/FS/discount.pm1
-rw-r--r--FS/FS/h_cust_main_exemption.pm19
-rw-r--r--FS/FS/h_part_pkg.pm37
-rw-r--r--FS/FS/part_event.pm4
-rw-r--r--FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm25
-rw-r--r--FS/FS/part_event/Action/Mixin/credit_pkg.pm7
-rw-r--r--FS/FS/part_event/Action/pkg_agent_credit.pm3
-rw-r--r--FS/FS/part_event/Action/pkg_agent_credit_pkg_class.pm9
-rw-r--r--FS/FS/part_event/Condition/after_event.pm81
-rw-r--r--FS/FS/part_export.pm206
-rw-r--r--FS/FS/part_export/acct_google.pm10
-rw-r--r--FS/FS/part_export/acct_http.pm1
-rw-r--r--FS/FS/part_export/acct_plesk.pm8
-rw-r--r--FS/FS/part_export/acct_sql.pm12
-rw-r--r--FS/FS/part_export/acct_sql_status.pm1
-rw-r--r--FS/FS/part_export/acct_xmlrpc.pm3
-rw-r--r--FS/FS/part_export/amazon_ec2.pm1
-rw-r--r--FS/FS/part_export/artera_turbo.pm1
-rw-r--r--FS/FS/part_export/broadband_http.pm1
-rw-r--r--FS/FS/part_export/broadband_nas.pm1
-rw-r--r--FS/FS/part_export/broadband_shellcommands.pm1
-rw-r--r--FS/FS/part_export/broadband_snmp.pm1
-rw-r--r--FS/FS/part_export/broadband_sql.pm1
-rw-r--r--FS/FS/part_export/broadband_sqlradius.pm1
-rw-r--r--FS/FS/part_export/communigate_pro.pm1
-rw-r--r--FS/FS/part_export/communigate_pro_singledomain.pm1
-rw-r--r--FS/FS/part_export/cp.pm1
-rw-r--r--FS/FS/part_export/cpanel.pm2
-rw-r--r--FS/FS/part_export/cust_http.pm1
-rw-r--r--FS/FS/part_export/cyrus.pm2
-rw-r--r--FS/FS/part_export/dashcs_e911.pm1
-rw-r--r--FS/FS/part_export/dma_radiusmanager.pm350
-rw-r--r--FS/FS/part_export/domain_sql.pm1
-rw-r--r--FS/FS/part_export/everyone_net.pm2
-rw-r--r--FS/FS/part_export/ez_prepaid.pm184
-rw-r--r--FS/FS/part_export/forward_sql.pm1
-rw-r--r--FS/FS/part_export/freeswitch.pm192
-rw-r--r--FS/FS/part_export/globalpops_voip.pm1
-rw-r--r--FS/FS/part_export/http.pm1
-rw-r--r--FS/FS/part_export/http_status.pm1
-rw-r--r--FS/FS/part_export/ikano.pm1
-rw-r--r--FS/FS/part_export/indosoft.pm1
-rw-r--r--FS/FS/part_export/infostreet.pm1
-rw-r--r--FS/FS/part_export/internal_diddb.pm1
-rw-r--r--FS/FS/part_export/ldap.pm1
-rw-r--r--FS/FS/part_export/netsapiens.pm9
-rw-r--r--FS/FS/part_export/null.pm1
-rw-r--r--FS/FS/part_export/phone_shellcommands.pm1
-rw-r--r--FS/FS/part_export/phone_sqlopensips.pm10
-rw-r--r--FS/FS/part_export/phone_sqlradius.pm9
-rw-r--r--FS/FS/part_export/postfix.pm1
-rw-r--r--FS/FS/part_export/prizm.pm11
-rw-r--r--FS/FS/part_export/radiator.pm2
-rw-r--r--FS/FS/part_export/router.pm1
-rw-r--r--FS/FS/part_export/rt_ticket.pm1
-rw-r--r--FS/FS/part_export/send_email.pm1
-rw-r--r--FS/FS/part_export/shellcommands.pm47
-rw-r--r--FS/FS/part_export/shellcommands_withdomain.pm9
-rw-r--r--FS/FS/part_export/sqlmail.pm1
-rw-r--r--FS/FS/part_export/sqlradius.pm126
-rw-r--r--FS/FS/part_export/sqlradius_withdomain.pm11
-rw-r--r--FS/FS/part_export/textradius.pm1
-rw-r--r--FS/FS/part_export/trango.pm1
-rw-r--r--FS/FS/part_export/vitelity.pm1
-rw-r--r--FS/FS/part_export/vpopmail.pm1
-rw-r--r--FS/FS/part_export/www_plesk.pm9
-rw-r--r--FS/FS/part_export/www_shellcommands.pm1
-rw-r--r--FS/FS/part_export_machine.pm155
-rw-r--r--FS/FS/part_pkg.pm81
-rw-r--r--FS/FS/part_pkg/delayed_Mixin.pm3
-rw-r--r--FS/FS/part_pkg/prepaid.pm2
-rw-r--r--FS/FS/part_pkg/prorate.pm10
-rw-r--r--FS/FS/part_pkg/recur_Common.pm15
-rw-r--r--FS/FS/part_pkg_taxrate.pm2
-rw-r--r--FS/FS/part_svc.pm2
-rw-r--r--FS/FS/pay_batch.pm11
-rw-r--r--FS/FS/pay_batch/BoM.pm10
-rw-r--r--FS/FS/pay_batch/td_eft1464.pm9
-rw-r--r--FS/FS/quotation.pm156
-rw-r--r--FS/FS/radius_group.pm4
-rw-r--r--FS/FS/rate.pm2
-rw-r--r--FS/FS/reason.pm27
-rw-r--r--FS/FS/svc_Common.pm54
-rw-r--r--FS/FS/svc_Tower_Mixin.pm1
-rw-r--r--FS/FS/svc_acct.pm26
-rwxr-xr-xFS/FS/svc_broadband.pm6
-rw-r--r--FS/FS/svc_export_machine.pm124
-rw-r--r--FS/FS/tax_class.pm2
-rw-r--r--FS/FS/tax_rate.pm43
-rw-r--r--FS/FS/tax_rate_location.pm2
-rw-r--r--FS/MANIFEST24
-rw-r--r--FS/bin/freeside-cdrd2
-rwxr-xr-xFS/bin/freeside-daily23
-rw-r--r--FS/t/agent_pkg_class.t5
-rw-r--r--FS/t/cust_bill_pkg_detail_void.t5
-rw-r--r--FS/t/cust_bill_pkg_discount_void.t5
-rw-r--r--FS/t/cust_bill_pkg_display_void.t5
-rw-r--r--FS/t/cust_bill_pkg_tax_location_void.t5
-rw-r--r--FS/t/cust_bill_pkg_tax_rate_location_void.t5
-rw-r--r--FS/t/cust_bill_pkg_void.t5
-rw-r--r--FS/t/cust_bill_void.t5
-rw-r--r--FS/t/cust_tax_exempt_pkg_void.t5
-rw-r--r--FS/t/part_export_machine.t5
-rw-r--r--FS/t/svc_export_machine.t5
-rw-r--r--Makefile16
-rwxr-xr-xbin/231commit4
-rwxr-xr-xbin/23diff3
-rwxr-xr-xbin/agent_email30
-rwxr-xr-x[-rw-r--r--]bin/cdr.import0
-rwxr-xr-xbin/cust_bill.export49
-rwxr-xr-x[-rw-r--r--]bin/cust_main-bill_now4
-rwxr-xr-xbin/cust_main.export109
-rwxr-xr-xbin/cust_pkg.export61
-rwxr-xr-xbin/pod2x8
-rwxr-xr-xbin/svc_acct.export54
-rwxr-xr-xbin/svc_broadband.export59
-rwxr-xr-xbin/svc_phone.export55
-rwxr-xr-xbin/tax_location.upgrade31
-rwxr-xr-xbin/v-rate-reimport172
-rw-r--r--conf/invoice_latex2
-rw-r--r--conf/quotation_latex2
-rw-r--r--etc/longtable.sty (renamed from etc/fslongtable.sty)2
-rwxr-xr-xfs_selfservice/DEPLOY2
-rwxr-xr-x[-rw-r--r--]fs_selfservice/FS-SelfService/cgi/agent.cgi0
-rwxr-xr-x[-rw-r--r--]fs_selfservice/FS-SelfService/cgi/cust_bill-logo.cgi0
-rw-r--r--fs_selfservice/FS-SelfService/cgi/make_payment.html24
-rwxr-xr-x[-rw-r--r--]fs_selfservice/FS-SelfService/cgi/selfservice.cgi16
-rwxr-xr-x[-rw-r--r--]fs_selfservice/FS-SelfService/cgi/xmlrpc.cgi0
-rw-r--r--htetc/handler.pl53
-rwxr-xr-xhttemplate/browse/agent.cgi28
-rw-r--r--httemplate/browse/cust_note_class.html2
-rwxr-xr-xhttemplate/browse/part_export.cgi7
-rwxr-xr-xhttemplate/browse/part_svc.cgi11
-rw-r--r--httemplate/browse/radius_group.html19
-rw-r--r--httemplate/browse/reason.html17
-rw-r--r--httemplate/config/config.cgi1
-rw-r--r--httemplate/docs/license.html2
-rwxr-xr-xhttemplate/edit/agent.cgi46
-rwxr-xr-xhttemplate/edit/cust_main.cgi8
-rw-r--r--httemplate/edit/cust_main/billing.html11
-rw-r--r--httemplate/edit/cust_main/birthdate.html47
-rwxr-xr-xhttemplate/edit/cust_refund.cgi2
-rw-r--r--httemplate/edit/discount.html14
-rw-r--r--httemplate/edit/nas.html2
-rw-r--r--httemplate/edit/part_export.cgi74
-rwxr-xr-xhttemplate/edit/part_pkg.cgi4
-rwxr-xr-xhttemplate/edit/part_svc.cgi7
-rw-r--r--httemplate/edit/payment_gateway.html3
-rw-r--r--httemplate/edit/prepay_credit.cgi1
-rwxr-xr-xhttemplate/edit/process/agent.cgi41
-rwxr-xr-xhttemplate/edit/process/cust_main.cgi7
-rw-r--r--httemplate/edit/process/cust_pkg_discount.html3
-rwxr-xr-xhttemplate/edit/process/cust_refund.cgi2
-rw-r--r--httemplate/edit/process/part_export.cgi5
-rw-r--r--httemplate/edit/process/quick-cust_pkg.cgi151
-rwxr-xr-xhttemplate/edit/process/svc_acct.cgi27
-rw-r--r--httemplate/edit/process/svc_broadband.cgi7
-rw-r--r--httemplate/edit/radius_group.html12
-rw-r--r--httemplate/edit/reason.html116
-rwxr-xr-xhttemplate/edit/svc_acct.cgi6
-rw-r--r--httemplate/elements/dashboard-toplist.html103
-rw-r--r--httemplate/elements/location.html8
-rw-r--r--httemplate/elements/menu.html9
-rw-r--r--httemplate/elements/select-rt-customfield.html3
-rw-r--r--httemplate/elements/select-table.html33
-rw-r--r--httemplate/elements/tr-amount_fee.html4
-rw-r--r--httemplate/elements/tr-select-cust_location.html1
-rw-r--r--httemplate/elements/tr-select-discount.html26
-rw-r--r--httemplate/elements/tr-select-part_referral.html8
-rwxr-xr-xhttemplate/elements/tr-select-reason.html57
-rw-r--r--httemplate/elements/tr-select-voip_class.html24
-rw-r--r--httemplate/elements/tr-svc_export_machine.html37
-rw-r--r--httemplate/graph/cust_bill_pkg.cgi54
-rw-r--r--httemplate/graph/elements/monthly.html36
-rw-r--r--httemplate/graph/elements/report.html15
-rw-r--r--httemplate/graph/report_cust_bill_pkg.html67
-rw-r--r--httemplate/index.html10
-rw-r--r--httemplate/misc/cust_main-import.cgi6
-rw-r--r--httemplate/misc/order_pkg.html64
-rw-r--r--httemplate/misc/payment.cgi26
-rwxr-xr-xhttemplate/misc/process/void-cust_bill.html26
-rwxr-xr-xhttemplate/misc/timeworked.html2
-rwxr-xr-xhttemplate/misc/unvoid-cust_bill_void.html25
-rwxr-xr-xhttemplate/misc/unvoid-cust_pay_void.cgi2
-rw-r--r--httemplate/misc/void-cust_bill.html45
-rwxr-xr-xhttemplate/misc/void-cust_pay.cgi2
-rw-r--r--httemplate/misc/xmlhttp-cust_main-search.cgi2
-rw-r--r--httemplate/pref/pref-process.html1
-rw-r--r--httemplate/pref/pref.html9
-rwxr-xr-xhttemplate/search/477.html54
-rwxr-xr-xhttemplate/search/477partIA_detail.html3
-rwxr-xr-xhttemplate/search/477partIA_summary.html3
-rwxr-xr-xhttemplate/search/477partIIA.html3
-rwxr-xr-xhttemplate/search/477partIIB.html194
-rwxr-xr-xhttemplate/search/477partV.html5
-rwxr-xr-xhttemplate/search/477partVI_census.html11
-rw-r--r--httemplate/search/cust_bill_pkg.cgi897
-rw-r--r--httemplate/search/cust_bill_pkg_referral.html10
-rw-r--r--httemplate/search/cust_main-zip.html48
-rwxr-xr-xhttemplate/search/cust_main.cgi35
-rwxr-xr-xhttemplate/search/cust_main.html4
-rwxr-xr-xhttemplate/search/cust_pay_pending.html1
-rw-r--r--httemplate/search/cust_tax_exempt_pkg.cgi8
-rw-r--r--httemplate/search/elements/cust_pay_batch_top.html3
-rw-r--r--httemplate/search/elements/search-csv.html13
-rw-r--r--httemplate/search/elements/search-html.html12
-rw-r--r--httemplate/search/elements/search-xls.html15
-rw-r--r--httemplate/search/elements/search.html6
-rwxr-xr-xhttemplate/search/quotation.html268
-rwxr-xr-xhttemplate/search/report_477.html18
-rw-r--r--httemplate/search/report_cdr.html7
-rw-r--r--httemplate/search/report_cust_bill_pkg_referral.html5
-rw-r--r--httemplate/search/report_cust_main-zip.html4
-rwxr-xr-xhttemplate/search/report_cust_main.html22
-rw-r--r--httemplate/search/report_quotation.html75
-rw-r--r--httemplate/search/report_rt_ticket.html1
-rw-r--r--httemplate/search/report_sqlradius_usage.html40
-rwxr-xr-xhttemplate/search/report_tax.cgi748
-rw-r--r--httemplate/search/sqlradius_usage.html201
-rwxr-xr-xhttemplate/view/cust_bill.cgi2
-rwxr-xr-xhttemplate/view/cust_bill_void.html79
-rw-r--r--httemplate/view/cust_main/billing.html8
-rw-r--r--httemplate/view/cust_main/contacts.html11
-rw-r--r--httemplate/view/cust_main/custom_content/.birthdate.html.swpbin12288 -> 0 bytes
-rw-r--r--httemplate/view/cust_main/custom_content/.small_custview.html.swpbin12288 -> 0 bytes
-rw-r--r--httemplate/view/cust_main/custom_content/.spouse_birthdate.html.swpbin12288 -> 0 bytes
-rw-r--r--httemplate/view/cust_main/custom_content/.svc_Common.html.swpbin12288 -> 0 bytes
-rw-r--r--httemplate/view/cust_main/custom_content/.svc_acct.html.swpbin12288 -> 0 bytes
-rw-r--r--httemplate/view/cust_main/custom_content/.svc_hardware.html.swpbin12288 -> 0 bytes
-rw-r--r--httemplate/view/cust_main/custom_content/.svc_phone.html.swpbin12288 -> 0 bytes
-rw-r--r--httemplate/view/cust_main/misc.html34
-rw-r--r--httemplate/view/cust_main/payment_history.html13
-rw-r--r--httemplate/view/cust_main/payment_history/invoice.html14
-rw-r--r--httemplate/view/cust_main/payment_history/payment.html2
-rw-r--r--httemplate/view/cust_main/payment_history/voided_invoice.html57
-rw-r--r--httemplate/view/cust_main/payment_history/voided_payment.html2
-rw-r--r--httemplate/view/elements/tr-svc_export_machine.html27
-rwxr-xr-xhttemplate/view/quotation.html2
-rw-r--r--httemplate/view/svc_acct/basics.html5
-rw-r--r--init.d/insserv-override-apache211
-rwxr-xr-xrt/bin/rt2
-rw-r--r--rt/bin/rt.in2
-rwxr-xr-xrt/configure149
-rw-r--r--rt/docs/web_deployment.pod2
-rw-r--r--rt/etc/RT_Config.pm.in24
-rw-r--r--rt/etc/RT_SiteConfig.pm2
-rw-r--r--rt/etc/initialdata2
-rw-r--r--rt/etc/schema.SQLite106
-rw-r--r--rt/etc/upgrade/3.3.0/schema.mysql53
-rw-r--r--rt/etc/upgrade/3.3.11/schema.mysql4
-rw-r--r--rt/etc/upgrade/3.9.5/schema.mysql24
-rw-r--r--rt/etc/upgrade/3.9.7/schema.mysql10
-rw-r--r--rt/lib/RT.pm11
-rw-r--r--rt/lib/RT.pm.in719
-rw-r--r--rt/lib/RT/Action/CreateTickets.pm3
-rw-r--r--rt/lib/RT/Articles.pm3
-rw-r--r--rt/lib/RT/Config.pm6
-rw-r--r--rt/lib/RT/Crypt/GnuPG.pm1
-rw-r--r--rt/lib/RT/Dashboard.pm30
-rw-r--r--rt/lib/RT/Generated.pm2
-rw-r--r--rt/lib/RT/I18N.pm6
-rwxr-xr-xrt/lib/RT/Interface/Email.pm39
-rw-r--r--rt/lib/RT/Interface/Web.pm44
-rwxr-xr-xrt/lib/RT/Record.pm4
-rwxr-xr-xrt/lib/RT/Scrip.pm2
-rwxr-xr-xrt/lib/RT/Scrips.pm80
-rw-r--r--rt/lib/RT/Search/Googleish.pm16
-rw-r--r--rt/lib/RT/SearchBuilder.pm40
-rw-r--r--rt/lib/RT/Shredder.pm6
-rw-r--r--rt/lib/RT/Test.pm6
-rwxr-xr-xrt/lib/RT/Ticket.pm76
-rwxr-xr-xrt/lib/RT/Tickets.pm11
-rwxr-xr-xrt/lib/RT/Transaction.pm11
-rw-r--r--rt/lib/RT/URI.pm19
-rwxr-xr-xrt/lib/RT/User.pm2
-rw-r--r--rt/sbin/rt-server.fcgi.in2
-rw-r--r--rt/sbin/rt-server.in2
-rw-r--r--rt/sbin/rt-test-dependencies.in15
-rwxr-xr-xrt/sbin/standalone_httpd2
-rw-r--r--rt/sbin/standalone_httpd.in2
-rwxr-xr-xrt/share/html/Admin/Queues/Modify.html2
-rwxr-xr-xrt/share/html/Approvals/Elements/PendingMyApproval2
-rw-r--r--rt/share/html/Approvals/autohandler9
-rw-r--r--rt/share/html/Dashboards/Subscription.html2
-rw-r--r--rt/share/html/Elements/AddCustomers2
-rw-r--r--rt/share/html/Elements/ColumnMap8
-rw-r--r--rt/share/html/Elements/EditCustomField2
-rwxr-xr-xrt/share/html/Elements/Header3
-rw-r--r--rt/share/html/Elements/HeaderJavascript2
-rwxr-xr-xrt/share/html/Elements/ListActions2
-rwxr-xr-xrt/share/html/Elements/MessageBox3
-rw-r--r--rt/share/html/Elements/QueueSummaryByStatus10
-rw-r--r--rt/share/html/Elements/RT__CustomField/ColumnMap4
-rwxr-xr-xrt/share/html/Elements/SelectWatcherType2
-rwxr-xr-xrt/share/html/Elements/Tabs2
-rw-r--r--rt/share/html/Helpers/Autocomplete/Users3
-rw-r--r--rt/share/html/NoAuth/css/aileron/boxes.css4
-rw-r--r--rt/share/html/NoAuth/css/aileron/ticket.css13
-rw-r--r--rt/share/html/NoAuth/css/ballard/boxes.css5
-rw-r--r--rt/share/html/NoAuth/css/ballard/layout.css4
-rw-r--r--rt/share/html/NoAuth/css/ballard/nav.css3
-rw-r--r--rt/share/html/NoAuth/css/ballard/ticket-search.css1
-rw-r--r--rt/share/html/NoAuth/css/ballard/ticket.css3
-rw-r--r--rt/share/html/NoAuth/css/base/forms.css1
-rw-r--r--rt/share/html/NoAuth/css/base/jquery-ui-timepicker-addon.css7
-rw-r--r--rt/share/html/NoAuth/css/base/jquery-ui.css2
-rw-r--r--rt/share/html/NoAuth/css/base/jquery-ui.custom.modified.css24
-rw-r--r--rt/share/html/NoAuth/css/base/main.css1
-rw-r--r--rt/share/html/NoAuth/css/base/superfish-navbar.css2
-rw-r--r--rt/share/html/NoAuth/css/base/superfish.css2
-rw-r--r--rt/share/html/NoAuth/css/base/ticket-form.css31
-rw-r--r--rt/share/html/NoAuth/css/base/ui.timepickr.css56
-rw-r--r--rt/share/html/NoAuth/css/base/ui.timepickr.custom.css54
-rw-r--r--rt/share/html/NoAuth/css/web2/nav.css1
-rw-r--r--rt/share/html/NoAuth/js/jquery-ui-1.8.4.custom.min.js50
-rw-r--r--rt/share/html/NoAuth/js/jquery-ui-patch-datepicker.js31
-rw-r--r--rt/share/html/NoAuth/js/jquery-ui-timepicker-addon.js1326
-rw-r--r--rt/share/html/NoAuth/js/ui.timepickr.js941
-rw-r--r--rt/share/html/NoAuth/js/util.js66
-rw-r--r--rt/share/html/Prefs/Other.html1
-rwxr-xr-xrt/share/html/REST/1.0/Forms/ticket/default22
-rw-r--r--rt/share/html/Search/Chart.html8
-rw-r--r--rt/share/html/Search/Elements/SelectPersonType2
-rwxr-xr-xrt/share/html/Search/Results.html1
-rw-r--r--rt/share/html/Search/Results.xls1
-rwxr-xr-xrt/share/html/Ticket/Attachment/dhandler9
-rw-r--r--rt/share/html/Ticket/Elements/AddCustomers2
-rwxr-xr-xrt/share/html/Ticket/Elements/ShowMembers7
-rw-r--r--rt/share/html/Ticket/Elements/ShowTransactionAttachments18
-rw-r--r--rt/share/html/m/_elements/raw_style4
-rw-r--r--rt/share/html/m/_elements/wrapper2
-rw-r--r--rt/t/api/config.t12
-rw-r--r--rt/t/api/template-insert.t26
-rw-r--r--rt/t/api/template-simple.t275
-rw-r--r--rt/t/api/template.t45
-rw-r--r--rt/t/articles/search-interface.t61
-rw-r--r--rt/t/articles/uri-a.t38
-rw-r--r--rt/t/data/configs/apache2.2+fastcgi.conf.in1
-rw-r--r--rt/t/data/configs/apache2.2+mod_perl.conf.in1
-rw-r--r--rt/t/mail/dashboard-chart-with-utf8.t12
-rw-r--r--rt/t/mail/dashboards.t30
-rw-r--r--rt/t/mail/gateway.t31
-rw-r--r--rt/t/shredder/01ticket.t6
-rw-r--r--rt/t/shredder/03plugin_tickets.t5
-rw-r--r--rt/t/shredder/03plugin_users.t62
-rw-r--r--rt/t/shredder/utils.pl2
-rw-r--r--rt/t/ticket/search_by_watcher.t10
-rw-r--r--rt/t/web/attachments.t23
-rw-r--r--rt/t/web/command_line.t4
-rw-r--r--rt/t/web/command_line_with_unknown_field.t7
-rw-r--r--rt/t/web/crypt-gnupg.t4
-rw-r--r--rt/t/web/googleish_search.t6
-rw-r--r--rt/t/web/query_builder_queue_limits.t3
-rw-r--r--rt/t/web/search_simple.t56
-rw-r--r--rt/t/web/ticket_modify_all.t43
-rw-r--r--rt/t/web/transaction_batch.t9
406 files changed, 12214 insertions, 4872 deletions
diff --git a/FS/FS.pm b/FS/FS.pm
index 8bbff12e5..2d963b54f 100644
--- a/FS/FS.pm
+++ b/FS/FS.pm
@@ -270,6 +270,8 @@ L<FS::sales> - Sales person class
L<FS::agent> - Agent (reseller) class
+L<FS::agent_pkg_class> - Agent (reseller) package class commission class
+
L<FS::agent_type> - Agent type class
L<FS::type_pkgs> - Class linking agent types (see L<FS::agent_type>) with package definitions (see L<FS::part_pkg>)
@@ -282,6 +284,10 @@ L<FS::agent_payment_gateway> - Agent payment gateway class
L<FS::cust_svc> - Service class
+L<FS::part_export_machine> - Export hostname choice class
+
+L<FS::svc_export_machine> - Customer export hostname class
+
L<FS::cust_pkg> - Customer package class
L<FS::cust_pkg_option> - Customer package option class
diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm
index 4de29481d..b38c2671d 100644
--- a/FS/FS/AccessRight.pm
+++ b/FS/FS/AccessRight.pm
@@ -117,6 +117,7 @@ tie my %rights, 'Tie::IxHash',
'Cancel customer',
'Complimentary customer', #aka users-allow_comp
'Merge customer',
+ 'Merge customer across agents',
{ rightname=>'Delete customer', desc=>"Enable customer deletions. Be very careful! Deleting a customer will remove all traces that this customer ever existed! It should probably only be used when auditing a legacy database. Normally, you cancel all of a customer's packages if they cancel service." }, #aka. deletecustomers
'Bill customer now', #NEW
'Bulk send customer notices', #NEW
@@ -177,7 +178,9 @@ tie my %rights, 'Tie::IxHash',
'Customer invoice / financial info rights' => [
'View invoices',
'Resend invoices', #NEWNEW
- 'Delete invoices', #new, but no need to phase in
+ 'Void invoices',
+ 'Unvoid invoices',
+ 'Delete invoices',
'View customer tax exemptions', #yow
'Add customer tax adjustment', #new, but no need to phase in
'View customer batched payments', #NEW
@@ -227,11 +230,11 @@ tie my %rights, 'Tie::IxHash',
###
# customer voiding rights..
###
- 'Customer void rights' => [
+ 'Customer payment void rights' => [
{ rightname=>'Credit card void', desc=>'Enable local-only voiding of echeck payments in addition to refunds against the payment gateway.' }, #aka. cc-void
{ rightname=>'Echeck void', desc=>'Enable local-only voiding of echeck payments in addition to refunds against the payment gateway.' }, #aka. echeck-void
- 'Regular void',
- { rightname=>'Unvoid', desc=>'Enable unvoiding of voided payments' }, #aka. unvoid
+ 'Void payments',
+ { rightname=>'Unvoid payments', desc=>'Enable unvoiding of voided payments' }, #aka. unvoid
],
@@ -261,6 +264,7 @@ tie my %rights, 'Tie::IxHash',
'List all customers',
'Advanced customer search',
'List zip codes', #NEW
+ 'List quotations',
'List invoices',
'List packages',
'Summarize packages',
@@ -396,6 +400,7 @@ sub default_superuser_rights {
'Delete refund', #?
'Edit customer package dates',
'Time queue',
+ 'Usage: Time worked',
'Redownload resolved batches',
'Raw SQL',
'Configuration download',
@@ -404,6 +409,7 @@ sub default_superuser_rights {
'Edit usage',
'Credit card void',
'Echeck void',
+ 'Edit customer package dates',
);
no warnings 'uninitialized';
diff --git a/FS/FS/ClientAPI/MasonComponent.pm b/FS/FS/ClientAPI/MasonComponent.pm
index 534b48a76..c4094ffe0 100644
--- a/FS/FS/ClientAPI/MasonComponent.pm
+++ b/FS/FS/ClientAPI/MasonComponent.pm
@@ -26,6 +26,7 @@ my %allowed_comps = map { $_=>1 } qw(
my %session_comps = map { $_=>1 } qw(
/elements/location.html
+ /elements/tr-amount_fee.html
/edit/cust_main/first_pkg/select-part_pkg.html
);
@@ -41,6 +42,29 @@ my %session_callbacks = (
return ''; #no error
},
+ '/elements/tr-amount_fee.html' => sub {
+ my( $custnum, $argsref ) = @_;
+
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+ or return "unknown custnum $custnum";
+
+ my $conf = new FS::Conf;
+
+ my %args = @$argsref;
+ %args = (
+ %args,
+ 'process-pkgpart' =>
+ scalar($conf->config('selfservice_process-pkgpart', $cust_main->agentnum)),
+ 'process-display' => scalar($conf->config('selfservice_process-display')),
+ 'process-skip_first' => $conf->exists('selfservice_process-skip_first'),
+ 'num_payments' => scalar($cust_main->cust_pay),
+ 'surcharge_percentage' => scalar($conf->config('credit-card-surcharge-percentage')),
+ );
+ @$argsref = ( %args );
+
+ return ''; #no error
+ },
+
'/edit/cust_main/first_pkg/select-part_pkg.html' => sub {
my( $custnum, $argsref ) = @_;
my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index 54799b817..3f7c00432 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -14,6 +14,7 @@ use Business::CreditCard;
use HTML::Entities;
use Text::CSV_XS;
use Spreadsheet::WriteExcel;
+use OLE::Storage_Lite;
use FS::UI::Web::small_custview qw(small_custview); #less doh
use FS::UI::Web;
use FS::UI::bytecount qw( display_bytecount );
@@ -38,6 +39,7 @@ use FS::cust_main;
use FS::cust_bill;
use FS::legacy_cust_bill;
use FS::cust_main_county;
+use FS::part_pkg;
use FS::cust_pkg;
use FS::payby;
use FS::acct_rt_transaction;
@@ -195,8 +197,6 @@ sub login {
} else {
-warn Dumper($p);
-
my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
or return { error => 'Domain '. $p->{'domain'}. ' not found' };
@@ -926,6 +926,21 @@ sub validate_payment {
my $amount = $1;
return { error => 'Amount must be greater than 0' } unless $amount > 0;
+ #false laziness w/tr-amount_fee.html, but we don't want selfservice users
+ #changing the hidden form values
+ my $conf = new FS::Conf;
+ my $fee_display = $conf->config('selfservice_process-display') || 'add';
+ my $fee_pkgpart = $conf->config('selfservice_process-pkgpart', $cust_main->agentnum);
+ my $fee_skip_first = $conf->exists('selfservice_process-skip_first');
+ if ( $fee_display eq 'add'
+ and $fee_pkgpart
+ and ! $fee_skip_first || scalar($cust_main->cust_pay)
+ )
+ {
+ my $fee_pkg = qsearchs('part_pkg', { pkgpart=>$fee_pkgpart } );
+ $amount = sprintf('%.2f', $amount + $fee_pkg->option('setup_fee') );
+ }
+
$p->{'discount_term'} =~ /^\s*(\d*)\s*$/
or return { 'error' => gettext('illegal_discount_term'). ': '. $p->{'discount_term'} };
my $discount_term = $1;
@@ -1085,6 +1100,26 @@ sub do_process_payment {
);
return { 'error' => $error } if $error;
+ #no error, so order the fee package if applicable...
+ my $conf = new FS::Conf;
+ my $fee_pkgpart = $conf->config('selfservice_process-pkgpart', $cust_main->agentnum);
+ my $fee_skip_first = $conf->exists('selfservice_process-skip_first');
+
+ if ( $fee_pkgpart and ! $fee_skip_first || scalar($cust_main->cust_pay) ) {
+
+ my $cust_pkg = new FS::cust_pkg { 'pkgpart' => $fee_pkgpart };
+
+ $error = $cust_main->order_pkg( 'cust_pkg' => $cust_pkg );
+ return { 'error' => "payment processed successfully, but error ordering fee: $error" }
+ if $error;
+
+ #and generate an invoice for it now too
+ $error = $cust_main->bill( 'pkg_list' => [ $cust_pkg ] );
+ return { 'error' => "payment processed and fee ordered sucessfully, but error billing fee: $error" }
+ if $error;
+
+ }
+
$cust_main->apply_payments;
if ( $validate->{'save'} ) {
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 13a84bcca..5c43b3ac9 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -839,6 +839,13 @@ sub reason_type_options {
},
{
+ 'key' => 'cust_main-select-prorate_day',
+ 'section' => 'billing',
+ 'description' => 'When used with prorate or anniversary packages, allows the selection of the prorate day of month, on a per-customer basis',
+ 'type' => 'checkbox',
+ },
+
+ {
'key' => 'encryption',
'section' => 'billing',
'description' => 'Enable encryption of credit cards and echeck numbers',
@@ -1356,7 +1363,7 @@ and customer address. Include units.',
{
'key' => 'invoice_latexextracouponspace',
'section' => 'invoicing',
- 'description' => 'Optional LaTeX invoice textheight space to reserve for a tear off coupon. Include units.',
+ 'description' => 'Optional LaTeX invoice textheight space to reserve for a tear off coupon. Include units. Default is 3.6cm',
'type' => 'text',
'per_agent' => 1,
'validate' => sub { shift =~
@@ -1978,6 +1985,14 @@ and customer address. Include units.',
},
{
+ 'key' => 'national_id-country',
+ 'section' => 'UI',
+ 'description' => 'Track a national identification number, for specific countries.',
+ 'type' => 'select',
+ 'select_enum' => [ '', 'MY' ],
+ },
+
+ {
'key' => 'show_bankstate',
'section' => 'UI',
'description' => "Turns on display/collection of state for bank accounts in the web interface. Sometimes required by electronic check (ACH) processors.",
@@ -2540,6 +2555,7 @@ and customer address. Include units.',
'section' => 'billing',
'description' => 'Package to add to each manual credit card and ACH payment entered by employees from the backend. Enabling this option may be in violation of your merchant agreement(s), so please check it(/them) carefully before enabling this option.',
'type' => 'select-part_pkg',
+ 'per_agent' => 1,
},
{
@@ -2565,6 +2581,7 @@ and customer address. Include units.',
'section' => 'billing',
'description' => 'Package to add to each manual credit card and ACH payment entered by the customer themselves in the self-service interface. Enabling this option may be in violation of your merchant agreement(s), so please check it(/them) carefully before enabling this option.',
'type' => 'select-part_pkg',
+ 'per_agent' => 1,
},
{
@@ -2585,30 +2602,30 @@ and customer address. Include units.',
'type' => 'checkbox',
},
- {
- 'key' => 'suto_process-pkgpart',
- 'section' => 'billing',
- 'description' => 'Package to add to each automatic credit card and ACH payment processed by billing events. Enabling this option may be in violation of your merchant agreement(s), so please check them carefully before enabling this option.',
- 'type' => 'select-part_pkg',
- },
-
# {
-# 'key' => 'auto_process-display',
+# 'key' => 'auto_process-pkgpart',
# 'section' => 'billing',
-# 'description' => 'When using auto_process-pkgpart, add the fee to the amount entered (default), or subtract the fee from the amount entered.',
-# 'type' => 'select',
-# 'select_hash' => [
-# 'add' => 'Add fee to amount entered',
-# 'subtract' => 'Subtract fee from amount entered',
-# ],
+# 'description' => 'Package to add to each automatic credit card and ACH payment processed by billing events. Enabling this option may be in violation of your merchant agreement(s), so please check them carefully before enabling this option.',
+# 'type' => 'select-part_pkg',
+# },
+#
+## {
+## 'key' => 'auto_process-display',
+## 'section' => 'billing',
+## 'description' => 'When using auto_process-pkgpart, add the fee to the amount entered (default), or subtract the fee from the amount entered.',
+## 'type' => 'select',
+## 'select_hash' => [
+## 'add' => 'Add fee to amount entered',
+## 'subtract' => 'Subtract fee from amount entered',
+## ],
+## },
+#
+# {
+# 'key' => 'auto_process-skip_first',
+# 'section' => 'billing',
+# 'description' => "When using auto_process-pkgpart, omit the fee if it is the customer's first payment.",
+# 'type' => 'checkbox',
# },
-
- {
- 'key' => 'auto_process-skip_first',
- 'section' => 'billing',
- 'description' => "When using auto_process-pkgpart, omit the fee if it is the customer's first payment.",
- 'type' => 'checkbox',
- },
{
'key' => 'allow_negative_charges',
@@ -3286,7 +3303,7 @@ and customer address. Include units.',
{
'key' => 'cust_pkg-show_fcc_voice_grade_equivalent',
'section' => 'UI',
- 'description' => "Show a field on package definitions for assigning a DS0 equivalency number suitable for use on FCC form 477.",
+ 'description' => "Show fields on package definitions for FCC Form 477 classification",
'type' => 'checkbox',
},
@@ -3423,7 +3440,7 @@ and customer address. Include units.',
{
'key' => 'invoice-unitprice',
'section' => 'invoicing',
- 'description' => 'Enable unit pricing on invoices.',
+ 'description' => 'Enable unit pricing on invoices and quantities on packages.',
'type' => 'checkbox',
},
@@ -3452,7 +3469,7 @@ and customer address. Include units.',
{
'key' => 'postal_invoice-recurring_only',
'section' => 'billing',
- 'description' => 'The postal invoice fee is omitted on invoices without reucrring charges when this is set.',
+ 'description' => 'The postal invoice fee is omitted on invoices without recurring charges when this is set.',
'type' => 'checkbox',
},
@@ -3710,6 +3727,13 @@ and customer address. Include units.',
},
{
+ 'key' => 'cust_main-enable_anniversary_date',
+ 'section' => 'UI',
+ 'description' => 'Enable tracking of an anniversary date with each customer record',
+ 'type' => 'checkbox',
+ },
+
+ {
'key' => 'cust_main-edit_calling_list_exempt',
'section' => 'UI',
'description' => 'Display the "calling_list_exempt" checkbox on customer edit.',
@@ -5223,6 +5247,13 @@ and customer address. Include units.',
],
},
+ {
+ 'key' => 'agent-email_day',
+ 'section' => '',
+ 'description' => 'On this day of each month, agents with master customer records containing email addresses will be emailed a list of their customers and balances.',
+ 'type' => 'text',
+ },
+
{ key => "apacheroot", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
{ key => "apachemachine", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
{ key => "apachemachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
diff --git a/FS/FS/Cron/agent_email.pm b/FS/FS/Cron/agent_email.pm
new file mode 100644
index 000000000..992aa35a2
--- /dev/null
+++ b/FS/FS/Cron/agent_email.pm
@@ -0,0 +1,79 @@
+package FS::Cron::agent_email;
+use base qw( Exporter );
+
+use strict;
+use vars qw( @EXPORT_OK $DEBUG );
+use Date::Simple qw(today);
+use URI::Escape;
+use FS::Mason qw( mason_interps );
+use FS::Conf;
+use FS::Misc qw(send_email);
+use FS::Record qw(qsearch);# qsearchs);
+use FS::agent;
+
+@EXPORT_OK = qw ( agent_email );
+$DEBUG = 0;
+
+sub agent_email {
+ my %opt = @_;
+
+ my $conf = new FS::Conf;
+
+ my $day = $conf->config('agent-email_day') or return;
+ return unless $day == today->day;
+
+ if ( 1 ) { #XXX if ( %%%RT_ENABLED%%% ) {
+ require RT;
+ RT::LoadConfig();
+ RT::Init();
+ RT::ConnectToDatabase();
+ }
+
+ my $from = $conf->config('invoice_from');
+
+ my $outbuf = '';;
+ my( $fs_interp, $rt_interp ) = mason_interps('standalone', 'outbuf'=>\$outbuf);
+
+ my $comp = '/search/cust_main.html';
+ my %args = (
+ 'cust_fields' => 'Cust# | Cust. Status | Customer | Current Balance',
+ '_type' => 'html-print',
+ );
+ my $query = join('&', map "$_=".uri_escape($args{$_}), keys %args );
+
+ my $extra_sql = $opt{a} ? " AND agentnum IN ( $opt{a} ) " : '';
+
+ foreach my $agent ( qsearch({
+ 'table' => 'agent',
+ 'hashref' => {
+ 'disabled' => '',
+ 'agent_custnum' => { op=>'!=', value=>'' },
+ },
+ 'extra_sql' => $extra_sql,
+ })
+ )
+ {
+
+ $FS::Mason::Request::QUERY_STRING = $query. '&agentnum='. $agent->agentnum;
+ $fs_interp->exec($comp);
+
+ my @email = $agent->agent_cust_main->invoicing_list or next;
+
+ warn "emailing ". join(',',@email). " for agent ". $agent->agent. "\n"
+ if $DEBUG;
+ send_email(
+ 'from' => $from,
+ 'to' => \@email,
+ 'subject' => 'Customer report',
+ 'body' => $outbuf,
+ 'content-type' => 'text/html',
+ #'content-encoding'
+ );
+
+ $outbuf = '';
+
+ }
+
+}
+
+1;
diff --git a/FS/FS/Cron/pay_batch.pm b/FS/FS/Cron/pay_batch.pm
index c7cedafb9..0ab37dd13 100644
--- a/FS/FS/Cron/pay_batch.pm
+++ b/FS/FS/Cron/pay_batch.pm
@@ -103,7 +103,7 @@ sub batch_receive {
if ( $gateway->batch_processor->can('default_transport') ) {
warn "Importing results from '".$gateway->label."'\n" if $DEBUG;
$error = eval {
- FS::pay_batch->import_from_gateway( $gateway, debug => $DEBUG )
+ FS::pay_batch->import_from_gateway( gateway =>$gateway, debug => $DEBUG )
} || $@;
if ( $error ) {
# this we can roll back
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 51edd97cc..11af25efa 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -91,8 +91,9 @@ if ( -e $addl_handler_use_file ) {
use Text::CSV_XS;
use Spreadsheet::WriteExcel;
use Spreadsheet::WriteExcel::Utility;
+ use OLE::Storage_Lite;
use Excel::Writer::XLSX;
- use Excel::Writer::XLSX::Utility;
+ #use Excel::Writer::XLSX::Utility; #redundant with above
use Business::CreditCard 0.30; #for mask-aware cardtype()
use NetAddr::IP;
@@ -315,6 +316,16 @@ if ( -e $addl_handler_use_file ) {
use FS::quotation;
use FS::quotation_pkg;
use FS::quotation_pkg_discount;
+ use FS::cust_bill_void;
+ use FS::cust_bill_pkg_void;
+ use FS::cust_bill_pkg_detail_void;
+ use FS::cust_bill_pkg_display_void;
+ use FS::cust_bill_pkg_tax_location_void;
+ use FS::cust_bill_pkg_tax_rate_location_void;
+ use FS::cust_tax_exempt_pkg_void;
+ use FS::cust_bill_pkg_discount_void;
+ use FS::agent_pkg_class;
+ use FS::svc_export_machine;
# Sammath Naur
if ( $FS::Mason::addl_handler_use ) {
@@ -358,7 +369,7 @@ if ( -e $addl_handler_use_file ) {
use RT::Interface::Web::Request;
- #nother undeclared web UI dep (for ticket links graph)
+ #another undeclared web UI dep (for ticket links graph)
use IPC::Run::SafeHandles;
#slow, unreliable, segfaults and is optional
diff --git a/FS/FS/Mason/Request.pm b/FS/FS/Mason/Request.pm
index 0d21df4ca..36c46dc41 100644
--- a/FS/FS/Mason/Request.pm
+++ b/FS/FS/Mason/Request.pm
@@ -4,6 +4,7 @@ use strict;
use warnings;
use vars qw( $FSURL $QUERY_STRING );
use base 'HTML::Mason::Request';
+use FS::Trace;
$FSURL = 'http://Set/FS_Mason_Request_FSURL/in_standalone_mode/';
$QUERY_STRING = '';
@@ -11,21 +12,27 @@ $QUERY_STRING = '';
sub new {
my $class = shift;
+ FS::Trace->log('creating new FS::Mason::Request object');
+
my $superclass = $HTML::Mason::ApacheHandler::VERSION ?
'HTML::Mason::Request::ApacheHandler' :
$HTML::Mason::CGIHandler::VERSION ?
'HTML::Mason::Request::CGI' :
'HTML::Mason::Request';
+ FS::Trace->log(' altering superclass');
$class->alter_superclass( $superclass );
+ FS::Trace->log(' setting valid params');
#huh... shouldn't alter_superclass take care of this for us?
__PACKAGE__->valid_params( %{ $superclass->valid_params() } );
+ FS::Trace->log(' freeside_setup');
my %opt = @_;
my $mode = $superclass =~ /Apache/i ? 'apache' : 'standalone';
$class->freeside_setup($opt{'comp'}, $mode);
+ FS::Trace->log(' SUPER::new');
$class->SUPER::new(@_);
}
@@ -38,6 +45,8 @@ my $protect_fds;
sub freeside_setup {
my( $class, $filename, $mode ) = @_;
+ FS::Trace->log(' protecting fds');
+
#from rt/bin/webmux.pl(.in)
if ( !$protect_fds && $ENV{'MOD_PERL'} && exists $ENV{'MOD_PERL_API_VERSION'}
&& $ENV{'MOD_PERL_API_VERSION'} >= 2
@@ -57,6 +66,8 @@ sub freeside_setup {
if ( $filename =~ qr(/REST/\d+\.\d+/NoAuth/) ) {
+ FS::Trace->log(' handling RT REST/NoAuth file');
+
package HTML::Mason::Commands; #?
use FS::UID qw( adminsuidsetup );
@@ -65,10 +76,13 @@ sub freeside_setup {
##old installs w/fs_selfs or selfserv??
#&adminsuidsetup('fs_selfservice');
+ FS::Trace->log(' adminsuidsetup fs_queue');
&adminsuidsetup('fs_queue');
} else {
+ FS::Trace->log(' handling regular file');
+
package HTML::Mason::Commands;
use vars qw( $cgi $p $fsurl ); # $lh ); #not using /mt
use Encode;
@@ -77,6 +91,7 @@ sub freeside_setup {
if ( $mode eq 'apache' ) {
$cgi = new CGI;
+ FS::Trace->log(' cgisuidsetup');
&cgisuidsetup($cgi);
#&cgisuidsetup($r);
$fsurl = rooturl();
@@ -91,6 +106,7 @@ sub freeside_setup {
die "unknown mode $mode";
}
+ FS::Trace->log(' UTF-8-decoding form data');
#
foreach my $param ( $cgi->param ) {
my @values = $cgi->param($param);
@@ -102,6 +118,8 @@ sub freeside_setup {
}
+ FS::Trace->log(' done');
+
}
sub callback {
diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm
index 2be9ec203..297e39fbc 100644
--- a/FS/FS/Misc.pm
+++ b/FS/FS/Misc.pm
@@ -913,16 +913,6 @@ sub ocr_image {
@lines;
}
-=item spool_formats
-
-Returns a list of the invoice spool formats.
-
-=cut
-
-sub spool_formats {
- qw(default oneline billco bridgestone)
-}
-
=back
=head1 BUGS
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index 0ac269f4c..ca68c3596 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -2421,10 +2421,9 @@ sub ut_coordn {
}
-
=item ut_domain COLUMN
-Check/untaint host and domain names.
+Check/untaint host and domain names. May not be null.
=cut
@@ -2432,11 +2431,27 @@ sub ut_domain {
my( $self, $field ) = @_;
#$self->getfield($field) =~/^(\w+\.)*\w+$/
$self->getfield($field) =~/^(([\w\-]+\.)*\w+)$/
- or return "Illegal (domain) $field: ". $self->getfield($field);
+ or return "Illegal (hostname) $field: ". $self->getfield($field);
$self->setfield($field,$1);
'';
}
+=item ut_domainn COLUMN
+
+Check/untaint host and domain names. May be null.
+
+=cut
+
+sub ut_domainn {
+ my( $self, $field ) = @_;
+ if ( $self->getfield($field) =~ /^()$/ ) {
+ $self->setfield($field,'');
+ '';
+ } else {
+ $self->ut_domain($field);
+ }
+}
+
=item ut_name COLUMN
Check/untaint proper names; allows alphanumerics, spaces and the following
diff --git a/FS/FS/Report/FCC_477.pm b/FS/FS/Report/FCC_477.pm
index 4c94fff2e..49bb8a852 100644
--- a/FS/FS/Report/FCC_477.pm
+++ b/FS/FS/Report/FCC_477.pm
@@ -45,8 +45,8 @@ Documentation.
);
@technology = (
- 'Asymetric xDSL',
- 'Symetric xDSL',
+ 'Asymmetric xDSL',
+ 'Symmetric xDSL',
'Other Wireline',
'Cable Modem',
'Optical Carrier',
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 61bd00cec..6ad4b742d 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -445,7 +445,7 @@ sub tables_hashref {
my @taxrate_type = ( 'decimal', '', '14,8' ); # requires pg 8 for
my @taxrate_typen = ( 'decimal', 'NULL', '14,8' ); # fs-upgrade to work
- my $username_len = 32; #usernamemax config file
+ my $username_len = 64; #usernamemax config file
# name type nullability length default local
@@ -473,16 +473,16 @@ sub tables_hashref {
'index' => [ ['typenum'], ['disabled'], ['agent_custnum'] ],
},
- 'sales' => {
+ 'agent_pkg_class' => {
'columns' => [
- 'salesnum', 'serial', '', '', '', '',
- 'salesperson', 'varchar', '', $char_d, '', '',
- 'agentnum', 'int', 'NULL', '', '', '',
- 'disabled', 'char', 'NULL', 1, '', '',
+ 'agentpkgclassnum', 'serial', '', '', '', '',
+ 'agentnum', 'int', '', '', '', '',
+ 'classnum', 'int', 'NULL', '', '', '',
+ 'commission_percent', 'decimal', '', '7,4', '', '',
],
- 'primary_key' => 'salesnum',
- 'unique' => [],
- 'index' => [ ['salesnum'], ['disabled'] ],
+ 'primary_key' => 'agentpkgclassnum',
+ 'unique' => [ [ 'agentnum', 'classnum' ], ],
+ 'index' => [],
},
'agent_type' => {
@@ -506,6 +506,18 @@ sub tables_hashref {
'index' => [ ['typenum'] ],
},
+ 'sales' => {
+ 'columns' => [
+ 'salesnum', 'serial', '', '', '', '',
+ 'salesperson', 'varchar', '', $char_d, '', '',
+ 'agentnum', 'int', 'NULL', '', '', '',
+ 'disabled', 'char', 'NULL', 1, '', '',
+ ],
+ 'primary_key' => 'salesnum',
+ 'unique' => [],
+ 'index' => [ ['salesnum'], ['disabled'] ],
+ },
+
'cust_attachment' => {
'columns' => [
'attachnum', 'serial', '', '', '', '',
@@ -551,6 +563,35 @@ sub tables_hashref {
'index' => [ ['custnum'], ['_date'], ['statementnum'], ['agent_invid'] ],
},
+ 'cust_bill_void' => {
+ 'columns' => [
+ #regular fields
+ 'invnum', 'int', '', '', '', '',
+ 'custnum', 'int', '', '', '', '',
+ '_date', @date_type, '', '',
+ 'charged', @money_type, '', '',
+ 'invoice_terms', 'varchar', 'NULL', $char_d, '', '',
+
+ #customer balance info at invoice generation time
+ 'previous_balance', @money_typen, '', '', #eventually not nullable
+ 'billing_balance', @money_typen, '', '', #eventually not nullable
+
+ #specific use cases
+ 'closed', 'char', 'NULL', 1, '', '', #not yet used much
+ 'statementnum', 'int', 'NULL', '', '', '', #invoice aggregate statements
+ 'agent_invid', 'int', 'NULL', '', '', '', #(varchar?) importing legacy
+ 'promised_date', @date_type, '', '',
+
+ #void fields
+ 'void_date', @date_type, '', '',
+ 'reason', 'varchar', 'NULL', $char_d, '', '',
+ 'void_usernum', 'int', 'NULL', '', '', '',
+ ],
+ 'primary_key' => 'invnum',
+ 'unique' => [ [ 'custnum', 'agent_invid' ] ], #agentnum? huh
+ 'index' => [ ['custnum'], ['_date'], ['statementnum'], ['agent_invid'], [ 'void_usernum' ] ],
+ },
+
#for importing invoices from a legacy system for display purposes only
# no effect upon balance
'legacy_cust_bill' => {
@@ -787,6 +828,101 @@ sub tables_hashref {
'index' => [ [ 'billpkgnum' ], [ 'taxnum' ], [ 'taxratelocationnum' ] ],
},
+ 'cust_bill_pkg_void' => {
+ 'columns' => [
+ 'billpkgnum', 'int', '', '', '', '',
+ 'invnum', 'int', '', '', '', '',
+ 'pkgnum', 'int', '', '', '', '',
+ 'pkgpart_override', 'int', 'NULL', '', '', '',
+ 'setup', @money_type, '', '',
+ 'recur', @money_type, '', '',
+ 'sdate', @date_type, '', '',
+ 'edate', @date_type, '', '',
+ 'itemdesc', 'varchar', 'NULL', $char_d, '', '',
+ 'itemcomment', 'varchar', 'NULL', $char_d, '', '',
+ 'section', 'varchar', 'NULL', $char_d, '', '',
+ 'freq', 'varchar', 'NULL', $char_d, '', '',
+ 'quantity', 'int', 'NULL', '', '', '',
+ 'unitsetup', @money_typen, '', '',
+ 'unitrecur', @money_typen, '', '',
+ 'hidden', 'char', 'NULL', 1, '', '',
+ #void fields
+ 'void_date', @date_type, '', '',
+ 'reason', 'varchar', 'NULL', $char_d, '', '',
+ 'void_usernum', 'int', 'NULL', '', '', '',
+ ],
+ 'primary_key' => 'billpkgnum',
+ 'unique' => [],
+ 'index' => [ ['invnum'], [ 'pkgnum' ], [ 'itemdesc' ], [ 'void_usernum' ], ],
+ },
+
+ 'cust_bill_pkg_detail_void' => {
+ 'columns' => [
+ 'detailnum', 'int', '', '', '', '',
+ 'billpkgnum', 'int', 'NULL', '', '', '', # should not be nullable
+ 'pkgnum', 'int', 'NULL', '', '', '', # deprecated
+ 'invnum', 'int', 'NULL', '', '', '', # deprecated
+ 'amount', 'decimal', 'NULL', '10,4', '', '',
+ 'format', 'char', 'NULL', 1, '', '',
+ 'classnum', 'int', 'NULL', '', '', '',
+ 'duration', 'int', 'NULL', '', 0, '',
+ 'phonenum', 'varchar', 'NULL', 15, '', '',
+ 'accountcode', 'varchar', 'NULL', 20, '', '',
+ 'startdate', @date_type, '', '',
+ 'regionname', 'varchar', 'NULL', $char_d, '', '',
+ 'detail', 'varchar', '', 255, '', '',
+ ],
+ 'primary_key' => 'detailnum',
+ 'unique' => [],
+ 'index' => [ [ 'billpkgnum' ], [ 'classnum' ], [ 'pkgnum', 'invnum' ] ],
+ },
+
+ 'cust_bill_pkg_display_void' => {
+ 'columns' => [
+ 'billpkgdisplaynum', 'int', '', '', '', '',
+ 'billpkgnum', 'int', '', '', '', '',
+ 'section', 'varchar', 'NULL', $char_d, '', '',
+ #'unitsetup', @money_typen, '', '', #override the linked real one?
+ #'unitrecur', @money_typen, '', '', #this too?
+ 'post_total', 'char', 'NULL', 1, '', '',
+ 'type', 'char', 'NULL', 1, '', '',
+ 'summary', 'char', 'NULL', 1, '', '',
+ ],
+ 'primary_key' => 'billpkgdisplaynum',
+ 'unique' => [],
+ 'index' => [ ['billpkgnum'], ],
+ },
+
+ 'cust_bill_pkg_tax_location_void' => {
+ 'columns' => [
+ 'billpkgtaxlocationnum', 'int', '', '', '', '',
+ 'billpkgnum', 'int', '', '', '', '',
+ 'taxnum', 'int', '', '', '', '',
+ 'taxtype', 'varchar', '', $char_d, '', '',
+ 'pkgnum', 'int', '', '', '', '',
+ 'locationnum', 'int', '', '', '', '', #redundant?
+ 'amount', @money_type, '', '',
+ ],
+ 'primary_key' => 'billpkgtaxlocationnum',
+ 'unique' => [],
+ 'index' => [ [ 'billpkgnum' ], [ 'taxnum' ], [ 'pkgnum' ], [ 'locationnum' ] ],
+ },
+
+ 'cust_bill_pkg_tax_rate_location_void' => {
+ 'columns' => [
+ 'billpkgtaxratelocationnum', 'int', '', '', '', '',
+ 'billpkgnum', 'int', '', '', '', '',
+ 'taxnum', 'int', '', '', '', '',
+ 'taxtype', 'varchar', '', $char_d, '', '',
+ 'locationtaxid', 'varchar', 'NULL', $char_d, '', '',
+ 'taxratelocationnum', 'int', '', '', '', '',
+ 'amount', @money_type, '', '',
+ ],
+ 'primary_key' => 'billpkgtaxratelocationnum',
+ 'unique' => [],
+ 'index' => [ [ 'billpkgnum' ], [ 'taxnum' ], [ 'taxratelocationnum' ] ],
+ },
+
'cust_credit' => {
'columns' => [
'crednum', 'serial', '', '', '', '',
@@ -801,6 +937,7 @@ sub tables_hashref {
'closed', 'char', 'NULL', 1, '', '',
'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances
'eventnum', 'int', 'NULL', '', '', '', #triggering event for commission
+ #'commission_agentnum', 'int', 'NULL', '', '', '', #
],
'primary_key' => 'crednum',
'unique' => [],
@@ -856,8 +993,10 @@ sub tables_hashref {
'ss', 'varchar', 'NULL', 11, '', '',
'stateid', 'varchar', 'NULL', $char_d, '', '',
'stateid_state', 'varchar', 'NULL', $char_d, '', '',
+ 'national_id', 'varchar', 'NULL', $char_d, '', '',
'birthdate' ,@date_type, '', '',
'spouse_birthdate' ,@date_type, '', '',
+ 'anniversary_date' ,@date_type, '', '',
'signupdate',@date_type, '', '',
'dundate', @date_type, '', '',
'company', 'varchar', 'NULL', $char_d, '', '',
@@ -925,6 +1064,7 @@ sub tables_hashref {
'email_csv_cdr', 'char', 'NULL', 1, '', '',
'accountcode_cdr', 'char', 'NULL', 1, '', '',
'billday', 'int', 'NULL', '', '', '',
+ 'prorate_day', 'int', 'NULL', '', '', '',
'edit_subject', 'char', 'NULL', 1, '', '',
'locale', 'varchar', 'NULL', 16, '', '',
'calling_list_exempt', 'char', 'NULL', 1, '', '',
@@ -993,7 +1133,7 @@ sub tables_hashref {
# 'middle', 'varchar', 'NULL', $char_d, '', '',
'first', 'varchar', '', $char_d, '', '',
'title', 'varchar', 'NULL', $char_d, '', '', #eg Head Bottle Washer
- 'comment', 'varchar', 'NULL', $char_d, '', '',
+ 'comment', 'varchar', 'NULL', 255, '', '',
'disabled', 'char', 'NULL', 1, '', '',
],
'primary_key' => 'contactnum',
@@ -1418,20 +1558,29 @@ sub tables_hashref {
'columns' => [
'paynum', 'int', '', '', '', '',
'custnum', 'int', '', '', '', '',
- 'paid', @money_type, '', '',
'_date', @date_type, '', '',
+ 'paid', @money_type, '', '',
+ 'otaker', 'varchar', 'NULL', 32, '', '',
+ 'usernum', 'int', 'NULL', '', '', '',
'payby', 'char', '', 4, '', '', # CARD/BILL/COMP, should be
# index into payby table
# eventually
'payinfo', 'varchar', 'NULL', 512, '', '', #see cust_main above
'paymask', 'varchar', 'NULL', $char_d, '', '',
+ #'paydate' ?
'paybatch', 'varchar', 'NULL', $char_d, '', '', #for auditing purposes.
'closed', 'char', 'NULL', 1, '', '',
'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances
+ # cash/check deposit info fields
+ 'bank', 'varchar', 'NULL', $char_d, '', '',
+ 'depositor', 'varchar', 'NULL', $char_d, '', '',
+ 'account', 'varchar', 'NULL', 20, '', '',
+ 'teller', 'varchar', 'NULL', 20, '', '',
+ 'batchnum', 'int', 'NULL', '', '', '', #pay_batch foreign key
+
+ #void fields
'void_date', @date_type, '', '',
'reason', 'varchar', 'NULL', $char_d, '', '',
- 'otaker', 'varchar', 'NULL', 32, '', '',
- 'usernum', 'int', 'NULL', '', '', '',
'void_usernum', 'int', 'NULL', '', '', '',
],
'primary_key' => 'paynum',
@@ -1650,6 +1799,19 @@ sub tables_hashref {
'index' => [ [ 'billpkgnum' ], [ 'pkgdiscountnum' ] ],
},
+ 'cust_bill_pkg_discount_void' => {
+ 'columns' => [
+ 'billpkgdiscountnum', 'int', '', '', '', '',
+ 'billpkgnum', 'int', '', '', '', '',
+ 'pkgdiscountnum', 'int', '', '', '', '',
+ 'amount', @money_type, '', '',
+ 'months', 'decimal', 'NULL', '7,4', '', '',
+ ],
+ 'primary_key' => 'billpkgdiscountnum',
+ 'unique' => [],
+ 'index' => [ [ 'billpkgnum' ], [ 'pkgdiscountnum' ] ],
+ },
+
'discount' => {
'columns' => [
'discountnum', 'serial', '', '', '', '',
@@ -1728,6 +1890,30 @@ sub tables_hashref {
'index' => [ [ 'svcnum' ], [ 'optionname' ] ],
},
+ 'svc_export_machine' => {
+ 'columns' => [
+ 'svcexportmachinenum', 'serial', '', '', '', '',
+ 'svcnum', 'int', '', '', '', '',
+ 'exportnum', 'int', '', '', '', '',
+ 'machinenum', 'int', '', '', '', '',
+ ],
+ 'primary_key' => 'svcexportmachinenum',
+ 'unique' => [ ['svcnum', 'exportnum'] ],
+ 'index' => [],
+ },
+
+ 'part_export_machine' => {
+ 'columns' => [
+ 'machinenum', 'serial', '', '', '', '',
+ 'exportnum', 'int', '', '', '', '',
+ 'machine', 'varchar', 'NULL', $char_d, '', '',
+ 'disabled', 'char', 'NULL', 1, '', '',
+ ],
+ 'primary_key' => 'machinenum',
+ 'unique' => [ [ 'exportnum', 'machine' ] ],
+ 'index' => [ [ 'exportnum' ] ],
+ },
+
'part_pkg' => {
'columns' => [
'pkgpart', 'serial', '', '', '', '',
@@ -1750,6 +1936,7 @@ sub tables_hashref {
'credit_weight', 'real', 'NULL', '', '', '',
'agentnum', 'int', 'NULL', '', '', '',
'fcc_ds0s', 'int', 'NULL', '', '', '',
+ 'fcc_voip_class','char', 'NULL', 1, '', '',
'no_auto', 'char', 'NULL', 1, '', '',
'recur_show_zero', 'char', 'NULL', 1, '', '',
'setup_show_zero', 'char', 'NULL', 1, '', '',
@@ -2460,11 +2647,11 @@ sub tables_hashref {
'part_export' => {
'columns' => [
- 'exportnum', 'serial', '', '', '', '',
+ 'exportnum', 'serial', '', '', '', '',
'exportname', 'varchar', 'NULL', $char_d, '', '',
- 'machine', 'varchar', '', $char_d, '', '',
- 'exporttype', 'varchar', '', $char_d, '', '',
- 'nodomain', 'char', 'NULL', 1, '', '',
+ 'machine', 'varchar', 'NULL', $char_d, '', '',
+ 'exporttype', 'varchar', '', $char_d, '', '',
+ 'nodomain', 'char', 'NULL', 1, '', '',
],
'primary_key' => 'exportnum',
'unique' => [],
@@ -2501,6 +2688,8 @@ sub tables_hashref {
'groupname', 'varchar', '', $char_d, '', '',
'description', 'varchar', 'NULL', $char_d, '', '',
'priority', 'int', '', '', '1', '',
+ 'speed_up', 'int', 'NULL', '', '', '',
+ 'speed_down', 'int', 'NULL', '', '', '',
],
'primary_key' => 'groupnum',
'unique' => [ ['groupname'] ],
@@ -2509,16 +2698,16 @@ sub tables_hashref {
'radius_attr' => {
'columns' => [
- 'attrnum', 'serial', '', '', '', '',
- 'groupnum', 'int', '', '', '', '',
+ 'attrnum', 'serial', '', '', '', '',
+ 'groupnum', 'int', '', '', '', '',
'attrname', 'varchar', '', $char_d, '', '',
- 'value', 'varchar', '', $char_d, '', '',
- 'attrtype', 'char', '', 1, '', '',
- 'op', 'char', '', 2, '', '',
+ 'value', 'varchar', '', 255, '', '',
+ 'attrtype', 'char', '', 1, '', '',
+ 'op', 'char', '', 2, '', '',
],
'primary_key' => 'attrnum',
- 'unique' => [ ['groupnum','attrname'] ], #?
- 'index' => [],
+ 'unique' => [],
+ 'index' => [ ['groupnum'], ],
},
'msgcat' => {
@@ -2553,10 +2742,42 @@ sub tables_hashref {
#'custnum', 'int', '', '', '', ''
'billpkgnum', 'int', '', '', '', '',
'taxnum', 'int', '', '', '', '',
- 'year', 'int', '', '', '', '',
- 'month', 'int', '', '', '', '',
+ 'year', 'int', 'NULL', '', '', '',
+ 'month', 'int', 'NULL', '', '', '',
+ 'creditbillpkgnum', 'int', 'NULL', '', '', '',
+ 'amount', @money_type, '', '',
+ # exemption type flags
+ 'exempt_cust', 'char', 'NULL', 1, '', '',
+ 'exempt_setup', 'char', 'NULL', 1, '', '',
+ 'exempt_recur', 'char', 'NULL', 1, '', '',
+ 'exempt_cust_taxname', 'char', 'NULL', 1, '', '',
+ 'exempt_monthly', 'char', 'NULL', 1, '', '',
+ ],
+ 'primary_key' => 'exemptpkgnum',
+ 'unique' => [],
+ 'index' => [ [ 'taxnum', 'year', 'month' ],
+ [ 'billpkgnum' ],
+ [ 'taxnum' ],
+ [ 'creditbillpkgnum' ],
+ ],
+ },
+
+ 'cust_tax_exempt_pkg_void' => {
+ 'columns' => [
+ 'exemptpkgnum', 'int', '', '', '', '',
+ #'custnum', 'int', '', '', '', ''
+ 'billpkgnum', 'int', '', '', '', '',
+ 'taxnum', 'int', '', '', '', '',
+ 'year', 'int', 'NULL', '', '', '',
+ 'month', 'int', 'NULL', '', '', '',
'creditbillpkgnum', 'int', 'NULL', '', '', '',
'amount', @money_type, '', '',
+ # exemption type flags
+ 'exempt_cust', 'char', 'NULL', 1, '', '',
+ 'exempt_setup', 'char', 'NULL', 1, '', '',
+ 'exempt_recur', 'char', 'NULL', 1, '', '',
+ 'exempt_cust_taxname', 'char', 'NULL', 1, '', '',
+ 'exempt_monthly', 'char', 'NULL', 1, '', '',
],
'primary_key' => 'exemptpkgnum',
'unique' => [],
@@ -3502,6 +3723,8 @@ sub tables_hashref {
'reason_type', 'int', '', '', '', '',
'reason', 'text', '', '', '', '',
'disabled', 'char', 'NULL', 1, '', '',
+ 'unsuspend_pkgpart', 'int', 'NULL', '', '', '',
+ 'unsuspend_hold','char', 'NULL', 1, '', '',
],
'primary_key' => 'reasonnum',
'unique' => [],
diff --git a/FS/FS/TemplateItem_Mixin.pm b/FS/FS/TemplateItem_Mixin.pm
new file mode 100644
index 000000000..6d7ea26bc
--- /dev/null
+++ b/FS/FS/TemplateItem_Mixin.pm
@@ -0,0 +1,317 @@
+package FS::TemplateItem_Mixin;
+
+use strict;
+use vars qw( $DEBUG $me ); # but NOT $conf
+use Carp;
+use FS::UID;
+use FS::Record qw( qsearch qsearchs dbh );
+use FS::part_pkg;
+use FS::cust_pkg;
+
+$DEBUG = 0;
+$me = '[FS::TemplateItem_Mixin]';
+
+=item cust_pkg
+
+Returns the package (see L<FS::cust_pkg>) for this invoice line item.
+
+=cut
+
+sub cust_pkg {
+ my $self = shift;
+ carp "$me $self -> cust_pkg" if $DEBUG;
+ qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
+}
+
+=item part_pkg
+
+Returns the package definition for this invoice line item.
+
+=cut
+
+sub part_pkg {
+ my $self = shift;
+ if ( $self->pkgpart_override ) {
+ qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
+ } else {
+ my $part_pkg;
+ my $cust_pkg = $self->cust_pkg;
+ $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
+ $part_pkg;
+ }
+
+}
+
+=item desc
+
+Returns a description for this line item. For typical line items, this is the
+I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
+For one-shot line items and named taxes, it is the I<itemdesc> field of this
+line item, and for generic taxes, simply returns "Tax".
+
+=cut
+
+sub desc {
+ my $self = shift;
+
+ if ( $self->pkgnum > 0 ) {
+ $self->itemdesc || $self->part_pkg->pkg;
+ } else {
+ my $desc = $self->itemdesc || 'Tax';
+ $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
+ $desc;
+ }
+}
+
+=item details [ OPTION => VALUE ... ]
+
+Returns an array of detail information for the invoice line item.
+
+Currently available options are: I<format>, I<escape_function> and
+I<format_function>.
+
+If I<format> is set to html or latex then the array members are improved
+for tabular appearance in those environments if possible.
+
+If I<escape_function> is set then the array members are processed by this
+function before being returned.
+
+I<format_function> overrides the normal HTML or LaTeX function for returning
+formatted CDRs. It can be set to a subroutine which returns an empty list
+to skip usage detail:
+
+ 'format_function' => sub { () },
+
+=cut
+
+sub details {
+ my ( $self, %opt ) = @_;
+ my $escape_function = $opt{escape_function} || sub { shift };
+
+ my $csv = new Text::CSV_XS;
+
+ if ( $opt{format_function} ) {
+
+ #this still expects to be passed a cust_bill_pkg_detail object as the
+ #second argument, which is expensive
+ carp "deprecated format_function passed to cust_bill_pkg->details";
+ my $format_sub = $opt{format_function} if $opt{format_function};
+
+ map { ( $_->format eq 'C'
+ ? &{$format_sub}( $_->detail, $_ )
+ : &{$escape_function}( $_->detail )
+ )
+ }
+ qsearch ({ 'table' => $self->detail_table,
+ 'hashref' => { 'billpkgnum' => $self->billpkgnum },
+ 'order_by' => 'ORDER BY detailnum',
+ });
+
+ } elsif ( $opt{'no_usage'} ) {
+
+ my $sql = "SELECT detail FROM ". $self->detail_table.
+ " WHERE billpkgnum = ". $self->billpkgnum.
+ " AND ( format IS NULL OR format != 'C' ) ".
+ " ORDER BY detailnum";
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+
+ map &{$escape_function}( $_->[0] ), @{ $sth->fetchall_arrayref };
+
+ } else {
+
+ my $format_sub;
+ my $format = $opt{format} || '';
+ if ( $format eq 'html' ) {
+
+ $format_sub = sub { my $detail = shift;
+ $csv->parse($detail) or return "can't parse $detail";
+ join('</TD><TD>', map { &$escape_function($_) }
+ $csv->fields
+ );
+ };
+
+ } elsif ( $format eq 'latex' ) {
+
+ $format_sub = sub {
+ my $detail = shift;
+ $csv->parse($detail) or return "can't parse $detail";
+ #join(' & ', map { '\small{'. &$escape_function($_). '}' }
+ # $csv->fields );
+ my $result = '';
+ my $column = 1;
+ foreach ($csv->fields) {
+ $result .= ' & ' if $column > 1;
+ if ($column > 6) { # KLUDGE ALERT!
+ $result .= '\multicolumn{1}{l}{\scriptsize{'.
+ &$escape_function($_). '}}';
+ }else{
+ $result .= '\scriptsize{'. &$escape_function($_). '}';
+ }
+ $column++;
+ }
+ $result;
+ };
+
+ } else {
+
+ $format_sub = sub { my $detail = shift;
+ $csv->parse($detail) or return "can't parse $detail";
+ join(' - ', map { &$escape_function($_) }
+ $csv->fields
+ );
+ };
+
+ }
+
+ my $sql = "SELECT format, detail FROM ". $self->detail_table.
+ " WHERE billpkgnum = ". $self->billpkgnum.
+ " ORDER BY detailnum";
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+
+ #avoid the fetchall_arrayref and loop for less memory usage?
+
+ map { (defined($_->[0]) && $_->[0] eq 'C')
+ ? &{$format_sub}( $_->[1] )
+ : &{$escape_function}( $_->[1] );
+ }
+ @{ $sth->fetchall_arrayref };
+
+ }
+
+}
+
+=item details_header [ OPTION => VALUE ... ]
+
+Returns a list representing an invoice line item detail header, if any.
+This relies on the behavior of voip_cdr in that it expects the header
+to be the first CSV formatted detail (as is expected by invoice generation
+routines). Returns the empty list otherwise.
+
+=cut
+
+sub details_header {
+ my $self = shift;
+
+ my $csv = new Text::CSV_XS;
+
+ my @detail =
+ qsearch ({ 'table' => $self->detail_table,
+ 'hashref' => { 'billpkgnum' => $self->billpkgnum,
+ 'format' => 'C',
+ },
+ 'order_by' => 'ORDER BY detailnum LIMIT 1',
+ });
+ return() unless scalar(@detail);
+ $csv->parse($detail[0]->detail) or return ();
+ $csv->fields;
+}
+
+=item quantity
+
+=cut
+
+sub quantity {
+ my( $self, $value ) = @_;
+ if ( defined($value) ) {
+ $self->setfield('quantity', $value);
+ }
+ $self->getfield('quantity') || 1;
+}
+
+=item unitsetup
+
+=cut
+
+sub unitsetup {
+ my( $self, $value ) = @_;
+ if ( defined($value) ) {
+ $self->setfield('unitsetup', $value);
+ }
+ $self->getfield('unitsetup') eq ''
+ ? $self->getfield('setup')
+ : $self->getfield('unitsetup');
+}
+
+=item unitrecur
+
+=cut
+
+sub unitrecur {
+ my( $self, $value ) = @_;
+ if ( defined($value) ) {
+ $self->setfield('unitrecur', $value);
+ }
+ $self->getfield('unitrecur') eq ''
+ ? $self->getfield('recur')
+ : $self->getfield('unitrecur');
+}
+
+=item cust_bill_pkg_display [ type => TYPE ]
+
+Returns an array of display information for the invoice line item optionally
+limited to 'TYPE'.
+
+=cut
+
+sub cust_bill_pkg_display {
+ my ( $self, %opt ) = @_;
+
+ my $class = 'FS::'. $self->display_table;
+
+ my $default = $class->new( { billpkgnum =>$self->billpkgnum } );
+
+ my $type = $opt{type} if exists $opt{type};
+ my @result;
+
+ if ( $self->get('display') ) {
+ @result = grep { defined($type) ? ($type eq $_->type) : 1 }
+ @{ $self->get('display') };
+ } else {
+ my $hashref = { 'billpkgnum' => $self->billpkgnum };
+ $hashref->{type} = $type if defined($type);
+
+ @result = qsearch ({ 'table' => $self->display_table,
+ 'hashref' => { 'billpkgnum' => $self->billpkgnum },
+ 'order_by' => 'ORDER BY billpkgdisplaynum',
+ });
+ }
+
+ push @result, $default unless ( scalar(@result) || $type );
+
+ @result;
+
+}
+
+=item cust_bill_pkg_detail [ CLASSNUM ]
+
+Returns the list of associated cust_bill_pkg_detail objects
+The optional CLASSNUM argument will limit the details to the specified usage
+class.
+
+=cut
+
+sub cust_bill_pkg_detail {
+ my $self = shift;
+ my $classnum = shift || '';
+
+ my %hash = ( 'billpkgnum' => $self->billpkgnum );
+ $hash{classnum} = $classnum if $classnum;
+
+ qsearch( $self->detail_table, \%hash ),
+
+}
+
+=item cust_bill_pkg_discount
+
+Returns the list of associated cust_bill_pkg_discount objects.
+
+=cut
+
+sub cust_bill_pkg_discount {
+ my $self = shift;
+ qsearch( $self->discount_table, { 'billpkgnum' => $self->billpkgnum } );
+}
+
+1;
diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm
index 61cfccba8..d35fd55f2 100644
--- a/FS/FS/Template_Mixin.pm
+++ b/FS/FS/Template_Mixin.pm
@@ -894,7 +894,6 @@ sub print_generic {
warn "$me setting options\n"
if $DEBUG > 1;
- my $multilocation = scalar($cust_main->cust_location); #too expensive?
my %options = ();
$options{'section'} = $section if $multisection;
$options{'format'} = $format;
@@ -904,7 +903,6 @@ sub print_generic {
$options{'summary_page'} = $summarypage;
$options{'skip_usage'} =
scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
- $options{'multilocation'} = $multilocation;
$options{'multisection'} = $multisection;
warn "$me searching for line items\n"
@@ -2111,8 +2109,6 @@ ignored.
multisection: a flag indicating that this is a multisection invoice,
which does something complicated.
-multilocation: a flag to display the location label for the package.
-
Returns a list of hashrefs, each of which may contain:
pkgnum, description, amount, unit_amount, quantity, _is_setup, and
@@ -2134,13 +2130,13 @@ sub _items_cust_bill_pkg {
my $unsquelched = $opt{unsquelched} || ''; #unused
my $section = $opt{section}->{description} if $opt{section};
my $summary_page = $opt{summary_page} || ''; #unused
- my $multilocation = $opt{multilocation} || '';
my $multisection = $opt{multisection} || '';
my $discount_show_always = 0;
my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
+ # and location labels
my @b = ();
my ($s, $r, $u) = ( undef, undef, undef );
@@ -2255,7 +2251,7 @@ sub _items_cust_bill_pkg {
$cust_pkg->h_labels_short($self->_date, undef, 'I')
unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
- if ( $multilocation ) {
+ if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
my $loc = $cust_pkg->location_label;
$loc = substr($loc, 0, $maxlength). '...'
if $format eq 'latex' && length($loc) > $maxlength;
@@ -2357,7 +2353,7 @@ sub _items_cust_bill_pkg {
warn "$me _items_cust_bill_pkg done adding service details\n"
if $DEBUG > 1;
- if ( $multilocation ) {
+ if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
my $loc = $cust_pkg->location_label;
$loc = substr($loc, 0, $maxlength). '...'
if $format eq 'latex' && length($loc) > $maxlength;
@@ -2411,6 +2407,10 @@ sub _items_cust_bill_pkg {
$amount = $cust_bill_pkg->usage;
}
+ my $unit_amount =
+ ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
+ : $amount;
+
if ( !$type || $type eq 'R' ) {
warn "$me _items_cust_bill_pkg adding recur\n"
@@ -2418,7 +2418,7 @@ sub _items_cust_bill_pkg {
if ( $cust_bill_pkg->hidden ) {
$r->{amount} += $amount;
- $r->{unit_amount} += $cust_bill_pkg->unitrecur;
+ $r->{unit_amount} += $unit_amount;
push @{ $r->{ext_description} }, @d;
} else {
$r = {
@@ -2427,7 +2427,7 @@ sub _items_cust_bill_pkg {
pkgnum => $cust_bill_pkg->pkgnum,
amount => $amount,
recur_show_zero => $cust_bill_pkg->recur_show_zero,
- unit_amount => $cust_bill_pkg->unitrecur,
+ unit_amount => $unit_amount,
quantity => $cust_bill_pkg->quantity,
%item_dates,
ext_description => \@d,
@@ -2442,7 +2442,7 @@ sub _items_cust_bill_pkg {
if ( $cust_bill_pkg->hidden ) {
$u->{amount} += $amount;
- $u->{unit_amount} += $cust_bill_pkg->unitrecur;
+ $u->{unit_amount} += $unit_amount,
push @{ $u->{ext_description} }, @d;
} else {
$u = {
@@ -2451,7 +2451,7 @@ sub _items_cust_bill_pkg {
pkgnum => $cust_bill_pkg->pkgnum,
amount => $amount,
recur_show_zero => $cust_bill_pkg->recur_show_zero,
- unit_amount => $cust_bill_pkg->unitrecur,
+ unit_amount => $unit_amount,
quantity => $cust_bill_pkg->quantity,
%item_dates,
ext_description => \@d,
diff --git a/FS/FS/TicketSystem/RT_Internal.pm b/FS/FS/TicketSystem/RT_Internal.pm
index e2dfce373..01e2e2966 100644
--- a/FS/FS/TicketSystem/RT_Internal.pm
+++ b/FS/FS/TicketSystem/RT_Internal.pm
@@ -50,7 +50,7 @@ sub access_right {
sub session {
my( $self, $session ) = @_;
- if ( $session && $session->{'Current_User'} ) { # does this even work?
+ if ( $session && $session->{'CurrentUser'} ) { # does this even work?
warn "$me session: using existing session and CurrentUser: \n".
Dumper($session->{'CurrentUser'})
if $DEBUG;
@@ -92,6 +92,7 @@ sub init {
# this needs to be done on each fork
warn "$me init: initializing RT\n" if $DEBUG;
{
+ local $SIG{__WARN__};
local $SIG{__DIE__};
eval 'RT::Init("NoSignalHandlers"=>1);';
}
diff --git a/FS/FS/Trace.pm b/FS/FS/Trace.pm
new file mode 100644
index 000000000..9ff39dd26
--- /dev/null
+++ b/FS/FS/Trace.pm
@@ -0,0 +1,35 @@
+package FS::Trace;
+
+use strict;
+use Date::Format;
+use File::Slurp;
+
+my @trace = ();
+
+sub log {
+ my( $class, $msg ) = @_;
+ push @trace, [ time, "[$$][". time2str('%r', time). "] $msg" ];
+}
+
+sub total {
+ $trace[-1]->[0] - $trace[0]->[0];
+}
+
+sub reset {
+ @trace = ();
+}
+
+sub dump_ary {
+ map $_->[1], @trace;
+}
+
+sub dump {
+ join("\n", map $_->[1], @trace). "\n";
+}
+
+sub dumpfile {
+ my( $class, $filename, $header ) = @_;
+ write_file( $filename, "$header\n". $class->dump );
+}
+
+1;
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index 417b2026c..3f76f5116 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -70,6 +70,20 @@ sub upgrade_config {
foreach grep { ! $conf->exists($_) && -s "$DIST_CONF/$_" }
qw( quotation_html quotation_latex quotation_latexnotes );
+ # change 'fslongtable' to 'longtable'
+ # in invoice and quotation main templates, and also in all secondary
+ # invoice templates
+ my @latex_confs =
+ qsearch('conf', { 'name' => {op=>'LIKE', value=>'%latex%'} });
+
+ foreach my $c (@latex_confs) {
+ my $value = $c->value;
+ if (length($value) and $value =~ /fslongtable/) {
+ $value =~ s/fslongtable/longtable/g;
+ $conf->set($c->name, $value, $c->agentnum);
+ }
+ }
+
}
sub upgrade_overlimit_groups {
@@ -278,6 +292,12 @@ sub upgrade_data {
#set up payment gateways if needed
'pay_batch' => [],
+
+ #flag monthly tax exemptions
+ 'cust_tax_exempt_pkg' => [],
+
+ #kick off tax location history upgrade
+ 'cust_bill_pkg' => [],
;
\%hash;
diff --git a/FS/FS/access_right.pm b/FS/FS/access_right.pm
index e6266b49b..397b456ce 100644
--- a/FS/FS/access_right.pm
+++ b/FS/FS/access_right.pm
@@ -152,6 +152,8 @@ sub _upgrade_data { # class method
'Process payment' => [ 'Process credit card payment', 'Process Echeck payment' ],
'Post refund' => [ 'Post check refund', 'Post cash refund' ],
'Refund payment' => [ 'Refund credit card payment', 'Refund Echeck payment' ],
+ 'Regular void' => [ 'Void payments' ],
+ 'Unvoid' => [ 'Unvoid payments', 'Unvoid invoices' ],
);
foreach my $oldright (keys %migrate) {
@@ -174,9 +176,10 @@ sub _upgrade_data { # class method
die $error if $error;
}
- #after the WEST stuff is sorted, etc.
- #my $error = $old->delete;
- #die $error if $error;
+ unless ( $oldright =~ / (payment|refund)$/ ) { #after the WEST stuff is sorted
+ my $error = $old->delete;
+ die $error if $error;
+ }
}
@@ -193,6 +196,8 @@ sub _upgrade_data { # class method
'Suspend customer package' => 'Suspend customer',
'Unsuspend customer package' => 'Unsuspend customer',
'New prospect' => 'Generate quotation',
+ 'Delete invoices' => 'Void invoices',
+ 'List invoices' => 'List quotations',
'List services' => [ 'Services: Accounts',
'Services: Domains',
@@ -261,7 +266,7 @@ sub _upgrade_data { # class method
'rightname' => 'Download report data',
} );
my $error = $access_right->insert;
- die $error if $error;
+ warn $error if $error;
}
FS::upgrade_journal->set_done('ACL_download_report_data');
diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm
index e00f587c6..686bdbd18 100755
--- a/FS/FS/addr_block.pm
+++ b/FS/FS/addr_block.pm
@@ -223,43 +223,45 @@ sub cidr {
$self->NetAddr->cidr;
}
-=item free_addrs
+=item next_free_addr
Returns a NetAddr::IP object corresponding to the first unassigned address
in the block (other than the network, broadcast, or gateway address). If
there are no free addresses, returns nothing. There are never free addresses
when manual_flag is true.
-=item next_free_addr
-
-Returns a NetAddr::IP object for the first unassigned address in the block,
-or '' if there are none.
+There is no longer a method to return all free addresses in a block.
=cut
-sub free_addrs {
+sub next_free_addr {
my $self = shift;
+ my $selfaddr = $self->NetAddr;
return if $self->manual_flag;
my $conf = new FS::Conf;
my @excludeaddr = $conf->config('exclude_ip_addr');
-
+
my %used = map { $_ => 1 }
(
+ @excludeaddr,
+ $selfaddr->addr,
+ $selfaddr->network->addr,
+ $selfaddr->broadcast->addr,
(map { $_->NetAddr->addr }
- ($self,
- qsearch('svc_broadband', { blocknum => $self->blocknum }))
+ qsearch('svc_broadband', { blocknum => $self->blocknum })
), @excludeaddr
);
- grep { !$used{$_->addr} } $self->NetAddr->hostenum;
-
-}
+ # just do a linear search of the block
+ my $freeaddr = $selfaddr->network + 1;
+ while ( $freeaddr < $selfaddr->broadcast ) {
+ return $freeaddr unless $used{ $freeaddr->addr };
+ $freeaddr++;
+ }
+ return;
-sub next_free_addr {
- my $self = shift;
- ($self->free_addrs, '')[0]
}
=item allocate -- deprecated
diff --git a/FS/FS/agent_pkg_class.pm b/FS/FS/agent_pkg_class.pm
new file mode 100644
index 000000000..1683c1a14
--- /dev/null
+++ b/FS/FS/agent_pkg_class.pm
@@ -0,0 +1,117 @@
+package FS::agent_pkg_class;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::agent_pkg_class - Object methods for agent_pkg_class records
+
+=head1 SYNOPSIS
+
+ use FS::agent_pkg_class;
+
+ $record = new FS::agent_pkg_class \%hash;
+ $record = new FS::agent_pkg_class { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::agent_pkg_class object represents an commission for a specific agent
+and package class. FS::agent_pkg_class inherits from FS::Record. The
+following fields are currently supported:
+
+=over 4
+
+=item agentpkgclassnum
+
+primary key
+
+=item agentnum
+
+agentnum
+
+=item classnum
+
+classnum
+
+=item commission_percent
+
+commission_percent
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'agent_pkg_class'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ $self->commission_percent(0) unless length($self->commission_percent);
+
+ my $error =
+ $self->ut_numbern('agentpkgclassnum')
+ || $self->ut_foreign_key('agentnum', 'agent', 'agentnum')
+ || $self->ut_foreign_keyn('classnum', 'pkg_class', 'classnum')
+ || $self->ut_float('commission_percent')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm
index 3a6b01ba5..05179f264 100644
--- a/FS/FS/cdr.pm
+++ b/FS/FS/cdr.pm
@@ -478,6 +478,80 @@ sub set_status_and_rated_price {
}
}
+=item parse_number [ OPTION => VALUE ... ]
+
+Returns two scalars, the countrycode and the rest of the number.
+
+Options are passed as name-value pairs. Currently available options are:
+
+=over 4
+
+=item column
+
+The column containing the number to be parsed. Defaults to dst.
+
+=item international_prefix
+
+The digits for international dialing. Defaults to '011' The value '+' is
+always recognized.
+
+=item domestic_prefix
+
+The digits for domestic long distance dialing. Defaults to '1'
+
+=back
+
+=cut
+
+sub parse_number {
+ my ($self, %options) = @_;
+
+ my $field = $options{column} || 'dst';
+ my $intl = $options{international_prefix} || '011';
+ my $countrycode = '';
+ my $number = $self->$field();
+
+ my $to_or_from = 'concerning';
+ $to_or_from = 'from' if $field eq 'src';
+ $to_or_from = 'to' if $field eq 'dst';
+ warn "parsing call $to_or_from $number\n" if $DEBUG;
+
+ #remove non-phone# stuff and whitespace
+ $number =~ s/\s//g;
+# my $proto = '';
+# $dest =~ s/^(\w+):// and $proto = $1; #sip:
+# my $siphost = '';
+# $dest =~ s/\@(.*)$// and $siphost = $1; # @10.54.32.1, @sip.example.com
+
+ if ( $number =~ /^$intl(((\d)(\d))(\d))(\d+)$/
+ || $number =~ /^\+(((\d)(\d))(\d))(\d+)$/
+ )
+ {
+
+ my( $three, $two, $one, $u1, $u2, $rest ) = ( $1,$2,$3,$4,$5,$6 );
+ #first look for 1 digit country code
+ if ( qsearch('rate_prefix', { 'countrycode' => $one } ) ) {
+ $countrycode = $one;
+ $number = $u1.$u2.$rest;
+ } elsif ( qsearch('rate_prefix', { 'countrycode' => $two } ) ) { #or 2
+ $countrycode = $two;
+ $number = $u2.$rest;
+ } else { #3 digit country code
+ $countrycode = $three;
+ $number = $rest;
+ }
+
+ } else {
+ my $domestic_prefix =
+ exists($options{domestic_prefix}) ? $options{domestic_prefix} : '';
+ $countrycode = length($domestic_prefix) ? $domestic_prefix : '1';
+ $number =~ s/^$countrycode//;# if length($number) > 10;
+ }
+
+ return($countrycode, $number);
+
+}
+
=item rate [ OPTION => VALUE ... ]
Rates this CDR according and sets the status to 'rated'.
@@ -557,51 +631,22 @@ sub rate_prefix {
# (or calling station id for toll free calls)
###
- my( $to_or_from, $number );
+ my( $to_or_from, $column );
if ( $self->is_tollfree && ! $part_pkg->option_cacheable('disable_tollfree') )
{ #tollfree call
$to_or_from = 'from';
- $number = $self->src;
+ $column = 'src';
} else { #regular call
$to_or_from = 'to';
- $number = $self->dst;
+ $column = 'dst';
}
- warn "parsing call $to_or_from $number\n" if $DEBUG;
-
- #remove non-phone# stuff and whitespace
- $number =~ s/\s//g;
-# my $proto = '';
-# $dest =~ s/^(\w+):// and $proto = $1; #sip:
-# my $siphost = '';
-# $dest =~ s/\@(.*)$// and $siphost = $1; # @10.54.32.1, @sip.example.com
-
#determine the country code
- my $intl = $part_pkg->option_cacheable('international_prefix') || '011';
- my $countrycode = '';
- if ( $number =~ /^$intl(((\d)(\d))(\d))(\d+)$/
- || $number =~ /^\+(((\d)(\d))(\d))(\d+)$/
- )
- {
-
- my( $three, $two, $one, $u1, $u2, $rest ) = ( $1,$2,$3,$4,$5,$6 );
- #first look for 1 digit country code
- if ( qsearch('rate_prefix', { 'countrycode' => $one } ) ) {
- $countrycode = $one;
- $number = $u1.$u2.$rest;
- } elsif ( qsearch('rate_prefix', { 'countrycode' => $two } ) ) { #or 2
- $countrycode = $two;
- $number = $u2.$rest;
- } else { #3 digit country code
- $countrycode = $three;
- $number = $rest;
- }
-
- } else {
- my $domestic_prefix = $part_pkg->option_cacheable('domestic_prefix');
- $countrycode = length($domestic_prefix) ? $domestic_prefix : '1';
- $number =~ s/^$countrycode//;# if length($number) > 10;
- }
+ my ($countrycode, $number) = $self->parse_number(
+ column => $column,
+ international_prefix => $part_pkg->option_cacheable('international_prefix'),
+ domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'),
+ );
warn "rating call $to_or_from +$countrycode $number\n" if $DEBUG;
my $pretty_dst = "+$countrycode $number";
@@ -622,12 +667,20 @@ sub rate_prefix {
# -disregard private or unknown numbers
# -there is exactly one record in rate_prefix for a given NPANXX
# -default to interstate if we can't find one or both of the prefixes
- my $dstprefix = $self->dst;
+ my (undef, $dstprefix) = $self->parse_number(
+ column => 'dst',
+ international_prefix => $part_pkg->option_cacheable('international_prefix'),
+ domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'),
+ );
$dstprefix =~ /^(\d{6})/;
$dstprefix = qsearchs('rate_prefix', { 'countrycode' => '1',
'npa' => $1,
}) || '';
- my $srcprefix = $self->src;
+ my (undef, $srcprefix) = $self->parse_number(
+ column => 'src',
+ international_prefix => $part_pkg->option_cacheable('international_prefix'),
+ domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'),
+ );
$srcprefix =~ /^(\d{6})/;
$srcprefix = qsearchs('rate_prefix', { 'countrycode' => '1',
'npa' => $1,
diff --git a/FS/FS/cdr/taqua.pm b/FS/FS/cdr/taqua.pm
index 390152a04..7ef6d769a 100644
--- a/FS/FS/cdr/taqua.pm
+++ b/FS/FS/cdr/taqua.pm
@@ -7,7 +7,7 @@ use FS::cdr qw(_cdr_date_parser_maker);
@ISA = qw(FS::cdr);
%info = (
- 'name' => 'Taqua',
+ 'name' => 'Taqua v6.0',
'weight' => 130,
'header' => 1,
'import_fields' => [ #some of these are kind arbitrary...
diff --git a/FS/FS/cdr/taqua62.pm b/FS/FS/cdr/taqua62.pm
new file mode 100644
index 000000000..862018e9c
--- /dev/null
+++ b/FS/FS/cdr/taqua62.pm
@@ -0,0 +1,178 @@
+package FS::cdr::taqua62;
+
+use strict;
+use vars qw(@ISA %info $da_rewrite);
+use FS::cdr qw(_cdr_date_parser_maker);
+
+@ISA = qw(FS::cdr);
+
+%info = (
+ 'name' => 'Taqua v6.2',
+ 'weight' => 131,
+ 'header' => 1,
+ 'import_fields' => [
+
+ #0
+ '', #Key
+ '', #InsertTime, irrelevant
+ #RecordType
+ sub {
+ my($cdr, $field, $conf, $hashref) = @_;
+ $hashref->{skiprow} = 1
+ unless ($field == 0 && $cdr->disposition == 100 ) #regular CDR
+ || ($field == 1 && $cdr->lastapp eq 'acctcode'); #accountcode
+ $cdr->cdrtypenum($field);
+ },
+
+ '', #RecordVersion
+ '', #OrigShelfNumber
+ '', #OrigCardNumber
+ '', #OrigCircuit
+ '', #OrigCircuitType
+ 'uniqueid', #SequenceNumber
+ 'sessionnum', #SessionNumber
+ #10
+ 'src', #CallingPartyNumber
+ #CalledPartyNumber
+ sub {
+ my( $cdr, $field, $conf ) = @_;
+ if ( $cdr->calltypenum == 6 && $cdr->cdrtypenum == 0 ) {
+ $cdr->dst("+$field");
+ } else {
+ $cdr->dst($field);
+ }
+ },
+
+ _cdr_date_parser_maker('startdate', 'gmt' => 1), #CallArrivalTime
+ _cdr_date_parser_maker('enddate', 'gmt' => 1), #CallCompletionTime
+
+ #Disposition
+ #sub { my($cdr, $d ) = @_; $cdr->disposition( $disposition{$d}): },
+ 'disposition',
+ # -1 => '',
+ # 0 => '',
+ # 100 => '', #regular cdr
+ # 101 => '',
+ # 102 => '',
+ # 103 => '',
+ # 104 => '',
+ # 105 => '',
+ # 201 => '',
+ # 203 => '',
+ # 204 => '',
+
+ _cdr_date_parser_maker('answerdate', 'gmt' => 1), #DispositionTime
+ '', #TCAP
+ '', #OutboundCarrierConnectTime
+ '', #OutboundCarrierDisconnectTime
+
+ #TermTrunkGroup
+ #it appears channels are actually part of trunk groups, but this data
+ #is interesting and we need a source and destination place to put it
+ 'dstchannel', #TermTrunkGroup
+
+ #20
+
+ '', #TermShelfNumber
+ '', #TermCardNumber
+ '', #TermCircuit
+ '', #TermCircuitType
+ 'carrierid', #OutboundCarrierId
+
+ #BillingNumber
+ #'charged_party',
+ sub {
+ my( $cdr, $field, $conf ) = @_;
+
+ #could be more efficient for the no config case, if anyone ever needs that
+ $da_rewrite ||= $conf->config('cdr-taqua-da_rewrite');
+
+ if ( $da_rewrite && $field =~ /\d/ ) {
+ my $rewrite = $da_rewrite;
+ $rewrite =~ s/\s//g;
+ my @rewrite = split(',', $conf->config('cdr-taqua-da_rewrite') );
+ if ( grep { $field eq $_ } @rewrite ) {
+ $cdr->charged_party( $cdr->src() );
+ $cdr->calltypenum(12);
+ return;
+ }
+ }
+ if ( $cdr->is_tollfree ) { # thankfully this is already available
+ $cdr->charged_party($cdr->dst); # and this
+ } else {
+ $cdr->charged_party($field);
+ }
+ },
+
+ 'subscriber', #SubscriberName
+ 'lastapp', #ServiceName
+ '', #some weirdness #ChargeTime
+ 'lastdata', #ServiceInformation
+
+ #30
+
+ '', #FacilityInfo
+ '', #all 1900-01-01 0#CallTraceTime
+ '', #all-1#UniqueIndicator
+ '', #all-1#PresentationIndicator
+ '', #empty#Pin
+ 'calltypenum', #CallType
+
+ #nothing below is used by QIS...
+
+ '', #Balt/empty #OrigRateCenter
+ '', #Balt/empty #TermRateCenter
+
+ #OrigTrunkGroup
+ #it appears channels are actually part of trunk groups, but this data
+ #is interesting and we need a source and destination place to put it
+ 'channel', #OrigTrunkGroup
+ 'userfield', #empty#UserDefined
+
+ #40
+
+ '', #empty#PseudoDestinationNumber
+ '', #all-1#PseudoCarrierCode
+ '', #empty#PseudoANI
+ '', #all-1#PseudoFacilityInfo
+ '', #OrigDialedDigits
+ '', #all-1#OrigOutboundCarrier
+ '', #IncomingCarrierID
+ 'dcontext', #JurisdictionInfo
+ '', #OrigDestDigits
+ '', #empty#AMALineNumber
+
+ #50
+
+ '', #empty#AMAslpID
+ '', #empty#AMADigitsDialedWC
+ '', #OpxOffHook
+ '', #OpxOnHook
+ '', #OrigCalledNumber
+ '', #RedirectingNumber
+ '', #RouteAttempts
+ '', #OrigMGCPTerm
+ '', #TermMGCPTerm
+ '', #ReasonCode
+
+ #60
+
+ '', #OrigIPCallID
+ '', #ESAIPTrunkGroup
+ '', #ESAReason
+ '', #BearerlessCall
+ '', #oCodec
+ '', #tCodec
+ '', #OrigTrunkGroupNumber
+ '', #TermTrunkGroupNumber
+ '', #TermRecord
+ '', #OrigRoutingIndicator
+
+ #70
+
+ '', #TermRoutingIndicator
+
+ ],
+);
+
+1;
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index c3d48a61c..c48c80627 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -38,6 +38,7 @@ use FS::cust_bill_batch;
use FS::cust_bill_pay_pkg;
use FS::cust_credit_bill_pkg;
use FS::discount_plan;
+use FS::cust_bill_void;
use FS::L10N;
$DEBUG = 0;
@@ -203,10 +204,63 @@ sub insert {
}
+=item void
+
+Voids this invoice: deletes the invoice and adds a record of the voided invoice
+to the FS::cust_bill_void table (and related tables starting from
+FS::cust_bill_pkg_void).
+
+=cut
+
+sub void {
+ my $self = shift;
+ my $reason = scalar(@_) ? shift : '';
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $cust_bill_void = new FS::cust_bill_void ( {
+ map { $_ => $self->get($_) } $self->fields
+ } );
+ $cust_bill_void->reason($reason);
+ my $error = $cust_bill_void->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ my $error = $cust_bill_pkg->void($reason);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ $error = $self->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ '';
+
+}
+
=item delete
This method now works but you probably shouldn't use it. Instead, apply a
-credit against the invoice.
+credit against the invoice, or use the new void method.
Using this method to delete invoices outright is really, really bad. There
would be no record you ever posted this invoice, and there are no check to
@@ -236,11 +290,10 @@ sub delete {
cust_event
cust_credit_bill
cust_bill_pay
- cust_credit_bill
cust_pay_batch
cust_bill_pay_batch
- cust_bill_pkg
cust_bill_batch
+ cust_bill_pkg
)) {
foreach my $linked ( $self->$table() ) {
@@ -380,7 +433,8 @@ sub previous {
my @cust_bill = sort { $a->_date <=> $b->_date }
grep { $_->owed != 0 }
qsearch( 'cust_bill', { 'custnum' => $self->custnum,
- '_date' => { op=>'<', value=>$self->_date },
+ #'_date' => { op=>'<', value=>$self->_date },
+ 'invnum' => { op=>'<', value=>$self->invnum },
} )
;
foreach ( @cust_bill ) { $total += $_->owed; }
diff --git a/FS/FS/cust_bill_ApplicationCommon.pm b/FS/FS/cust_bill_ApplicationCommon.pm
index cadb8a796..cb0705041 100644
--- a/FS/FS/cust_bill_ApplicationCommon.pm
+++ b/FS/FS/cust_bill_ApplicationCommon.pm
@@ -337,6 +337,7 @@ sub calculate_applications {
# could expand @open above, instead, for a slightly different magic effect
my @result = ();
foreach my $apply ( @apply ) {
+ # $apply = [ FS::cust_bill_pkg_tax_location record, amount ]
my @sub_lines = $apply->[0]->cust_bill_pkg_tax_Xlocation;
my $amount = $apply->[1];
warn "applying ". $apply->[1]. " to ". $apply->[0]->desc
@@ -346,6 +347,10 @@ sub calculate_applications {
my $owed = $subline->owed;
push @result, [ $apply->[0],
sprintf('%.2f', min($amount, $owed) ),
+ # $subline->primary_key is "billpkgtaxlocationnum"
+ # or "billpkgtaxratelocationnum"
+ # This is the ONLY place either of those fields will
+ # be set.
{ $subline->primary_key => $subline->get($subline->primary_key) },
];
$amount -= $owed;
diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm
index 4220d3c06..20c8e5a55 100644
--- a/FS/FS/cust_bill_pkg.pm
+++ b/FS/FS/cust_bill_pkg.pm
@@ -1,13 +1,13 @@
package FS::cust_bill_pkg;
+use base qw( FS::TemplateItem_Mixin FS::cust_main_Mixin FS::Record );
use strict;
use vars qw( @ISA $DEBUG $me );
use Carp;
+use List::Util qw( sum min );
use Text::CSV_XS;
-use FS::Record qw( qsearch qsearchs dbdef dbh );
-use FS::cust_main_Mixin;
+use FS::Record qw( qsearch qsearchs dbh );
use FS::cust_pkg;
-use FS::part_pkg;
use FS::cust_bill;
use FS::cust_bill_pkg_detail;
use FS::cust_bill_pkg_display;
@@ -18,10 +18,13 @@ use FS::cust_tax_exempt_pkg;
use FS::cust_bill_pkg_tax_location;
use FS::cust_bill_pkg_tax_rate_location;
use FS::cust_tax_adjustment;
-
-use List::Util qw(sum);
-
-@ISA = qw( FS::cust_main_Mixin FS::Record );
+use FS::cust_bill_pkg_void;
+use FS::cust_bill_pkg_detail_void;
+use FS::cust_bill_pkg_display_void;
+use FS::cust_bill_pkg_discount_void;
+use FS::cust_bill_pkg_tax_location_void;
+use FS::cust_bill_pkg_tax_rate_location_void;
+use FS::cust_tax_exempt_pkg_void;
$DEBUG = 0;
$me = '[FS::cust_bill_pkg]';
@@ -120,6 +123,13 @@ customer object (see L<FS::cust_main>).
sub table { 'cust_bill_pkg'; }
+sub detail_table { 'cust_bill_pkg_detail'; }
+sub display_table { 'cust_bill_pkg_display'; }
+sub discount_table { 'cust_bill_pkg_discount'; }
+#sub tax_location_table { 'cust_bill_pkg_tax_location'; }
+#sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; }
+#sub tax_exempt_pkg_table { 'cust_tax_exempt_pkg'; }
+
=item insert
Adds this line item to the database. If there is an error, returns the error,
@@ -180,14 +190,12 @@ sub insert {
}
}
- if ( $self->_cust_tax_exempt_pkg ) {
- foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
- $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
- $error = $cust_tax_exempt_pkg->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "error inserting cust_tax_exempt_pkg: $error";
- }
+ foreach my $cust_tax_exempt_pkg ( @{$self->cust_tax_exempt_pkg} ) {
+ $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
+ $error = $cust_tax_exempt_pkg->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error inserting cust_tax_exempt_pkg: $error";
}
}
@@ -230,6 +238,75 @@ sub insert {
}
+=item void
+
+Voids this line item: deletes the line item and adds a record of the voided
+line item to the FS::cust_bill_pkg_void table (and related tables).
+
+=cut
+
+sub void {
+ my $self = shift;
+ my $reason = scalar(@_) ? shift : '';
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
+ map { $_ => $self->get($_) } $self->fields
+ } );
+ $cust_bill_pkg_void->reason($reason);
+ my $error = $cust_bill_pkg_void->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ foreach my $table (qw(
+ cust_bill_pkg_detail
+ cust_bill_pkg_display
+ cust_bill_pkg_discount
+ cust_bill_pkg_tax_location
+ cust_bill_pkg_tax_rate_location
+ cust_tax_exempt_pkg
+ )) {
+
+ foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
+
+ my $vclass = 'FS::'.$table.'_void';
+ my $void = $vclass->new( {
+ map { $_ => $linked->get($_) } $linked->fields
+ });
+ my $error = $void->insert || $linked->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
+ }
+
+ $error = $self->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ '';
+
+}
+
=item delete
Not recommended.
@@ -253,6 +330,7 @@ sub delete {
foreach my $table (qw(
cust_bill_pkg_detail
cust_bill_pkg_display
+ cust_bill_pkg_discount
cust_bill_pkg_tax_location
cust_bill_pkg_tax_rate_location
cust_tax_exempt_pkg
@@ -389,36 +467,6 @@ sub regularize_details {
return;
}
-=item cust_pkg
-
-Returns the package (see L<FS::cust_pkg>) for this invoice line item.
-
-=cut
-
-sub cust_pkg {
- my $self = shift;
- carp "$me $self -> cust_pkg" if $DEBUG;
- qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
-}
-
-=item part_pkg
-
-Returns the package definition for this invoice line item.
-
-=cut
-
-sub part_pkg {
- my $self = shift;
- if ( $self->pkgpart_override ) {
- qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
- } else {
- my $part_pkg;
- my $cust_pkg = $self->cust_pkg;
- $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
- $part_pkg;
- }
-}
-
=item cust_bill
Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
@@ -448,173 +496,6 @@ sub previous_cust_bill_pkg {
});
}
-=item details [ OPTION => VALUE ... ]
-
-Returns an array of detail information for the invoice line item.
-
-Currently available options are: I<format>, I<escape_function> and
-I<format_function>.
-
-If I<format> is set to html or latex then the array members are improved
-for tabular appearance in those environments if possible.
-
-If I<escape_function> is set then the array members are processed by this
-function before being returned.
-
-I<format_function> overrides the normal HTML or LaTeX function for returning
-formatted CDRs. It can be set to a subroutine which returns an empty list
-to skip usage detail:
-
- 'format_function' => sub { () },
-
-=cut
-
-sub details {
- my ( $self, %opt ) = @_;
- my $escape_function = $opt{escape_function} || sub { shift };
-
- my $csv = new Text::CSV_XS;
-
- if ( $opt{format_function} ) {
-
- #this still expects to be passed a cust_bill_pkg_detail object as the
- #second argument, which is expensive
- carp "deprecated format_function passed to cust_bill_pkg->details";
- my $format_sub = $opt{format_function} if $opt{format_function};
-
- map { ( $_->format eq 'C'
- ? &{$format_sub}( $_->detail, $_ )
- : &{$escape_function}( $_->detail )
- )
- }
- qsearch ({ 'table' => 'cust_bill_pkg_detail',
- 'hashref' => { 'billpkgnum' => $self->billpkgnum },
- 'order_by' => 'ORDER BY detailnum',
- });
-
- } elsif ( $opt{'no_usage'} ) {
-
- my $sql = "SELECT detail FROM cust_bill_pkg_detail ".
- " WHERE billpkgnum = ". $self->billpkgnum.
- " AND ( format IS NULL OR format != 'C' ) ".
- " ORDER BY detailnum";
- my $sth = dbh->prepare($sql) or die dbh->errstr;
- $sth->execute or die $sth->errstr;
-
- map &{$escape_function}( $_->[0] ), @{ $sth->fetchall_arrayref };
-
- } else {
-
- my $format_sub;
- my $format = $opt{format} || '';
- if ( $format eq 'html' ) {
-
- $format_sub = sub { my $detail = shift;
- $csv->parse($detail) or return "can't parse $detail";
- join('</TD><TD>', map { &$escape_function($_) }
- $csv->fields
- );
- };
-
- } elsif ( $format eq 'latex' ) {
-
- $format_sub = sub {
- my $detail = shift;
- $csv->parse($detail) or return "can't parse $detail";
- #join(' & ', map { '\small{'. &$escape_function($_). '}' }
- # $csv->fields );
- my $result = '';
- my $column = 1;
- foreach ($csv->fields) {
- $result .= ' & ' if $column > 1;
- if ($column > 6) { # KLUDGE ALERT!
- $result .= '\multicolumn{1}{l}{\scriptsize{'.
- &$escape_function($_). '}}';
- }else{
- $result .= '\scriptsize{'. &$escape_function($_). '}';
- }
- $column++;
- }
- $result;
- };
-
- } else {
-
- $format_sub = sub { my $detail = shift;
- $csv->parse($detail) or return "can't parse $detail";
- join(' - ', map { &$escape_function($_) }
- $csv->fields
- );
- };
-
- }
-
- my $sql = "SELECT format, detail FROM cust_bill_pkg_detail ".
- " WHERE billpkgnum = ". $self->billpkgnum.
- " ORDER BY detailnum";
- my $sth = dbh->prepare($sql) or die dbh->errstr;
- $sth->execute or die $sth->errstr;
-
- #avoid the fetchall_arrayref and loop for less memory usage?
-
- map { (defined($_->[0]) && $_->[0] eq 'C')
- ? &{$format_sub}( $_->[1] )
- : &{$escape_function}( $_->[1] );
- }
- @{ $sth->fetchall_arrayref };
-
- }
-
-}
-
-=item details_header [ OPTION => VALUE ... ]
-
-Returns a list representing an invoice line item detail header, if any.
-This relies on the behavior of voip_cdr in that it expects the header
-to be the first CSV formatted detail (as is expected by invoice generation
-routines). Returns the empty list otherwise.
-
-=cut
-
-sub details_header {
- my $self = shift;
- return '' unless defined dbdef->table('cust_bill_pkg_detail');
-
- my $csv = new Text::CSV_XS;
-
- my @detail =
- qsearch ({ 'table' => 'cust_bill_pkg_detail',
- 'hashref' => { 'billpkgnum' => $self->billpkgnum,
- 'format' => 'C',
- },
- 'order_by' => 'ORDER BY detailnum LIMIT 1',
- });
- return() unless scalar(@detail);
- $csv->parse($detail[0]->detail) or return ();
- $csv->fields;
-}
-
-=item desc
-
-Returns a description for this line item. For typical line items, this is the
-I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
-For one-shot line items and named taxes, it is the I<itemdesc> field of this
-line item, and for generic taxes, simply returns "Tax".
-
-=cut
-
-sub desc {
- my $self = shift;
-
- if ( $self->pkgnum > 0 ) {
- $self->itemdesc || $self->part_pkg->pkg;
- } else {
- my $desc = $self->itemdesc || 'Tax';
- $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
- $desc;
- }
-}
-
=item owed_setup
Returns the amount owed (still outstanding) on this line item's setup fee,
@@ -692,45 +573,6 @@ sub units {
$self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
}
-=item quantity
-
-=cut
-
-sub quantity {
- my( $self, $value ) = @_;
- if ( defined($value) ) {
- $self->setfield('quantity', $value);
- }
- $self->getfield('quantity') || 1;
-}
-
-=item unitsetup
-
-=cut
-
-sub unitsetup {
- my( $self, $value ) = @_;
- if ( defined($value) ) {
- $self->setfield('unitsetup', $value);
- }
- $self->getfield('unitsetup') eq ''
- ? $self->getfield('setup')
- : $self->getfield('unitsetup');
-}
-
-=item unitrecur
-
-=cut
-
-sub unitrecur {
- my( $self, $value ) = @_;
- if ( defined($value) ) {
- $self->setfield('unitrecur', $value);
- }
- $self->getfield('unitrecur') eq ''
- ? $self->getfield('recur')
- : $self->getfield('unitrecur');
-}
=item set_display OPTION => VALUE ...
@@ -942,50 +784,10 @@ sub usage_classes {
}
-=item cust_bill_pkg_display [ type => TYPE ]
-
-Returns an array of display information for the invoice line item optionally
-limited to 'TYPE'.
-
-=cut
-
-sub cust_bill_pkg_display {
- my ( $self, %opt ) = @_;
-
- my $default =
- new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
-
- my $type = $opt{type} if exists $opt{type};
- my @result;
-
- if ( $self->get('display') ) {
- @result = grep { defined($type) ? ($type eq $_->type) : 1 }
- @{ $self->get('display') };
- } else {
- my $hashref = { 'billpkgnum' => $self->billpkgnum };
- $hashref->{type} = $type if defined($type);
-
- @result = qsearch ({ 'table' => 'cust_bill_pkg_display',
- 'hashref' => { 'billpkgnum' => $self->billpkgnum },
- 'order_by' => 'ORDER BY billpkgdisplaynum',
- });
- }
-
- push @result, $default unless ( scalar(@result) || $type );
-
- @result;
-
-}
-
-# reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
-# and FS::cust_main::bill
-
-sub _cust_tax_exempt_pkg {
+sub cust_tax_exempt_pkg {
my ( $self ) = @_;
- $self->{Hash}->{_cust_tax_exempt_pkg} or
- $self->{Hash}->{_cust_tax_exempt_pkg} = [];
-
+ $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
}
=item cust_bill_pkg_tax_Xlocation
@@ -1007,36 +809,6 @@ sub cust_bill_pkg_tax_Xlocation {
}
-=item cust_bill_pkg_detail [ CLASSNUM ]
-
-Returns the list of associated cust_bill_pkg_detail objects
-The optional CLASSNUM argument will limit the details to the specified usage
-class.
-
-=cut
-
-sub cust_bill_pkg_detail {
- my $self = shift;
- my $classnum = shift || '';
-
- my %hash = ( 'billpkgnum' => $self->billpkgnum );
- $hash{classnum} = $classnum if $classnum;
-
- qsearch( 'cust_bill_pkg_detail', \%hash ),
-
-}
-
-=item cust_bill_pkg_discount
-
-Returns the list of associated cust_bill_pkg_discount objects.
-
-=cut
-
-sub cust_bill_pkg_discount {
- my $self = shift;
- qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
-}
-
=item recur_show_zero
=cut
@@ -1162,6 +934,485 @@ sub credited_sql {
}
+sub upgrade_tax_location {
+ # For taxes that were calculated/invoiced before cust_location refactoring
+ # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
+ # they were calculated on a package-location basis. Create them here,
+ # along with any necessary cust_location records and any tax exemption
+ # records.
+
+ my ($class, %opt) = @_;
+ # %opt may include 's' and 'e': start and end date ranges
+ # and 'X': abort on any error, instead of just rolling back changes to
+ # that invoice
+ my $dbh = dbh;
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+
+ eval {
+ use FS::h_cust_main;
+ use FS::h_cust_bill;
+ use FS::h_part_pkg;
+ use FS::h_cust_main_exemption;
+ };
+
+ local $FS::cust_location::import = 1;
+
+ my $conf = FS::Conf->new; # h_conf?
+ return if $conf->exists('enable_taxproducts'); #don't touch this case
+ my $use_ship = $conf->exists('tax-ship_address');
+
+ my $date_where = '';
+ if ($opt{s}) {
+ $date_where .= " AND cust_bill._date >= $opt{s}";
+ }
+ if ($opt{e}) {
+ $date_where .= " AND cust_bill._date < $opt{e}";
+ }
+
+ my $commit_each_invoice = 1 unless $opt{X};
+
+ # if an invoice has either of these kinds of objects, then it doesn't
+ # need to be upgraded...probably
+ my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
+ ' JOIN cust_bill_pkg USING (billpkgnum)'.
+ ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
+ my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
+ ' JOIN cust_bill_pkg USING (billpkgnum)'.
+ ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
+ ' AND exempt_monthly IS NULL';
+
+ my @invnums = map { $_->invnum } qsearch({
+ select => 'cust_bill.invnum',
+ table => 'cust_bill',
+ hashref => {},
+ extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
+ "AND NOT EXISTS($sub_has_exempt) ".
+ $date_where,
+ });
+
+ print "Processing ".scalar(@invnums)." invoices...\n";
+
+ my $committed;
+ INVOICE:
+ foreach my $invnum (@invnums) {
+ $committed = 0;
+ print STDERR "Invoice #$invnum\n";
+ my $pre = '';
+ my %pkgpart_taxclass; # pkgpart => taxclass
+ my %pkgpart_exempt_setup;
+ my %pkgpart_exempt_recur;
+ my $h_cust_bill = qsearchs('h_cust_bill',
+ { invnum => $invnum,
+ history_action => 'insert' });
+ if (!$h_cust_bill) {
+ warn "no insert record for invoice $invnum; skipped\n";
+ #$date = $cust_bill->_date as a fallback?
+ # We're trying to avoid using non-real dates (-d/-y invoice dates)
+ # when looking up history records in other tables.
+ next INVOICE;
+ }
+ my $custnum = $h_cust_bill->custnum;
+
+ # Determine the address corresponding to this tax region.
+ # It's either the bill or ship address of the customer as of the
+ # invoice date-of-insertion. (Not necessarily the invoice date.)
+ my $date = $h_cust_bill->history_date;
+ my $h_cust_main = qsearchs('h_cust_main',
+ { custnum => $custnum },
+ FS::h_cust_main->sql_h_searchs($date)
+ );
+ if (!$h_cust_main ) {
+ warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
+ next INVOICE;
+ # fallback to current $cust_main? sounds dangerous.
+ }
+
+ # This is a historical customer record, so it has a historical address.
+ # If there's no cust_location matching this custnum and address (there
+ # probably isn't), create one.
+ $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
+ my %hash = map { $_ => $h_cust_main->get($pre.$_) }
+ FS::cust_main->location_fields;
+ # not really needed for this, and often result in duplicate locations
+ delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
+
+ $hash{custnum} = $h_cust_main->custnum;
+ my $tax_loc = qsearchs('cust_location', \%hash) # unlikely
+ || FS::cust_location->new({ %hash });
+ if ( !$tax_loc->locationnum ) {
+ $tax_loc->disabled('Y');
+ my $error = $tax_loc->insert;
+ if ( $error ) {
+ warn "couldn't create historical location record for cust#".
+ $h_cust_main->custnum.": $error\n";
+ next INVOICE;
+ }
+ }
+ my $exempt_cust = 1 if $h_cust_main->tax;
+
+ # Get any per-customer taxname exemptions that were in effect.
+ my %exempt_cust_taxname = map {
+ $_->taxname => 1
+ } qsearch('h_cust_main_exemption', { 'custnum' => $custnum },
+ FS::h_cust_main_exemption->sql_h_searchs($date)
+ );
+
+ # classify line items
+ my @tax_items;
+ my %nontax_items; # taxclass => array of cust_bill_pkg
+ foreach my $item ($h_cust_bill->cust_bill_pkg) {
+ my $pkgnum = $item->pkgnum;
+
+ if ( $pkgnum == 0 ) {
+
+ push @tax_items, $item;
+
+ } else {
+ # (pkgparts really shouldn't change, right?)
+ my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
+ FS::h_cust_pkg->sql_h_searchs($date)
+ );
+ if ( !$h_cust_pkg ) {
+ warn "no historical package #".$item->pkgpart."; skipped\n";
+ next INVOICE;
+ }
+ my $pkgpart = $h_cust_pkg->pkgpart;
+
+ if (!exists $pkgpart_taxclass{$pkgpart}) {
+ my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
+ FS::h_part_pkg->sql_h_searchs($date)
+ );
+ if ( !$h_part_pkg ) {
+ warn "no historical package def #$pkgpart; skipped\n";
+ next INVOICE;
+ }
+ $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
+ $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
+ $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
+ }
+
+ # mark any exemptions that apply
+ if ( $pkgpart_exempt_setup{$pkgpart} ) {
+ $item->set('exempt_setup' => 1);
+ }
+
+ if ( $pkgpart_exempt_recur{$pkgpart} ) {
+ $item->set('exempt_recur' => 1);
+ }
+
+ my $taxclass = $pkgpart_taxclass{ $pkgpart };
+
+ $nontax_items{$taxclass} ||= [];
+ push @{ $nontax_items{$taxclass} }, $item;
+ }
+ }
+ printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
+ if @tax_items;
+
+ # Use a variation on the procedure in
+ # FS::cust_main::Billing::_handle_taxes to identify taxes that apply
+ # to this bill.
+ my @loc_keys = qw( district city county state country );
+ my %taxhash = map { $_ => $h_cust_main->get($pre.$_) } @loc_keys;
+ my %taxdef_by_name; # by name, and then by taxclass
+ my %est_tax; # by name, and then by taxclass
+ my %taxable_items; # by taxnum, and then an array
+
+ foreach my $taxclass (keys %nontax_items) {
+ my %myhash = %taxhash;
+ my @elim = qw( district city county state );
+ my @taxdefs; # because there may be several with different taxnames
+ do {
+ $myhash{taxclass} = $taxclass;
+ @taxdefs = qsearch('cust_main_county', \%myhash);
+ if ( !@taxdefs ) {
+ $myhash{taxclass} = '';
+ @taxdefs = qsearch('cust_main_county', \%myhash);
+ }
+ $myhash{ shift @elim } = '';
+ } while scalar(@elim) and !@taxdefs;
+
+ print "Class '$taxclass': ". scalar(@{ $nontax_items{$taxclass} }).
+ " items, ". scalar(@taxdefs)." tax defs found.\n";
+ foreach my $taxdef (@taxdefs) {
+ next if $taxdef->tax == 0;
+ $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
+
+ $taxable_items{$taxdef->taxnum} ||= [];
+ foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
+ # clone the item so that taxdef-dependent changes don't
+ # change it for other taxdefs
+ my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
+
+ # these flags are already set if the part_pkg declares itself exempt
+ $item->set('exempt_setup' => 1) if $taxdef->setuptax;
+ $item->set('exempt_recur' => 1) if $taxdef->recurtax;
+
+ my @new_exempt;
+ my $taxable = $item->setup + $item->recur;
+ # credits
+ # h_cust_credit_bill_pkg?
+ # NO. Because if these exemptions HAD been created at the time of
+ # billing, and then a credit applied later, the exemption would
+ # have been adjusted by the amount of the credit. So we adjust
+ # the taxable amount before creating the exemption.
+ # But don't deduct the credit from taxable, because the tax was
+ # calculated before the credit was applied.
+ foreach my $f (qw(setup recur)) {
+ my $credited = FS::Record->scalar_sql(
+ "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
+ "WHERE billpkgnum = ? AND setuprecur = ?",
+ $item->billpkgnum,
+ $f
+ );
+ $item->set($f, $item->get($f) - $credited) if $credited;
+ }
+ my $existing_exempt = FS::Record->scalar_sql(
+ "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
+ "billpkgnum = ? AND taxnum = ?",
+ $item->billpkgnum, $taxdef->taxnum
+ ) || 0;
+ $taxable -= $existing_exempt;
+
+ if ( $taxable and $exempt_cust ) {
+ push @new_exempt, { exempt_cust => 'Y', amount => $taxable };
+ $taxable = 0;
+ }
+ if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
+ push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
+ $taxable = 0;
+ }
+ if ( $taxable and $item->exempt_setup ) {
+ push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
+ $taxable -= $item->setup;
+ }
+ if ( $taxable and $item->exempt_recur ) {
+ push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
+ $taxable -= $item->recur;
+ }
+
+ $item->set('taxable' => $taxable);
+ push @{ $taxable_items{$taxdef->taxnum} }, $item
+ if $taxable > 0;
+
+ # estimate the amount of tax (this is necessary because different
+ # taxdefs with the same taxname may have different tax rates)
+ # and sum that for each taxname/taxclass combination
+ # (in cents)
+ $est_tax{$taxdef->taxname} ||= {};
+ $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
+ $est_tax{$taxdef->taxname}{$taxdef->taxclass} +=
+ $taxable * $taxdef->tax;
+
+ foreach (@new_exempt) {
+ next if $_->{amount} == 0;
+ my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
+ %$_,
+ billpkgnum => $item->billpkgnum,
+ taxnum => $taxdef->taxnum,
+ });
+ my $error = $cust_tax_exempt_pkg->insert;
+ if ($error) {
+ my $pkgnum = $item->pkgnum;
+ warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
+ "\n$error\n\n";
+ next INVOICE;
+ }
+ } #foreach @new_exempt
+ } #foreach $item
+ } #foreach $taxdef
+ } #foreach $taxclass
+
+ # Now go through the billed taxes and match them up with the line items.
+ TAX_ITEM: foreach my $tax_item ( @tax_items )
+ {
+ my $taxname = $tax_item->itemdesc;
+ $taxname = '' if $taxname eq 'Tax';
+
+ if ( !exists( $taxdef_by_name{$taxname} ) ) {
+ # then we didn't find any applicable taxes with this name
+ warn "no definition found for tax item '$taxname'.\n".
+ '('.join(' ', @hash{qw(country state county city district)}).")\n";
+ # possibly all of these should be "next TAX_ITEM", but whole invoices
+ # are transaction protected and we can go back and retry them.
+ next INVOICE;
+ }
+ # classname => cust_main_county
+ my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
+
+ # Divide the tax item among taxclasses, if necessary
+ # classname => estimated tax amount
+ my $this_est_tax = $est_tax{$taxname};
+ if (!defined $this_est_tax) {
+ warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
+ next INVOICE;
+ }
+ my $est_total = sum(values %$this_est_tax);
+ if ( $est_total == 0 ) {
+ # shouldn't happen
+ warn "estimated tax on invoice #$invnum is zero.\n";
+ next INVOICE;
+ }
+
+ my $real_tax = $tax_item->setup;
+ printf ("Distributing \$%.2f tax:\n", $real_tax);
+ my $cents_remaining = $real_tax * 100; # for rounding error
+ my @tax_links; # partial CBPTL hashrefs
+ foreach my $taxclass (keys %taxdef_by_class) {
+ my $taxdef = $taxdef_by_class{$taxclass};
+ # these items already have "taxable" set to their charge amount
+ # after applying any credits or exemptions
+ my @items = @{ $taxable_items{$taxdef->taxnum} };
+ my $subtotal = sum(map {$_->get('taxable')} @items);
+ printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
+
+ foreach my $nontax (@items) {
+ my $part = int($real_tax
+ # class allocation
+ * ($this_est_tax->{$taxclass}/$est_total)
+ # item allocation
+ * ($nontax->get('taxable'))/$subtotal
+ # convert to cents
+ * 100
+ );
+ $cents_remaining -= $part;
+ push @tax_links, {
+ taxnum => $taxdef->taxnum,
+ pkgnum => $nontax->pkgnum,
+ cents => $part,
+ };
+ } #foreach $nontax
+ } #foreach $taxclass
+ # Distribute any leftover tax round-robin style, one cent at a time.
+ my $i = 0;
+ my $nlinks = scalar(@tax_links);
+ if ( $nlinks ) {
+ while (int($cents_remaining) > 0) {
+ $tax_links[$i % $nlinks]->{cents} += 1;
+ $cents_remaining--;
+ $i++;
+ }
+ } else {
+ warn "Can't create tax links--no taxable items found.\n";
+ next INVOICE;
+ }
+
+ # Gather credit/payment applications so that we can link them
+ # appropriately.
+ my @unlinked = (
+ qsearch( 'cust_credit_bill_pkg',
+ { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
+ ),
+ qsearch( 'cust_bill_pay_pkg',
+ { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
+ )
+ );
+
+ # grab the first one
+ my $this_unlinked = shift @unlinked;
+ my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
+
+ # Create tax links (yay!)
+ printf("Creating %d tax links.\n",scalar(@tax_links));
+ foreach (@tax_links) {
+ my $link = FS::cust_bill_pkg_tax_location->new({
+ billpkgnum => $tax_item->billpkgnum,
+ taxtype => 'FS::cust_main_county',
+ locationnum => $tax_loc->locationnum,
+ taxnum => $_->{taxnum},
+ pkgnum => $_->{pkgnum},
+ amount => sprintf('%.2f', $_->{cents} / 100),
+ });
+ my $error = $link->insert;
+ if ( $error ) {
+ warn "Can't create tax link for inv#$invnum: $error\n";
+ next INVOICE;
+ }
+
+ my $link_cents = $_->{cents};
+ # update/create subitem links
+ #
+ # If $this_unlinked is undef, then we've allocated all of the
+ # credit/payment applications to the tax item. If $link_cents is 0,
+ # then we've applied credits/payments to all of this package fraction,
+ # so go on to the next.
+ while ($this_unlinked and $link_cents) {
+ # apply as much as possible of $link_amount to this credit/payment
+ # link
+ my $apply_cents = min($link_cents, $unlinked_cents);
+ $link_cents -= $apply_cents;
+ $unlinked_cents -= $apply_cents;
+ # $link_cents or $unlinked_cents or both are now zero
+ $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
+ $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
+ my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
+ if ( $this_unlinked->$pkey ) {
+ # then it's an existing link--replace it
+ $error = $this_unlinked->replace;
+ } else {
+ $this_unlinked->insert;
+ }
+ # what do we do with errors at this stage?
+ if ( $error ) {
+ warn "Error creating tax application link: $error\n";
+ next INVOICE; # for lack of a better idea
+ }
+
+ if ( $unlinked_cents == 0 ) {
+ # then we've allocated all of this payment/credit application,
+ # so grab the next one
+ $this_unlinked = shift @unlinked;
+ $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
+ } elsif ( $link_cents == 0 ) {
+ # then we've covered all of this package tax fraction, so split
+ # off a new application from this one
+ $this_unlinked = $this_unlinked->new({
+ $this_unlinked->hash,
+ $pkey => '',
+ });
+ # $unlinked_cents is still what it is
+ }
+
+ } #while $this_unlinked and $link_cents
+ } #foreach (@tax_links)
+ } #foreach $tax_item
+
+ $dbh->commit if $commit_each_invoice and $oldAutoCommit;
+ $committed = 1;
+
+ } #foreach $invnum
+ continue {
+ if (!$committed) {
+ $dbh->rollback if $oldAutoCommit;
+ die "Upgrade halted.\n" unless $commit_each_invoice;
+ }
+ }
+
+ $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
+ '';
+}
+
+sub _upgrade_data {
+ # Create a queue job to run upgrade_tax_location from January 1, 2012 to
+ # the present date.
+ eval {
+ use FS::queue;
+ use Date::Parse 'str2time';
+ };
+ my $class = shift;
+ my $upgrade = 'tax_location_2012';
+ return if FS::upgrade_journal->is_done($upgrade);
+ my $job = FS::queue->new({
+ 'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
+ });
+ # call it kind of like a class method, not that it matters much
+ $job->insert($class, 's' => str2time('2012-01-01'));
+ # Then mark the upgrade as done, so that we don't queue the job twice
+ # and somehow run two of them concurrently.
+ FS::upgrade_journal->set_done($upgrade);
+}
+
=back
=head1 BUGS
@@ -1179,6 +1430,8 @@ owed_setup and owed_recur could then be repaced by just owed, and
cust_bill::open_cust_bill_pkg and
cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
+The upgrade procedure is pretty sketchy.
+
=head1 SEE ALSO
L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
diff --git a/FS/FS/cust_bill_pkg_detail_void.pm b/FS/FS/cust_bill_pkg_detail_void.pm
new file mode 100644
index 000000000..cebe7c1f8
--- /dev/null
+++ b/FS/FS/cust_bill_pkg_detail_void.pm
@@ -0,0 +1,168 @@
+package FS::cust_bill_pkg_detail_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+use FS::cust_bill_pkg_void;
+use FS::usage_class;
+
+=head1 NAME
+
+FS::cust_bill_pkg_detail_void - Object methods for cust_bill_pkg_detail_void records
+
+=head1 SYNOPSIS
+
+ use FS::cust_bill_pkg_detail_void;
+
+ $record = new FS::cust_bill_pkg_detail_void \%hash;
+ $record = new FS::cust_bill_pkg_detail_void { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_detail_void object represents additional detail
+information for a voided invoice line item. FS::cust_bill_pkg_detail_void
+inherits from FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item detailnum
+
+primary key
+
+=item billpkgnum
+
+billpkgnum
+
+=item pkgnum
+
+pkgnum
+
+=item invnum
+
+invnum
+
+=item amount
+
+amount
+
+=item format
+
+format
+
+=item classnum
+
+classnum
+
+=item duration
+
+duration
+
+=item phonenum
+
+phonenum
+
+=item accountcode
+
+accountcode
+
+=item startdate
+
+startdate
+
+=item regionname
+
+regionname
+
+=item detail
+
+detail
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_pkg_detail_void'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_number('detailnum')
+ || $self->ut_foreign_keyn('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum')
+ || $self->ut_numbern('pkgnum')
+ || $self->ut_numbern('invnum')
+ || $self->ut_floatn('amount')
+ || $self->ut_enum('format', [ '', 'C' ] )
+ || $self->ut_foreign_keyn('classnum', 'usage_class', 'classnum')
+ || $self->ut_numbern('duration')
+ || $self->ut_textn('phonenum')
+ || $self->ut_textn('accountcode')
+ || $self->ut_numbern('startdate')
+ || $self->ut_textn('regionname')
+ || $self->ut_text('detail')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pkg_discount.pm b/FS/FS/cust_bill_pkg_discount.pm
index e7dd5f22f..dfa83d393 100644
--- a/FS/FS/cust_bill_pkg_discount.pm
+++ b/FS/FS/cust_bill_pkg_discount.pm
@@ -28,8 +28,8 @@ FS::cust_bill_pkg_discount - Object methods for cust_bill_pkg_discount records
=head1 DESCRIPTION
An FS::cust_bill_pkg_discount object represents the slice of a customer
-applied to a line item. FS::cust_bill_pkg_discount inherits from
-FS::Record. The following fields are currently supported:
+discount applied to a specific line item. FS::cust_bill_pkg_discount inherits
+from FS::Record. The following fields are currently supported:
=over 4
diff --git a/FS/FS/cust_bill_pkg_discount_void.pm b/FS/FS/cust_bill_pkg_discount_void.pm
new file mode 100644
index 000000000..859ef3cf2
--- /dev/null
+++ b/FS/FS/cust_bill_pkg_discount_void.pm
@@ -0,0 +1,129 @@
+package FS::cust_bill_pkg_discount_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+use FS::cust_bill_pkg_void;
+use FS::cust_pkg_discount;
+
+=head1 NAME
+
+FS::cust_bill_pkg_discount_void - Object methods for cust_bill_pkg_discount_void records
+
+=head1 SYNOPSIS
+
+ use FS::cust_bill_pkg_discount_void;
+
+ $record = new FS::cust_bill_pkg_discount_void \%hash;
+ $record = new FS::cust_bill_pkg_discount_void { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_discount_void object represents the slice of a customer
+discount applied to a specific voided line item.
+FS::cust_bill_pkg_discount_void inherits from FS::Record. The following fields
+are currently supported:
+
+=over 4
+
+=item billpkgdiscountnum
+
+primary key
+
+=item billpkgnum
+
+billpkgnum
+
+=item pkgdiscountnum
+
+pkgdiscountnum
+
+=item amount
+
+amount
+
+=item months
+
+months
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example. To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_pkg_discount_void'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid example. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_number('billpkgdiscountnum')
+ || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum' )
+ || $self->ut_foreign_key('pkgdiscountnum', 'cust_pkg_discount', 'pkgdiscountnum' )
+ || $self->ut_money('amount')
+ || $self->ut_float('months')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pkg_display_void.pm b/FS/FS/cust_bill_pkg_display_void.pm
new file mode 100644
index 000000000..e78801a36
--- /dev/null
+++ b/FS/FS/cust_bill_pkg_display_void.pm
@@ -0,0 +1,132 @@
+package FS::cust_bill_pkg_display_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+use FS::cust_bill_pkg_void;
+
+=head1 NAME
+
+FS::cust_bill_pkg_display_void - Object methods for cust_bill_pkg_display_void records
+
+=head1 SYNOPSIS
+
+ use FS::cust_bill_pkg_display_void;
+
+ $record = new FS::cust_bill_pkg_display_void \%hash;
+ $record = new FS::cust_bill_pkg_display_void { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_display_void object represents voided line item display
+information. FS::cust_bill_pkg_display_void inherits from FS::Record. The
+following fields are currently supported:
+
+=over 4
+
+=item billpkgdisplaynum
+
+primary key
+
+=item billpkgnum
+
+billpkgnum
+
+=item section
+
+section
+
+=item post_total
+
+post_total
+
+=item type
+
+type
+
+=item summary
+
+summary
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_pkg_display_void'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_number('billpkgdisplaynum')
+ || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum')
+ || $self->ut_textn('section')
+ || $self->ut_enum('post_total', [ '', 'Y' ])
+ || $self->ut_enum('type', [ '', 'S', 'R', 'U' ])
+ || $self->ut_enum('summary', [ '', 'Y' ])
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pkg_tax_location_void.pm b/FS/FS/cust_bill_pkg_tax_location_void.pm
new file mode 100644
index 000000000..9e0794bad
--- /dev/null
+++ b/FS/FS/cust_bill_pkg_tax_location_void.pm
@@ -0,0 +1,139 @@
+package FS::cust_bill_pkg_tax_location_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+use FS::cust_bill_pkg_void;
+use FS::cust_pkg;
+use FS::cust_location;
+
+=head1 NAME
+
+FS::cust_bill_pkg_tax_location_void - Object methods for cust_bill_pkg_tax_location_void records
+
+=head1 SYNOPSIS
+
+ use FS::cust_bill_pkg_tax_location_void;
+
+ $record = new FS::cust_bill_pkg_tax_location_void \%hash;
+ $record = new FS::cust_bill_pkg_tax_location_void { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_tax_location_void object represents a voided record
+of taxation based on package location. FS::cust_bill_pkg_tax_location_void
+inherits from FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item billpkgtaxlocationnum
+
+primary key
+
+=item billpkgnum
+
+billpkgnum
+
+=item taxnum
+
+taxnum
+
+=item taxtype
+
+taxtype
+
+=item pkgnum
+
+pkgnum
+
+=item locationnum
+
+locationnum
+
+=item amount
+
+amount
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_pkg_tax_location_void'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_number('billpkgtaxlocationnum')
+ || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum' )
+ || $self->ut_number('taxnum') #cust_bill_pkg/tax_rate key, based on taxtype
+ || $self->ut_enum('taxtype', [ qw( FS::cust_main_county FS::tax_rate ) ] )
+ || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum' )
+ || $self->ut_foreign_key('locationnum', 'cust_location', 'locationnum' )
+ || $self->ut_money('amount')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pkg_tax_rate_location_void.pm b/FS/FS/cust_bill_pkg_tax_rate_location_void.pm
new file mode 100644
index 000000000..f2e85c085
--- /dev/null
+++ b/FS/FS/cust_bill_pkg_tax_rate_location_void.pm
@@ -0,0 +1,139 @@
+package FS::cust_bill_pkg_tax_rate_location_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+use FS::cust_bill_pkg_void;
+use FS::tax_rate_location;
+
+=head1 NAME
+
+FS::cust_bill_pkg_tax_rate_location_void - Object methods for cust_bill_pkg_tax_rate_location_void records
+
+=head1 SYNOPSIS
+
+ use FS::cust_bill_pkg_tax_rate_location_void;
+
+ $record = new FS::cust_bill_pkg_tax_rate_location_void \%hash;
+ $record = new FS::cust_bill_pkg_tax_rate_location_void { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_tax_rate_location_void object represents a voided record
+of taxation based on package location.
+FS::cust_bill_pkg_tax_rate_location_void inherits from FS::Record. The
+following fields are currently supported:
+
+=over 4
+
+=item billpkgtaxratelocationnum
+
+primary key
+
+=item billpkgnum
+
+billpkgnum
+
+=item taxnum
+
+taxnum
+
+=item taxtype
+
+taxtype
+
+=item locationtaxid
+
+locationtaxid
+
+=item taxratelocationnum
+
+taxratelocationnum
+
+=item amount
+
+amount
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_pkg_tax_rate_location_void'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_number('billpkgtaxratelocationnum')
+ || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum' )
+ || $self->ut_number('taxnum') #cust_bill_pkg/tax_rate key, based on taxtype
+ || $self->ut_text('taxtype', [ qw( FS::cust_main_county FS::tax_rate ) ] )
+ || $self->ut_textn('locationtaxid')
+ || $self->ut_foreign_key('taxratelocationnum', 'tax_rate_location', 'taxratelocationnum' )
+ || $self->ut_money('amount')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::cust_bill_pkg_tax_rate_location>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pkg_void.pm b/FS/FS/cust_bill_pkg_void.pm
new file mode 100644
index 000000000..8949ba7a3
--- /dev/null
+++ b/FS/FS/cust_bill_pkg_void.pm
@@ -0,0 +1,272 @@
+package FS::cust_bill_pkg_void;
+use base qw( FS::TemplateItem_Mixin FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs dbh fields );
+use FS::cust_bill_void;
+use FS::cust_bill_pkg_detail;
+use FS::cust_bill_pkg_display;
+use FS::cust_bill_pkg_discount;
+use FS::cust_bill_pkg;
+use FS::cust_bill_pkg_tax_location;
+use FS::cust_bill_pkg_tax_rate_location;
+use FS::cust_tax_exempt_pkg;
+
+=head1 NAME
+
+FS::cust_bill_pkg_void - Object methods for cust_bill_pkg_void records
+
+=head1 SYNOPSIS
+
+ use FS::cust_bill_pkg_void;
+
+ $record = new FS::cust_bill_pkg_void \%hash;
+ $record = new FS::cust_bill_pkg_void { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_void object represents a voided invoice line item.
+FS::cust_bill_pkg_void inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item billpkgnum
+
+primary key
+
+=item invnum
+
+invnum
+
+=item pkgnum
+
+pkgnum
+
+=item pkgpart_override
+
+pkgpart_override
+
+=item setup
+
+setup
+
+=item recur
+
+recur
+
+=item sdate
+
+sdate
+
+=item edate
+
+edate
+
+=item itemdesc
+
+itemdesc
+
+=item itemcomment
+
+itemcomment
+
+=item section
+
+section
+
+=item freq
+
+freq
+
+=item quantity
+
+quantity
+
+=item unitsetup
+
+unitsetup
+
+=item unitrecur
+
+unitrecur
+
+=item hidden
+
+hidden
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_pkg_void'; }
+
+sub detail_table { 'cust_bill_pkg_detail_void'; }
+sub display_table { 'cust_bill_pkg_display_void'; }
+sub discount_table { 'cust_bill_pkg_discount_void'; }
+#sub tax_location_table { 'cust_bill_pkg_tax_location'; }
+#sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; }
+#sub tax_exempt_pkg_table { 'cust_tax_exempt_pkg'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item unvoid
+
+"Un-void"s this line item: Deletes the voided line item from the database and
+adds back a normal line item (and related tables).
+
+=cut
+
+sub unvoid {
+ my $self = shift;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $cust_bill_pkg = new FS::cust_bill_pkg ( {
+ map { $_ => $self->get($_) } fields('cust_bill_pkg')
+ } );
+ my $error = $cust_bill_pkg->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ foreach my $table (qw(
+ cust_bill_pkg_detail
+ cust_bill_pkg_display
+ cust_bill_pkg_discount
+ cust_bill_pkg_tax_location
+ cust_bill_pkg_tax_rate_location
+ cust_tax_exempt_pkg
+ )) {
+
+ foreach my $voided (
+ qsearch($table.'_void', { billpkgnum=>$self->billpkgnum })
+ ) {
+
+ my $class = 'FS::'.$table;
+ my $unvoid = $class->new( {
+ map { $_ => $voided->get($_) } fields($table)
+ });
+ my $error = $unvoid->insert || $voided->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
+ }
+
+ $error = $self->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ '';
+
+}
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_number('billpkgnum')
+ || $self->ut_snumber('pkgnum')
+ || $self->ut_number('invnum') #cust_bill or cust_bill_void, if we ever support line item voiding
+ || $self->ut_numbern('pkgpart_override')
+ || $self->ut_money('setup')
+ || $self->ut_money('recur')
+ || $self->ut_numbern('sdate')
+ || $self->ut_numbern('edate')
+ || $self->ut_textn('itemdesc')
+ || $self->ut_textn('itemcomment')
+ || $self->ut_textn('section')
+ || $self->ut_textn('freq')
+ || $self->ut_numbern('quantity')
+ || $self->ut_moneyn('unitsetup')
+ || $self->ut_moneyn('unitrecur')
+ || $self->ut_enum('hidden', [ '', 'Y' ])
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item cust_bill
+
+Returns the voided invoice (see L<FS::cust_bill_void>) for this voided line
+item.
+
+=cut
+
+sub cust_bill {
+ my $self = shift;
+ #cust_bill or cust_bill_void, if we ever support line item voiding
+ qsearchs( 'cust_bill_void', { 'invnum' => $self->invnum } );
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_void.pm b/FS/FS/cust_bill_void.pm
new file mode 100644
index 000000000..cce77b3aa
--- /dev/null
+++ b/FS/FS/cust_bill_void.pm
@@ -0,0 +1,286 @@
+package FS::cust_bill_void;
+use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs dbh fields );
+use FS::cust_main;
+use FS::cust_statement;
+use FS::access_user;
+use FS::cust_bill_pkg_void;
+use FS::cust_bill;
+
+=head1 NAME
+
+FS::cust_bill_void - Object methods for cust_bill_void records
+
+=head1 SYNOPSIS
+
+ use FS::cust_bill_void;
+
+ $record = new FS::cust_bill_void \%hash;
+ $record = new FS::cust_bill_void { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_void object represents a voided invoice. FS::cust_bill_void
+inherits from FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item invnum
+
+primary key
+
+=item custnum
+
+custnum
+
+=item _date
+
+_date
+
+=item charged
+
+charged
+
+=item invoice_terms
+
+invoice_terms
+
+=item previous_balance
+
+previous_balance
+
+=item billing_balance
+
+billing_balance
+
+=item closed
+
+closed
+
+=item statementnum
+
+statementnum
+
+=item agent_invid
+
+agent_invid
+
+=item promised_date
+
+promised_date
+
+=item void_date
+
+void_date
+
+=item reason
+
+reason
+
+=item void_usernum
+
+void_usernum
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new voided invoice. To add the voided invoice to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_void'; }
+sub notice_name { 'VOIDED Invoice'; }
+#XXXsub template_conf { 'quotation_'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item unvoid
+
+"Un-void"s this invoice: Deletes the voided invoice from the database and adds
+back a normal invoice (and related tables).
+
+=cut
+
+sub unvoid {
+ my $self = shift;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $cust_bill = new FS::cust_bill ( {
+ map { $_ => $self->get($_) } fields('cust_bill')
+ } );
+ my $error = $cust_bill->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ foreach my $cust_bill_pkg_void ( $self->cust_bill_pkg ) {
+ my $error = $cust_bill_pkg_void->unvoid;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ $error = $self->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ '';
+
+}
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid voided invoice. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_number('invnum')
+ || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
+ || $self->ut_numbern('_date')
+ || $self->ut_money('charged')
+ || $self->ut_textn('invoice_terms')
+ || $self->ut_moneyn('previous_balance')
+ || $self->ut_moneyn('billing_balance')
+ || $self->ut_enum('closed', [ '', 'Y' ])
+ || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum')
+ || $self->ut_numbern('agent_invid')
+ || $self->ut_numbern('promised_date')
+ || $self->ut_numbern('void_date')
+ || $self->ut_textn('reason')
+ || $self->ut_numbern('void_usernum')
+ ;
+ return $error if $error;
+
+ $self->void_date(time) unless $self->void_date;
+
+ $self->void_usernum($FS::CurrentUser::CurrentUser->usernum)
+ unless $self->void_usernum;
+
+ $self->SUPER::check;
+}
+
+=item display_invnum
+
+Returns the displayed invoice number for this invoice: agent_invid if
+cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
+
+=cut
+
+sub display_invnum {
+ my $self = shift;
+ my $conf = $self->conf;
+ if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
+ return $self->agent_invid;
+ } else {
+ return $self->invnum;
+ }
+}
+
+=item void_access_user
+
+Returns the voiding employee object (see L<FS::access_user>).
+
+=cut
+
+sub void_access_user {
+ my $self = shift;
+ qsearchs('access_user', { 'usernum' => $self->void_usernum } );
+}
+
+=item cust_main
+
+=cut
+
+sub cust_main {
+ my $self = shift;
+ qsearchs('cust_main', { 'custnum' => $self->custnum } );
+}
+
+=item cust_bill_pkg
+
+=cut
+
+sub cust_bill_pkg { #actually cust_bill_pkg_void objects
+ my $self = shift;
+ qsearch('cust_bill_pkg_void', { invnum=>$self->invnum });
+}
+
+=back
+
+=item enable_previous
+
+=cut
+
+sub enable_previous { 0 }
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_credit_bill_pkg.pm b/FS/FS/cust_credit_bill_pkg.pm
index 64f1f297e..418900785 100644
--- a/FS/FS/cust_credit_bill_pkg.pm
+++ b/FS/FS/cust_credit_bill_pkg.pm
@@ -103,18 +103,22 @@ sub insert {
return $error;
}
- my $payable = $self->cust_bill_pkg->payable($self->setuprecur);
- my $taxable = $self->_is_taxable ? $payable : 0;
- my $part_pkg = $self->cust_bill_pkg->part_pkg;
- my $freq = $self->cust_bill_pkg->freq;
+ my $cust_bill_pkg = $self->cust_bill_pkg;
+ #'payable' is the amount charged (either setup or recur)
+ # minus any credit applications, including this one
+ my $payable = $cust_bill_pkg->payable($self->setuprecur);
+ my $part_pkg = $cust_bill_pkg->part_pkg;
+ my $freq = $cust_bill_pkg->freq;
unless ($freq) {
$freq = $part_pkg ? ($part_pkg->freq || 1) : 1;#fallback.. assumes unchanged
}
- my $taxable_per_month = sprintf("%.2f", $taxable / $freq );
+ my $taxable_per_month = sprintf("%.2f", $payable / $freq );
my $credit_per_month = sprintf("%.2f", $self->amount / $freq ); #pennies?
if ($taxable_per_month >= 0) { #panic if its subzero?
- my $groupby = 'taxnum,year,month';
+ my $groupby = join(',',
+ qw(taxnum year month exempt_monthly exempt_cust
+ exempt_cust_taxname exempt_setup exempt_recur));
my $sum = 'SUM(amount)';
my @exemptions = qsearch(
{
@@ -124,25 +128,55 @@ sub insert {
'extra_sql' => "GROUP BY $groupby HAVING $sum > 0",
}
);
+ # each $exemption is now the sum of all monthly exemptions applied to
+ # this line item for a particular taxnum and month.
foreach my $exemption ( @exemptions ) {
- next if $taxable_per_month >= $exemption->amount;
- my $amount = $exemption->amount - $taxable_per_month;
- if ($amount > $credit_per_month) {
- "cust_bill_pkg ". $self->billpkgnum. " Reducing.\n";
- $amount = $credit_per_month;
+ my $amount = 0;
+ if ( $exemption->exempt_monthly ) {
+ # finite exemptions
+ # $taxable_per_month is AFTER inserting the credit application, so
+ # if it's still larger than the exemption, we don't need to adjust
+ next if $taxable_per_month >= $exemption->amount;
+ # the amount of 'excess' exemption already in place (above the
+ # remaining charged amount). We'll de-exempt that much, or the
+ # amount of the new credit, whichever is smaller.
+ $amount = $exemption->amount - $taxable_per_month;
+ # $amount is the amount of 'excess' exemption already existing
+ # (above the remaining taxable charge amount). We'll "de-exempt"
+ # that much, or the amount of the new credit, whichever is smaller.
+ if ($amount > $credit_per_month) {
+ "cust_bill_pkg ". $self->billpkgnum. " Reducing.\n";
+ $amount = $credit_per_month;
+ }
+ } elsif ( $exemption->exempt_setup or $exemption->exempt_recur ) {
+ # package defined exemptions: may be setup only, recur only, or both
+ my $method = 'exempt_'.$self->setuprecur;
+ if ( $exemption->$method ) {
+ # then it's exempt from the portion of the charge that this
+ # credit is being applied to
+ $amount = $self->amount;
+ }
+ } else {
+ # other types of exemptions: always equal to the amount of
+ # the charge
+ $amount = $self->amount;
}
+ next if $amount == 0;
+
+ # create a negative exemption
my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg {
+ $exemption->hash, # for exempt_ flags, taxnum, month/year
'billpkgnum' => $self->billpkgnum,
'creditbillpkgnum' => $self->creditbillpkgnum,
'amount' => sprintf('%.2f', 0-$amount),
- map { $_ => $exemption->$_ } split(',', $groupby)
};
+
my $error = $cust_tax_exempt_pkg->insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "error inserting cust_tax_exempt_pkg: $error";
}
- }
+ } #foreach $exemption
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -233,7 +267,7 @@ sub delete {
return "error calculating taxes: $hashref_or_error";
}
- push @generated_exemptions, @{ $cust_bill_pkg->_cust_tax_exempt_pkg || [] };
+ push @generated_exemptions, @{ $cust_bill_pkg->cust_tax_exempt_pkg };
}
foreach my $taxnum ( keys %seen ) {
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index d6a86c786..9e39b3006 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -4,6 +4,7 @@ require 5.006;
use strict;
#FS::cust_main:_Marketgear when they're ready to move to 2.1
use base qw( FS::cust_main::Packages FS::cust_main::Status
+ FS::cust_main::NationalID
FS::cust_main::Billing FS::cust_main::Billing_Realtime
FS::cust_main::Billing_Discount
FS::cust_main::Location
@@ -42,6 +43,7 @@ use FS::payby;
use FS::cust_pkg;
use FS::cust_svc;
use FS::cust_bill;
+use FS::cust_bill_void;
use FS::legacy_cust_bill;
use FS::cust_pay;
use FS::cust_pay_pending;
@@ -453,8 +455,10 @@ sub insert {
warn " setting $l.custnum\n"
if $DEBUG > 1;
my $loc = $self->$l;
- $loc->set(custnum => $self->custnum);
- $error ||= $loc->replace;
+ unless ( $loc->custnum ) {
+ $loc->set(custnum => $self->custnum);
+ $error ||= $loc->replace;
+ }
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
@@ -1242,9 +1246,12 @@ sub merge {
return "Can't merge a customer into self" if $self->custnum == $new_custnum;
- unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
- return "Invalid new customer number: $new_custnum";
- }
+ my $new_cust_main = qsearchs( 'cust_main', { 'custnum' => $new_custnum } )
+ or return "Invalid new customer number: $new_custnum";
+
+ return 'Access denied: "Merge customer across agents" access right required to merge into a customer of a different agent'
+ if $self->agentnum != $new_cust_main->agentnum
+ && ! $FS::CurrentUser::CurrentUser->access_right('Merge customer across agents');
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
@@ -1279,6 +1286,7 @@ sub merge {
tie my %financial_tables, 'Tie::IxHash',
'cust_bill' => 'invoices',
+ 'cust_bill_void' => 'voided invoices',
'cust_statement' => 'statements',
'cust_credit' => 'credits',
'cust_pay' => 'payments',
@@ -1779,8 +1787,10 @@ sub check {
|| $self->ut_textn('custbatch')
|| $self->ut_name('last')
|| $self->ut_name('first')
- || $self->ut_snumbern('birthdate')
|| $self->ut_snumbern('signupdate')
+ || $self->ut_snumbern('birthdate')
+ || $self->ut_snumbern('spouse_birthdate')
+ || $self->ut_snumbern('anniversary_date')
|| $self->ut_textn('company')
|| $self->ut_anything('comments')
|| $self->ut_numbern('referral_custnum')
@@ -1790,6 +1800,7 @@ sub check {
|| $self->ut_floatn('cdr_termination_percentage')
|| $self->ut_floatn('credit_limit')
|| $self->ut_numbern('billday')
+ || $self->ut_numbern('prorate_day')
|| $self->ut_enum('edit_subject', [ '', 'Y' ] )
|| $self->ut_enum('calling_list_exempt', [ '', 'Y' ] )
|| $self->ut_enum('invoice_noemail', [ '', 'Y' ] )
@@ -3644,6 +3655,20 @@ be passed.
=cut
+=item cust_bill_void
+
+Returns all the voided invoices (see L<FS::cust_bill_void>) for this customer.
+
+=cut
+
+sub cust_bill_void {
+ my $self = shift;
+
+ map { $_ } #return $self->num_cust_bill_void unless wantarray;
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_bill_void', { 'custnum' => $self->custnum } )
+}
+
sub cust_statement {
my $self = shift;
my $opt = ref($_[0]) ? shift : { @_ };
@@ -3800,7 +3825,7 @@ sub cust_pay_void {
=item cust_pay_batch [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
-Returns all batched payments (see L<FS::cust_pay_void>) for this customer.
+Returns all batched payments (see L<FS::cust_pay_batch>) for this customer.
Optionally, a list or hashref of additional arguments to the qsearch call can
be passed.
diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm
index bab94c31d..11247a28f 100644
--- a/FS/FS/cust_main/Billing.pm
+++ b/FS/FS/cust_main/Billing.pm
@@ -735,21 +735,25 @@ sub calculate_taxes {
my @tax_line_items = ();
# keys are tax names (as printed on invoices / itemdesc )
- # values are listrefs of taxlisthash keys (internal identifiers)
+ # values are arrayrefs of taxlisthash keys (internal identifiers)
my %taxname = ();
# keys are taxlisthash keys (internal identifiers)
# values are (cumulative) amounts
- my %tax = ();
+ my %tax_amount = ();
# keys are taxlisthash keys (internal identifiers)
- # values are listrefs of cust_bill_pkg_tax_location hashrefs
+ # values are arrayrefs of cust_bill_pkg_tax_location hashrefs
my %tax_location = ();
# keys are taxlisthash keys (internal identifiers)
- # values are listrefs of cust_bill_pkg_tax_rate_location hashrefs
+ # values are arrayrefs of cust_bill_pkg_tax_rate_location hashrefs
my %tax_rate_location = ();
+ # keys are taxnums (not internal identifiers!)
+ # values are arrayrefs of cust_tax_exempt_pkg objects
+ my %tax_exemption;
+
foreach my $tax ( keys %$taxlisthash ) {
# $tax is a tax identifier
my $tax_object = shift @{ $taxlisthash->{$tax} };
@@ -759,14 +763,24 @@ sub calculate_taxes {
warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2;
# taxline calculates the tax on all cust_bill_pkgs in the
- # first (arrayref) argument
+ # first (arrayref) argument, and returns a hashref of 'name'
+ # (the line item description) and 'amount'.
+ # It also calculates exemptions and attaches them to the cust_bill_pkgs
+ # in the argument.
+ my $taxables = $taxlisthash->{$tax};
+ my $exemptions = $tax_exemption{$tax_object->taxnum} ||= [];
my $hashref_or_error =
- $tax_object->taxline( $taxlisthash->{$tax},
+ $tax_object->taxline( $taxables,
'custnum' => $self->custnum,
- 'invoice_time' => $invoice_time
+ 'invoice_time' => $invoice_time,
+ 'exemptions' => $exemptions,
);
return $hashref_or_error unless ref($hashref_or_error);
+ # then collect any new exemptions generated for this tax
+ push @$exemptions, @{ $_->cust_tax_exempt_pkg }
+ foreach @$taxables;
+
unshift @{ $taxlisthash->{$tax} }, $tax_object;
my $name = $hashref_or_error->{'name'};
@@ -776,7 +790,7 @@ sub calculate_taxes {
$taxname{ $name } ||= [];
push @{ $taxname{ $name } }, $tax;
- $tax{ $tax } += $amount;
+ $tax_amount{ $tax } += $amount;
# link records between cust_main_county/tax_rate and cust_location
$tax_location{ $tax } ||= [];
@@ -809,17 +823,21 @@ sub calculate_taxes {
#move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
my %packagemap = map { $_->pkgnum => $_ } @$cust_bill_pkg;
foreach my $tax ( keys %$taxlisthash ) {
- foreach ( @{ $taxlisthash->{$tax} }[1 ... scalar(@{ $taxlisthash->{$tax} })] ) {
- next unless ref($_) eq 'FS::cust_bill_pkg';
-
- my @cust_tax_exempt_pkg = splice( @{ $_->_cust_tax_exempt_pkg } );
+ my $taxables = $taxlisthash->{$tax};
+ my $tax_object = shift @$taxables; # the rest are line items
+ foreach my $cust_bill_pkg ( @$taxables ) {
+ next unless ref($cust_bill_pkg) eq 'FS::cust_bill_pkg';
- next unless @cust_tax_exempt_pkg; #just avoiding the prob when irrelevant?
- die "can't distribute tax exemptions: no line item for ". Dumper($_).
- " in packagemap ". join(',', sort {$a<=>$b} keys %packagemap). "\n"
- unless $packagemap{$_->pkgnum};
+ my @cust_tax_exempt_pkg = splice @{ $cust_bill_pkg->cust_tax_exempt_pkg };
- push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
+ next unless @cust_tax_exempt_pkg;
+ # get the non-disintegrated version
+ my $real_cust_bill_pkg = $packagemap{$cust_bill_pkg->pkgnum}
+ or die "can't distribute tax exemptions: no line item for ".
+ Dumper($_). " in packagemap ".
+ join(',', sort {$a<=>$b} keys %packagemap). "\n";
+
+ push @{ $real_cust_bill_pkg->cust_tax_exempt_pkg },
@cust_tax_exempt_pkg;
}
}
@@ -827,15 +845,15 @@ sub calculate_taxes {
#consolidate and create tax line items
warn "consolidating and generating...\n" if $DEBUG > 2;
foreach my $taxname ( keys %taxname ) {
- my $tax = 0;
+ my $tax_total = 0;
my %seen = ();
my @cust_bill_pkg_tax_location = ();
my @cust_bill_pkg_tax_rate_location = ();
warn "adding $taxname\n" if $DEBUG > 1;
foreach my $taxitem ( @{ $taxname{$taxname} } ) {
next if $seen{$taxitem}++;
- warn "adding $tax{$taxitem}\n" if $DEBUG > 1;
- $tax += $tax{$taxitem};
+ warn "adding $tax_amount{$taxitem}\n" if $DEBUG > 1;
+ $tax_total += $tax_amount{$taxitem};
push @cust_bill_pkg_tax_location,
map { new FS::cust_bill_pkg_tax_location $_ }
@{ $tax_location{ $taxitem } };
@@ -843,9 +861,9 @@ sub calculate_taxes {
map { new FS::cust_bill_pkg_tax_rate_location $_ }
@{ $tax_rate_location{ $taxitem } };
}
- next unless $tax;
+ next unless $tax_total;
- $tax = sprintf('%.2f', $tax );
+ $tax_total = sprintf('%.2f', $tax_total );
my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
'disabled' => '',
@@ -866,7 +884,7 @@ sub calculate_taxes {
push @tax_line_items, new FS::cust_bill_pkg {
'pkgnum' => 0,
- 'setup' => $tax,
+ 'setup' => $tax_total,
'recur' => 0,
'sdate' => '',
'edate' => '',
@@ -1197,8 +1215,11 @@ sub _handle_taxes {
my $exempt = $conf->exists('cust_class-tax_exempt')
? ( $self->cust_class ? $self->cust_class->tax : '' )
: $self->tax;
+ # standardize this just to be sure
+ $exempt = ($exempt eq 'Y') ? 'Y' : '';
- if ( $exempt !~ /Y/i && $self->payby ne 'COMP' ) {
+ #if ( $exempt !~ /Y/i && $self->payby ne 'COMP' ) {
+ if ( $self->payby ne 'COMP' ) {
if ( $conf->exists('enable_taxproducts')
&& ( scalar($part_pkg->part_pkg_taxoverride)
@@ -1207,19 +1228,26 @@ sub _handle_taxes {
)
{
- foreach my $class (@classes) {
- my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
- return $err_or_ref unless ref($err_or_ref);
- $taxes{$class} = $err_or_ref;
- }
+ if ( !$exempt ) {
+
+ foreach my $class (@classes) {
+ my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
+ return $err_or_ref unless ref($err_or_ref);
+ $taxes{$class} = $err_or_ref;
+ }
+
+ unless (exists $taxes{''}) {
+ my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
+ return $err_or_ref unless ref($err_or_ref);
+ $taxes{''} = $err_or_ref;
+ }
- unless (exists $taxes{''}) {
- my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
- return $err_or_ref unless ref($err_or_ref);
- $taxes{''} = $err_or_ref;
}
- } else {
+ } else { # cust_main_county tax system
+
+ # We fetch taxes even if the customer is completely exempt,
+ # because we need to record that fact.
my @loc_keys = qw( district city county state country );
my $location = $cust_pkg->tax_location;
@@ -1227,6 +1255,8 @@ sub _handle_taxes {
$taxhash{'taxclass'} = $part_pkg->taxclass;
+ warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
+
my @taxes = (); # entries are cust_main_county objects
my %taxhash_elim = %taxhash;
my @elim = qw( district city county state );
@@ -1246,17 +1276,11 @@ sub _handle_taxes {
} while ( !scalar(@taxes) && scalar(@elim) );
- @taxes = grep { ! $_->taxname or ! $self->tax_exemption($_->taxname) }
- @taxes
- if $self->cust_main_exemption; #just to be safe
-
- # all packages now have a locationnum and should get a
- # cust_bill_pkg_tax_location record. The tax_locationnum
- # may be the package's locationnum, or the customer's bill
- # or service location.
foreach (@taxes) {
- $_->set('pkgnum', $cust_pkg->pkgnum);
- $_->set('locationnum', $cust_pkg->tax_locationnum);
+ # These could become cust_bill_pkg_tax_location records,
+ # or cust_tax_exempt_pkg. We'll decide later.
+ $_->set('pkgnum', $cust_pkg->pkgnum);
+ $_->set('locationnum', $cust_pkg->tax_locationnum);
}
$taxes{''} = [ @taxes ];
@@ -1273,7 +1297,7 @@ sub _handle_taxes {
} #if $conf->exists('enable_taxproducts') ...
- }
+ } # if $self->payby eq 'COMP'
#what's this doing in the middle of _handle_taxes? probably should split
#this into three parts above in _make_lines
@@ -1296,14 +1320,15 @@ sub _handle_taxes {
# this is the tax identifier, not the taxname
my $taxname = ref( $tax ). ' '. $tax->taxnum;
-# $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
-# ' locationnum'. $cust_pkg->locationnum
-# if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
+ $taxname .= ' pkgnum'. $cust_pkg->pkgnum;
+ # We need to create a separate $taxlisthash entry for each pkgnum
+ # on the invoice, so that cust_bill_pkg_tax_location records will
+ # be linked correctly.
- # $taxlisthash: keys are "setup", "recur", and usage classes
- # values are arrayrefs, first the tax object (cust_main_county
+ # $taxlisthash: keys are "setup", "recur", and usage classes.
+ # Values are arrayrefs, first the tax object (cust_main_county
# or tax_rate) and then any cust_bill_pkg objects that the
- # tax applies to
+ # tax applies to.
$taxlisthash->{ $taxname } ||= [ $tax ];
push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
diff --git a/FS/FS/cust_main/Import.pm b/FS/FS/cust_main/Import.pm
index 6681f9ec2..eadcc1a55 100644
--- a/FS/FS/cust_main/Import.pm
+++ b/FS/FS/cust_main/Import.pm
@@ -210,8 +210,23 @@ sub batch_import {
cust_pkg.pkgpart cust_pkg.bill
svc_acct.username svc_acct._password
);
- push @fields, map "svc_phone.$_", qw(countrycode phonenum sip_password pin);
- push @fields, map "svc_hardware.$_", qw(typenum ip_addr hw_addr serial);
+ push @fields, map "svc_phone.$_", qw(countrycode phonenum sip_password pin);
+ push @fields, map "svc_hardware.$_", qw(typenum ip_addr hw_addr serial);
+
+ $payby = 'BILL';
+ } elsif ( $format eq 'national_id-acct_phone') {
+ @fields = qw( agent_custid refnum
+ last first company address1 address2 city state zip country
+ daytime night
+ ship_last ship_first ship_company ship_address1 ship_address2
+ ship_city ship_state ship_zip ship_country
+ national_id
+ payinfo paycvv paydate
+ invoicing_list
+ cust_pkg.pkgpart cust_pkg.bill
+ svc_acct.username svc_acct._password svc_acct.slipip
+ );
+ push @fields, map "svc_phone.$_", qw(countrycode phonenum sip_password pin);
$payby = 'BILL';
} else {
@@ -321,7 +336,7 @@ sub batch_import {
$cust_pkg{$1} = parse_datetime( shift @columns );
}
- } elsif ( $field =~ /^svc_acct\.(username|_password)$/ ) {
+ } elsif ( $field =~ /^svc_acct\.(username|_password|slipip)$/ ) {
$svc_x{$1} = shift @columns;
@@ -375,7 +390,8 @@ sub batch_import {
}
$cust_main{$_} = parse_datetime($cust_main{$_})
- foreach grep $cust_main{$_}, qw( birthdate spouse_birthdate );
+ foreach grep $cust_main{$_},
+ qw( birthdate spouse_birthdate anniversary_date );
my $invoicing_list = $cust_main{'invoicing_list'}
? [ delete $cust_main{'invoicing_list'} ]
diff --git a/FS/FS/cust_main/NationalID.pm b/FS/FS/cust_main/NationalID.pm
new file mode 100644
index 000000000..a742b7637
--- /dev/null
+++ b/FS/FS/cust_main/NationalID.pm
@@ -0,0 +1,64 @@
+package FS::cust_main::NationalID;
+
+use strict;
+use vars qw( $conf );
+use Date::Simple qw( days_in_month );
+use FS::UID;
+
+install_callback FS::UID sub {
+ $conf = new FS::Conf;
+};
+
+sub set_national_id_from_cgi {
+ my( $self, $cgi ) = @_;
+
+ my $error = '';
+
+ if ( my $id_country = $conf->config('national_id-country') ) {
+ if ( $id_country eq 'MY' ) {
+
+ if ( $cgi->param('national_id1') =~ /\S/ ) {
+ my $nric = $cgi->param('national_id1');
+ $nric =~ s/\s//g;
+ if ( $nric =~ /^(\d{2})(\d{2})(\d{2})\-?(\d{2})\-?(\d{4})$/ ) {
+ my( $y, $m, $d, $bp, $n ) = ( $1, $2, $3, $4, $5 );
+ $self->national_id( "$y$m$d-$bp-$n" );
+
+ my @lt = localtime(time);
+ my $year = ( $y <= substr( $lt[5]+1900, -2) ) ? 2000 + $y
+ : 1900 + $y;
+ $error ||= "Illegal NRIC: ". $cgi->param('national_id1')
+ if $m < 1 || $m > 12 || $d < 1 || $d > days_in_month($year, $m);
+ #$bp validation per http://en.wikipedia.org/wiki/National_Registration_Identity_Card_Number_%28Malaysia%29#Second_section:_Birthplace ? seems like a bad idea, some could be missing or get added
+ } else {
+ $error ||= "Illegal NRIC: ". $cgi->param('national_id1');
+ }
+ } elsif ( $cgi->param('national_id2') =~ /\S/ ) {
+ my $oldic = $cgi->param('national_id2');
+ $oldic =~ s/\s//g;
+
+ # can you please remove validation for "Old IC/Passport:" field, customer
+ # will have other field format like, RF/123456, I/5234234 ...
+ #if ( $oldic =~ /^\w\d{9}$/ ) {
+ $self->national_id($oldic);
+ #} else {
+ # $error ||= "Illegal Old IC/Passport: ". $cgi->param('national_id2');
+ #}
+
+ } else {
+ $error ||= 'Either NRIC or Old IC/Passport is required';
+ }
+
+ } else {
+ warn "unknown national_id-country $id_country";
+ }
+ } elsif ( $cgi->param('national_id0') ) {
+ $self->national_id( $cgi->param('national_id0') );
+ }
+
+ $error;
+
+}
+
+1;
+
diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm
index b528a689c..b07223ec5 100644
--- a/FS/FS/cust_main/Search.pm
+++ b/FS/FS/cust_main/Search.pm
@@ -85,7 +85,7 @@ sub smart_search {
'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
' ( '.
join(' OR ', map "$_ = '$phonen'",
- qw( daytime night fax )
+ qw( daytime night mobile fax )
).
' ) '.
" AND $agentnums_sql", #agent virtualization
@@ -457,6 +457,8 @@ HASHREF. Valid parameters are
=item address
+=item zip
+
=item refnum
=item cancelled_pkgs
@@ -475,6 +477,10 @@ listref of start date, end date
listref of start date, end date
+=item anniversary_date
+
+listref of start date, end date
+
=item payby
listref
@@ -512,6 +518,7 @@ sub search {
'usernum' => '',
'status' => '',
'address' => '',
+ 'zip' => '',
'paydate_year' => '',
'invoice_terms' => '',
'custbatch' => '',
@@ -574,6 +581,18 @@ sub search {
)";
}
+ ##
+ # zipcode
+ ##
+ if ( $params->{'zip'} =~ /\S/ ) {
+ my $zip = dbh->quote($params->{'zip'} . '%');
+ push @where, "EXISTS(
+ SELECT 1 FROM cust_location
+ WHERE cust_location.custnum = cust_main.custnum
+ AND cust_location.zip LIKE $zip
+ )";
+ }
+
###
# refnum
###
@@ -617,7 +636,7 @@ sub search {
# dates
##
- foreach my $field (qw( signupdate birthdate spouse_birthdate )) {
+ foreach my $field (qw( signupdate birthdate spouse_birthdate anniversary_date )) {
next unless exists($params->{$field});
@@ -779,6 +798,9 @@ sub search {
my @select = (
'cust_main.custnum',
+ # there's a good chance that we'll need these
+ 'cust_main.bill_locationnum',
+ 'cust_main.ship_locationnum',
FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
);
diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm
index 6316f239a..143f62ed3 100644
--- a/FS/FS/cust_main_county.pm
+++ b/FS/FS/cust_main_county.pm
@@ -4,7 +4,7 @@ use strict;
use vars qw( @ISA @EXPORT_OK $conf
@cust_main_county %cust_main_county $countyflag ); # $cityflag );
use Exporter;
-use FS::Record qw( qsearch dbh );
+use FS::Record qw( qsearch qsearchs dbh );
use FS::cust_bill_pkg;
use FS::cust_bill;
use FS::cust_pkg;
@@ -164,6 +164,57 @@ sub recurtax {
return '';
}
+=item label OPTIONS
+
+Returns a label looking like "Anytown, Alameda County, CA, US".
+
+If the taxname field is set, it will look like
+"CA Sales Tax (Anytown, Alameda County, CA, US)".
+
+If the taxclass is set, then it will be
+"Anytown, Alameda County, CA, US (International)".
+
+Currently it will not contain the district, even if the city+county+state
+is not unique.
+
+OPTIONS may contain "no_taxclass" (hides taxclass) and/or "no_city"
+(hides city). It may also contain "out", in which case, if this
+region (district+city+county+state+country) contains no non-zero
+taxes, the label will read "Out of taxable region(s)".
+
+=cut
+
+sub label {
+ my ($self, %opt) = @_;
+ if ( $opt{'out'}
+ and $self->tax == 0
+ and !defined(qsearchs('cust_main_county', {
+ 'district' => $self->district,
+ 'city' => $self->city,
+ 'county' => $self->county,
+ 'state' => $self->state,
+ 'country' => $self->country,
+ 'tax' => { op => '>', value => 0 },
+ })) )
+ {
+ return 'Out of taxable region(s)';
+ }
+ my $label = $self->country;
+ $label = $self->state.", $label" if $self->state;
+ $label = $self->county." County, $label" if $self->county;
+ if (!$opt{no_city}) {
+ $label = $self->city.", $label" if $self->city;
+ }
+ # ugly labels when taxclass and taxname are both non-null...
+ # but this is how the tax report does it
+ if (!$opt{no_taxclass}) {
+ $label = "$label (".$self->taxclass.')' if $self->taxclass;
+ }
+ $label = $self->taxname." ($label)" if $self->taxname;
+
+ $label;
+}
+
=item sql_taxclass_sameregion
Returns an SQL WHERE fragment or the empty string to search for entries
@@ -207,21 +258,30 @@ sub _list_sql {
=item taxline TAXABLES_ARRAYREF, [ OPTION => VALUE ... ]
-Returns a listref of a name and an amount of tax calculated for the list of
-packages or amounts referenced by TAXABLES_ARRAYREF. Returns a scalar error
-message on error.
+Returns an hashref of a name and an amount of tax calculated for the
+line items (L<FS::cust_bill_pkg> objects) in TAXABLES_ARRAYREF. The line
+items must come from the same invoice. Returns a scalar error message
+on error.
+
+In addition to calculating the tax for the line items, this will calculate
+any appropriate tax exemptions and attach them to the line items.
-Options include custnum and invoice_date and are hints to this method
+Options may include 'custnum' and 'invoice_date' in case the cust_bill_pkg
+objects belong to an invoice that hasn't been inserted yet.
+
+Options may include 'exemptions', an arrayref of L<FS::cust_tax_exempt_pkg>
+objects belonging to the same customer, to be counted against the monthly
+tax exemption limit if there is one.
=cut
+# XXX this should just return a cust_bill_pkg object for the tax,
+# but that requires changing stuff in tax_rate.pm also.
+
sub taxline {
my( $self, $taxables, %opt ) = @_;
+ return 'taxline called with no line items' unless @$taxables;
- my @exemptions = ();
- push @exemptions, @{ $_->_cust_tax_exempt_pkg }
- for grep { ref($_) } @$taxables;
-
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
@@ -236,29 +296,92 @@ sub taxline {
my $name = $self->taxname || 'Tax';
my $amount = 0;
+ my $cust_bill = $taxables->[0]->cust_bill;
+ my $custnum = $cust_bill ? $cust_bill->custnum : $opt{'custnum'};
+ my $invoice_date = $cust_bill ? $cust_bill->_date : $opt{'invoice_date'};
+ my $cust_main = FS::cust_main->by_key($custnum) if $custnum > 0;
+ if (!$cust_main) {
+ # better way to handle this? should we just assume that it's taxable?
+ die "unable to calculate taxes for an unknown customer\n";
+ }
+
+ # set a flag if the customer is tax-exempt
+ my $exempt_cust;
+ my $conf = FS::Conf->new;
+ if ( $conf->exists('cust_class-tax_exempt') ) {
+ my $cust_class = $cust_main->cust_class;
+ $exempt_cust = $cust_class->tax if $cust_class;
+ } else {
+ $exempt_cust = $cust_main->tax;
+ }
+
+ # set a flag if the customer is exempt from this tax here
+ my $exempt_cust_taxname = $cust_main->tax_exemption($self->taxname)
+ if $self->taxname;
+
+ # Gather any exemptions that are already attached to these cust_bill_pkgs
+ # so that we can deduct them from the customer's monthly limit.
+ my @existing_exemptions = @{ $opt{'exemptions'} };
+ push @existing_exemptions, @{ $_->cust_tax_exempt_pkg }
+ for @$taxables;
+
foreach my $cust_bill_pkg (@$taxables) {
my $cust_pkg = $cust_bill_pkg->cust_pkg;
- my $cust_bill = $cust_pkg->cust_bill if $cust_pkg;
- my $custnum = $cust_pkg ? $cust_pkg->custnum : $opt{custnum};
my $part_pkg = $cust_bill_pkg->part_pkg;
- my $invoice_date = $cust_bill ? $cust_bill->_date : $opt{invoice_date};
-
- my $taxable_charged = 0;
- $taxable_charged += $cust_bill_pkg->setup
- unless $part_pkg->setuptax =~ /^Y$/i
- || $self->setuptax =~ /^Y$/i;
- $taxable_charged += $cust_bill_pkg->recur
- unless $part_pkg->recurtax =~ /^Y$/i
- || $self->recurtax =~ /^Y$/i;
-
- next unless $taxable_charged;
+
+ my @new_exemptions;
+ my $taxable_charged = $cust_bill_pkg->setup + $cust_bill_pkg->recur
+ or next; # don't create zero-amount exemptions
+
+ # XXX the following procedure should probably be in cust_bill_pkg
+
+ if ( $exempt_cust ) {
+
+ push @new_exemptions, FS::cust_tax_exempt_pkg->new({
+ amount => $taxable_charged,
+ exempt_cust => 'Y',
+ });
+ $taxable_charged = 0;
+
+ } elsif ( $exempt_cust_taxname ) {
+
+ push @new_exemptions, FS::cust_tax_exempt_pkg->new({
+ amount => $taxable_charged,
+ exempt_cust_taxname => 'Y',
+ });
+ $taxable_charged = 0;
+
+ }
+
+ if ( ($part_pkg->setuptax eq 'Y' or $self->setuptax eq 'Y')
+ and $cust_bill_pkg->setup > 0 and $taxable_charged > 0 ) {
+
+ push @new_exemptions, FS::cust_tax_exempt_pkg->new({
+ amount => $cust_bill_pkg->setup,
+ exempt_setup => 'Y'
+ });
+ $taxable_charged -= $cust_bill_pkg->setup;
+
+ }
+ if ( ($part_pkg->recurtax eq 'Y' or $self->recurtax eq 'Y')
+ and $cust_bill_pkg->recur > 0 and $taxable_charged > 0 ) {
+
+ push @new_exemptions, FS::cust_tax_exempt_pkg->new({
+ amount => $cust_bill_pkg->recur,
+ exempt_recur => 'Y'
+ });
+ $taxable_charged -= $cust_bill_pkg->recur;
+
+ }
- if ( $self->exempt_amount && $self->exempt_amount > 0 ) {
+ if ( $self->exempt_amount && $self->exempt_amount > 0
+ and $taxable_charged > 0 ) {
#my ($mon,$year) = (localtime($cust_bill_pkg->sdate) )[4,5];
my ($mon,$year) =
(localtime( $cust_bill_pkg->sdate || $invoice_date ) )[4,5];
$mon++;
+ $year += 1900;
my $freq = $cust_bill_pkg->freq;
unless ($freq) {
$freq = $part_pkg->freq || 1; # less trustworthy fallback
@@ -294,6 +417,7 @@ sub taxline {
AND taxnum = ?
AND year = ?
AND month = ?
+ AND exempt_monthly = 'Y'
";
my $sth = dbh->prepare($sql) or do {
$dbh->rollback if $oldAutoCommit;
@@ -302,7 +426,7 @@ sub taxline {
$sth->execute(
$custnum,
$self->taxnum,
- 1900+$year,
+ $year,
$mon,
) or do {
$dbh->rollback if $oldAutoCommit;
@@ -311,9 +435,10 @@ sub taxline {
my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0;
foreach ( grep { $_->taxnum == $self->taxnum &&
+ $_->exempt_monthly eq 'Y' &&
$_->month == $mon &&
- $_->year == 1900+$year
- } @exemptions
+ $_->year == $year
+ } @existing_exemptions
)
{
$existing_exemption += $_->amount;
@@ -325,42 +450,50 @@ sub taxline {
my $addl = $remaining_exemption > $taxable_per_month
? $taxable_per_month
: $remaining_exemption;
+ push @new_exemptions, FS::cust_tax_exempt_pkg->new({
+ amount => sprintf('%.2f', $addl),
+ exempt_monthly => 'Y',
+ year => $year,
+ month => $mon,
+ });
$taxable_charged -= $addl;
-
- my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg ( {
- 'taxnum' => $self->taxnum,
- 'year' => 1900+$year,
- 'month' => $mon,
- 'amount' => sprintf('%.2f', $addl ),
- } );
- if ($cust_bill_pkg->billpkgnum) {
- $cust_tax_exempt_pkg->billpkgnum($cust_bill_pkg->billpkgnum);
- my $error = $cust_tax_exempt_pkg->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "fatal: can't insert cust_tax_exempt_pkg: $error";
- }
- }else{
- push @exemptions, $cust_tax_exempt_pkg;
- push @{ $cust_bill_pkg->_cust_tax_exempt_pkg }, $cust_tax_exempt_pkg;
- } # if $cust_bill_pkg->billpkgnum
- } # if $remaining_exemption > 0
-
- #++
+ }
+ last if $taxable_charged < 0.005;
+ # if they're using multiple months of exemption for a multi-month
+ # package, then record the exemptions in separate months
$mon++;
- #until ( $mon < 12 ) { $mon -= 12; $year++; }
- until ( $mon < 13 ) { $mon -= 12; $year++; }
+ if ( $mon > 12 ) {
+ $mon -= 12;
+ $year++;
+ }
} #foreach $which_month
+ } # if exempt_amount
+
+ $_->taxnum($self->taxnum) foreach @new_exemptions;
+
+ if ( $cust_bill_pkg->billpkgnum ) {
+ die "tried to calculate tax exemptions on a previously billed line item\n";
+ # this is unnecessary
+# foreach my $cust_tax_exempt_pkg (@new_exemptions) {
+# my $error = $cust_tax_exempt_pkg->insert;
+# if ( $error ) {
+# $dbh->rollback if $oldAutoCommit;
+# return "can't insert cust_tax_exempt_pkg: $error";
+# }
+# }
+ }
- } #if $tax->exempt_amount
+ # attach them to the line item
+ push @{ $cust_bill_pkg->cust_tax_exempt_pkg }, @new_exemptions;
+ push @existing_exemptions, @new_exemptions;
+ # If we were smart, we'd also generate a cust_bill_pkg_tax_location
+ # record at this point, but that would require redesigning more stuff.
$taxable_charged = sprintf( "%.2f", $taxable_charged);
- $amount += $taxable_charged * $self->tax / 100
- }
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ $amount += $taxable_charged * $self->tax / 100;
+ } #foreach $cust_bill_pkg
return {
'name' => $name,
diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm
index c11738665..d28997ccd 100644
--- a/FS/FS/cust_pay.pm
+++ b/FS/FS/cust_pay.pm
@@ -662,7 +662,7 @@ sub send_receipt {
}
- } else { #not manual
+ } elsif ( ! $cust_main->invoice_noemail ) { #not manual
my $queue = new FS::queue {
'paynum' => $self->paynum,
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index aed99e51d..16adea3d7 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -338,6 +338,9 @@ sub insert {
if ( $conf->config('ticket_system') && $options{ticket_subject} ) {
+ #this init stuff is still inefficient, but at least its limited to
+ # the small number (any?) folks using ticket emailing on pkg order
+
#eval '
# use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" );
# use RT;
@@ -1319,7 +1322,8 @@ sub credit_remaining {
Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
package, then unsuspends the package itself (clears the susp field and the
-adjourn field if it is in the past).
+adjourn field if it is in the past). If the suspend reason includes an
+unsuspension package, that package will be ordered.
Available options are:
@@ -1423,6 +1427,9 @@ sub unsuspend {
}
+ my $cust_pkg_reason = $self->last_cust_pkg_reason('susp');
+ my $reason = $cust_pkg_reason ? $cust_pkg_reason->reason : '';
+
my %hash = $self->hash;
my $inactive = time - $hash{'susp'};
@@ -1449,6 +1456,33 @@ sub unsuspend {
return $error;
}
+ my $unsusp_pkg;
+
+ if ( $reason && $reason->unsuspend_pkgpart ) {
+ my $part_pkg = FS::part_pkg->by_key($reason->unsuspend_pkgpart)
+ or $error = "Unsuspend package definition ".$reason->unsuspend_pkgpart.
+ " not found.";
+ my $start_date = $self->cust_main->next_bill_date
+ if $reason->unsuspend_hold;
+
+ if ( $part_pkg ) {
+ $unsusp_pkg = FS::cust_pkg->new({
+ 'custnum' => $self->custnum,
+ 'pkgpart' => $reason->unsuspend_pkgpart,
+ 'start_date' => $start_date,
+ 'locationnum' => $self->locationnum,
+ # discount? probably not...
+ });
+
+ $error ||= $self->cust_main->order_pkg( 'cust_pkg' => $unsusp_pkg );
+ }
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
if ( $conf->config('unsuspend_email_admin') ) {
my $error = send_email(
@@ -1462,6 +1496,11 @@ sub unsuspend {
'Customer: #'. $self->custnum. ' '. $self->cust_main->name. "\n",
'Package : #'. $self->pkgnum. " (". $self->part_pkg->pkg_comment. ")\n",
( map { "Service : $_\n" } @labels ),
+ ($unsusp_pkg ?
+ "An unsuspension fee was charged: ".
+ $unsusp_pkg->part_pkg->pkg_comment."\n"
+ : ''
+ ),
],
);
@@ -3279,7 +3318,12 @@ specifies the user for agent virtualization
=item fcc_line
- boolean selects packages containing fcc form 477 telco lines
+boolean; if true, returns only packages with more than 0 FCC phone lines.
+
+=item state, country
+
+Limit to packages with a service location in the specified state and country.
+For FCC 477 reporting, mostly.
=back
@@ -3453,8 +3497,8 @@ sub search {
if ( exists($params->{'censustract'}) ) {
$params->{'censustract'} =~ /^([.\d]*)$/;
- my $censustract = "cust_main.censustract = '$1'";
- $censustract .= ' OR cust_main.censustract is NULL' unless $1;
+ my $censustract = "cust_location.censustract = '$1'";
+ $censustract .= ' OR cust_location.censustract is NULL' unless $1;
push @where, "( $censustract )";
}
@@ -3466,10 +3510,22 @@ sub search {
)
{
if ($1) {
- push @where, "cust_main.censustract LIKE '$1%'";
+ push @where, "cust_location.censustract LIKE '$1%'";
} else {
push @where,
- "( cust_main.censustract = '' OR cust_main.censustract IS NULL )";
+ "( cust_location.censustract = '' OR cust_location.censustract IS NULL )";
+ }
+ }
+
+ ###
+ # parse country/state
+ ###
+ for (qw(state country)) { # parsing rules are the same for these
+ if ( exists($params->{$_})
+ && uc($params->{$_}) =~ /^([A-Z]{2})$/ )
+ {
+ # XXX post-2.3 only--before that, state/country may be in cust_main
+ push @where, "cust_location.$_ = '$1'";
}
}
@@ -3597,7 +3653,8 @@ sub search {
my $addl_from = 'LEFT JOIN cust_main USING ( custnum ) '.
'LEFT JOIN part_pkg USING ( pkgpart ) '.
- 'LEFT JOIN pkg_class ON ( part_pkg.classnum = pkg_class.classnum ) ';
+ 'LEFT JOIN pkg_class ON ( part_pkg.classnum = pkg_class.classnum ) '.
+ 'LEFT JOIN cust_location USING ( locationnum ) ';
my $select;
my $count_query;
@@ -3606,13 +3663,6 @@ sub search {
$select = "DISTINCT substr($zip,1,5) as zip";
$orderby = "ORDER BY substr($zip,1,5)";
- $addl_from .= 'LEFT JOIN cust_location ON (
- cust_location.locationnum = COALESCE(
- cust_pkg.locationnum,
- cust_main.ship_locationnum,
- cust_main.bill_locationnum
- )
- )';
$count_query = "SELECT COUNT( DISTINCT substr($zip,1,5) )";
} else {
$select = join(', ',
diff --git a/FS/FS/cust_pkg_discount.pm b/FS/FS/cust_pkg_discount.pm
index a20794027..5f4d0dccf 100644
--- a/FS/FS/cust_pkg_discount.pm
+++ b/FS/FS/cust_pkg_discount.pm
@@ -106,7 +106,8 @@ sub insert {
'amount' => $self->amount,
'percent' => $self->percent,
'months' => $self->months,
- 'setup' => $self->setup,
+ 'setup' => $self->setup,
+ #'linked' => $self->linked,
'disabled' => 'Y',
};
my $error = $discount->insert;
diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index 2ec8f12c2..52069316d 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -335,10 +335,10 @@ sub check {
($part_svc) = grep { $_->svcpart == $self->svcpart } $cust_pkg->part_svc;
return "No svcpart ". $self->svcpart.
" services in pkgpart ". $cust_pkg->pkgpart
- unless $part_svc;
+ unless $part_svc || $ignore_quantity;
return "Already ". $part_svc->get('num_cust_svc'). " ". $part_svc->svc.
" services for pkgnum ". $self->pkgnum
- if $part_svc->get('num_avail') == 0 and !$ignore_quantity;
+ if !$ignore_quantity && $part_svc->get('num_avail') <= 0 ;
}
$self->SUPER::check;
diff --git a/FS/FS/cust_tax_exempt_pkg.pm b/FS/FS/cust_tax_exempt_pkg.pm
index e63b84b30..bbabb5b0a 100644
--- a/FS/FS/cust_tax_exempt_pkg.pm
+++ b/FS/FS/cust_tax_exempt_pkg.pm
@@ -7,6 +7,10 @@ use FS::cust_main_Mixin;
use FS::cust_bill_pkg;
use FS::cust_main_county;
use FS::cust_credit_bill_pkg;
+use FS::UID qw(dbh);
+use FS::upgrade_journal;
+
+# some kind of common ancestor with cust_bill_pkg_tax_location would make sense
@ISA = qw( FS::cust_main_Mixin FS::Record );
@@ -32,22 +36,45 @@ FS::cust_tax_exempt_pkg - Object methods for cust_tax_exempt_pkg records
=head1 DESCRIPTION
An FS::cust_tax_exempt_pkg object represents a record of a customer tax
-exemption. Currently this is only used for "texas tax". FS::cust_tax_exempt
-inherits from FS::Record. The following fields are currently supported:
+exemption. Whenever a package would be taxed (based on its location and
+taxclass), but some or all of it is exempt from taxation, an
+FS::cust_tax_exempt_pkg record is created.
+
+FS::cust_tax_exempt inherits from FS::Record. The following fields are
+currently supported:
=over 4
=item exemptpkgnum - primary key
-=item billpkgnum - invoice line item (see L<FS::cust_bill_pkg>)
+=item billpkgnum - invoice line item (see L<FS::cust_bill_pkg>) that
+was exempted from tax.
=item taxnum - tax rate (see L<FS::cust_main_county>)
-=item year
+=item year - the year in which the exemption occurred. NULL if this
+is a customer or package exemption rather than a monthly exemption.
+
+=item month - the month in which the exemption occurred. NULL if this
+is a customer or package exemption.
+
+=item amount - the amount of revenue exempted. For monthly exemptions
+this may be anything up to the monthly exemption limit defined in
+L<FS::cust_main_county> for this tax. For customer exemptions it is
+always the full price of the line item. For package exemptions it
+may be the setup fee, the recurring fee, or the sum of those.
+
+=item exempt_cust - flag indicating that the customer is tax-exempt
+(cust_main.tax = 'Y').
-=item month
+=item exempt_cust_taxname - flag indicating that the customer is exempt
+from the tax with this name (see L<FS::cust_main_exemption).
-=item amount
+=item exempt_setup, exempt_recur: flag indicating that the package's setup
+or recurring fee is not taxable (part_pkg.setuptax and part_pkg.recurtax).
+
+=item exempt_monthly: flag indicating that this is a monthly per-customer
+exemption (Texas tax).
=back
@@ -109,18 +136,44 @@ and replace methods.
sub check {
my $self = shift;
- $self->ut_numbern('exemptnum')
-# || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
+ my $error = $self->ut_numbern('exemptnum')
|| $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg', 'billpkgnum')
|| $self->ut_foreign_key('taxnum', 'cust_main_county', 'taxnum')
|| $self->ut_foreign_keyn('creditbillpkgnum',
'cust_credit_bill_pkg',
'creditbillpkgnum')
- || $self->ut_number('year') #check better
- || $self->ut_number('month') #check better
+ || $self->ut_numbern('year') #check better
+ || $self->ut_numbern('month') #check better
|| $self->ut_money('amount')
+ || $self->ut_flag('exempt_cust')
+ || $self->ut_flag('exempt_setup')
+ || $self->ut_flag('exempt_recur')
+ || $self->ut_flag('exempt_cust_taxname')
|| $self->SUPER::check
;
+
+ return $error if $error;
+
+ if ( $self->get('exempt_cust') ) {
+ $self->set($_ => '') for qw(
+ exempt_cust_taxname exempt_setup exempt_recur exempt_monthly month year
+ );
+ } elsif ( $self->get('exempt_cust_taxname') ) {
+ $self->set($_ => '') for qw(
+ exempt_setup exempt_recur exempt_monthly month year
+ );
+ } elsif ( $self->get('exempt_setup') || $self->get('exempt_recur') ) {
+ $self->set($_ => '') for qw(exempt_monthly month year);
+ } elsif ( $self->get('exempt_monthly') ) {
+ $self->year =~ /^\d{4}$/
+ or return "illegal exemption year: '".$self->year."'";
+ $self->month >= 1 && $self->month <= 12
+ or return "illegal exemption month: '".$self->month."'";
+ } else {
+ return "no exemption type selected";
+ }
+
+ '';
}
=item cust_main_county
@@ -135,6 +188,18 @@ sub cust_main_county {
qsearchs( 'cust_main_county', { 'taxnum', $self->taxnum } );
}
+sub _upgrade_data {
+ my $class = shift;
+
+ my $journal = 'cust_tax_exempt_pkg_flags';
+ if ( !FS::upgrade_journal->is_done($journal) ) {
+ my $sql = "UPDATE cust_tax_exempt_pkg SET exempt_monthly = 'Y' ".
+ "WHERE month IS NOT NULL";
+ dbh->do($sql) or die dbh->errstr;
+ FS::upgrade_journal->set_done($journal);
+ }
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/cust_tax_exempt_pkg_void.pm b/FS/FS/cust_tax_exempt_pkg_void.pm
new file mode 100644
index 000000000..bfbc8c739
--- /dev/null
+++ b/FS/FS/cust_tax_exempt_pkg_void.pm
@@ -0,0 +1,143 @@
+package FS::cust_tax_exempt_pkg_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+use FS::cust_bill_pkg_void;
+use FS::cust_main_county;
+
+=head1 NAME
+
+FS::cust_tax_exempt_pkg_void - Object methods for cust_tax_exempt_pkg_void records
+
+=head1 SYNOPSIS
+
+ use FS::cust_tax_exempt_pkg_void;
+
+ $record = new FS::cust_tax_exempt_pkg_void \%hash;
+ $record = new FS::cust_tax_exempt_pkg_void { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_tax_exempt_pkg_void object represents a voided record of a customer
+tax exemption. FS::cust_tax_exempt_pkg_void inherits from FS::Record. The
+following fields are currently supported:
+
+=over 4
+
+=item exemptpkgnum
+
+primary key
+
+=item billpkgnum
+
+billpkgnum
+
+=item taxnum
+
+taxnum
+
+=item year
+
+year
+
+=item month
+
+month
+
+=item creditbillpkgnum
+
+creditbillpkgnum
+
+=item amount
+
+amount
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_tax_exempt_pkg_void'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_number('exemptpkgnum')
+ || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum' )
+ || $self->ut_foreign_key('taxnum', 'cust_main_county', 'taxnum')
+ || $self->ut_numbern('year')
+ || $self->ut_numbern('month')
+ || $self->ut_numbern('creditbillpkgnum') #no FK check, will have been del'ed
+ || $self->ut_money('amount')
+ || $self->ut_flag('exempt_cust')
+ || $self->ut_flag('exempt_setup')
+ || $self->ut_flag('exempt_recur')
+ || $self->ut_flag('exempt_cust_taxname')
+ || $self->ut_flag('exempt_monthly')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_tax_location.pm b/FS/FS/cust_tax_location.pm
index 161a6547b..1a9bf5a41 100644
--- a/FS/FS/cust_tax_location.pm
+++ b/FS/FS/cust_tax_location.pm
@@ -298,7 +298,7 @@ sub batch_import {
}
if ( scalar( @columns ) ) {
$dbh->rollback if $oldAutoCommit;
- return "Unexpected trailing columns in line (wrong format?): $line";
+ return "Unexpected trailing columns in line (wrong format?) importing cust_tax_location: $line";
}
my $error = &{$hook}(\%cust_tax_location);
diff --git a/FS/FS/detail_format/sum_duration_prefix.pm b/FS/FS/detail_format/sum_duration_prefix.pm
index 04590415c..cd7bbe3cc 100644
--- a/FS/FS/detail_format/sum_duration_prefix.pm
+++ b/FS/FS/detail_format/sum_duration_prefix.pm
@@ -25,7 +25,10 @@ sub append {
my $self = shift;
my $prefixes = ($self->{prefixes} ||= {});
foreach my $cdr (@_) {
- my $phonenum = $self->{inbound} ? $cdr->src : $cdr->dst;
+ my (undef, $phonenum) = $cdr->parse_number(
+ column => ( $self->{inbound} ? 'src' : 'dst' ),
+ );
+
$phonenum =~ /^(\d{$prefix_length})/;
my $prefix = $1 || 'other';
warn "$me appending ".$cdr->dst." to $prefix\n" if $DEBUG;
diff --git a/FS/FS/discount.pm b/FS/FS/discount.pm
index 88cbdd41c..f6f994599 100644
--- a/FS/FS/discount.pm
+++ b/FS/FS/discount.pm
@@ -136,6 +136,7 @@ sub check {
|| $self->ut_floatn('months') #actually decimal, but this will do
|| $self->ut_enum('disabled', [ '', 'Y' ])
|| $self->ut_enum('setup', [ '', 'Y' ])
+ #|| $self->ut_enum('linked', [ '', 'Y' ])
;
return $error if $error;
diff --git a/FS/FS/h_cust_main_exemption.pm b/FS/FS/h_cust_main_exemption.pm
new file mode 100644
index 000000000..072c4123e
--- /dev/null
+++ b/FS/FS/h_cust_main_exemption.pm
@@ -0,0 +1,19 @@
+package FS::h_cust_main_exemption;
+
+use strict;
+use base qw( FS::h_Common FS::cust_main_exemption );
+
+sub table { 'h_cust_main_exemption' };
+
+=head1 NAME
+
+FS::h_cust_main_exemption - Historical customer tax exemption records.
+
+=head1 SEE ALSO
+
+L<FS::cust_main_exemption>, L<FS::h_Common>, L<FS::Record>.
+
+=cut
+
+1;
+
diff --git a/FS/FS/h_part_pkg.pm b/FS/FS/h_part_pkg.pm
new file mode 100644
index 000000000..2c0e65f22
--- /dev/null
+++ b/FS/FS/h_part_pkg.pm
@@ -0,0 +1,37 @@
+package FS::h_part_pkg;
+
+use strict;
+use vars qw( @ISA );
+use base qw(FS::h_Common FS::part_pkg);
+
+sub table { 'h_part_pkg' };
+
+sub _rebless {}; # don't try to rebless these
+
+=head1 NAME
+
+FS::h_part_pkg - Historical record of package definition.
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+An FS::h_part_pkg object represents historical changes to package
+definitions.
+
+=head1 BUGS
+
+Many important properties of a part_pkg are in other tables, especially
+plan options, service allotments, and link/bundle relationships. The
+methods to access those from the part_pkg will work, but they're
+really accessing current, not historical, data. Be careful.
+
+=head1 SEE ALSO
+
+L<FS::part_pkg>, L<FS::h_Common>, L<FS::Record>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_event.pm b/FS/FS/part_event.pm
index 62f16fa1c..b7371c9ab 100644
--- a/FS/FS/part_event.pm
+++ b/FS/FS/part_event.pm
@@ -306,8 +306,8 @@ sub targets {
});
my @tested_objects;
foreach my $object ( @objects ) {
- my $cust_event = $self->new_cust_event($object);
- next unless $cust_event->test_conditions('time' => $time);
+ my $cust_event = $self->new_cust_event($object, 'time' => $time);
+ next unless $cust_event->test_conditions;
$object->set('cust_event', $cust_event);
push @tested_objects, $object;
diff --git a/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm b/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm
new file mode 100644
index 000000000..73d32e0a7
--- /dev/null
+++ b/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm
@@ -0,0 +1,25 @@
+package FS::part_event::Action::Mixin::credit_agent_pkg_class;
+use base qw( FS::part_event::Action::Mixin::credit_pkg );
+
+use strict;
+
+sub option_fields {
+ my $class = shift;
+ my %option_fields = $class->SUPER::option_fields;
+ delete $option_fields{'percent'};
+ %option_fields;
+}
+
+sub _calc_credit_percent {
+ my( $self, $cust_pkg ) = @_;
+
+ my $agent_pkg_class = qsearchs( 'agent_pkg_class', {
+ 'agentnum' => $self->cust_main($cust_pkg)->agentnum,
+ 'classnum' => $cust_pkg->classnum,
+ });
+
+ $agent_pkg_class ? $agent_pkg_class->commission_percent : 0;
+
+}
+
+1;
diff --git a/FS/FS/part_event/Action/Mixin/credit_pkg.pm b/FS/FS/part_event/Action/Mixin/credit_pkg.pm
index aeda92f91..9dcd701a9 100644
--- a/FS/FS/part_event/Action/Mixin/credit_pkg.pm
+++ b/FS/FS/part_event/Action/Mixin/credit_pkg.pm
@@ -51,7 +51,7 @@ sub _calc_credit {
}
}
- my $percent = $self->option('percent');
+ my $percent = $self->_calc_credit_percent($cust_pkg);
#my @arg = $no_cust_pkg{$what} ? () : ($cust_pkg);
my @arg = ($what eq 'setup_cost') ? () : ($cust_pkg);
@@ -60,4 +60,9 @@ sub _calc_credit {
}
+sub _calc_credit_percent {
+ my( $self, $cust_pkg ) = @_;
+ $self->option('percent');
+}
+
1;
diff --git a/FS/FS/part_event/Action/pkg_agent_credit.pm b/FS/FS/part_event/Action/pkg_agent_credit.pm
index 4bcee983b..e1c77be07 100644
--- a/FS/FS/part_event/Action/pkg_agent_credit.pm
+++ b/FS/FS/part_event/Action/pkg_agent_credit.pm
@@ -18,7 +18,7 @@ sub do_action {
my $agent_cust_main = $agent->agent_cust_main;
#? or return "No customer record for agent ". $agent->agent;
- my $amount = $self->_calc_credit($cust_pkg);
+ my $amount = $self->_calc_credit($cust_pkg);
return '' unless $amount > 0;
my $reasonnum = $self->option('reasonnum');
@@ -29,6 +29,7 @@ sub do_action {
'eventnum' => $cust_event->eventnum,
'addlinfo' => 'for customer #'. $cust_main->display_custnum.
': '.$cust_main->name,
+ #'commission_agentnum' => $agent->agentnum,
);
die "Error crediting customer ". $agent_cust_main->custnum.
" for agent commission: $error"
diff --git a/FS/FS/part_event/Action/pkg_agent_credit_pkg_class.pm b/FS/FS/part_event/Action/pkg_agent_credit_pkg_class.pm
new file mode 100644
index 000000000..3dcf668f9
--- /dev/null
+++ b/FS/FS/part_event/Action/pkg_agent_credit_pkg_class.pm
@@ -0,0 +1,9 @@
+package FS::part_event::Action::pkg_agent_credit_pkg_class;
+
+use strict;
+use base qw( FS::part_event::Action::Mixin::credit_agent_pkg_class
+ FS::part_event::Action::pkg_agent_credit );
+
+sub description { 'Credit the agent an amount based on their commission percentage for the referred package class'; }
+
+1;
diff --git a/FS/FS/part_event/Condition/after_event.pm b/FS/FS/part_event/Condition/after_event.pm
new file mode 100644
index 000000000..1d8d2124e
--- /dev/null
+++ b/FS/FS/part_event/Condition/after_event.pm
@@ -0,0 +1,81 @@
+package FS::part_event::Condition::after_event;
+
+use strict;
+use FS::Record qw( qsearchs );
+use FS::part_event;
+use FS::cust_event;
+
+use base qw( FS::part_event::Condition );
+
+sub description { "After running another event" }
+
+# Runs the event at least X days after the most recent time another event
+# ran on the same object.
+
+sub option_fields {
+ (
+ 'eventpart' => { label=>'Event', type=>'select-part_event',
+ disable_empty => 1,
+ hashref => { disabled => '' },
+ },
+ 'run_delay' => { label=>'Delay', type=>'freq', value=>'1', },
+ );
+}
+
+# Specification:
+# Given an event B that has this condition, where the "eventpart"
+# option is set to event A, and the "run_delay" option is set to
+# X days.
+# This condition is TRUE if:
+# - Event A last ran X or more days in the past,
+# AND
+# - Event B has not run since the most recent occurrence of event A.
+
+sub condition {
+ # similar to "once_every", but with a different eventpart
+ my($self, $object, %opt) = @_;
+
+ my $obj_pkey = $object->primary_key;
+ my $tablenum = $object->$obj_pkey();
+
+ my $before = $self->option_age_from('run_delay',$opt{'time'});
+ my $eventpart = $self->option('eventpart');
+
+ my %hash = (
+ 'eventpart' => $eventpart,
+ 'tablenum' => $tablenum,
+ 'status' => { op => '!=', value => 'failed' },
+ );
+
+ my $most_recent_other = qsearchs( {
+ 'table' => 'cust_event',
+ 'hashref' => \%hash,
+ 'order_by' => " ORDER BY _date DESC LIMIT 1",
+ } )
+ or return 0; # if it hasn't run at all, return false
+
+ return 0 if $most_recent_other->_date > $before; # we're still in the delay
+
+ # now see if there's been an instance of this event since the one we're
+ # following...
+ $hash{'eventpart'} = $self->eventpart;
+ if ( $opt{'cust_event'} and $opt{'cust_event'}->eventnum =~ /^(\d+)$/ ) {
+ $hash{'eventnum'} = { op => '!=', value => $1 };
+ }
+
+ my $most_recent_self = qsearchs( {
+ 'table' => 'cust_event',
+ 'hashref' => \%hash,
+ 'order_by' => " ORDER BY _date DESC LIMIT 1",
+ } );
+
+ return 0 if defined($most_recent_self)
+ and $most_recent_self->_date >= $most_recent_other->_date;
+ # the follower has already run
+
+ 1;
+}
+
+# condition_sql, maybe someday
+
+1;
diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm
index 45773e097..b0f708a66 100644
--- a/FS/FS/part_export.pm
+++ b/FS/FS/part_export.pm
@@ -4,10 +4,12 @@ use strict;
use vars qw( @ISA @EXPORT_OK $DEBUG %exports );
use Exporter;
use Tie::IxHash;
-use base qw( FS::option_Common FS::m2m_Common ); # m2m for 'export_nas'
+use base qw( FS::option_Common FS::m2m_Common );
use FS::Record qw( qsearch qsearchs dbh );
use FS::part_svc;
use FS::part_export_option;
+use FS::part_export_machine;
+use FS::svc_export_machine;
use FS::export_svc;
#for export modules, though they should probably just use it themselves
@@ -108,6 +110,50 @@ otherwise returns false.
If a hash reference of options is supplied, part_export_option records are
created (see L<FS::part_export_option>).
+=cut
+
+sub insert {
+ my $self = shift;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $error = $self->SUPER::insert(@_);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ #kinda false laziness with process_m2name
+ my @machines = map { $_ =~ s/^\s+//; $_ =~ s/\s+$//; $_ }
+ grep /\S/,
+ split /[\n\r]{1,2}/,
+ $self->part_export_machine_textarea;
+
+ foreach my $machine ( @machines ) {
+
+ my $part_export_machine = new FS::part_export_machine {
+ 'exportnum' => $self->exportnum,
+ 'machine' => $machine,
+ };
+ $error = $part_export_machine->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+}
+
=item delete
Delete this record from the database.
@@ -117,13 +163,13 @@ Delete this record from the database.
#foreign keys would make this much less tedious... grr dumb mysql
sub delete {
my $self = shift;
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
local $SIG{TERM} = 'IGNORE';
local $SIG{TSTP} = 'IGNORE';
local $SIG{PIPE} = 'IGNORE';
-
my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
@@ -147,10 +193,103 @@ sub delete {
}
}
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ foreach my $part_export_machine ( $self->part_export_machine ) {
+ my $error = $part_export_machine->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
+}
+
+=item replace [ OLD_RECORD ] [ HASHREF | OPTION => VALUE ... ]
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+If a list or hash reference of options is supplied, option records are created
+or modified.
+
+=cut
+
+sub replace {
+ my $self = shift;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $error = $self->SUPER::replace(@_);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ if ( $self->part_export_machine_textarea ) {
+
+ my %part_export_machine = map { $_->machine => $_ }
+ $self->part_export_machine;
+
+ my @machines = map { $_ =~ s/^\s+//; $_ =~ s/\s+$//; $_ }
+ grep /\S/,
+ split /[\n\r]{1,2}/,
+ $self->part_export_machine_textarea;
+
+ foreach my $machine ( @machines ) {
+
+ if ( $part_export_machine{$machine} ) {
+
+ if ( $part_export_machine{$machine}->disabled eq 'Y' ) {
+ $part_export_machine{$machine}->disabled('');
+ $error = $part_export_machine{$machine}->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ delete $part_export_machine{$machine}; #so we don't disable it below
+
+ } else {
+
+ my $part_export_machine = new FS::part_export_machine {
+ 'exportnum' => $self->exportnum,
+ 'machine' => $machine
+ };
+ $error = $part_export_machine->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
+ }
+
+
+ foreach my $part_export_machine ( values %part_export_machine ) {
+ $part_export_machine->disabled('Y');
+ $error = $part_export_machine->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
}
=item check
@@ -166,7 +305,7 @@ sub check {
my $error =
$self->ut_numbern('exportnum')
|| $self->ut_textn('exportname')
- || $self->ut_domain('machine')
+ || $self->ut_domainn('machine')
|| $self->ut_alpha('exporttype')
;
return $error if $error;
@@ -192,6 +331,31 @@ sub label {
($self->exportname || $self->exporttype ). ' ('. $self->machine. ')';
}
+=item label_html
+
+Returns a label for this export, "exportname: exporttype to machine".
+
+=cut
+
+sub label_html {
+ my $self = shift;
+
+ my $label = $self->exportname
+ ? '<B>'. $self->exportname. '</B>: ' #<BR>'.
+ : '';
+
+ $label .= $self->exporttype;
+
+ $label .= ' to '. ( $self->machine eq '_SVC_MACHINE'
+ ? 'per-service hostname'
+ : $self->machine
+ )
+ if $self->machine;
+
+ $label;
+
+}
+
#=item part_svc
#
#Returns the service definition (see L<FS::part_svc>) for this export.
@@ -233,6 +397,20 @@ sub cust_svc {
$self->export_svc;
}
+=item part_export_machine
+
+Returns all machines as FS::part_export_machine objects (see
+L<FS::part_export_machine>).
+
+=cut
+
+sub part_export_machine {
+ my $self = shift;
+ map { $_ } #behavior of sort undefined in scalar context
+ sort { $a->machine cmp $b->machine }
+ qsearch('part_export_machine', { 'exportnum' => $self->exportnum } );
+}
+
=item export_svc
Returns a list of associated FS::export_svc records.
@@ -293,6 +471,26 @@ sub _rebless {
$self;
}
+=item svc_machine
+
+=cut
+
+sub svc_machine {
+ my( $self, $svc_x ) = @_;
+
+ return $self->machine unless $self->machine eq '_SVC_MACHINE';
+
+ my $svc_export_machine = qsearchs('svc_export_machine', {
+ 'svcnum' => $svc_x->svcnum,
+ 'exportnum' => $self->exportnum,
+ })
+ #would only happen if you add this export to existing services without a
+ #machine set then try to run exports without setting it... right?
+ or die "No hostname selected for ".($self->exportname || $self->exporttype);
+
+ return $svc_export_machine->part_export_machine->machine;
+}
+
#these should probably all go away, just let the subclasses define em
=item export_insert SVC_OBJECT
diff --git a/FS/FS/part_export/acct_google.pm b/FS/FS/part_export/acct_google.pm
index afc45db81..d153728e9 100644
--- a/FS/FS/part_export/acct_google.pm
+++ b/FS/FS/part_export/acct_google.pm
@@ -16,10 +16,12 @@ tie my %options, 'Tie::IxHash',
# admin logins.
%info = (
- 'svc' => 'svc_acct',
- 'desc' => 'Google hosted mail',
- 'options' => \%options,
- 'nodomain' => 'Y',
+ 'svc' => 'svc_acct',
+ 'desc' => 'Google hosted mail',
+ 'options' => \%options,
+ 'nodomain' => 'Y',
+ 'no_machine' => 1,
+ 'default_svc_class' => 'Email',
'notes' => <<'END'
Export accounts to the Google Provisioning API. Requires
REST::Google::Apps::Provisioning from CPAN.
diff --git a/FS/FS/part_export/acct_http.pm b/FS/FS/part_export/acct_http.pm
index b4c64ac62..23df7b37d 100644
--- a/FS/FS/part_export/acct_http.pm
+++ b/FS/FS/part_export/acct_http.pm
@@ -51,6 +51,7 @@ tie %options, 'Tie::IxHash',
'svc' => 'svc_acct',
'desc' => 'Send an HTTP or HTTPS GET or POST request, for accounts.',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => <<'END'
Send an HTTP or HTTPS GET or POST to the specified URL on account addition,
modification and deletion. For HTTPS support,
diff --git a/FS/FS/part_export/acct_plesk.pm b/FS/FS/part_export/acct_plesk.pm
index d8d70a30e..50b6faebf 100644
--- a/FS/FS/part_export/acct_plesk.pm
+++ b/FS/FS/part_export/acct_plesk.pm
@@ -15,9 +15,11 @@ tie my %options, 'Tie::IxHash',
;
%info = (
- 'svc' => 'svc_acct',
- 'desc' => 'Real-time export to Plesk managed mail service',
- 'options'=> \%options,
+ 'svc' => 'svc_acct',
+ 'desc' => 'Real-time export to Plesk managed mail service',
+ 'options' => \%options,
+ 'no_machine' => 1,
+ 'default_svc_class' => 'Email',
'notes' => <<'END'
Real-time export to
<a href="http://www.swsoft.com/">Plesk</a> managed server.
diff --git a/FS/FS/part_export/acct_sql.pm b/FS/FS/part_export/acct_sql.pm
index ffe39caa5..8163f2017 100644
--- a/FS/FS/part_export/acct_sql.pm
+++ b/FS/FS/part_export/acct_sql.pm
@@ -60,11 +60,13 @@ my $postfix_native_mailbox_map =
keys %postfix_native_mailbox_map );
%info = (
- 'svc' => 'svc_acct',
- 'desc' => 'Real-time export of accounts to SQL databases '.
- '(vpopmail, Postfix+Courier IMAP, others?)',
- 'options' => \%options,
- 'nodomain' => '',
+ 'svc' => 'svc_acct',
+ 'desc' => 'Real-time export of accounts to SQL databases '.
+ '(vpopmail, Postfix+Courier IMAP, others?)',
+ 'options' => \%options,
+ 'nodomain' => '',
+ 'no_machine' => 1,
+ 'default_svc_class' => 'Email',
'notes' => <<END
Export accounts (svc_acct records) to SQL databases. Currently has default
configurations for vpopmail and Postfix+Courier IMAP but intended to be
diff --git a/FS/FS/part_export/acct_sql_status.pm b/FS/FS/part_export/acct_sql_status.pm
index e6aeb2071..248105f18 100644
--- a/FS/FS/part_export/acct_sql_status.pm
+++ b/FS/FS/part_export/acct_sql_status.pm
@@ -14,6 +14,7 @@ delete $options{$_} for qw( table schema static primary_key );
'desc' => 'Mailbox status information from SQL',
'options' => \%options,
'nodomain' => '',
+ 'no_machine' => 1,
'notes' => <<END
Read mailbox status information (vacation and spam settings) from an SQL
database, tables "vacation" and "users" respectively.
diff --git a/FS/FS/part_export/acct_xmlrpc.pm b/FS/FS/part_export/acct_xmlrpc.pm
index d746f29bc..3070f281a 100644
--- a/FS/FS/part_export/acct_xmlrpc.pm
+++ b/FS/FS/part_export/acct_xmlrpc.pm
@@ -34,10 +34,11 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_acct',
'desc' => 'Configurable provisioning of accounts via the XML-RPC protocol',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => <<'END',
Configurable, real-time export of accounts via the XML-RPC protocol.<BR>
<BR>
-If using "Individual values" parameter style, specfify one parameter per line.<BR>
+If using "Individual values" parameter style, specify one parameter per line.<BR>
<BR>
If using "Struct of name/value pairs" parameter style, specify one name and
value on each line, separated by whitespace.<BR>
diff --git a/FS/FS/part_export/amazon_ec2.pm b/FS/FS/part_export/amazon_ec2.pm
index 0e65ca00c..06e2c238e 100644
--- a/FS/FS/part_export/amazon_ec2.pm
+++ b/FS/FS/part_export/amazon_ec2.pm
@@ -20,6 +20,7 @@ tie my %options, 'Tie::IxHash',
'desc' =>
'Export to Amazon EC2',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => <<'END'
Create instances in the Amazon EC2 (Elastic compute cloud). Install
Net::Amazon::EC2 perl module. Advisable to set svc_external-skip_manual config
diff --git a/FS/FS/part_export/artera_turbo.pm b/FS/FS/part_export/artera_turbo.pm
index c006db9cd..e22bbf2af 100644
--- a/FS/FS/part_export/artera_turbo.pm
+++ b/FS/FS/part_export/artera_turbo.pm
@@ -37,6 +37,7 @@ tie my %options, 'Tie::IxHash',
'Real-time export to Artera Turbo Reseller API',
'options' => \%options,
#'nodomain' => 'Y',
+ 'no_machine' => 1,
'notes' => <<'END'
Real-time export to <a href="http://www.arteraturbo.com/">Artera Turbo</a>
Reseller API. Requires installation of
diff --git a/FS/FS/part_export/broadband_http.pm b/FS/FS/part_export/broadband_http.pm
index 9edfee5d3..c1ed7fca6 100644
--- a/FS/FS/part_export/broadband_http.pm
+++ b/FS/FS/part_export/broadband_http.pm
@@ -45,6 +45,7 @@ tie %options, 'Tie::IxHash',
'svc' => 'svc_broadband',
'desc' => 'Send an HTTP or HTTPS GET or POST request, for accounts.',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => <<'END'
<p>Send an HTTP or HTTPS GET or POST to the specified URL on account addition,
modification and deletion. For HTTPS support,
diff --git a/FS/FS/part_export/broadband_nas.pm b/FS/FS/part_export/broadband_nas.pm
index a160c9944..5a8ffac3b 100644
--- a/FS/FS/part_export/broadband_nas.pm
+++ b/FS/FS/part_export/broadband_nas.pm
@@ -43,6 +43,7 @@ FS::UID->install_callback(
'svc' => 'svc_broadband',
'desc' => 'Create a NAS entry in Freeside',
'options' => \%options,
+ 'no_machine' => 1,
'weight' => 10,
'notes' => <<'END'
<p>Create an entry in the NAS (RADIUS client) table, inheriting the IP
diff --git a/FS/FS/part_export/broadband_shellcommands.pm b/FS/FS/part_export/broadband_shellcommands.pm
index c7f0fbb33..cf9c36c8f 100644
--- a/FS/FS/part_export/broadband_shellcommands.pm
+++ b/FS/FS/part_export/broadband_shellcommands.pm
@@ -107,3 +107,4 @@ sub ssh_cmd { #subroutine, not method
'';
}
+1;
diff --git a/FS/FS/part_export/broadband_snmp.pm b/FS/FS/part_export/broadband_snmp.pm
index cb1740efc..44b4dbabb 100644
--- a/FS/FS/part_export/broadband_snmp.pm
+++ b/FS/FS/part_export/broadband_snmp.pm
@@ -52,6 +52,7 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_broadband',
'desc' => 'Send SNMP requests to the service IP address',
'options' => \%options,
+ 'no_machine' => 1,
'weight' => 10,
'notes' => <<'END'
Send one or more SNMP SET requests to the IP address registered to the service.
diff --git a/FS/FS/part_export/broadband_sql.pm b/FS/FS/part_export/broadband_sql.pm
index 697d3cdac..4f526c805 100644
--- a/FS/FS/part_export/broadband_sql.pm
+++ b/FS/FS/part_export/broadband_sql.pm
@@ -24,6 +24,7 @@ tie my %options, 'Tie::IxHash',
'desc' => 'Real-time export of broadband services to SQL databases ',
'options' => \%options,
'nodomain' => '',
+ 'no_machine' => 1,
'notes' => <<END
END
);
diff --git a/FS/FS/part_export/broadband_sqlradius.pm b/FS/FS/part_export/broadband_sqlradius.pm
index 5806362b5..b5d1a80cb 100644
--- a/FS/FS/part_export/broadband_sqlradius.pm
+++ b/FS/FS/part_export/broadband_sqlradius.pm
@@ -55,6 +55,7 @@ tie %options, 'Tie::IxHash',
'svc' => 'svc_broadband',
'desc' => 'Real-time export to SQL-backed RADIUS (such as FreeRadius) for broadband services',
'options' => \%options,
+ 'no_machine' => 1,
'nas' => 'Y',
'notes' => <<END,
Real-time export of <b>radcheck</b>, <b>radreply</b>, and <b>usergroup</b>
diff --git a/FS/FS/part_export/communigate_pro.pm b/FS/FS/part_export/communigate_pro.pm
index a3ec5e0be..8b66225d2 100644
--- a/FS/FS/part_export/communigate_pro.pm
+++ b/FS/FS/part_export/communigate_pro.pm
@@ -36,6 +36,7 @@ tie %options, 'Tie::IxHash',
'svc' => [qw( svc_acct svc_domain svc_forward svc_mailinglist )],
'desc' => 'Real-time export of accounts, domains, mail forwards and mailing lists to a CommuniGate Pro mail server',
'options' => \%options,
+ 'default_svc_class' => 'Email',
'notes' => <<'END'
Real time export of accounts, domains, mail forwards and mailing lists to a
<a href="http://www.stalker.com/CommuniGatePro/">CommuniGate Pro</a>
diff --git a/FS/FS/part_export/communigate_pro_singledomain.pm b/FS/FS/part_export/communigate_pro_singledomain.pm
index e25043fbb..cecea2826 100644
--- a/FS/FS/part_export/communigate_pro_singledomain.pm
+++ b/FS/FS/part_export/communigate_pro_singledomain.pm
@@ -16,6 +16,7 @@ tie my %options, 'Tie::IxHash', %FS::part_export::communigate_pro::options,
'Real-time export to a CommuniGate Pro mail server, one domain only',
'options' => \%options,
'nodomain' => 'Y',
+ 'default_svc_class' => 'Email',
'notes' => <<'END'
Real time export to a
<a href="http://www.stalker.com/CommuniGatePro/">CommuniGate Pro</a>
diff --git a/FS/FS/part_export/cp.pm b/FS/FS/part_export/cp.pm
index 96fa43710..2ae97e12d 100644
--- a/FS/FS/part_export/cp.pm
+++ b/FS/FS/part_export/cp.pm
@@ -18,6 +18,7 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_acct',
'desc' => 'Real-time export to Critical Path Account Provisioning Protocol',
'options'=> \%options,
+ 'default_svc_class' => 'Email',
'notes' => <<'END'
Real-time export to
<a href="http://www.cp.net/">Critial Path Account Provisioning Protocol</a>.
diff --git a/FS/FS/part_export/cpanel.pm b/FS/FS/part_export/cpanel.pm
index 0ad00df01..6c61e3d2b 100644
--- a/FS/FS/part_export/cpanel.pm
+++ b/FS/FS/part_export/cpanel.pm
@@ -190,3 +190,5 @@ sub cpanel_connect {
$whm;
}
+
+1;
diff --git a/FS/FS/part_export/cust_http.pm b/FS/FS/part_export/cust_http.pm
index e8b677be2..e834f93ea 100644
--- a/FS/FS/part_export/cust_http.pm
+++ b/FS/FS/part_export/cust_http.pm
@@ -55,6 +55,7 @@ tie %options, 'Tie::IxHash',
'svc' => 'cust_main',
'desc' => 'Send an HTTP or HTTPS GET or POST request, for customers.',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => <<'END'
Send an HTTP or HTTPS GET or POST to the specified URL on customer addition,
modification and deletion. For HTTPS support,
diff --git a/FS/FS/part_export/cyrus.pm b/FS/FS/part_export/cyrus.pm
index 84c9e5a30..246d5b3dc 100644
--- a/FS/FS/part_export/cyrus.pm
+++ b/FS/FS/part_export/cyrus.pm
@@ -17,6 +17,8 @@ tie my %options, 'Tie::IxHash',
'desc' => 'Real-time export to Cyrus IMAP server',
'options' => \%options,
'nodomain' => 'Y',
+ 'no_machine' => 1, #de facto... but "server" option should move to it
+ 'default_svc_class' => 'Email',
'notes' => <<'END'
Integration with
<a href="http://asg.web.cmu.edu/cyrus/imapd/">Cyrus IMAP Server</a>.
diff --git a/FS/FS/part_export/dashcs_e911.pm b/FS/FS/part_export/dashcs_e911.pm
index 320d0a67b..2717233cf 100644
--- a/FS/FS/part_export/dashcs_e911.pm
+++ b/FS/FS/part_export/dashcs_e911.pm
@@ -20,6 +20,7 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_phone',
'desc' => 'Provision e911 services via Dash Carrier Services',
'notes' => 'Provision e911 services via Dash Carrier Services',
+ 'no_machine' => 1,
'options' => \%options,
);
diff --git a/FS/FS/part_export/dma_radiusmanager.pm b/FS/FS/part_export/dma_radiusmanager.pm
new file mode 100644
index 000000000..6e56c996b
--- /dev/null
+++ b/FS/FS/part_export/dma_radiusmanager.pm
@@ -0,0 +1,350 @@
+package FS::part_export::dma_radiusmanager;
+
+use strict;
+use vars qw($DEBUG %info %options);
+use base 'FS::part_export';
+use FS::part_svc;
+use FS::svc_acct;
+use FS::radius_group;
+use Tie::IxHash;
+use Digest::MD5 'md5_hex';
+
+use Locale::Country qw(code2country);
+use Locale::SubCountry;
+use Date::Format 'time2str';
+
+tie %options, 'Tie::IxHash',
+ 'dbname' => { label=>'Database name', default=>'radius' },
+ 'username' => { label=>'Database username' },
+ 'password' => { label=>'Database password' },
+ 'manager' => { label=>'Manager name' },
+ 'groupid' => { label=>'Group ID', default=>'1' },
+ 'service_prefix' => { label=>'Service name prefix' },
+ 'nasnames' => { label=>'NAS IDs/addresses' },
+ 'debug' => { label=>'Enable debugging', type=>'checkbox' },
+;
+
+%info = (
+ 'svc' => 'svc_acct',
+ 'desc' => 'Export to DMA Radius Manager',
+ 'options' => \%options,
+ 'nodomain' => 'Y',
+ 'notes' => '', #XXX
+);
+
+$DEBUG = 0;
+
+sub connect {
+ my $self = shift;
+ my $datasrc = 'dbi:mysql:host='.$self->machine.
+ ':database='.$self->option('dbname');
+ DBI->connect(
+ $datasrc,
+ $self->option('username'),
+ $self->option('password'),
+ { AutoCommit => 0 }
+ ) or die $DBI::errstr;
+}
+
+sub export_insert { my $self = shift; $self->dma_rm_queue('insert', @_) }
+sub export_delete { my $self = shift; $self->dma_rm_queue('delete', @_) }
+sub export_replace { my $self = shift; $self->dma_rm_queue('replace', @_) }
+sub export_suspend { my $self = shift; $self->dma_rm_queue('suspend', @_) }
+sub export_unsuspend { my $self = shift; $self->dma_rm_queue('unsuspend', @_) }
+
+sub dma_rm_queue {
+ my ($self, $action, $svc_acct, $old) = @_;
+
+ my $svcnum = $svc_acct->svcnum;
+
+ my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+ my $cust_main = $cust_pkg->cust_main;
+ my $location = $cust_pkg->cust_location;
+
+ my $address = $location->address1;
+ $address .= ' '.$location->address2 if $location->address2;
+ my $country = code2country($location->country);
+ my $lsc = Locale::SubCountry->new($location->country);
+ my $state = $lsc->full_name($location->state) if defined($lsc);
+
+ my %params = (
+ # for the remote side
+ username => $svc_acct->username,
+ password => md5_hex($svc_acct->_password),
+ groupid => $self->option('groupid'),
+ enableuser => 1,
+ firstname => $cust_main->first,
+ lastname => $cust_main->last,
+ company => $cust_main->company,
+ phone => ($cust_main->daytime || $cust_main->night),
+ mobile => $cust_main->mobile,
+ address => $location->address1, # address2?
+ city => $location->city,
+ state => $state, #full name
+ zip => $location->zip,
+ country => $country, #full name
+ gpslat => $location->latitude,
+ gpslong => $location->longitude,
+ comment => 'svcnum'.$svcnum,
+ createdby => $self->option('manager'),
+ owner => $self->option('manager'),
+ email => $cust_main->invoicing_list_emailonly_scalar,
+
+ # used internally by the export
+ exportnum => $self->exportnum,
+ svcnum => $svcnum,
+ action => $action,
+ svcpart => $svc_acct->cust_svc->svcpart,
+ _password => $svc_acct->_password,
+ );
+ if ( $action eq 'replace' ) {
+ $params{'old_username'} = $old->username;
+ $params{'old_password'} = $old->_password;
+ }
+ my $queue = FS::queue->new({
+ 'svcnum' => $svcnum,
+ 'job' => "FS::part_export::dma_radiusmanager::dma_rm_action",
+ });
+ $queue->insert(%params);
+}
+
+sub dma_rm_action {
+ my %params = @_;
+ my $svcnum = delete $params{svcnum};
+ my $action = delete $params{action};
+ my $svcpart = delete $params{svcpart};
+ my $exportnum = delete $params{exportnum};
+
+ my $username = $params{username};
+ my $password = delete $params{_password};
+
+ my $self = FS::part_export->by_key($exportnum);
+ my $dbh = $self->connect;
+ local $DEBUG = 1 if $self->option('debug');
+
+ # export the part_svc if needed, and get its srvid
+ my $part_svc = FS::part_svc->by_key($svcpart);
+ my $srvid = $self->export_part_svc($part_svc, $dbh); # dies on error
+ $params{srvid} = $srvid;
+
+ if ( $action eq 'insert' ) {
+ $params{'createdon'} = time2str('%Y-%m-%d', time);
+ $params{'expiration'} = time2str('%Y-%m-%d', time);
+ warn "rm_users: inserting svcnum$svcnum\n" if $DEBUG;
+ my $sth = $dbh->prepare( 'INSERT INTO rm_users ( '.
+ join(', ', keys(%params)).
+ ') VALUES ('.
+ join(', ', ('?') x keys(%params)).
+ ')'
+ );
+ $sth->execute(values(%params)) or die $dbh->errstr;
+
+ # minor false laziness w/ sqlradius_insert
+ warn "radcheck: inserting $username\n" if $DEBUG;
+ $sth = $dbh->prepare( 'INSERT INTO radcheck (
+ username, attribute, op, value
+ ) VALUES (?, ?, ?, ?)' );
+ $sth->execute(
+ $username,
+ 'Cleartext-Password',
+ ':=', # :=(
+ $password,
+ ) or die $dbh->errstr;
+
+ $sth->execute(
+ $username,
+ 'Simultaneous-Use',
+ ':=',
+ 1, # should this be an option?
+ ) or die $dbh->errstr;
+ # also, we don't support exporting any other radius attrs...
+ # those should go in 'custattr' if we need them
+ } elsif ( $action eq 'replace' ) {
+
+ my $old_username = delete $params{old_username};
+ my $old_password = delete $params{old_password};
+ # svcnum is invariant and on the remote side, so we don't need any
+ # of the old fields to do this
+ warn "rm_users: updating svcnum$svcnum\n" if $DEBUG;
+ my $sth = $dbh->prepare( 'UPDATE rm_users SET '.
+ join(', ', map { "$_ = ?" } keys(%params)).
+ ' WHERE comment = ?'
+ );
+ $sth->execute(values(%params), $params{comment}) or die $dbh->errstr;
+ # except for username/password changes
+ if ( $old_password ne $password ) {
+ warn "radcheck: changing password for $old_username\n" if $DEBUG;
+ $sth = $dbh->prepare( 'UPDATE radcheck SET value = ? '.
+ 'WHERE username = ? and attribute = \'Cleartext-Password\''
+ );
+ $sth->execute($password, $old_username) or die $dbh->errstr;
+ }
+ if ( $old_username ne $username ) {
+ warn "radcheck: changing username $old_username to $username\n"
+ if $DEBUG;
+ $sth = $dbh->prepare( 'UPDATE radcheck SET username = ? '.
+ 'WHERE username = ?'
+ );
+ $sth->execute($username, $old_username) or die $dbh->errstr;
+ }
+
+ } elsif ( $action eq 'suspend' ) {
+
+ # this is sufficient
+ warn "rm_users: disabling svcnum#$svcnum\n" if $DEBUG;
+ my $sth = $dbh->prepare( 'UPDATE rm_users SET enableuser = 0 '.
+ 'WHERE comment = ?'
+ );
+ $sth->execute($params{comment}) or die $dbh->errstr;
+
+ } elsif ( $action eq 'unsuspend' ) {
+
+ warn "rm_users: enabling svcnum#$svcnum\n" if $DEBUG;
+ my $sth = $dbh->prepare( 'UPDATE rm_users SET enableuser = 1 '.
+ 'WHERE comment = ?'
+ );
+ $sth->execute($params{comment}) or die $dbh->errstr;
+
+ } elsif ( $action eq 'delete' ) {
+
+ warn "rm_users: deleting svcnum#$svcnum\n" if $DEBUG;
+ my $sth = $dbh->prepare( 'DELETE FROM rm_users WHERE comment = ?' );
+ $sth->execute($params{comment}) or die $dbh->errstr;
+
+ warn "radcheck: deleting $username\n" if $DEBUG;
+ $sth = $dbh->prepare( 'DELETE FROM radcheck WHERE username = ?' );
+ $sth->execute($username) or die $dbh->errstr;
+
+ # if this were smarter it would also delete the rm_services record
+ # if it was no longer in use, but that's not really necessary
+ }
+
+ $dbh->commit;
+ '';
+}
+
+=item export_part_svc PART_SVC DBH
+
+Query Radius Manager for a service definition matching the name of
+PART_SVC (optionally with a prefix defined in the export options).
+If there is one, update it to match the attributes of PART_SVC; if
+not, create one. Then return its srvid.
+
+=cut
+
+sub export_part_svc {
+ my ($self, $part_svc, $dbh) = @_;
+
+ my $name = $self->option('service_prefix').$part_svc->svc;
+
+ my %params = (
+ 'srvname' => $name,
+ 'enableservice' => 1,
+ 'nextsrvid' => -1,
+ 'dailynextsrvid' => -1,
+ );
+ my @fixed_groups;
+ # use speed settings from fixed usergroups configured on this part_svc
+ if ( my $psc = $part_svc->part_svc_column('usergroup') ) {
+ if ( $psc->columnflag eq 'F' ) {
+ # each part_svc really should only have one fixed group with non-null
+ # speed settings, but go by priority order for consistency
+ @fixed_groups =
+ sort { $a->priority <=> $b->priority }
+ grep { $_ }
+ map { FS::radius_group->by_key($_) }
+ split(/\s*,\s*/, $psc->columnvalue);
+ }
+ } # otherwise there are no fixed groups, so leave speed empty
+
+ foreach (qw(down up)) {
+ my $speed = "speed_$_";
+ foreach my $group (@fixed_groups) {
+ if ( ($group->$speed || 0) > 0 ) {
+ $params{$_.'rate'} = $group->$speed;
+ last;
+ }
+ }
+ }
+ # anything else we need here? poolname, maybe?
+
+ warn "rm_services: looking for '$name'\n" if $DEBUG;
+ my $sth = $dbh->prepare(
+ 'SELECT srvid FROM rm_services WHERE srvname = ? AND enableservice = 1'
+ );
+ $sth->execute($name) or die $dbh->errstr;
+ if ( $sth->rows > 1 ) {
+ die "Multiple services with name '$name' found in Radius Manager.\n";
+ } elsif ( $sth->rows == 1 ) {
+ my $row = $sth->fetchrow_arrayref;
+ my $srvid = $row->[0];
+ warn "rm_services: updating srvid#$srvid\n" if $DEBUG;
+ $sth = $dbh->prepare(
+ 'UPDATE rm_services SET '.join(', ', map {"$_ = ?"} keys %params) .
+ ' WHERE srvid = ?'
+ );
+ $sth->execute(values(%params), $srvid) or die $dbh->errstr;
+ return $srvid;
+ } else { # $sth->rows == 0
+ # create a new one
+ # but first... get the next available srvid
+ $sth = $dbh->prepare('SELECT MAX(srvid) FROM rm_services');
+ $sth->execute or die $dbh->errstr;
+ my $srvid = 1; # just in case you somehow have nothing in your database
+ if ( $sth->rows ) {
+ $srvid = $sth->fetchrow_arrayref->[0] + 1;
+ }
+ $params{'srvid'} = $srvid;
+ # NOW create a new one
+ warn "rm_services: inserting '$name' as srvid#$srvid\n" if $DEBUG;
+ $sth = $dbh->prepare(
+ 'INSERT INTO rm_services ('.join(', ', keys %params).
+ ') VALUES ('.join(', ', map {'?'} keys %params).')'
+ );
+ $sth->execute(values(%params)) or die $dbh->errstr;
+ # also link it to our manager name
+ warn "rm_services: linking to manager\n" if $DEBUG;
+ $sth = $dbh->prepare(
+ 'INSERT INTO rm_allowedmanagers (srvid, managername) VALUES (?, ?)'
+ );
+ $sth->execute($srvid, $self->option('manager')) or die $dbh->errstr;
+ # and allow it on our NAS
+ $sth = $dbh->prepare(
+ 'INSERT INTO rm_allowednases (srvid, nasid) VALUES (?, ?)'
+ );
+ foreach my $nasid ($self->nas_ids($dbh)) {
+ warn "rm_services: linking to nasid#$nasid\n" if $DEBUG;
+ $sth->execute($srvid, $nasid) or die $dbh->errstr;
+ }
+ return $srvid;
+ }
+}
+
+=item nas_ids DBH
+
+Convert the 'nasnames option into a list of real NAS ids.
+
+=cut
+
+sub nas_ids {
+ my $self = shift;
+ my $dbh = shift;
+
+ my @nasnames = split(/\s*,\s*/, $self->option('nasnames'));
+ return unless @nasnames;
+ # pass these through unchanged
+ my @ids = grep { /^\d+$/ } @nasnames;
+ @nasnames = grep { not /^\d+$/ } @nasnames;
+ if ( @nasnames ) {
+ my $in_nasnames = join(',', map {$dbh->quote($_)} @nasnames);
+
+ my $sth = $dbh->prepare("SELECT id FROM nas WHERE nasname IN ($in_nasnames)");
+ $sth->execute or die $dbh->errstr;
+ my $rows = $sth->fetchall_arrayref;
+ push @ids, $_->[0] foreach @$rows;
+ }
+
+ return @ids;
+}
+
+1;
diff --git a/FS/FS/part_export/domain_sql.pm b/FS/FS/part_export/domain_sql.pm
index 0749fec09..ff0d949f1 100644
--- a/FS/FS/part_export/domain_sql.pm
+++ b/FS/FS/part_export/domain_sql.pm
@@ -26,6 +26,7 @@ my $postfix_transport_static =
'desc' => 'Real time export of domains to SQL databases '.
'(postfix, others?)',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => <<END
Export domains (svc_domain records) to SQL databases. Currently this is a
simple export with a default for Postfix, but it can be extended for other
diff --git a/FS/FS/part_export/everyone_net.pm b/FS/FS/part_export/everyone_net.pm
index 0fd32fa8b..7386973e4 100644
--- a/FS/FS/part_export/everyone_net.pm
+++ b/FS/FS/part_export/everyone_net.pm
@@ -18,6 +18,8 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_acct',
'desc' => 'Real-time export to Everyone.net outsourced mail service',
'options'=> \%options,
+ 'no_machine' => 1,
+ 'default_svc_class' => 'Email',
'notes' => <<'END'
Real-time export to
<a href="http://www.everyone.net/">Everyone.net</a> via the XRC Remote API.
diff --git a/FS/FS/part_export/ez_prepaid.pm b/FS/FS/part_export/ez_prepaid.pm
new file mode 100644
index 000000000..9f454df54
--- /dev/null
+++ b/FS/FS/part_export/ez_prepaid.pm
@@ -0,0 +1,184 @@
+package FS::part_export::ez_prepaid;
+
+use base qw( FS::part_export );
+
+use strict;
+use vars qw(@ISA %info $version $replace_ok_kludge $product_info);
+use Tie::IxHash;
+use FS::Record qw( qsearchs );
+use FS::svc_external;
+use SOAP::Lite;
+use XML::Simple qw( xml_in );
+use Data::Dumper;
+
+$version = '01';
+
+my $product_info;
+my %language_id = ( English => 1, Spanish => 2 );
+
+tie my %options, 'Tie::IxHash',
+ 'site_id' => { label => 'Site ID' },
+ 'clerk_id' => { label => 'Clerk ID' },
+# 'product_id' => { label => 'Product ID' }, use the 'title' field
+# 'amount' => { label => 'Purchase amount' },
+ 'language' => { label => 'Language',
+ type => 'select',
+ options => [ 'English', 'Spanish' ],
+ },
+
+ 'debug' => { label => 'Debug level',
+ type => 'select', options => [0, 1, 2 ] },
+;
+
+%info = (
+ 'svc' => 'svc_external',
+ 'desc' => 'Purchase EZ-Prepaid PIN',
+ 'options' => \%options,
+ 'no_machine' => 1,
+ 'notes' => <<'END'
+<P>Export to the EZ-Prepaid PIN purchase service. If the purchase is allowed,
+the PIN will be stored as svc_external.id.</P>
+<P>svc_external.title must contain the product ID, and should be set as a fixed
+field in the service definition. For a list of product IDs, see the
+"Merchant Info" tab in the EZ Prepaid reseller portal.</P>
+END
+ );
+
+$replace_ok_kludge = 0;
+
+sub _export_insert {
+ my ($self, $svc_external) = @_;
+
+ # the name on the certificate is 'debisys.com', for some reason
+ local $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME}=0;
+
+ my $pin = eval { $self->ez_prepaid_PinDistSale( $svc_external->title ) };
+ return $@ if $@;
+
+ local($replace_ok_kludge) = 1;
+ $svc_external->set('id', $pin);
+ $svc_external->replace;
+}
+
+sub _export_replace {
+ $replace_ok_kludge ? '' : "can't change PIN after purchase";
+}
+
+sub _export_delete {
+ "can't delete PIN after purchase";
+}
+
+# possibly options at some point to relate these to agentnum/usernum
+sub site_id { $_[0]->option('site_id') }
+
+sub clerk_id { $_[0]->option('clerk_id') }
+
+sub ez_prepaid_PinDistSale {
+ my $self = shift;
+ my $product_id = shift;
+ $self->ez_prepaid_init; # populate product ID cache
+ my $info = $product_info->{$product_id};
+ if ( $info ) {
+ if ( $self->option('debug') ) {
+ warn "Purchasing PIN product #$product_id:\n" .
+ $info->{Description}."\n".
+ $info->{CurrencyCode} . ' ' .$info->{Amount}."\n";
+ }
+ } else { #no $info
+ die "Unknown PIN product #$product_id.\n";
+ }
+
+ my $response = $self->ez_prepaid_request(
+ 'PinDistSale',
+ $version,
+ $self->site_id,
+ $self->clerk_id,
+ $product_id,
+ '', # AccountID, not used for PIN sale
+ $product_info->{$product_id}->{Amount},
+ $self->svcnum,
+ ($language_id{ $self->option('language') } || 1),
+ );
+ if ( $self->option('debug') ) {
+ warn Dumper($response);
+ # includes serial number and transaction ID, possibly useful
+ # (but we don't have a structured place to store it--maybe in
+ # a customer note?)
+ }
+ $response->{Pin};
+}
+
+sub ez_prepaid_init {
+ # returns the SOAP client object
+ my $self = shift;
+ my $wsdl = 'https://webservice.ez-prepaid.com/soap/webServices.wsdl';
+
+ if ( $self->option('debug') >= 2 ) {
+ SOAP::Lite->import(+trace => [transport => \&log_transport ]);
+ }
+
+ if ( !$self->client ) {
+ $self->set(client => SOAP::Lite->new->service($wsdl));
+ # I don't know if this can happen, but better to bail out here
+ # than go into recursion.
+ die "Error creating SOAP client\n" if !$self->client;
+ }
+
+ if ( !defined($product_info) ) {
+ # for now we only support the 'PIN' type
+ my $response = $self->ez_prepaid_request(
+ 'GetTransTypeList', $version, $self->site_id, '', '', '', ''
+ );
+ my %transtype = map { $_->{Description} => $_->{TransTypeId} }
+ @{ $response->{TransType} };
+
+ if ( !exists $transtype{PIN} ) {
+ warn "'PIN' transaction type not available.\n";
+ # or else your site ID is wrong
+ return;
+ }
+
+ $response = $self->ez_prepaid_request(
+ 'GetProductList',
+ $version,
+ $self->option('site_id'),
+ $transtype{PIN},
+ '', #CarrierId
+ '', #CategoryId
+ '', #ProductId
+ );
+ $product_info = +{
+ map { $_->{ProductId} => $_ }
+ @{ $response->{Product} }
+ };
+ } #!defined $product_info
+}
+
+sub log_transport {
+ my $in = shift;
+ if ( UNIVERSAL::can($in, 'content') ) {
+ warn $in->content."\n";
+ }
+}
+
+my @ForceArray = qw(TransType Product); # add others as needed
+sub ez_prepaid_request {
+ my $self = shift;
+ # takes a method name and param list,
+ # returns a hashref containing the unpacked response
+ # or dies on error
+
+ $self->ez_prepaid_init if !$self->client;
+
+ my $method = shift;
+ my $xml = $self->client->$method(@_);
+ # All of their response data types are one part, a string, containing
+ # an encoded XML structure, containing the fields described in the docs.
+ my $response = xml_in($xml, ForceArray => \@ForceArray);
+ if ( exists($response->{ResponseCode}) && $response->{ResponseCode} > 0 ) {
+ die "[$method] ".$response->{ResponseMessage};
+ }
+ $response;
+}
+
+1;
diff --git a/FS/FS/part_export/forward_sql.pm b/FS/FS/part_export/forward_sql.pm
index 563efcc44..eb4137801 100644
--- a/FS/FS/part_export/forward_sql.pm
+++ b/FS/FS/part_export/forward_sql.pm
@@ -10,6 +10,7 @@ use FS::Record;
'desc' => 'Real-time export of forwards to SQL databases ',
#.' (vpopmail, Postfix+Courier IMAP, others?)',
'options' => __PACKAGE__->sql_options,
+ 'no_machine' => 1,
'notes' => <<END
Export mail forwards (svc_forward records) to SQL databases.
diff --git a/FS/FS/part_export/freeswitch.pm b/FS/FS/part_export/freeswitch.pm
new file mode 100644
index 000000000..eb490fd85
--- /dev/null
+++ b/FS/FS/part_export/freeswitch.pm
@@ -0,0 +1,192 @@
+package FS::part_export::freeswitch;
+use base qw( FS::part_export );
+
+use vars qw( %info ); # $DEBUG );
+#use Data::Dumper;
+use Tie::IxHash;
+use Text::Template;
+use FS::Record qw( qsearch ); #qsearchs );
+use FS::svc_phone;
+#use FS::Schema qw( dbdef );
+
+#$DEBUG = 1;
+
+tie my %options, 'Tie::IxHash',
+ 'user' => { label => 'SSH username', default=>'root', },
+ 'directory' => { label => 'Directory to store FreeSWITCH account XML files',
+ default => '/usr/local/freeswitch/conf/directory/',
+ },
+ #'domain' => { label => 'Optional fixed SIP domain to use, overrides svc_phone domain', },
+ 'reload' => { label => 'Reload command',
+ default => '/usr/local/freeswitch/bin/fs_cli -x reloadxml',
+ },
+ 'user_template' => { label => 'User XML configuration template',
+ type => 'textarea',
+ default => <<'END',
+<domain name="<% $domain %>">
+ <user id="<% $phonenum %>">
+ <params>
+ <param name="password" value="<% $sip_password %>"/>
+ </params>
+ </user>
+</domain>
+END
+ },
+;
+
+%info = (
+ 'svc' => 'svc_phone',
+ 'desc' => 'Provision phone services to FreeSWITCH XML configuration files',
+ 'options' => \%options,
+ 'notes' => <<'END',
+Export XML account configuration files to FreeSWITCH, one per domain.
+<br><br>
+You will need to enable the svc_phone-domain configuration setting and
+<a href="http://www.freeside.biz/mediawiki/index.php/Freeside:1.9:Documentation:Administration:SSH_Keys">setup SSH for unattended operation</a>.
+END
+);
+
+sub rebless { shift; }
+
+sub _export_insert {
+ my( $self, $svc_phone ) = ( shift, shift );
+
+ $self->_export_rebuild_domain($svc_phone);
+
+}
+
+sub _export_replace {
+ my( $self, $new, $old ) = ( shift, shift, shift );
+
+ my $error = $self->_export_rebuild_domain($new);
+ return $error if $error;
+
+ if ( $new->domsvc ne $old->domsvc && $old->domsvc ) {
+ $error = $self->_export_rebuild_domain($old);
+ return $error if $error;
+ }
+
+ '';
+}
+
+sub _export_delete {
+ my( $self, $svc_phone ) = ( shift, shift );
+
+ $self->_export_rebuild_domain($svc_phone);
+}
+
+sub _export_rebuild_domain {
+ my($self, $svc_phone) = ( shift, shift );
+
+ eval "use Net::SCP;";
+ die $@ if $@;
+
+ #create and copy over file
+
+ my $tempdir = '%%%FREESIDE_CONF%%%/cache.'. $FS::UID::datasrc;
+
+ my $domain = $svc_phone->domain or return "domain required";
+
+ my $fh = new File::Temp(
+ TEMPLATE => "$tempdir/freeswitch.$domain.XXXXXXXX",
+ DIR => $dir,
+ #UNLINK => 0,
+ );
+
+ print $fh qq(<domain name="$domain">\n);
+
+ my @dom_svc_phone = qsearch( 'svc_phone', { 'domsvc'=>$svc_phone->domsvc } );
+
+ foreach my $dom_svc_phone (@dom_svc_phone) {
+
+ print $fh $self->freeswitch_template_fillin( $dom_svc_phone, 'user' )
+ or die "print to freeswitch template failed: $!";
+
+ }
+
+ print $fh qq(</domain>\n);
+ $fh->flush;
+
+ my $scp = new Net::SCP;
+ my $user = $self->option('user')||'root';
+ my $host = $self->machine;
+ my $dir = $self->option('directory');
+
+ $scp->scp( $fh->filename, "$user\@$host:$dir/$domain.xml" )
+ or return $scp->{errstr};
+
+ #signal freeswitch to reload config
+ $self->freeswitch_ssh( command => $self->option('reload') );
+
+ '';
+
+}
+
+sub freeswitch_template_fillin {
+ my( $self, $svc_phone, $template ) = (shift, shift, shift);
+
+ $template ||= 'user'; #?
+
+ #cache a %tt hash?
+ my $tt = new Text::Template (
+ TYPE => 'STRING',
+ SOURCE => $self->option($template.'_template'),
+ DELIMITERS => [ '<%', '%>' ],
+ );
+
+ #false lazinessish w/phone_shellcommands::_export_command
+ my %hash = (
+ map { $_ => $svc_phone->getfield($_) } $svc_phone->fields
+ );
+
+ #might as well do em all, they're all going in an XML file as attribs
+ foreach ( keys %hash ) {
+ $hash{$_} =~ s/'/&apos;/g;
+ $hash{$_} =~ s/"/&quot;/g;
+ }
+
+ $tt->fill_in(
+ HASH => \%hash,
+ );
+}
+
+##a good idea to queue anything that could fail or take any time
+#sub shellcommands_queue {
+# my( $self, $svcnum ) = (shift, shift);
+# my $queue = new FS::queue {
+# 'svcnum' => $svcnum,
+# 'job' => "FS::part_export::freeswitch::ssh_cmd",
+# };
+# $queue->insert( @_ );
+#}
+
+sub freeswitch_ssh { #method
+ my $self = shift;
+ ssh_cmd( user => $self->option('user')||'root',
+ host => $self->machine,
+ @_,
+ );
+}
+
+sub ssh_cmd { #subroutine, not method
+ use Net::OpenSSH;
+ my $opt = { @_ };
+ open my $def_in, '<', '/dev/null' or die "unable to open /dev/null";
+ my $ssh = Net::OpenSSH->new( $opt->{'user'}.'@'.$opt->{'host'},
+ default_stdin_fh => $def_in,
+ );
+ die "Couldn't establish SSH connection: ". $ssh->error if $ssh->error;
+ my ($output, $errput) = $ssh->capture2( #{stdin_discard => 1},
+ $opt->{'command'}
+ );
+ die "Error running SSH command: ". $ssh->error if $ssh->error;
+
+ #who the fuck knows what freeswitch reload outputs, probably a fucking
+ # ascii advertisement for cluecon
+ #die $errput if $errput;
+ #die $output if $output;
+
+ '';
+}
+
+1;
diff --git a/FS/FS/part_export/globalpops_voip.pm b/FS/FS/part_export/globalpops_voip.pm
index 6df21f406..9fe45ba0a 100644
--- a/FS/FS/part_export/globalpops_voip.pm
+++ b/FS/FS/part_export/globalpops_voip.pm
@@ -19,6 +19,7 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_phone',
'desc' => 'Provision phone numbers to VoIP Innovations (formerly GlobalPOPs VoIP)',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => <<'END'
Requires installation of
<a href="http://search.cpan.org/dist/Net-GlobalPOPs-MediaServicesAPI">Net::GlobalPOPs::MediaServicesAPI</a>
diff --git a/FS/FS/part_export/http.pm b/FS/FS/part_export/http.pm
index 3749224ff..c35c89f12 100644
--- a/FS/FS/part_export/http.pm
+++ b/FS/FS/part_export/http.pm
@@ -43,6 +43,7 @@ tie %options, 'Tie::IxHash',
'svc' => 'svc_domain',
'desc' => 'Send an HTTP or HTTPS GET or POST request',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => <<'END'
Send an HTTP or HTTPS GET or POST to the specified URL. For HTTPS support,
<a href="http://search.cpan.org/dist/Crypt-SSLeay">Crypt::SSLeay</a>
diff --git a/FS/FS/part_export/http_status.pm b/FS/FS/part_export/http_status.pm
index 5342106b4..6fbd3fbe6 100644
--- a/FS/FS/part_export/http_status.pm
+++ b/FS/FS/part_export/http_status.pm
@@ -17,6 +17,7 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_dsl',
'desc' => 'Retrieve status information via HTTP or HTTPS',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => <<'END'
Fields from the service can be substituted in the URL as $field.
END
diff --git a/FS/FS/part_export/ikano.pm b/FS/FS/part_export/ikano.pm
index eedc9d0ac..23917bf9e 100644
--- a/FS/FS/part_export/ikano.pm
+++ b/FS/FS/part_export/ikano.pm
@@ -31,6 +31,7 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_dsl',
'desc' => 'Provision DSL to Ikano',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => <<'END'
Requires installation of
<a href="http://search.cpan.org/dist/Net-Ikano">Net::Ikano</a> from CPAN.
diff --git a/FS/FS/part_export/indosoft.pm b/FS/FS/part_export/indosoft.pm
index b5734019b..02ae5efc5 100644
--- a/FS/FS/part_export/indosoft.pm
+++ b/FS/FS/part_export/indosoft.pm
@@ -17,6 +17,7 @@ tie my %options, 'Tie::IxHash',
'desc' =>
'Export conferences to the Indosoft Conference Bridge',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => <<'END'
Export conferences to the Indosoft conference bridge.
Net::Indosoft::Voicebridge is required.
diff --git a/FS/FS/part_export/infostreet.pm b/FS/FS/part_export/infostreet.pm
index ef16c7c54..51f57605a 100644
--- a/FS/FS/part_export/infostreet.pm
+++ b/FS/FS/part_export/infostreet.pm
@@ -19,6 +19,7 @@ tie my %options, 'Tie::IxHash',
'desc' => 'Real-time export to InfoStreet streetSmartAPI',
'options' => \%options,
'nodomain' => 'Y',
+ 'no_machine' => 1,
'notes' => <<'END'
Real-time export to
<a href="http://www.infostreet.com/">InfoStreet</a> streetSmartAPI.
diff --git a/FS/FS/part_export/internal_diddb.pm b/FS/FS/part_export/internal_diddb.pm
index a94e43e28..b51f63173 100644
--- a/FS/FS/part_export/internal_diddb.pm
+++ b/FS/FS/part_export/internal_diddb.pm
@@ -17,6 +17,7 @@ tie my %options, 'Tie::IxHash',
'desc' => 'Provision phone numbers from the internal DID database',
'notes' => 'After adding the export, DIDs may be imported under Tools -> Importing -> Import phone numbers (DIDs)',
'options' => \%options,
+ 'no_machine' => 1,
);
sub rebless { shift; }
diff --git a/FS/FS/part_export/ldap.pm b/FS/FS/part_export/ldap.pm
index 838532021..fe634d230 100644
--- a/FS/FS/part_export/ldap.pm
+++ b/FS/FS/part_export/ldap.pm
@@ -41,6 +41,7 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_acct',
'desc' => 'Real-time export to LDAP',
'options' => \%options,
+ 'default_svc_class' => 'Email',
'notes' => <<'END'
Real-time export to arbitrary LDAP attributes. Requires installation of
<a href="http://search.cpan.org/dist/Net-LDAP">Net::LDAP</a> from CPAN.
diff --git a/FS/FS/part_export/netsapiens.pm b/FS/FS/part_export/netsapiens.pm
index 6e2ee8ae3..2e37d04b6 100644
--- a/FS/FS/part_export/netsapiens.pm
+++ b/FS/FS/part_export/netsapiens.pm
@@ -72,10 +72,11 @@ tie my %options, 'Tie::IxHash',
;
%info = (
- 'svc' => [ 'svc_phone', ], # 'part_device',
- 'desc' => 'Provision phone numbers to NetSapiens',
- 'options' => \%options,
- 'notes' => <<'END'
+ 'svc' => [ 'svc_phone', ], # 'part_device',
+ 'desc' => 'Provision phone numbers to NetSapiens',
+ 'options' => \%options,
+ 'no_machine' => 1,
+ 'notes' => <<'END'
Requires installation of
<a href="http://search.cpan.org/dist/REST-Client">REST::Client</a>
from CPAN.
diff --git a/FS/FS/part_export/null.pm b/FS/FS/part_export/null.pm
index 0145af3a4..3a764883c 100644
--- a/FS/FS/part_export/null.pm
+++ b/FS/FS/part_export/null.pm
@@ -11,3 +11,4 @@ sub _export_insert {}
sub _export_replace {}
sub _export_delete {}
+1;
diff --git a/FS/FS/part_export/phone_shellcommands.pm b/FS/FS/part_export/phone_shellcommands.pm
index 040af27a7..5c1ae0153 100644
--- a/FS/FS/part_export/phone_shellcommands.pm
+++ b/FS/FS/part_export/phone_shellcommands.pm
@@ -138,3 +138,4 @@ sub ssh_cmd { #subroutine, not method
&Net::SSH::ssh_cmd( { @_ } );
}
+1;
diff --git a/FS/FS/part_export/phone_sqlopensips.pm b/FS/FS/part_export/phone_sqlopensips.pm
index 3d01c1624..7b07ecf4a 100644
--- a/FS/FS/part_export/phone_sqlopensips.pm
+++ b/FS/FS/part_export/phone_sqlopensips.pm
@@ -21,10 +21,11 @@ tie %options, 'Tie::IxHash',
;
%info = (
- 'svc' => 'svc_phone',
- 'desc' => 'Export DIDs to OpenSIPs dr_rules table',
- 'options' => \%options,
- 'notes' => 'Export DIDs to OpenSIPs dr_rules table',
+ 'svc' => 'svc_phone',
+ 'desc' => 'Export DIDs to OpenSIPs dr_rules table',
+ 'options' => \%options,
+ 'no_machine' => 1,
+ 'notes' => 'Export DIDs to OpenSIPs dr_rules table',
);
sub rebless { shift; }
@@ -93,3 +94,4 @@ sub dr_reload {
'';
}
+1;
diff --git a/FS/FS/part_export/phone_sqlradius.pm b/FS/FS/part_export/phone_sqlradius.pm
index 6b14bed3c..46c372cb4 100644
--- a/FS/FS/part_export/phone_sqlradius.pm
+++ b/FS/FS/part_export/phone_sqlradius.pm
@@ -39,10 +39,11 @@ tie %options, 'Tie::IxHash',
;
%info = (
- 'svc' => 'svc_phone',
- 'desc' => 'Real-time export to SQL-backed RADIUS (FreeRADIUS, ICRADIUS) for phone provisioning and rating',
- 'options' => \%options,
- 'notes' => <<END,
+ 'svc' => 'svc_phone',
+ 'desc' => 'Real-time export to SQL-backed RADIUS (FreeRADIUS, ICRADIUS) for phone provisioning and rating',
+ 'options' => \%options,
+ 'no_machine' => 1,
+ 'notes' => <<END,
Real-time export of <b>radcheck</b> table
to any SQL database for <a href="http://www.freeradius.org/">FreeRADIUS</a>
or <a href="http://radius.innercite.com/">ICRADIUS</a>.
diff --git a/FS/FS/part_export/postfix.pm b/FS/FS/part_export/postfix.pm
index 4fd19ee61..9a8d617f3 100644
--- a/FS/FS/part_export/postfix.pm
+++ b/FS/FS/part_export/postfix.pm
@@ -22,6 +22,7 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_forward',
'desc' => 'Postfix text files',
'options' => \%options,
+ 'default_svc_class' => 'Email',
'notes' => <<'END'
Batch export of Postfix aliases and virtual files.
<a href="http://search.cpan.org/dist/File-Rsync">File::Rsync</a>
diff --git a/FS/FS/part_export/prizm.pm b/FS/FS/part_export/prizm.pm
index 02e89c6d3..996448951 100644
--- a/FS/FS/part_export/prizm.pm
+++ b/FS/FS/part_export/prizm.pm
@@ -79,11 +79,12 @@ possibly harmful.
EOT
%info = (
- 'svc' => 'svc_broadband',
- 'desc' => 'Real-time export to Northbound Interface',
- 'options' => \%options,
- 'nodomain' => 'Y',
- 'notes' => $notes,
+ 'svc' => 'svc_broadband',
+ 'desc' => 'Real-time export to Northbound Interface',
+ 'options' => \%options,
+ 'nodomain' => 'Y',
+ 'no_machine' => 1,
+ 'notes' => $notes,
);
sub prizm_command {
diff --git a/FS/FS/part_export/radiator.pm b/FS/FS/part_export/radiator.pm
index 2ac3edb22..f09d36abb 100644
--- a/FS/FS/part_export/radiator.pm
+++ b/FS/FS/part_export/radiator.pm
@@ -11,6 +11,8 @@ tie my %options, 'Tie::IxHash', %FS::part_export::sqlradius::options;
'desc' => 'Real-time export to RADIATOR',
'options' => \%options,
'nodomain' => '',
+ 'no_machine' => 1,
+ 'default_svc_class' => 'Internet',
'notes' => <<'END',
Real-time export of the <b>radusers</b> table to any SQL database in
<a href="http://www.open.com.au/radiator/">Radiator</a>-native format.
diff --git a/FS/FS/part_export/router.pm b/FS/FS/part_export/router.pm
index 6a1d676f4..3071ece74 100644
--- a/FS/FS/part_export/router.pm
+++ b/FS/FS/part_export/router.pm
@@ -87,6 +87,7 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_broadband',
'desc' => 'Send a command to a router.',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => 'Installation of Net::Telnet from CPAN is required for telnet connections. This export will execute if the following virtual fields are set on the router: admin_user, admin_password, admin_address, admin_timeout, admin_prompt. Option virtual fields are: admin_cmd_insert, admin_cmd_replace, admin_cmd_delete, admin_cmd_suspend, admin_cmd_unsuspend. See the module documentation for a full list of required/supported router virtual fields.',
);
diff --git a/FS/FS/part_export/rt_ticket.pm b/FS/FS/part_export/rt_ticket.pm
index b53b7da8a..7ae6105a0 100644
--- a/FS/FS/part_export/rt_ticket.pm
+++ b/FS/FS/part_export/rt_ticket.pm
@@ -127,6 +127,7 @@ tie my %options, 'Tie::IxHash', (
'Create an RT ticket',
'options' => \%options,
'nodomain' => '',
+ 'no_machine' => 1,
'notes' => '
Create a ticket in RT. The subject and body of the ticket
will be generated from a message template.'
diff --git a/FS/FS/part_export/send_email.pm b/FS/FS/part_export/send_email.pm
index 05f623633..6ba131f18 100644
--- a/FS/FS/part_export/send_email.pm
+++ b/FS/FS/part_export/send_email.pm
@@ -85,6 +85,7 @@ tie my %options, 'Tie::IxHash', (
'Send an email message',
'options' => \%options,
'nodomain' => '',
+ 'no_machine' => 1,
'notes' => '
Send an email message. The subject and body of the message
will be generated from a message template.'
diff --git a/FS/FS/part_export/shellcommands.pm b/FS/FS/part_export/shellcommands.pm
index 20e909135..f964af31c 100644
--- a/FS/FS/part_export/shellcommands.pm
+++ b/FS/FS/part_export/shellcommands.pm
@@ -97,12 +97,12 @@ tie my %options, 'Tie::IxHash',
;
%info = (
- 'svc' => 'svc_acct',
- 'desc' =>
- 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
- 'options' => \%options,
- 'nodomain' => 'Y',
- 'notes' => <<'END'
+ 'svc' => 'svc_acct',
+ 'desc' => 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
+ 'options' => \%options,
+ 'nodomain' => 'Y',
+ 'svc_machine' => 1,
+ 'notes' => <<'END'
Run remote commands via SSH. Usernames are considered unique (also see
shellcommands_withdomain). You probably want this if the commands you are
running will not accept a domain as a parameter. You will need to
@@ -124,24 +124,7 @@ running will not accept a domain as a parameter. You will need to
this.form.unsuspend_stdin.value="";
'>
<LI>
- <INPUT TYPE="button" VALUE="FreeBSD before 4.10 / 5.3" onClick='
- this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0";
- this.form.useradd_stdin.value = "$_password\n";
- this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
- this.form.usermod.value = "lockf /etc/passwd.lock pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -c $new_finger -h 0";
- this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
- this.form.suspend_stdin.value="";
- this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
- '>
- Note: On FreeBSD versions before 5.3 and 4.10 (4.10 is after 4.9, not
- 4.1!), due to deficient locking in pw(1), you must disable the chpass(1),
- chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with
- wrappers that prepend "lockf /etc/passwd.lock". Alternatively, apply the
- patch in
- <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
- and use the "FreeBSD 4.10 / 5.3 or later" button below.
- <LI>
- <INPUT TYPE="button" VALUE="FreeBSD 4.10 / 5.3 or later" onClick='
+ <INPUT TYPE="button" VALUE="FreeBSD" onClick='
this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
this.form.useradd_stdin.value = "$_password\n";
this.form.userdel.value = "pw userdel $username -r";
@@ -360,7 +343,7 @@ sub _export_command {
my @ssh_cmd_args = (
user => $self->option('user') || 'root',
- host => $self->machine,
+ host => $self->svc_machine($svc_acct),
command => $command_string,
stdin_string => $stdin_string,
ignored_errors => $self->option('ignored_errors') || '',
@@ -373,7 +356,7 @@ sub _export_command {
eval { ssh_cmd(@ssh_cmd_args) };
$error = $@;
$error = $error->full_message if ref $error; # Exception::Class::Base
- return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
+ return $error. ' ('. $self->exporttype. ' to '. $self->svc_machine($svc_acct). ')'
if $error;
}
else {
@@ -433,7 +416,7 @@ sub _export_replace {
# $error ||= "can't change RADIUS groups";
#}
}
- return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
+ return $error. ' ('. $self->exporttype. ' to '. $self->svc_machine($new). ')'
if $error;
$new_agent_custid = $new_cust_main ? $new_cust_main->agent_custid : '';
@@ -457,7 +440,7 @@ sub _export_replace {
my @ssh_cmd_args = (
user => $self->option('user') || 'root',
- host => $self->machine,
+ host => $self->svc_machine($new),
command => $command_string,
stdin_string => $stdin_string,
ignored_errors => $self->option('ignored_errors') || '',
@@ -470,7 +453,7 @@ sub _export_replace {
eval { ssh_cmd(@ssh_cmd_args) };
$error = $@;
$error = $error->full_message if ref $error; # Exception::Class::Base
- return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
+ return $error. ' ('. $self->exporttype. ' to '. $self->svc_machine($new). ')'
if $error;
}
else {
@@ -507,7 +490,7 @@ sub ssh_cmd { #subroutine, not method
my ($output, $errput) = $ssh->capture2($ssh_opt, $opt->{'command'});
return if $opt->{'ignore_all_errors'};
- die "Error running SSH command: ". $ssh->error if $ssh->error;
+ #die "Error running SSH command: ". $ssh->error if $ssh->error;
if ( ($output || $errput)
&& $opt->{'ignored_errors'} && length($opt->{'ignored_errors'})
@@ -521,7 +504,9 @@ sub ssh_cmd { #subroutine, not method
$errput =~ s/[\s\n]//g;
}
- die "$errput\n" if $errput;
+ die (($errput || $ssh->error). "\n") if $errput || $ssh->error;
+ #die "$errput\n" if $errput;
+
die "$output\n" if $output and $opt->{'fail_on_output'};
'';
}
diff --git a/FS/FS/part_export/shellcommands_withdomain.pm b/FS/FS/part_export/shellcommands_withdomain.pm
index 1ebf5f633..1b59589bf 100644
--- a/FS/FS/part_export/shellcommands_withdomain.pm
+++ b/FS/FS/part_export/shellcommands_withdomain.pm
@@ -80,10 +80,11 @@ tie my %options, 'Tie::IxHash',
;
%info = (
- 'svc' => 'svc_acct',
- 'desc' => 'Real-time export via remote SSH (vpopmail, ISPMan)',
- 'options' => \%options,
- 'notes' => <<'END'
+ 'svc' => 'svc_acct',
+ 'desc' => 'Real-time export via remote SSH (vpopmail, ISPMan, MagicMail)',
+ 'options' => \%options,
+ 'svc_machine' => 1,
+ 'notes' => <<'END'
Run remote commands via SSH. username@domain (rather than just usernames) are
considered unique (also see shellcommands). You probably want this if the
commands you are running will accept a domain as a parameter, and will allow
diff --git a/FS/FS/part_export/sqlmail.pm b/FS/FS/part_export/sqlmail.pm
index cbdaf7f52..19505b488 100644
--- a/FS/FS/part_export/sqlmail.pm
+++ b/FS/FS/part_export/sqlmail.pm
@@ -37,6 +37,7 @@ tie my %options, 'Tie::IxHash',
'desc' => 'Real-time export to SQL-backed mail server',
'options' => \%options,
'nodomain' => '',
+ 'default_svc_class' => 'Email',
'notes' => <<'END'
Database schema can be made to work with Courier IMAP, Exim and Dovecot.
Others could work but are untested. (more detailed description from
diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm
index c360c9ef0..6760d09b7 100644
--- a/FS/FS/part_export/sqlradius.pm
+++ b/FS/FS/part_export/sqlradius.pm
@@ -110,6 +110,7 @@ END
'desc' => 'Real-time export to SQL-backed RADIUS (FreeRADIUS, ICRADIUS)',
'options' => \%options,
'nodomain' => 'Y',
+ 'no_machine' => 1,
'nas' => 'Y', # show export_nas selection in UI
'default_svc_class' => 'Internet',
'notes' => $notes1.
@@ -347,6 +348,7 @@ sub _export_delete {
sub sqlradius_queue {
my( $self, $svcnum, $method ) = (shift, shift, shift);
+ my %args = @_;
my $queue = new FS::queue {
'svcnum' => $svcnum,
'job' => "FS::part_export::sqlradius::sqlradius_$method",
@@ -966,8 +968,7 @@ are identified by the combination of group name and attribute name.
In the special case where attributes are being replaced because a group
name (L<FS::radius_group>->groupname) is changing, the pseudo-field
-'groupname' must be set in OLD_RADIUS_ATTR. It's probably best to do this
-
+'groupname' must be set in OLD_RADIUS_ATTR.
=cut
@@ -982,41 +983,43 @@ sub export_attr_replace { shift->export_attr_action('replace', @_); }
sub export_attr_action {
my $self = shift;
my ($action, $new, $old) = @_;
- my ($attrname, $attrtype, $groupname) =
- ($new->attrname, $new->attrtype, $new->radius_group->groupname);
- if ( $action eq 'replace' ) {
-
- if ( $new->attrtype ne $old->attrtype ) {
- # they're in separate tables in the target
- return $self->export_attr_action('delete', $old)
- || $self->export_attr_action('insert', $new)
- ;
- }
+ my $err_or_queue;
- # otherwise, just make sure we know the old attribute/group names
- # so we can find the existing record
- $attrname = $old->attrname;
- $groupname = $old->groupname || $old->radius_group->groupname;
- # maybe this should be enforced more strictly
- warn "WARNING: attribute replace without 'groupname' set; assuming '$groupname'\n"
- if !defined($old->groupname);
+ if ( $action eq 'delete' ) {
+ $old = $new;
+ }
+ if ( $action eq 'delete' or $action eq 'replace' ) {
+ # delete based on an exact match
+ my %opt = (
+ attrname => $old->attrname,
+ attrtype => $old->attrtype,
+ groupname => $old->groupname || $old->radius_group->groupname,
+ op => $old->op,
+ value => $old->value,
+ );
+ $err_or_queue = $self->sqlradius_queue('', 'attr_delete', %opt);
+ return $err_or_queue unless ref $err_or_queue;
+ }
+ # this probably doesn't matter, but just to be safe...
+ my $jobnum = $err_or_queue->jobnum if $action eq 'replace';
+ if ( $action eq 'replace' or $action eq 'insert' ) {
+ my %opt = (
+ attrname => $new->attrname,
+ attrtype => $new->attrtype,
+ groupname => $new->radius_group->groupname,
+ op => $new->op,
+ value => $new->value,
+ );
+ $err_or_queue = $self->sqlradius_queue('', 'attr_insert', %opt);
+ $err_or_queue->depend_insert($jobnum) if $jobnum;
+ return $err_or_queue unless ref $err_or_queue;
}
-
- my $err_or_queue = $self->sqlradius_queue('', "attr_$action",
- attrnum => $new->attrnum,
- attrname => $attrname,
- attrtype => $attrtype,
- groupname => $groupname,
- );
- return $err_or_queue unless ref $err_or_queue;
'';
}
sub sqlradius_attr_insert {
my $dbh = sqlradius_connect(shift, shift, shift);
my %opt = @_;
- my $radius_attr = qsearchs('radius_attr', { attrnum => $opt{'attrnum'} })
- or die 'attrnum '.$opt{'attrnum'}.' not found';
my $table;
# make sure $table is completely safe
@@ -1027,12 +1030,10 @@ sub sqlradius_attr_insert {
$table = 'radgroupreply';
}
else {
- die "unknown attribute type '".$radius_attr->attrtype."'";
+ die "unknown attribute type '$opt{attrtype}'";
}
- my @values = (
- $opt{'groupname'}, map { $radius_attr->$_ } qw(attrname op value)
- );
+ my @values = @opt{ qw(groupname attrname op value) };
my $sth = $dbh->prepare(
'INSERT INTO '.$table.' (groupname, attribute, op, value) VALUES (?,?,?,?)'
);
@@ -1054,41 +1055,16 @@ sub sqlradius_attr_delete {
die "unknown attribute type '".$opt{'attrtype'}."'";
}
+ my @values = @opt{ qw(groupname attrname op value) };
my $sth = $dbh->prepare(
- 'DELETE FROM '.$table.' WHERE groupname = ? AND attribute = ?'
+ 'DELETE FROM '.$table.
+ ' WHERE groupname = ? AND attribute = ? AND op = ? AND value = ?'.
+ ' LIMIT 1'
);
- $sth->execute( @opt{'groupname', 'attrname'} ) or die $dbh->errstr;
+ $sth->execute(@values) or die $dbh->errstr;
}
-sub sqlradius_attr_replace {
- my $dbh = sqlradius_connect(shift, shift, shift);
- my %opt = @_;
- my $radius_attr = qsearchs('radius_attr', { attrnum => $opt{'attrnum'} })
- or die 'attrnum '.$opt{'attrnum'}.' not found';
-
- my $table;
- if ( $opt{'attrtype'} eq 'C' ) {
- $table = 'radgroupcheck';
- }
- elsif ( $opt{'attrtype'} eq 'R' ) {
- $table = 'radgroupreply';
- }
- else {
- die "unknown attribute type '".$opt{'attrtype'}."'";
- }
-
- my $sth = $dbh->prepare(
- 'UPDATE '.$table.' SET groupname = ?, attribute = ?, op = ?, value = ?
- WHERE groupname = ? AND attribute = ?'
- );
-
- my $new_groupname = $radius_attr->radius_group->groupname;
- my @new_values = (
- $new_groupname, map { $radius_attr->$_ } qw(attrname op value)
- );
- $sth->execute( @new_values, @opt{'groupname', 'attrname'} )
- or die $dbh->errstr;
-}
+#sub sqlradius_attr_replace { no longer needed
=item export_group_replace NEW OLD
@@ -1185,6 +1161,7 @@ sub import_attrs {
SELECT groupname, attribute, op, value, \'C\' FROM radgroupcheck
UNION
SELECT groupname, attribute, op, value, \'R\' FROM radgroupreply';
+ my @fixes; # things that need to be changed on the radius db
foreach my $row ( @{ $dbh->selectall_arrayref($sql) } ) {
my ($groupname, $attrname, $op, $value, $attrtype) = @$row;
warn "$groupname.$attrname\n";
@@ -1206,6 +1183,20 @@ SELECT groupname, attribute, op, value, \'R\' FROM radgroupreply';
my $old = $a->{$attrname};
my $new;
+ if ( $attrtype eq 'R' ) {
+ # Freeradius tolerates illegal operators in reply attributes. We don't.
+ if ( !grep ($_ eq $op, FS::radius_attr->ops('R')) ) {
+ warn "$groupname.$attrname: changing $op to +=\n";
+ # Make a note to change it in the db
+ push @fixes, [
+ 'UPDATE radgroupreply SET op = \'+=\' WHERE groupname = ? AND attribute = ? AND op = ? AND VALUE = ?',
+ $groupname, $attrname, $op, $value
+ ];
+ # and import it correctly.
+ $op = '+=';
+ }
+ }
+
if ( defined $old ) {
# replace
$new = new FS::radius_attr {
@@ -1235,6 +1226,13 @@ SELECT groupname, attribute, op, value, \'R\' FROM radgroupreply';
}
$attrs_of{$groupname}->{$attrname} = $new;
} #foreach $row
+
+ foreach (@fixes) {
+ my ($sql, @args) = @$_;
+ my $sth = $dbh->prepare($sql);
+ $sth->execute(@args) or warn $sth->errstr;
+ }
+
return;
}
diff --git a/FS/FS/part_export/sqlradius_withdomain.pm b/FS/FS/part_export/sqlradius_withdomain.pm
index e5a7151a2..2af9e8d76 100644
--- a/FS/FS/part_export/sqlradius_withdomain.pm
+++ b/FS/FS/part_export/sqlradius_withdomain.pm
@@ -6,11 +6,16 @@ use FS::part_export::sqlradius;
tie my %options, 'Tie::IxHash', %FS::part_export::sqlradius::options;
+$options{'strip_tld'} = { type => 'checkbox',
+ label => 'Strip TLD from realm names',
+ };
+
%info = (
'svc' => 'svc_acct',
'desc' => 'Real-time export to SQL-backed RADIUS (FreeRADIUS, ICRADIUS) with realms',
'options' => \%options,
'nodomain' => '',
+ 'default_svc_class' => 'Internet',
'notes' => $FS::part_export::sqlradius::notes1.
'This export exports domains to RADIUS realms (see also '.
'sqlradius). '.
@@ -21,7 +26,11 @@ tie my %options, 'Tie::IxHash', %FS::part_export::sqlradius::options;
sub export_username {
my($self, $svc_acct) = (shift, shift);
- $svc_acct->email;
+ my $email = $svc_acct->email;
+ if ( $self->option('strip_tld') ) {
+ $email =~ s/\.\w+$//;
+ }
+ $email;
}
1;
diff --git a/FS/FS/part_export/textradius.pm b/FS/FS/part_export/textradius.pm
index 869c7c7dc..07de87563 100644
--- a/FS/FS/part_export/textradius.pm
+++ b/FS/FS/part_export/textradius.pm
@@ -18,6 +18,7 @@ tie my %options, 'Tie::IxHash',
'desc' =>
'Real-time export to a text /etc/raddb/users file (Livingston, Cistron)',
'options' => \%options,
+ 'default_svc_class' => 'Internet',
'notes' => <<'END'
This will edit a text RADIUS users file in place on a remote server.
Requires installation of
diff --git a/FS/FS/part_export/trango.pm b/FS/FS/part_export/trango.pm
index e7f1126dd..64d2cc4ec 100644
--- a/FS/FS/part_export/trango.pm
+++ b/FS/FS/part_export/trango.pm
@@ -68,6 +68,7 @@ tie my %options, 'Tie::IxHash', (
'svc' => 'svc_broadband',
'desc' => 'Sends SNMP SETs to a Trango AP.',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => 'Requires Net::SNMP. See the documentation for FS::part_export::trango for required virtual fields and usage information.',
);
diff --git a/FS/FS/part_export/vitelity.pm b/FS/FS/part_export/vitelity.pm
index 12c3a7fce..350a5ad48 100644
--- a/FS/FS/part_export/vitelity.pm
+++ b/FS/FS/part_export/vitelity.pm
@@ -26,6 +26,7 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_phone',
'desc' => 'Provision phone numbers to Vitelity',
'options' => \%options,
+ 'no_machine' => 1,
'notes' => <<'END'
Requires installation of
<a href="http://search.cpan.org/dist/Net-Vitelity">Net::Vitelity</a>
diff --git a/FS/FS/part_export/vpopmail.pm b/FS/FS/part_export/vpopmail.pm
index 799a8e1c1..5fca1704c 100644
--- a/FS/FS/part_export/vpopmail.pm
+++ b/FS/FS/part_export/vpopmail.pm
@@ -23,6 +23,7 @@ tie my %options, 'Tie::IxHash',
'svc' => 'svc_acct',
'desc' => 'Real-time export to vpopmail text files',
'options' => \%options,
+ 'default_svc_class' => 'Email',
'notes' => <<'END'
This export is currently unmaintained. See shellcommands_withdomain for an
export that uses vpopmail CLI commands instead.<BR>
diff --git a/FS/FS/part_export/www_plesk.pm b/FS/FS/part_export/www_plesk.pm
index ccf9b3e17..a247f054e 100644
--- a/FS/FS/part_export/www_plesk.pm
+++ b/FS/FS/part_export/www_plesk.pm
@@ -18,10 +18,11 @@ tie my %options, 'Tie::IxHash',
;
%info = (
- 'svc' => 'svc_www',
- 'desc' => 'Real-time export to Plesk managed hosting service',
- 'options'=> \%options,
- 'notes' => <<'END'
+ 'svc' => 'svc_www',
+ 'desc' => 'Real-time export to Plesk managed hosting service',
+ 'options' => \%options,
+ 'no_machine' => 1,
+ 'notes' => <<'END'
Real-time export to
<a href="http://www.swsoft.com/">Plesk</a> managed server.
Requires installation of
diff --git a/FS/FS/part_export/www_shellcommands.pm b/FS/FS/part_export/www_shellcommands.pm
index d6116aba1..bef2e9470 100644
--- a/FS/FS/part_export/www_shellcommands.pm
+++ b/FS/FS/part_export/www_shellcommands.pm
@@ -188,3 +188,4 @@ sub ssh_cmd { #subroutine, not method
'';
}
+1;
diff --git a/FS/FS/part_export_machine.pm b/FS/FS/part_export_machine.pm
new file mode 100644
index 000000000..1598e0372
--- /dev/null
+++ b/FS/FS/part_export_machine.pm
@@ -0,0 +1,155 @@
+package FS::part_export_machine;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( dbh qsearch ); #qsearchs );
+use FS::part_export;
+use FS::svc_export_machine;
+
+=head1 NAME
+
+FS::part_export_machine - Object methods for part_export_machine records
+
+=head1 SYNOPSIS
+
+ use FS::part_export_machine;
+
+ $record = new FS::part_export_machine \%hash;
+ $record = new FS::part_export_machine { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_export_machine object represents an export hostname choice.
+FS::part_export_machine inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item machinenum
+
+primary key
+
+=item exportnum
+
+Export, see L<FS::part_export>
+
+=item machine
+
+Hostname or IP address
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'part_export_machine'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+sub delete {
+ my $self = shift;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $error = $self->SUPER::delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ foreach my $svc_export_machine ( $self->svc_export_machine ) {
+ my $error = $svc_export_machine->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('machinenum')
+ || $self->ut_foreign_key('exportnum', 'part_export', 'exportnum')
+ || $self->ut_domain('machine')
+ || $self->ut_enum('disabled', [ '', 'Y' ])
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item svc_export_machine
+
+=cut
+
+sub svc_export_machine {
+ my $self = shift;
+ qsearch( 'svc_export_machine', { 'machinenum' => $self->machinenum } );
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::part_export>, L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 061001bdc..6e7f8f87e 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -103,6 +103,9 @@ inherits from FS::Record. The following fields are currently supported:
=item fcc_ds0s - Optional DS0 equivalency number for FCC form 477
+=item fcc_voip_class - Which column of FCC form 477 part II.B this package
+belongs in.
+
=item successor - Foreign key for the part_pkg that replaced this record.
If this record is not obsolete, will be null.
@@ -622,6 +625,7 @@ sub check {
: $self->ut_agentnum_acl('agentnum', \@null_agentnum_right)
)
|| $self->ut_numbern('fcc_ds0s')
+ || $self->ut_numbern('fcc_voip_class')
|| $self->ut_foreign_keyn('successor', 'part_pkg', 'pkgpart')
|| $self->ut_foreign_keyn('family_pkgpart', 'part_pkg', 'pkgpart')
|| $self->SUPER::check
@@ -1592,6 +1596,83 @@ sub _upgrade_data { # class method
}
}
+ # set any package with FCC voice lines to the "VoIP with broadband" category
+ # for backward compatibility
+ #
+ # recover from a bad upgrade bug
+ my $upgrade = 'part_pkg_fcc_voip_class_FIX';
+ if (!FS::upgrade_journal->is_done($upgrade)) {
+ my $bad_upgrade = qsearchs('upgrade_journal',
+ { upgrade => 'part_pkg_fcc_voip_class' }
+ );
+ if ( $bad_upgrade ) {
+ my $where = 'WHERE history_date <= '.$bad_upgrade->_date.
+ ' AND history_date > '.($bad_upgrade->_date - 3600);
+ my @h_part_pkg_option = map { FS::part_pkg_option->new($_->hashref) }
+ qsearch({
+ 'select' => '*',
+ 'table' => 'h_part_pkg_option',
+ 'hashref' => {},
+ 'extra_sql' => "$where AND history_action = 'delete'",
+ 'order_by' => 'ORDER BY history_date ASC',
+ });
+ my @h_pkg_svc = map { FS::pkg_svc->new($_->hashref) }
+ qsearch({
+ 'select' => '*',
+ 'table' => 'h_pkg_svc',
+ 'hashref' => {},
+ 'extra_sql' => "$where AND history_action = 'replace_old'",
+ 'order_by' => 'ORDER BY history_date ASC',
+ });
+ my %opt;
+ foreach my $deleted (@h_part_pkg_option, @h_pkg_svc) {
+ my $pkgpart ||= $deleted->pkgpart;
+ $opt{$pkgpart} ||= {
+ options => {},
+ pkg_svc => {},
+ primary_svc => '',
+ hidden_svc => {},
+ };
+ if ( $deleted->isa('FS::part_pkg_option') ) {
+ $opt{$pkgpart}{options}{ $deleted->optionname } = $deleted->optionvalue;
+ } else { # pkg_svc
+ my $svcpart = $deleted->svcpart;
+ $opt{$pkgpart}{pkg_svc}{$svcpart} = $deleted->quantity;
+ $opt{$pkgpart}{hidden_svc}{$svcpart} ||= $deleted->hidden;
+ $opt{$pkgpart}{primary_svc} = $svcpart if $deleted->primary_svc;
+ }
+ }
+ foreach my $pkgpart (keys %opt) {
+ my $part_pkg = FS::part_pkg->by_key($pkgpart);
+ my $error = $part_pkg->replace( $part_pkg->replace_old, $opt{$pkgpart} );
+ if ( $error ) {
+ die "error recovering damaged pkgpart $pkgpart:\n$error\n";
+ }
+ }
+ } # $bad_upgrade exists
+ else { # do the original upgrade, but correctly this time
+ @part_pkg = qsearch('part_pkg', {
+ fcc_ds0s => { op => '>', value => 0 },
+ fcc_voip_class => ''
+ });
+ foreach my $part_pkg (@part_pkg) {
+ $part_pkg->set(fcc_voip_class => 2);
+ my @pkg_svc = $part_pkg->pkg_svc;
+ my %quantity = map {$_->svcpart, $_->quantity} @pkg_svc;
+ my %hidden = map {$_->svcpart, $_->hidden } @pkg_svc;
+ my $error = $part_pkg->replace(
+ $part_pkg->replace_old,
+ options => { $part_pkg->options },
+ pkg_svc => \%quantity,
+ hidden_svc => \%hidden,
+ primary_svc => ($part_pkg->svcpart || ''),
+ );
+ die $error if $error;
+ }
+ }
+ FS::upgrade_journal->set_done($upgrade);
+ }
+
}
=item curuser_pkgs_sql
diff --git a/FS/FS/part_pkg/delayed_Mixin.pm b/FS/FS/part_pkg/delayed_Mixin.pm
index d28480db2..83e543a4f 100644
--- a/FS/FS/part_pkg/delayed_Mixin.pm
+++ b/FS/FS/part_pkg/delayed_Mixin.pm
@@ -2,6 +2,7 @@ package FS::part_pkg::delayed_Mixin;
use strict;
use vars qw(%info);
+use NEXT;
%info = (
'disabled' => 1,
@@ -45,7 +46,7 @@ sub calc_remain {
&& $last_bill == $cust_pkg->setup;
}
- return $self->SUPER::calc_remain($cust_pkg, %options);
+ return $self->NEXT::calc_remain($cust_pkg, %options);
}
sub can_start_date { ! shift->option('delay_setup', 1) }
diff --git a/FS/FS/part_pkg/prepaid.pm b/FS/FS/part_pkg/prepaid.pm
index 407343bc8..50f908c6d 100644
--- a/FS/FS/part_pkg/prepaid.pm
+++ b/FS/FS/part_pkg/prepaid.pm
@@ -23,7 +23,7 @@ tie my %overlimit_action, 'Tie::IxHash',
'shortname' => 'Prepaid, no automatic cycle',
'inherit_fields' => [ 'usage_Mixin', 'global_Mixin' ],
'fields' => {
- 'recur_action' => { 'name' => 'Action to take upon reaching end of prepaid preiod',
+ 'recur_action' => { 'name' => 'Action to take upon reaching end of prepaid period',
'type' => 'select',
'select_options' => \%recur_action,
},
diff --git a/FS/FS/part_pkg/prorate.pm b/FS/FS/part_pkg/prorate.pm
index f8d03dcb5..ac86f3918 100644
--- a/FS/FS/part_pkg/prorate.pm
+++ b/FS/FS/part_pkg/prorate.pm
@@ -44,12 +44,16 @@ use FS::part_pkg::flat;
sub calc_recur {
my $self = shift;
- return $self->calc_prorate(@_, $self->cutoff_day) - $self->calc_discount(@_);
+ my $cust_pkg = $_[0];
+ $self->calc_prorate(@_, $self->cutoff_day($cust_pkg))
+ - $self->calc_discount(@_);
}
sub cutoff_day {
- my $self = shift;
- split(/\s*,\s*/, $self->option('cutoff_day', 1) || '1');
+ my( $self, $cust_pkg ) = @_;
+ my $prorate_day = $cust_pkg->cust_main->prorate_day;
+ $prorate_day ? ( $prorate_day )
+ : split(/\s*,\s*/, $self->option('cutoff_day', 1) || '1');
}
1;
diff --git a/FS/FS/part_pkg/recur_Common.pm b/FS/FS/part_pkg/recur_Common.pm
index 9d7341b76..03d5c2cb2 100644
--- a/FS/FS/part_pkg/recur_Common.pm
+++ b/FS/FS/part_pkg/recur_Common.pm
@@ -39,14 +39,15 @@ sub calc_setup {
sub cutoff_day {
# prorate/subscription only; we don't support sync_bill_date here
- my $self = shift;
- my $cust_pkg = shift;
+ my( $self, $cust_pkg ) = @_;
my $recur_method = $self->option('recur_method',1) || 'anniversary';
- if ( $recur_method eq 'prorate' or $recur_method eq 'subscription' ) {
- return $self->option('cutoff_day',1) || 1;
- } else {
- return ();
- }
+ return () unless $recur_method eq 'prorate'
+ || $recur_method eq 'subscription';
+
+ #false laziness w/prorate.pm::cutoff_day
+ my $prorate_day = $cust_pkg->cust_main->prorate_day;
+ $prorate_day ? ( $prorate_day )
+ : split(/\s*,\s*/, $self->option('cutoff_day', 1) || '1');
}
sub calc_recur_Common {
diff --git a/FS/FS/part_pkg_taxrate.pm b/FS/FS/part_pkg_taxrate.pm
index e29c3d0b4..c83f700d9 100644
--- a/FS/FS/part_pkg_taxrate.pm
+++ b/FS/FS/part_pkg_taxrate.pm
@@ -384,7 +384,7 @@ sub batch_import {
}
if ( scalar( @columns ) ) {
$dbh->rollback if $oldAutoCommit;
- return "Unexpected trailing columns in line (wrong format?): $line";
+ return "Unexpected trailing columns in line (wrong format?) importing part_pkg_taxrate: $line";
}
my $error = &{$hook}(\%part_pkg_taxrate);
diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm
index dd18e87f9..7f22411e0 100644
--- a/FS/FS/part_svc.pm
+++ b/FS/FS/part_svc.pm
@@ -591,7 +591,7 @@ sub _svc_defs {
};
my $mod = $1;
- if ( $mod =~ /^svc_[A-Z]/ or $mod =~ /^svc_acct_pop$/ ) {
+ if ( $mod =~ /^svc_[A-Z]/ or $mod =~ /^(svc_acct_pop|svc_export_machine)$/ ) {
warn "skipping FS::$mod" if $DEBUG;
next;
}
diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm
index 813d096b4..b8da9b49b 100644
--- a/FS/FS/pay_batch.pm
+++ b/FS/FS/pay_batch.pm
@@ -807,8 +807,8 @@ sub try_to_resolve {
}
);
- if ( @unresolved ) {
- my $days = $conf->config('batch-auto_resolve_days') || '';
+ if ( @unresolved and $conf->exists('batch-auto_resolve_days') ) {
+ my $days = $conf->config('batch-auto_resolve_days'); # can be zero
# either 'approve' or 'decline'
my $action = $conf->config('batch-auto_resolve_status') || '';
return unless
@@ -861,6 +861,9 @@ sub prepare_for_export {
return "error updating pay_batch status: $error\n" if $error;
} elsif ($status eq 'I' && $curuser->access_right('Reprocess batches')) {
$first_download = 0;
+ } elsif ($status eq 'R' &&
+ $curuser->access_right('Redownload resolved batches')) {
+ $first_download = 0;
} else {
die "No pending batch.\n";
}
@@ -1080,7 +1083,7 @@ sub _upgrade_data {
for my $format (keys %export_info) {
my $mod = "FS::pay_batch::$format";
if ( $mod->can('_upgrade_gateway')
- and length( $conf->config("batchconfig-$format") ) ) {
+ and $conf->exists("batchconfig-$format") ) {
local $@;
my ($module, %gw_options) = $mod->_upgrade_gateway;
@@ -1109,7 +1112,7 @@ sub _upgrade_data {
# and if appropriate, make it the system default
for my $payby (qw(CARD CHEK)) {
- if ( $conf->config("batch-fixed_format-$payby") eq $format ) {
+ if ( ($conf->config("batch-fixed_format-$payby") || '') eq $format ) {
warn "Setting as default for $payby.\n";
$conf->set("batch-gateway-$payby", $gateway->gatewaynum);
$conf->delete("batch-fixed_format-$payby");
diff --git a/FS/FS/pay_batch/BoM.pm b/FS/FS/pay_batch/BoM.pm
index 7bfc22a64..719b504e5 100644
--- a/FS/FS/pay_batch/BoM.pm
+++ b/FS/FS/pay_batch/BoM.pm
@@ -31,13 +31,13 @@ $name = 'BoM';
},
header => sub {
my $pay_batch = shift;
- sprintf( "A%10s%04u%06u%05u%54s\n",
+ sprintf( "A%10s%04u%06u%05u%54s\n", #80
$origid,
$pay_batch->batchnum,
jdate($pay_batch->download),
$datacenter,
"") .
- sprintf( "XD%03u%06u%-15s%-30s%09u%-12s \n",
+ sprintf( "XD%03u%06u%-15s%-30s%09u%-12s \n", #80
$typecode,
jdate($pay_batch->download),
$shortname,
@@ -48,7 +48,7 @@ $name = 'BoM';
row => sub {
my ($cust_pay_batch, $pay_batch) = @_;
my ($account, $aba) = split('@', $cust_pay_batch->payinfo);
- sprintf( "D%010.0f%09u%-12s%-29s%-19s\n",
+ sprintf( "D%010.0f%09u%-12s%-29s%-19s\n", #80
$cust_pay_batch->amount * 100,
$aba,
$account,
@@ -58,8 +58,8 @@ $name = 'BoM';
},
footer => sub {
my ($pay_batch, $batchcount, $batchtotal) = @_;
- sprintf( "YD%08u%014.0f%56s\n", $batchcount, $batchtotal*100, "").
- sprintf( "Z%014u%04u%014u%05u%41s\n",
+ sprintf( "YD%08u%014.0f%56s\n", $batchcount, $batchtotal*100, ""). #80
+ sprintf( "Z%014u%04u%014u%05u%42s\n", #80 now
$batchtotal*100, $batchcount, "0", "0", "");
},
);
diff --git a/FS/FS/pay_batch/td_eft1464.pm b/FS/FS/pay_batch/td_eft1464.pm
index 3a6befef5..93612f1ea 100644
--- a/FS/FS/pay_batch/td_eft1464.pm
+++ b/FS/FS/pay_batch/td_eft1464.pm
@@ -154,5 +154,14 @@ $name = 'td_eft1464';
},
);
+sub _upgrade_gateway {
+ my $conf = FS::Conf->new;
+ my @batchconfig = $conf->config('batchconfig-td_eft1464');
+ my %options;
+ @options{ qw(originator datacentre short_name long_name return_branch
+ return_account cpa_code) } = @batchconfig;
+ ( 'TD_EFT', %options );
+}
+
1;
diff --git a/FS/FS/quotation.pm b/FS/FS/quotation.pm
index ccaa1c34b..bf2711b0a 100644
--- a/FS/FS/quotation.pm
+++ b/FS/FS/quotation.pm
@@ -142,13 +142,39 @@ sub cust_main {
=cut
-sub cust_bill_pkg {
+sub cust_bill_pkg { #actually quotation_pkg objects
my $self = shift;
- #actually quotation_pkg objects
qsearch('quotation_pkg', { quotationnum=>$self->quotationnum });
}
-=back
+=item total_setup
+
+=cut
+
+sub total_setup {
+ my $self = shift;
+ $self->_total('setup');
+}
+
+=item total_recur [ FREQ ]
+
+=cut
+
+sub total_recur {
+ my $self = shift;
+#=item total_recur [ FREQ ]
+ #my $freq = @_ ? shift : '';
+ $self->_total('recur');
+}
+
+sub _total {
+ my( $self, $method ) = @_;
+
+ my $total = 0;
+ $total += $_->$method() for $self->cust_bill_pkg;
+ sprintf('%.2f', $total);
+
+}
=item enable_previous
@@ -156,6 +182,130 @@ sub cust_bill_pkg {
sub enable_previous { 0 }
+=back
+
+=head1 CLASS METHODS
+
+=over 4
+
+
+=item search_sql_where HASHREF
+
+Class method which returns an SQL WHERE fragment to search for parameters
+specified in HASHREF. Valid parameters are
+
+=over 4
+
+=item _date
+
+List reference of start date, end date, as UNIX timestamps.
+
+=item invnum_min
+
+=item invnum_max
+
+=item agentnum
+
+=item charged
+
+List reference of charged limits (exclusive).
+
+=item owed
+
+List reference of charged limits (exclusive).
+
+=item open
+
+flag, return open invoices only
+
+=item net
+
+flag, return net invoices only
+
+=item days
+
+=item newest_percust
+
+=back
+
+Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
+
+=cut
+
+sub search_sql_where {
+ my($class, $param) = @_;
+ #if ( $DEBUG ) {
+ # warn "$me search_sql_where called with params: \n".
+ # join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
+ #}
+
+ my @search = ();
+
+ #agentnum
+ if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
+ push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
+ }
+
+# #refnum
+# if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
+# push @search, "cust_main.refnum = $1";
+# }
+
+ #prospectnum
+ if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
+ push @search, "quotation.prospectnum = $1";
+ }
+
+ #custnum
+ if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
+ push @search, "cust_bill.custnum = $1";
+ }
+
+ #_date
+ if ( $param->{_date} ) {
+ my($beginning, $ending) = @{$param->{_date}};
+
+ push @search, "quotation._date >= $beginning",
+ "quotation._date < $ending";
+ }
+
+ #quotationnum
+ if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
+ push @search, "quotation.quotationnum >= $1";
+ }
+ if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
+ push @search, "quotation.quotationnum <= $1";
+ }
+
+# #charged
+# if ( $param->{charged} ) {
+# my @charged = ref($param->{charged})
+# ? @{ $param->{charged} }
+# : ($param->{charged});
+#
+# push @search, map { s/^charged/cust_bill.charged/; $_; }
+# @charged;
+# }
+
+ my $owed_sql = FS::cust_bill->owed_sql;
+
+ #days
+ push @search, "quotation._date < ". (time-86400*$param->{'days'})
+ if $param->{'days'};
+
+ #agent virtualization
+ my $curuser = $FS::CurrentUser::CurrentUser;
+ #false laziness w/search/quotation.html
+ push @search,' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
+ ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
+ ' ) ';
+
+ join(' AND ', @search );
+
+}
+
+=back
+
=head1 BUGS
=head1 SEE ALSO
diff --git a/FS/FS/radius_group.pm b/FS/FS/radius_group.pm
index 37aa0f37b..f1a4efe7f 100644
--- a/FS/FS/radius_group.pm
+++ b/FS/FS/radius_group.pm
@@ -47,6 +47,8 @@ description
priority - for export
+=item speed_up, speed_down - connection speeds in bits per second. Some
+exports may use this to generate appropriate RADIUS attributes.
=back
@@ -176,6 +178,8 @@ sub check {
|| $self->ut_text('groupname')
|| $self->ut_textn('description')
|| $self->ut_numbern('priority')
+ || $self->ut_numbern('speed_up')
+ || $self->ut_numbern('speed_down')
;
return $error if $error;
diff --git a/FS/FS/rate.pm b/FS/FS/rate.pm
index 02d8250eb..a2511cf99 100644
--- a/FS/FS/rate.pm
+++ b/FS/FS/rate.pm
@@ -387,7 +387,7 @@ sub rate_detail {
=item process
-Experimental job-queue processor for web interface adds/edits
+Job-queue processor for web interface adds/edits
=cut
diff --git a/FS/FS/reason.pm b/FS/FS/reason.pm
index 377da4985..a9a7d745d 100644
--- a/FS/FS/reason.pm
+++ b/FS/FS/reason.pm
@@ -46,6 +46,15 @@ FS::Record. The following fields are currently supported:
=item disabled - 'Y' or ''
+=item unsuspend_pkgpart - for suspension reasons only, the pkgpart (see
+L<FS::part_pkg>) of a package to be ordered when the package is unsuspended.
+Typically this will be some kind of reactivation fee. Attaching it to
+a suspension reason allows the reactivation fee to be charged for some
+suspensions but not others.
+
+=item unsuspend_hold - 'Y' or ''. If unsuspend_pkgpart is set, this tells
+whether to bill the unsuspend package immediately ('') or to wait until
+the customer's next invoice ('Y').
=back
@@ -97,16 +106,30 @@ sub check {
my $error =
$self->ut_numbern('reasonnum')
+ || $self->ut_number('reason_type')
+ || $self->ut_foreign_key('reason_type', 'reason_type', 'typenum')
|| $self->ut_text('reason')
+ || $self->ut_flag('disabled')
;
return $error if $error;
+ if ( $self->reasontype->class eq 'S' ) {
+ $error = $self->ut_numbern('unsuspend_pkgpart')
+ || $self->ut_foreign_keyn('unsuspend_pkgpart', 'part_pkg', 'pkgpart')
+ || $self->ut_flag('unsuspend_hold')
+ ;
+ return $error if $error;
+ } else {
+ $self->set('unsuspend_pkgpart' => '');
+ $self->set('unsuspend_hold' => '');
+ }
+
$self->SUPER::check;
}
=item reasontype
-Returns the reason_type (see <I>FS::reason_type</I>) associated with this reason.
+Returns the reason_type (see L<FS::reason_type>) associated with this reason.
=cut
@@ -118,7 +141,7 @@ sub reasontype {
=head1 BUGS
-Here be termintes. Don't use on wooden computers.
+Here by termintes. Don't use on wooden computers.
=head1 SEE ALSO
diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm
index a6daf44c8..7aede54a6 100644
--- a/FS/FS/svc_Common.pm
+++ b/FS/FS/svc_Common.pm
@@ -200,12 +200,13 @@ I<depend_jobnum>.
If I<jobnum> is set to an array reference, the jobnums of any export jobs will
be added to the referenced array.
-If I<child_objects> is set to an array reference of FS::tablename objects (for
-example, FS::acct_snarf objects), they will have their svcnum field set and
-will be inserted after this record, but before any exports are run. Each
-element of the array can also optionally be a two-element array reference
-containing the child object and the name of an alternate field to be filled in
-with the newly-inserted svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
+If I<child_objects> is set to an array reference of FS::tablename objects
+(for example, FS::svc_export_machine or FS::acct_snarf objects), they
+will have their svcnum field set and will be inserted after this record,
+but before any exports are run. Each element of the array can also
+optionally be a two-element array reference containing the child object
+and the name of an alternate field to be filled in with the newly-inserted
+svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
jobnums), all provisioning jobs will have a dependancy on the supplied
@@ -439,7 +440,16 @@ sub expire {
Replaces OLD_RECORD with this one. If there is an error, returns the error,
otherwise returns false.
-Currently available options are: I<export_args> and I<depend_jobnum>.
+Currently available options are: I<child_objects>, I<export_args> and
+I<depend_jobnum>.
+
+If I<child_objects> is set to an array reference of FS::tablename objects
+(for example, FS::svc_export_machine or FS::acct_snarf objects), they
+will have their svcnum field set and will be inserted or replaced after
+this record, but before any exports are run. Each element of the array
+can also optionally be a two-element array reference containing the
+child object and the name of an alternate field to be filled in with
+the newly-inserted svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
jobnums), all provisioning jobs will have a dependancy on the supplied
@@ -462,6 +472,8 @@ sub replace {
? shift
: { @_ };
+ my $objects = $options->{'child_objects'} || [];
+
my @jobnums = ();
local $FS::queue::jobnums = \@jobnums;
warn "[$me] replace: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
@@ -511,6 +523,34 @@ sub replace {
return $error;
}
+ foreach my $object ( @$objects ) {
+ my($field, $obj);
+ if ( ref($object) eq 'ARRAY' ) {
+ ($obj, $field) = @$object;
+ } else {
+ $obj = $object;
+ $field = 'svcnum';
+ }
+ $obj->$field($new->svcnum);
+
+ my $oldobj = qsearchs( $obj->table, {
+ $field => $new->svcnum,
+ map { $_ => $obj->$_ } $obj->_svc_child_partfields,
+ });
+
+ if ( $oldobj ) {
+ my $pkey = $oldobj->primary_key;
+ $obj->$pkey($oldobj->$pkey);
+ $obj->replace($oldobj);
+ } else {
+ $error = $obj->insert;
+ }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
#new-style exports!
unless ( $noexport_hack ) {
diff --git a/FS/FS/svc_Tower_Mixin.pm b/FS/FS/svc_Tower_Mixin.pm
index 0b5588466..6adbc6f5e 100644
--- a/FS/FS/svc_Tower_Mixin.pm
+++ b/FS/FS/svc_Tower_Mixin.pm
@@ -52,5 +52,4 @@ sub tower_sector_sql {
@where;
}
-
1;
diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm
index e67db43c6..7ce79ae01 100644
--- a/FS/FS/svc_acct.pm
+++ b/FS/FS/svc_acct.pm
@@ -2808,6 +2808,13 @@ Arrayref of additional WHERE clauses, will be ANDed together.
sub search {
my ($class, $params) = @_;
+ my @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 ) ',
+ );
+
my @where = ();
# domain
@@ -2852,9 +2859,17 @@ sub search {
push @where, "svcpart = $1";
}
+ if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
+ push @from, ' LEFT JOIN export_svc USING ( svcpart )';
+ push @where, "exportnum = $1";
+ }
+
# sector and tower
my @where_sector = $class->tower_sector_sql($params);
- push @where, @where_sector if @where_sector;
+ if ( @where_sector ) {
+ push @where, @where_sector;
+ push @from, ' LEFT JOIN tower_sector USING ( sectornum )';
+ }
# here is the agent virtualization
#if ($params->{CurrentUser}) {
@@ -2875,16 +2890,9 @@ sub search {
push @where, @{ $params->{'where'} } if $params->{'where'};
+ my $addl_from = join(' ', @from);
my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
- 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 ) ';
-
- $addl_from .= ' LEFT JOIN tower_sector USING ( sectornum )'
- if @where_sector;
-
my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql";
#if ( keys %svc_acct ) {
# $count_query .= ' WHERE '.
diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm
index 82102697d..26659d52a 100755
--- a/FS/FS/svc_broadband.pm
+++ b/FS/FS/svc_broadband.pm
@@ -245,6 +245,12 @@ sub search {
push @where, "svcpart = $1";
}
+ #exportnum
+ if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
+ push @from, 'LEFT JOIN export_svc USING ( svcpart )';
+ push @where, "exportnum = $1";
+ }
+
#ip_addr
if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
push @where, "ip_addr = '$1'";
diff --git a/FS/FS/svc_export_machine.pm b/FS/FS/svc_export_machine.pm
new file mode 100644
index 000000000..10f7b6821
--- /dev/null
+++ b/FS/FS/svc_export_machine.pm
@@ -0,0 +1,124 @@
+package FS::svc_export_machine;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearchs ); #qsearch );
+use FS::cust_svc;
+use FS::part_export;
+use FS::part_export_machine;
+
+sub _svc_child_partfields { ('exportnum') };
+
+=head1 NAME
+
+FS::svc_export_machine - Object methods for svc_export_machine records
+
+=head1 SYNOPSIS
+
+ use FS::svc_export_machine;
+
+ $record = new FS::svc_export_machine \%hash;
+ $record = new FS::svc_export_machine { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::svc_export_machine object represents a customer service export
+hostname. FS::svc_export_machine inherits from FS::Record. The following
+fields are currently supported:
+
+=over 4
+
+=item svcexportmachinenum
+
+primary key
+
+=item svcnum
+
+Customer service, see L<FS::cust_svc>
+
+=item machinenum
+
+Export hostname, see L<FS::part_export_machine>
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'svc_export_machine'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('svcexportmachinenum')
+ || $self->ut_foreign_key('svcnum', 'cust_svc', 'svcnum' )
+ || $self->ut_foreign_key('exportnum', 'part_export', 'exportnum' )
+ || $self->ut_foreign_key('machinenum', 'part_export_machine', 'machinenum')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item part_export_machine
+
+=cut
+
+sub part_export_machine {
+ my $self = shift;
+ qsearchs('part_export_machine', { 'machinenum' => $self->machinenum } );
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::cust_svc>, L<FS::part_export_machine>, L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/tax_class.pm b/FS/FS/tax_class.pm
index 4f0396982..bfec2c06c 100644
--- a/FS/FS/tax_class.pm
+++ b/FS/FS/tax_class.pm
@@ -339,7 +339,7 @@ sub batch_import {
}
if ( scalar( @columns ) ) {
$dbh->rollback if $oldAutoCommit;
- return "Unexpected trailing columns in line (wrong format?): $line";
+ return "Unexpected trailing columns in line (wrong format?) importing tax_class: $line";
}
my $error = &{$hook}(\%tax_class);
diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm
index e9496e4f5..a5a623d94 100644
--- a/FS/FS/tax_rate.pm
+++ b/FS/FS/tax_rate.pm
@@ -10,6 +10,7 @@ use DateTime::Format::Strptime;
use Storable qw( thaw nfreeze );
use IO::File;
use File::Temp;
+use Text::CSV_XS;
use LWP::UserAgent;
use HTTP::Request;
use HTTP::Response;
@@ -637,6 +638,7 @@ sub batch_import {
$count *=2;
if ( $format eq 'cch' || $format eq 'cch-update' ) {
+ #false laziness w/below (sub _perform_cch_diff)
@fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax
excessrate effective_date taxauth taxtype taxcat taxname
usetax useexcessrate fee unittype feemax maxtype passflag
@@ -715,9 +717,6 @@ sub batch_import {
die "unknown format $format";
}
- eval "use Text::CSV_XS;";
- die $@ if $@;
-
my $csv = new Text::CSV_XS;
my $imported = 0;
@@ -758,9 +757,10 @@ sub batch_import {
foreach my $field ( @fields ) {
$tax_rate{$field} = shift @columns;
}
+
if ( scalar( @columns ) ) {
$dbh->rollback if $oldAutoCommit;
- return "Unexpected trailing columns in line (wrong format?): $line";
+ return "Unexpected trailing columns in line (wrong format?) importing tax_rate: $line";
}
my $error = &{$hook}(\%tax_rate);
@@ -1115,8 +1115,26 @@ sub _perform_cch_diff {
}
close $newcsvfh;
- for (keys %oldlines) {
- print $dfh $_, ',"D"', "\n" if $oldlines{$_};
+ #false laziness w/above (sub batch_import)
+ my @fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax
+ excessrate effective_date taxauth taxtype taxcat taxname
+ usetax useexcessrate fee unittype feemax maxtype passflag
+ passtype basetype );
+ my $numfields = scalar(@fields);
+
+ my $csv = new Text::CSV_XS { 'always_quote' => 1 };
+
+ for my $line (grep $oldlines{$_}, keys %oldlines) {
+
+ $csv->parse($line) or do {
+ #$dbh->rollback if $oldAutoCommit;
+ die "can't parse: ". $csv->error_input();
+ };
+ my @columns = $csv->fields();
+
+ $csv->combine( splice(@columns, 0, $numfields) );
+
+ print $dfh $csv->string, ',"D"', "\n";
}
close $dfh;
@@ -1170,9 +1188,6 @@ sub _cch_fetch_and_unzip {
sub _cch_extract_csv_from_dbf {
my ( $job, $dir, $name ) = @_;
- eval "use Text::CSV_XS;";
- die $@ if $@;
-
eval "use XBase;";
die $@ if $@;
@@ -1635,16 +1650,16 @@ sub process_download_and_update {
if (-d $dir) {
- if (-d "$dir.4") {
- opendir(my $dirh, "$dir.4") or die "failed to open $dir.4: $!\n";
+ if (-d "$dir.9") {
+ opendir(my $dirh, "$dir.9") or die "failed to open $dir.9: $!\n";
foreach my $file (readdir($dirh)) {
- unlink "$dir.4/$file" if (-f "$dir.4/$file");
+ unlink "$dir.9/$file" if (-f "$dir.9/$file");
}
closedir($dirh);
- rmdir "$dir.4";
+ rmdir "$dir.9";
}
- for (3, 2, 1) {
+ for (8, 7, 6, 5, 4, 3, 2, 1) {
if ( -e "$dir.$_" ) {
rename "$dir.$_", "$dir.". ($_+1) or die "can't rename $dir.$_: $!\n";
}
diff --git a/FS/FS/tax_rate_location.pm b/FS/FS/tax_rate_location.pm
index 1a6c47dcf..b4be8b90e 100644
--- a/FS/FS/tax_rate_location.pm
+++ b/FS/FS/tax_rate_location.pm
@@ -301,7 +301,7 @@ sub batch_import {
}
if ( scalar( @columns ) ) {
$dbh->rollback if $oldAutoCommit;
- return "Unexpected trailing columns in line (wrong format?): $line";
+ return "Unexpected trailing columns in line (wrong format?) importing tax-rate_location: $line";
}
my $error = &{$hook}(\%tax_rate_location);
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 590874d46..b5ee87e93 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -94,6 +94,7 @@ FS/h_cust_pkg_reason.pm
FS/h_cust_svc.pm
FS/h_cust_tax_exempt.pm
FS/h_domain_record.pm
+FS/h_part_pkg.pm
FS/h_svc_acct.pm
FS/h_svc_broadband.pm
FS/h_svc_domain.pm
@@ -649,3 +650,26 @@ FS/quotation_pkg_discount.pm
t/quotation_pkg_discount.t
FS/Quotable_Mixin.pm
t/Quotable_Mixin.t
+FS/cust_bill_void.pm
+t/cust_bill_void.t
+FS/cust_bill_pkg_void.pm
+t/cust_bill_pkg_void.t
+FS/cust_bill_pkg_detail_void.pm
+t/cust_bill_pkg_detail_void.t
+FS/cust_bill_pkg_display_void.pm
+t/cust_bill_pkg_display_void.t
+FS/cust_bill_pkg_tax_location_void.pm
+t/cust_bill_pkg_tax_location_void.t
+FS/cust_bill_pkg_tax_rate_location_void.pm
+t/cust_bill_pkg_tax_rate_location_void.t
+FS/cust_tax_exempt_pkg_void.pm
+t/cust_tax_exempt_pkg_void.t
+FS/cust_bill_pkg_discount_void.pm
+t/cust_bill_pkg_discount_void.t
+FS/Trace.pm
+FS/agent_pkg_class.pm
+t/agent_pkg_class.t
+FS/part_export_machine.pm
+t/part_export_machine.t
+FS/svc_export_machine.pm
+t/svc_export_machine.t
diff --git a/FS/bin/freeside-cdrd b/FS/bin/freeside-cdrd
index 2cf75f31c..b21bd5b07 100644
--- a/FS/bin/freeside-cdrd
+++ b/FS/bin/freeside-cdrd
@@ -108,7 +108,7 @@ while (1) {
}
myexit() if sigterm() || sigint();
- sleep 1 unless $found;
+ sleep 5 unless $found;
}
diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily
index 2b33d1671..8e8ae4ff9 100755
--- a/FS/bin/freeside-daily
+++ b/FS/bin/freeside-daily
@@ -7,7 +7,7 @@ use FS::Conf;
&untaint_argv; #what it sounds like (eww)
use vars qw(%opt);
-getopts("p:a:d:vl:sy:nmrkg:uo", \%opt);
+getopts("p:a:d:vl:sy:nmrkg:o", \%opt);
my $user = shift or die &usage;
adminsuidsetup $user;
@@ -51,16 +51,6 @@ unless ( $opt{k} ) {
notify_flat_delay(%opt);
}
-#debian Pg 8.1+ auto-vaccums, 7.4 w/postgresql-contrib
-if ( $opt{u} ) {
- use FS::Cron::vacuum qw(vacuum);
- vacuum();
-}
-
-#you can skip this just by not having the config
-use FS::Cron::backup qw(backup);
-backup();
-
#same
use FS::Cron::rt_tasks qw(rt_daily);
rt_daily(%opt);
@@ -70,11 +60,20 @@ use FS::Cron::pay_batch qw(batch_submit batch_receive);
batch_submit(%opt);
batch_receive(%opt);
+#you can skip this by not having the config
+use FS::Cron::agent_email qw(agent_email);
+agent_email(%opt);
+
my $deldir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/";
unlink <${deldir}.invoice*>;
unlink <${deldir}.letter*>;
unlink <${deldir}.CGItemp*>;
+#backup should be last
+#you can skip this just by not having the config
+use FS::Cron::backup qw(backup);
+backup();
+
###
# subroutines
###
@@ -145,8 +144,6 @@ the bill and collect methods of a cust_main object. See L<FS::cust_main>.
-k: skip notify_flat_delay
- -u: Do a vacuum (starting with version 1.9, this is not run by default).
-
user: From the mapsecrets file - see config.html from the base documentation
custnum: if one or more customer numbers are specified, only bills those
diff --git a/FS/t/agent_pkg_class.t b/FS/t/agent_pkg_class.t
new file mode 100644
index 000000000..dc0fa12b2
--- /dev/null
+++ b/FS/t/agent_pkg_class.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::agent_pkg_class;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_bill_pkg_detail_void.t b/FS/t/cust_bill_pkg_detail_void.t
new file mode 100644
index 000000000..bd58c4eab
--- /dev/null
+++ b/FS/t/cust_bill_pkg_detail_void.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_pkg_detail_void;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_bill_pkg_discount_void.t b/FS/t/cust_bill_pkg_discount_void.t
new file mode 100644
index 000000000..e591eb03d
--- /dev/null
+++ b/FS/t/cust_bill_pkg_discount_void.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_pkg_discount_void;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_bill_pkg_display_void.t b/FS/t/cust_bill_pkg_display_void.t
new file mode 100644
index 000000000..87403e12e
--- /dev/null
+++ b/FS/t/cust_bill_pkg_display_void.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_pkg_display_void;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_bill_pkg_tax_location_void.t b/FS/t/cust_bill_pkg_tax_location_void.t
new file mode 100644
index 000000000..dbfea5131
--- /dev/null
+++ b/FS/t/cust_bill_pkg_tax_location_void.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_pkg_tax_location_void;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_bill_pkg_tax_rate_location_void.t b/FS/t/cust_bill_pkg_tax_rate_location_void.t
new file mode 100644
index 000000000..8ebda6528
--- /dev/null
+++ b/FS/t/cust_bill_pkg_tax_rate_location_void.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_pkg_tax_rate_location_void;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_bill_pkg_void.t b/FS/t/cust_bill_pkg_void.t
new file mode 100644
index 000000000..9256b469f
--- /dev/null
+++ b/FS/t/cust_bill_pkg_void.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_pkg_void;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_bill_void.t b/FS/t/cust_bill_void.t
new file mode 100644
index 000000000..95ff4a45c
--- /dev/null
+++ b/FS/t/cust_bill_void.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_void;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_tax_exempt_pkg_void.t b/FS/t/cust_tax_exempt_pkg_void.t
new file mode 100644
index 000000000..42d86205f
--- /dev/null
+++ b/FS/t/cust_tax_exempt_pkg_void.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_tax_exempt_pkg_void;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export_machine.t b/FS/t/part_export_machine.t
new file mode 100644
index 000000000..792bb5092
--- /dev/null
+++ b/FS/t/part_export_machine.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export_machine;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_export_machine.t b/FS/t/svc_export_machine.t
new file mode 100644
index 000000000..5279be2ca
--- /dev/null
+++ b/FS/t/svc_export_machine.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_export_machine;
+$loaded=1;
+print "ok 1\n";
diff --git a/Makefile b/Makefile
index 95ffbf27b..10c06ebb0 100644
--- a/Makefile
+++ b/Makefile
@@ -67,6 +67,7 @@ HTTPD_RESTART = /etc/init.d/apache2 restart
#(an include directory, not a file, "Include /etc/apache/conf.d" in httpd.conf)
#deb (3.1+), apache2
APACHE_CONF = /etc/apache2/conf.d
+INSSERV_OVERRIDE = /etc/insserv/overrides
FREESIDE_RESTART = ${INIT_FILE} restart
@@ -168,6 +169,12 @@ install-docs: check-conflicts docs
cp -r masondocs ${FREESIDE_DOCUMENT_ROOT}
chown -R freeside:freeside ${FREESIDE_DOCUMENT_ROOT}
cp htetc/handler.pl ${MASON_HANDLER}
+ perl -p -i -e "\
+ s|%%%FREESIDE_EXPORT%%%|${FREESIDE_EXPORT}|g;\
+ s'%%%RT_ENABLED%%%'${RT_ENABLED}'g; \
+ " ${MASON_HANDLER} || true
+ mkdir -p ${FREESIDE_EXPORT}/profile
+ chown freeside ${FREESIDE_EXPORT}/profile
cp htetc/htpasswd.logout ${FREESIDE_CONF}
[ ! -e ${MASONDATA} ] && mkdir ${MASONDATA} || true
chown -R freeside ${MASONDATA}
@@ -179,6 +186,8 @@ dev-docs:
perl -p -i -e "\
s'###use Module::Refresh;###'use Module::Refresh;'; \
s'###Module::Refresh->refresh;###'Module::Refresh->refresh;'; \
+ s|%%%FREESIDE_EXPORT%%%|${FREESIDE_EXPORT}|g;\
+ s'%%%RT_ENABLED%%%'${RT_ENABLED}'g; \
" ${MASON_HANDLER} || true
perl-modules:
@@ -238,9 +247,9 @@ dev-perl-modules: perl-modules
ln -sf ${FREESIDE_PATH}/FS/blib/lib/FS ${PERL_INC_DEV_KLUDGE}/FS
install-texmf:
- install -D -o freeside -m 444 etc/fslongtable.sty \
- `kpsewhich -expand-var \\\$$TEXMFLOCAL`/tex/generic/fslongtable.sty
- texhash `kpsewhich -expand-var \\\$$TEXMFLOCAL`
+ install -D -o freeside -m 444 etc/longtable.sty \
+ ~freeside/texmf/tex/longtable.sty
+ texhash ~freeside
install-init:
#[ -e ${INIT_FILE} ] || install -o root -g ${INSTALLGROUP} -m 711 init.d/freeside-init ${INIT_FILE}
@@ -264,6 +273,7 @@ install-apache:
s'%%%MASON_HANDLER%%%'${MASON_HANDLER}'g; \
" ${APACHE_CONF}/freeside-*.conf \
) || true
+ [ -d ${INSSERV_OVERRIDE} ] && [ -x /sbin/insserv ] && ( install -o root -m 755 init.d/insserv-override-apache2 ${INSSERV_OVERRIDE}/apache2 && insserv -d ) || true
install-selfservice:
[ -e ~freeside ] || cp -pr /etc/skel ~freeside && chown -R freeside ~freeside
diff --git a/bin/231commit b/bin/231commit
index ca28ede1e..6d09863ca 100755
--- a/bin/231commit
+++ b/bin/231commit
@@ -20,8 +20,8 @@ die "no files!" unless @ARGV;
system join('',
"( cd /home/$USER/freeside2.3/$prefix; git pull ) && ",
"( cd /home/$USER/freeside2.1/$prefix; git pull ) && ",
- "git diff -u @ARGV | ( cd /home/$USER/freeside2.3/$prefix; patch ) ",
- " && git diff -u @ARGV | ( cd /home/$USER/freeside2.1/$prefix; patch ) ",
+ "git diff -u @ARGV | ( cd /home/$USER/freeside2.3/$prefix; patch -p1 ) ",
+ " && git diff -u @ARGV | ( cd /home/$USER/freeside2.1/$prefix; patch -p1 ) ",
" && ( ( git commit -m $desc @ARGV && git push); ",
"( cd /home/$USER/freeside2.3/$prefix; git commit -m $desc @ARGV && git push); ",
"( cd /home/$USER/freeside2.1/$prefix; git commit -m $desc @ARGV && git push) )"
diff --git a/bin/23diff b/bin/23diff
index 0c0575aa6..d38c84834 100755
--- a/bin/23diff
+++ b/bin/23diff
@@ -3,7 +3,8 @@
my $file = shift;
chomp(my $dir = `pwd`);
-$dir =~ s/freeside\//freeside2.3\//;
+$dir =~ s/freeside(\/?)/freeside2.3$1/;
+warn $dir;
#$cmd = "diff -u $file $dir/$file";
$cmd = "diff -u $dir/$file $file";
diff --git a/bin/agent_email b/bin/agent_email
new file mode 100755
index 000000000..2fe47c4ba
--- /dev/null
+++ b/bin/agent_email
@@ -0,0 +1,30 @@
+#!/usr/bin/perl
+
+use strict;
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup);
+
+&untaint_argv; #what it sounds like (eww)
+use vars qw(%opt);
+getopts("a:", \%opt);
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+use FS::Cron::agent_email qw(agent_email);
+agent_email(%opt);
+
+###
+# subroutines
+###
+
+sub untaint_argv {
+ foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+ #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+ # Date::Parse
+ $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+ $ARGV[$_]=$1;
+ }
+}
+
+1;
diff --git a/bin/cdr.import b/bin/cdr.import
index 36266efbf..36266efbf 100644..100755
--- a/bin/cdr.import
+++ b/bin/cdr.import
diff --git a/bin/cust_bill.export b/bin/cust_bill.export
new file mode 100755
index 000000000..40c32e539
--- /dev/null
+++ b/bin/cust_bill.export
@@ -0,0 +1,49 @@
+#!/usr/bin/perl
+
+use strict;
+use Text::CSV_XS;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::cust_bill;
+use Date::Format;
+
+my @fields = qw(
+ invnum
+ custnum
+);
+
+push @fields,
+ { 'header' => 'Date',
+ 'callback' => sub { time2str('%x', shift->_date); },
+ },
+;
+
+push @fields, qw( charged owed );
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $agentnum = shift or die &usage;
+
+my $csv = new Text::CSV_XS;
+
+$csv->combine( map { ref($_) ? $_->{'header'} : $_ } @fields ) or die;
+print $csv->string."\n";
+
+my @cust_bill = qsearch({
+ 'table' => 'cust_bill',
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => {},
+ 'extra_sql' => "WHERE cust_main.agentnum = $agentnum",
+});
+
+foreach my $cust_bill ( @cust_bill ) {
+ $csv->combine( map { ref($_) ? &{$_->{'callback'}}($cust_bill)
+ : $cust_bill->$_()
+ }
+ @fields
+ ) or die;
+ print $csv->string."\n";
+}
+
+1;
diff --git a/bin/cust_main-bill_now b/bin/cust_main-bill_now
index 17e48fbcf..f8a15803b 100644..100755
--- a/bin/cust_main-bill_now
+++ b/bin/cust_main-bill_now
@@ -13,7 +13,9 @@ my $custnum = shift or die &usage;
my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
or die "unknown custnum $custnum\n";
-$cust_main->bill_and_collect( debug=>2, check_freq=>'1d' );
+$FS::cust_main::DEBUG = 3;
+
+$cust_main->bill_and_collect( debug=>3, check_freq=>'1d' );
sub usage {
die "Usage:\n cust_main-bill_now user custnum\n";
diff --git a/bin/cust_main.export b/bin/cust_main.export
new file mode 100755
index 000000000..4adfeeb7c
--- /dev/null
+++ b/bin/cust_main.export
@@ -0,0 +1,109 @@
+#!/usr/bin/perl
+
+use strict;
+use Text::CSV_XS;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::cust_main;
+
+my @fields = qw(
+ custnum
+ status
+ last
+ first
+ company
+ address1
+ address2
+ city
+ county
+ state
+ zip
+ country
+ daytime
+ night
+ mobile
+ fax
+ ship_address1
+ ship_address2
+ ship_city
+ ship_county
+ ship_state
+ ship_zip
+ ship_country
+ ship_daytime
+ ship_night
+ ship_mobile
+ ship_fax
+ invoicing_list_emailonly_scalar
+ payby
+ balance
+);
+
+push @fields,
+ #Billing Type: Credit Card
+ { 'header' => 'Credit Card number',
+ 'callback' => sub { my $c_m = shift;
+ $c_m->payby =~ /^(CARD|DCRD)$/ ? $c_m->payinfo : '' ;
+ },
+ },
+ { 'header' => 'Expiration on card',
+ 'callback' => sub { my $c_m = shift;
+ return '' unless $c_m->payby =~ /^(CARD|DCRD)$/;
+ $c_m->paydate =~ /^(\d{4})-(\d{2})-\d{2}$/ or die;
+ return "$2/$1";
+ },
+ },
+ { 'header' => 'Name on card',
+ 'callback' => sub { my $c_m = shift;
+ $c_m->payby =~ /^(CARD|DCRD)$/ ? $c_m->paydname : '' ;
+ },
+ },
+
+ #Billing Type: Electronic check
+ { 'header' => 'ABA/Routing number',
+ 'callback' => sub { my $c_m = shift;
+ return '' unless $c_m->payby =~ /^(CHEK|DCHK)$/;
+ (split('@', $c_m->payinfo))[1];
+ },
+ },
+ { 'header' => 'Account number',
+ 'callback' => sub { my $c_m = shift;
+ return '' unless $c_m->payby =~ /^(CHEK|DCHK)$/;
+ (split('@', $c_m->payinfo))[0];
+ },
+ },
+ { 'header' => 'Account type',
+ 'callback' => sub { my $c_m = shift;
+ $c_m->payby =~ /^(CHEK|DCHK)$/ ? $c_m->paytype : '';
+ },
+ },
+ { 'header' => 'Bank Name',
+ 'callback' => sub { my $c_m = shift;
+ $c_m->payby =~ /^(CHEK|DCHK)$/ ? $c_m->payname : '';
+ },
+ },
+
+;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $agentnum = shift or die &usage;
+
+my $csv = new Text::CSV_XS;
+
+$csv->combine( map { ref($_) ? $_->{'header'} : $_ } @fields ) or die;
+print $csv->string."\n";
+
+my @cust_main = qsearch('cust_main', { 'agentnum'=>$agentnum });
+
+foreach my $cust_main( @cust_main ) {
+ $csv->combine( map { ref($_) ? &{$_->{'callback'}}($cust_main)
+ : $cust_main->$_()
+ }
+ @fields
+ ) or die;
+ print $csv->string."\n";
+}
+
+1;
diff --git a/bin/cust_pkg.export b/bin/cust_pkg.export
new file mode 100755
index 000000000..f922e02f0
--- /dev/null
+++ b/bin/cust_pkg.export
@@ -0,0 +1,61 @@
+#!/usr/bin/perl
+
+use strict;
+use Text::CSV_XS;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::cust_pkg;
+use Date::Format;
+
+my @fields = qw(
+ pkgnum
+ custnum
+ status
+ pkgpart
+);
+
+push @fields,
+ { 'header' => 'Package',
+ 'callback' => sub { shift->part_pkg->pkg_comment('nopkgpart'=>1) },
+ },
+ map {
+ my $field = $_;
+ { 'header' => $field,
+ 'callback' => sub { my $d = shift->get($field) or return '';
+ time2str('%x', $d); # %X", $d);
+ },
+ };
+ } qw( order_date start_date setup last_bill bill
+ adjourn susp resume
+ expire cancel uncancel
+ contract_end
+ )
+;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $agentnum = shift or die &usage;
+
+my $csv = new Text::CSV_XS;
+
+$csv->combine( map { ref($_) ? $_->{'header'} : $_ } @fields ) or die;
+print $csv->string."\n";
+
+my @cust_pkg = qsearch({
+ 'table' => 'cust_pkg',
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => {},
+ 'extra_sql' => "WHERE cust_main.agentnum = $agentnum",
+});
+
+foreach my $cust_pkg ( @cust_pkg ) {
+ $csv->combine( map { ref($_) ? &{$_->{'callback'}}($cust_pkg)
+ : $cust_pkg->$_()
+ }
+ @fields
+ ) or die;
+ print $csv->string."\n";
+}
+
+1;
diff --git a/bin/pod2x b/bin/pod2x
index ecb7f913b..1ec998fc2 100755
--- a/bin/pod2x
+++ b/bin/pod2x
@@ -7,12 +7,15 @@ chomp( my $mw_password = `cat .mw-password` );
my $site_perl = "./FS";
#my $html = "Freeside:1.7:Documentation:Developer";
-my $html = "Freeside:1.9:Documentation:Developer";
+#my $html = "Freeside:1.9:Documentation:Developer";
+my $html = "Freeside:3:Documentation:Developer";
foreach my $dir (
$html,
- map "$html/$_", qw( bin FS FS/UI FS/part_export FS/part_pkg
+ map "$html/$_", qw( bin FS
+ FS/cdr FS/cust_main FS/cust_pkg FS/detail_format
FS/part_event FS/part_event/Condition FS/part_event/Action
+ FS/part_export FS/part_pkg FS/pay_batch
FS/ClientAPI FS/Cron FS/Misc FS/Report FS/Report/Table
FS/TicketSystem FS/UI
FS/SelfService
@@ -43,6 +46,7 @@ foreach my $file (
use WWW::Mediawiki::Client;
my $mvs = WWW::Mediawiki::Client->new(
'host' => 'www.freeside.biz',
+ 'protocol' => 'https',
'wiki_path' => 'mediawiki/index.php',
'username' => $mw_username,
'password' => $mw_password,
diff --git a/bin/svc_acct.export b/bin/svc_acct.export
new file mode 100755
index 000000000..dba4ac98d
--- /dev/null
+++ b/bin/svc_acct.export
@@ -0,0 +1,54 @@
+#!/usr/bin/perl
+
+use strict;
+use Text::CSV_XS;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::svc_acct;
+
+my @fields = (
+ { 'header' => 'pkgnum',
+ 'callback' => sub { shift->cust_svc->pkgnum; },
+ },
+ { 'header' => 'svcpart',
+ 'callback' => sub { shift->cust_svc->svcpart; },
+ },
+ { 'header' => 'Service',
+ 'callback' => sub { shift->cust_svc->part_svc->svc; },
+ },
+ qw(
+ username
+ _password
+ slipip
+ )
+);
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $agentnum = shift or die &usage;
+
+my $csv = new Text::CSV_XS;
+
+$csv->combine( map { ref($_) ? $_->{'header'} : $_ } @fields ) or die;
+print $csv->string."\n";
+
+my @svc_acct = qsearch({
+ 'table' => 'svc_acct',
+ 'addl_from' => 'LEFT JOIN cust_svc USING (svcnum)
+ LEFT JOIN cust_pkg USING (pkgnum)
+ LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => {},
+ 'extra_sql' => "WHERE cust_main.agentnum = $agentnum",
+});
+
+foreach my $svc_acct ( @svc_acct ) {
+ $csv->combine( map { ref($_) ? &{$_->{'callback'}}($svc_acct)
+ : $svc_acct->$_()
+ }
+ @fields
+ ) or die;
+ print $csv->string."\n";
+}
+
+1;
diff --git a/bin/svc_broadband.export b/bin/svc_broadband.export
new file mode 100755
index 000000000..1d5c71318
--- /dev/null
+++ b/bin/svc_broadband.export
@@ -0,0 +1,59 @@
+#!/usr/bin/perl
+
+use strict;
+use Text::CSV_XS;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::svc_broadband;
+
+my @fields = (
+ { 'header' => 'pkgnum',
+ 'callback' => sub { shift->cust_svc->pkgnum; },
+ },
+ { 'header' => 'svcpart',
+ 'callback' => sub { shift->cust_svc->svcpart; },
+ },
+ { 'header' => 'Service',
+ 'callback' => sub { shift->cust_svc->part_svc->svc; },
+ },
+ qw(
+ description
+ speed_up
+ speed_down
+ ip_addr
+ mac_addr
+ latitude
+ longitude
+ )
+);
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $agentnum = shift or die &usage;
+
+my $csv = new Text::CSV_XS;
+
+$csv->combine( map { ref($_) ? $_->{'header'} : $_ } @fields ) or die;
+print $csv->string."\n";
+
+my @svc_broadband = qsearch({
+ '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' => {},
+ 'extra_sql' => "WHERE cust_main.agentnum = $agentnum",
+});
+
+foreach my $svc_broadband ( @svc_broadband ) {
+ $csv->combine( map { ref($_) ? &{$_->{'callback'}}($svc_broadband)
+ : $svc_broadband->$_()
+ }
+ @fields
+ ) or die;
+ print $csv->string."\n";
+}
+
+1;
diff --git a/bin/svc_phone.export b/bin/svc_phone.export
new file mode 100755
index 000000000..aa0eb2082
--- /dev/null
+++ b/bin/svc_phone.export
@@ -0,0 +1,55 @@
+#!/usr/bin/perl
+
+use strict;
+use Text::CSV_XS;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::svc_phone;
+
+my @fields = (
+ { 'header' => 'pkgnum',
+ 'callback' => sub { shift->cust_svc->pkgnum; },
+ },
+ { 'header' => 'svcpart',
+ 'callback' => sub { shift->cust_svc->svcpart; },
+ },
+ { 'header' => 'Service',
+ 'callback' => sub { shift->cust_svc->part_svc->svc; },
+ },
+ qw(
+ phonenum
+ pin
+ sip_password
+ phone_name
+ )
+);
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $agentnum = shift or die &usage;
+
+my $csv = new Text::CSV_XS;
+
+$csv->combine( map { ref($_) ? $_->{'header'} : $_ } @fields ) or die;
+print $csv->string."\n";
+
+my @svc_phone = qsearch({
+ 'table' => 'svc_phone',
+ 'addl_from' => 'LEFT JOIN cust_svc USING (svcnum)
+ LEFT JOIN cust_pkg USING (pkgnum)
+ LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => {},
+ 'extra_sql' => "WHERE cust_main.agentnum = $agentnum",
+});
+
+foreach my $svc_phone ( @svc_phone ) {
+ $csv->combine( map { ref($_) ? &{$_->{'callback'}}($svc_phone)
+ : $svc_phone->$_()
+ }
+ @fields
+ ) or die;
+ print $csv->string."\n";
+}
+
+1;
diff --git a/bin/tax_location.upgrade b/bin/tax_location.upgrade
new file mode 100755
index 000000000..814094551
--- /dev/null
+++ b/bin/tax_location.upgrade
@@ -0,0 +1,31 @@
+#!/usr/bin/perl
+
+use FS::UID qw(adminsuidsetup);
+use FS::Record;
+use FS::cust_bill_pkg;
+use Date::Parse qw(str2time);
+use Getopt::Std;
+getopts('s:e:');
+my $username = shift @ARGV;
+
+if (!$username) {
+ print
+"Usage: tax_location.upgrade [ -s START ] [ -e END ] username
+
+This script creates cust_bill_pkg_tax_location and cust_tax_exempt_pkg records
+for existing sales tax records prior to the 3.0 cust_location changes. Changes
+will be committed immediately; back up your data and run 'make
+install-perl-modules' and 'freeside-upgrade' before running this script.
+START and END specify an optional range of invoice dates to upgrade.
+
+";
+ exit(1);
+}
+
+my %opt;
+$opt{s} = str2time($opt_s) if $opt_s;
+$opt{e} = str2time($opt_e) if $opt_e;
+
+adminsuidsetup($username);
+FS::cust_bill_pkg->upgrade_tax_location(%opt);
+1;
diff --git a/bin/v-rate-reimport b/bin/v-rate-reimport
new file mode 100755
index 000000000..8b5305895
--- /dev/null
+++ b/bin/v-rate-reimport
@@ -0,0 +1,172 @@
+#!/usr/bin/perl
+
+use strict;
+use DBI;
+use FS::UID qw(adminsuidsetup);
+use FS::rate_prefix;
+use FS::rate_region;
+use FS::rate_detail;
+use FS::Record qw(qsearch qsearchs dbh);
+
+# #delete from rate;
+# Create interstate and intrastate rate plans
+#
+# #delete from rate_detail;
+# #delete from rate_region;
+# #delete from rate_prefix;
+
+# Assumption: 1-to-1 relationship between rate_region and rate_prefix, with
+# two rate_detail per rate_region: one for interstate; one for intrastate
+#
+# run the script, setting the appropriate values below.
+
+####### SET THESE! ####################
+
+my $DRY_RUN = 0;
+
+my $intra_ratenum = 5;
+my $inter_ratenum = 6;
+my $intra_class = 1;
+my $inter_class = 2;
+#my $file = "/home/levinse/domestic_interstate.xls";
+#my $file = "/home/ivan/vnes/New VNES Rate Table.xlsx";
+my $file = "/home/ivan/New VNES Rate Table.csv";
+#my $sheet_name = 'Sheet1';
+#######################################
+
+my $user = shift or die "no user specified";
+adminsuidsetup $user;
+
+local $SIG{HUP} = 'IGNORE';
+local $SIG{INT} = 'IGNORE';
+local $SIG{QUIT} = 'IGNORE';
+local $SIG{TERM} = 'IGNORE';
+local $SIG{TSTP} = 'IGNORE';
+local $SIG{PIPE} = 'IGNORE';
+
+my $oldAutoCommit = $FS::UID::AutoCommit;
+local $FS::UID::AutoCommit = 0;
+my $dbhfs = dbh;
+
+#my $dbh = DBI->connect("DBI:Excel:file=$file")
+# or die "can't connect: $DBI::errstr";
+
+#my $sth = $dbh->prepare("select * from $sheet_name")
+# or die "can't prepare: ". $dbh->errstr;
+#$sth->execute
+# or die "can't execute: ". $sth->errstr;
+
+use Text::CSV_XS;
+my $csv = Text::CSV_XS->new or die Text::CSV->error_diag;
+
+open(my $fh, "<$file") or die $!;
+my $header = scalar(<$fh>); #NPA, NXX, LATA, State, Intrastate, Interstate
+
+my @rp_cache = qsearch('rate_prefix', {} );# or die "can't cache rate_prefix";
+my %rp_cache = map { $_->npa => $_ } @rp_cache;
+
+sub fatal {
+ my $msg = shift;
+ $dbhfs->rollback; # if $oldAutoCommit;
+ die $msg;
+}
+
+while ( my $row = $csv->getline($fh) ) {
+
+ #my $lata = $row->{'lata'};
+ #my $ocn = $row->{'ocn'};
+ #my $state = $row->{'state'};
+ #my $rate = $row->{'rate'};
+ #my $npanxx = $row->{'lrn'};
+
+ #NPA, NXX, LATA, State, Intrastate, Interstate
+ my $npa = $row->[0];
+ my $nxx = $row->[1];
+ my $lata = $row->[2];
+ my $state = $row->[3];
+ ( my $intra_rate = $row->[4] ) =~ s/^\s*\$//;
+ ( my $inter_rate = $row->[5] ) =~ s/^\s*\$//;
+
+ #in the new data, instead of being "$-", these are all identical to the
+ #rate from the immediatelly preceeding cell/NPANXX... probably an artifact
+ #rather than real rates then? so also skipping this import
+ #import
+ next if $lata == '99999';
+
+ my $error = '';
+
+ my $rp;
+ if ( $rp_cache{$npa.$nxx} ) {
+ $rp = $rp_cache{$npa.$nxx};
+ }
+ else {
+
+ #warn "inserting new rate_region / rate_prefix for $npa-$nxx\n";
+ die "new rate_region / rate_prefix $npa-$nxx\n";
+
+ my $rr = new FS::rate_region { 'regionname' => $state };
+ $error = $rr->insert;
+ fatal("can't insert rr") if $error;
+
+ $rp = new FS::rate_prefix { 'countrycode' => '1',
+ 'npa' => $npa.$nxx, #$npanxx
+ #'ocn' => $ocn,
+ 'state' => $state,
+ 'latanum' => $lata,
+ 'regionnum' => $rr->regionnum,
+ };
+ $error = $rp->insert;
+ fatal("can't insert rp") if $error;
+ $rp_cache{$npa.$nxx} = $rp;
+ }
+
+ #use Data::Dumper;
+ #warn Dumper($rp);
+
+ my %hash = ( 'dest_regionnum' => $rp->regionnum, );
+
+ my %intra_hash = ( 'ratenum' => $intra_ratenum,
+ 'intra_class' => $intra_class,
+ %hash,
+ );
+
+ my $intra_rd = qsearchs( 'rate_detail', \%intra_hash )
+ || die; #new FS::rate_detail \%intra_hash;
+
+ $intra_rd->min_included( 0 );
+ $intra_rd->sec_granularity( 6 ); #60
+ die if $intra_rd->min_charge > 0;
+ $intra_rd->min_charge( $intra_rate );
+
+ #$error = $intra_rd->ratedetailnum ? $intra_rd->replace : $intra_rd->insert;
+ $error = $intra_rd->replace;
+ fatal("can't insert/replace (intra) rd: $error") if $error;
+
+ my %inter_hash = ( 'ratenum' => $inter_ratenum,
+ 'inter_class' => $inter_class,
+ %hash,
+ );
+
+ my $inter_rd = qsearchs( 'rate_detail', \%inter_hash )
+ || die; #new FS::rate_detail \%inter_hash;
+
+ $inter_rd->min_included( 0 );
+ $inter_rd->sec_granularity( 6 ); #60
+ die if $inter_rd->min_charge > 0;
+ $inter_rd->min_charge( $inter_rate );
+
+ #$error = $inter_rd->ratedetailnum ? $inter_rd->replace : $inter_rd->insert;
+ $error = $inter_rd->replace;
+ fatal("can't insert/replace (inter) rd: $error") if $error;
+}
+$csv->eof or $csv->error_diag ();
+close $fh;
+
+if ( $DRY_RUN ) {
+ $dbhfs->rollback or die $dbhfs->errstr; # if $oldAutoCommit;
+} else {
+ $dbhfs->commit or die $dbhfs->errstr; # if $oldAutoCommit;
+}
+
+1;
+
diff --git a/conf/invoice_latex b/conf/invoice_latex
index 772c2eb95..d56a7fbdc 100644
--- a/conf/invoice_latex
+++ b/conf/invoice_latex
@@ -19,7 +19,7 @@
\documentclass[letterpaper]{article}
-\usepackage{fancyhdr,lastpage,ifthen,array,fslongtable,afterpage,caption,multirow,bigstrut}
+\usepackage{fancyhdr,lastpage,ifthen,array,longtable,afterpage,caption,multirow,bigstrut}
\usepackage{graphicx} % required for logo graphic
\usepackage[utf8]{inputenc} % multilanguage support
\usepackage[T1]{fontenc}
diff --git a/conf/quotation_latex b/conf/quotation_latex
index 772c2eb95..d56a7fbdc 100644
--- a/conf/quotation_latex
+++ b/conf/quotation_latex
@@ -19,7 +19,7 @@
\documentclass[letterpaper]{article}
-\usepackage{fancyhdr,lastpage,ifthen,array,fslongtable,afterpage,caption,multirow,bigstrut}
+\usepackage{fancyhdr,lastpage,ifthen,array,longtable,afterpage,caption,multirow,bigstrut}
\usepackage{graphicx} % required for logo graphic
\usepackage[utf8]{inputenc} % multilanguage support
\usepackage[T1]{fontenc}
diff --git a/etc/fslongtable.sty b/etc/longtable.sty
index e322b55f1..66e2bf9d0 100644
--- a/etc/fslongtable.sty
+++ b/etc/longtable.sty
@@ -1,5 +1,5 @@
%%
-%% This is file `fslongtable.sty',
+%% This is file `longtable.sty',
%%
%% Copyright 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003
%% The LaTeX3 Project and any individual authors listed elsewhere
diff --git a/fs_selfservice/DEPLOY b/fs_selfservice/DEPLOY
index e73012f4b..bedb5eca9 100755
--- a/fs_selfservice/DEPLOY
+++ b/fs_selfservice/DEPLOY
@@ -11,7 +11,7 @@ perl Makefile.PL && make && make install
cd ..
#( cd ..; make deploy; cd fs_selfservice )
-( cd ..; make clean; make install-perl-modules; /etc/init.d/freeside restart; cd fs_selfservice )
+( cd ..; make clean; make configure-rt; make install-perl-modules; /etc/init.d/freeside restart; cd fs_selfservice )
#cp /home/ivan/freeside/fs_selfservice/FS-SelfService/cgi/* /var/www/MyAccount
#chown freeside /var/www/MyAccount/*.cgi
diff --git a/fs_selfservice/FS-SelfService/cgi/agent.cgi b/fs_selfservice/FS-SelfService/cgi/agent.cgi
index 0af94cd9e..0af94cd9e 100644..100755
--- a/fs_selfservice/FS-SelfService/cgi/agent.cgi
+++ b/fs_selfservice/FS-SelfService/cgi/agent.cgi
diff --git a/fs_selfservice/FS-SelfService/cgi/cust_bill-logo.cgi b/fs_selfservice/FS-SelfService/cgi/cust_bill-logo.cgi
index 253f853f8..253f853f8 100644..100755
--- a/fs_selfservice/FS-SelfService/cgi/cust_bill-logo.cgi
+++ b/fs_selfservice/FS-SelfService/cgi/cust_bill-logo.cgi
diff --git a/fs_selfservice/FS-SelfService/cgi/make_payment.html b/fs_selfservice/FS-SelfService/cgi/make_payment.html
index bec37cac3..defd4a551 100644
--- a/fs_selfservice/FS-SelfService/cgi/make_payment.html
+++ b/fs_selfservice/FS-SelfService/cgi/make_payment.html
@@ -9,29 +9,15 @@
<TH ALIGN="right">Amount&nbsp;Due</TH>
<TD COLSPAN=7>
<TABLE><TR><TD BGCOLOR="#ffffff">
- $<%=sprintf("%.2f",$balance)%>
- </TD></TR></TABLE>
- </TD>
-</TR>
-<TR>
- <TH ALIGN="right">Payment&nbsp;amount</TH>
- <TD COLSPAN=7>
- <TABLE><TR><TD BGCOLOR="#ffffff">
-<%=
- $amt = '';
- if ( $balance > 0 ) {
- $amt = $balance;
- $amt += $amt * $credit_card_surcharge_percentage/100
- if $credit_card_surcharge_percentage > 0;
- $amt = sprintf("%.2f",$amt);
- }
- '';
-%>
- $<INPUT TYPE="text" NAME="amount" SIZE=8 VALUE="<%=$amt%>">
+ <FONT COLOR="#000000">$<%=sprintf("%.2f",$balance)%></FONT>
</TD></TR></TABLE>
</TD>
</TR>
+
+<%= $tr_amount_fee %>
+
<%= include('discount_term') %>
+
<TR>
<TH ALIGN="right">Card&nbsp;type</TH>
<TD COLSPAN=7>
diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
index fe8d08209..61361b8ee 100644..100755
--- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
+++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
@@ -476,7 +476,21 @@ sub process_order_recharge {
}
sub make_payment {
- payment_info( 'session_id' => $session_id );
+
+ my $payment_info = payment_info( 'session_id' => $session_id );
+
+ my $tr_amount_fee = mason_comp(
+ 'session_id' => $session_id,
+ 'comp' => '/elements/tr-amount_fee.html',
+ 'args' => [ 'amount' => $payment_info->{'balance'},
+ ],
+ );
+
+ $tr_amount_fee = $tr_amount_fee->{'error'} || $tr_amount_fee->{'output'};
+
+ $payment_info->{'tr_amount_fee'} = $tr_amount_fee;
+
+ $payment_info;
}
sub payment_results {
diff --git a/fs_selfservice/FS-SelfService/cgi/xmlrpc.cgi b/fs_selfservice/FS-SelfService/cgi/xmlrpc.cgi
index d5a8e2063..d5a8e2063 100644..100755
--- a/fs_selfservice/FS-SelfService/cgi/xmlrpc.cgi
+++ b/fs_selfservice/FS-SelfService/cgi/xmlrpc.cgi
diff --git a/htetc/handler.pl b/htetc/handler.pl
index cea366134..3c68e83ed 100644
--- a/htetc/handler.pl
+++ b/htetc/handler.pl
@@ -5,6 +5,24 @@ package HTML::Mason;
use strict;
use warnings;
use FS::Mason qw( mason_interps );
+use FS::Trace;
+
+if ( %%%RT_ENABLED%%% ) {
+
+ require RT;
+
+ $> = scalar(getpwnam('freeside'));
+
+ RT::LoadConfig();
+ RT::Init();
+
+ # disconnect DB before fork:
+ # (avoid 'prepared statement "dbdpg_p\d+_\d+" already exists' errors?)
+ $RT::Handle->dbh(undef);
+ undef $RT::Handle;
+
+ $> = $<;
+}
#use vars qw($r);
@@ -38,6 +56,8 @@ sub handler
#($r) = @_;
my $r = shift;
+ FS::Trace->log('protecting fds');
+
#from rt/bin/webmux.pl(.in)
if ( !$protect_fds && $ENV{'MOD_PERL'} && exists $ENV{'MOD_PERL_API_VERSION'}
&& $ENV{'MOD_PERL_API_VERSION'} >= 2
@@ -63,6 +83,8 @@ sub handler
###Module::Refresh->refresh;###
+ FS::Trace->log('setting content_type / headers');
+
$r->content_type('text/html; charset=utf-8');
#$r->content_type('text/html; charset=iso-8859-1');
#eorar
@@ -76,6 +98,8 @@ sub handler
if ( $r->filename =~ /\/rt\// ) { #RT
+ FS::Trace->log('handling RT file');
+
# We don't need to handle non-text, non-xml items
return -1 if defined( $r->content_type )
&& $r->content_type !~ m!(^text/|\bxml\b)!io;
@@ -84,15 +108,20 @@ sub handler
local $SIG{__WARN__};
local $SIG{__DIE__};
+ FS::Trace->log('initializing RT');
my_rt_init();
+ FS::Trace->log('setting RT interpreter');
$ah->interp($rt_interp);
} else {
+ FS::Trace->log('handling Freeside file');
+
local $SIG{__WARN__};
local $SIG{__DIE__};
+ FS::Trace->log('initializing RT');
my_rt_init();
#we don't want the RT error handlers under FS
@@ -102,10 +131,12 @@ sub handler
undef($SIG{__DIE__}) if defined($SIG{__DIE__} );
}
+ FS::Trace->log('setting Freeside interpreter');
$ah->interp($fs_interp);
}
+ FS::Trace->log('handling request');
my %session;
my $status;
eval { $status = $ah->handle_request($r); };
@@ -125,22 +156,22 @@ sub handler
# );
# }
+ FS::Trace->log('done');
+
+ FS::Trace->dumpfile( "%%%FREESIDE_EXPORT%%%/profile/$$.".time,
+ FS::Trace->total. ' '. $r->filename
+ )
+ if FS::Trace->total > 5; #10?
+
+ FS::Trace->reset;
+
$status;
}
-my $rt_initialized = 0;
-
sub my_rt_init {
return unless $RT::VERSION;
-
- if ( $rt_initialized ) {
- RT::ConnectToDatabase();
- RT::InitSignalHandlers();
- } else {
- RT::LoadConfig();
- RT::Init();
- $rt_initialized++;
- }
+ RT::ConnectToDatabase();
+ RT::InitSignalHandlers();
}
1;
diff --git a/httemplate/browse/agent.cgi b/httemplate/browse/agent.cgi
index 64288b830..fc9ce5413 100755
--- a/httemplate/browse/agent.cgi
+++ b/httemplate/browse/agent.cgi
@@ -25,6 +25,7 @@ full offerings (via their type).<BR><BR>
<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">Master Customer</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc">Commissions</TH>
<TH CLASS="grid" BGCOLOR="#cccccc">Access Groups</TH>
<TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>Invoice<BR>Template</FONT></TH>
<TH CLASS="grid" BGCOLOR="#cccccc">Customers</TH>
@@ -93,6 +94,33 @@ full offerings (via their type).<BR><BR>
</TD>
<TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+
+ <TABLE>
+
+% #surprising amount of false laziness w/ edit/process/agent.cgi
+% my @pkg_class = qsearch('pkg_class', { 'disabled'=>'' });
+% foreach my $pkg_class ( '', @pkg_class ) {
+% my %agent_pkg_class = ( 'agentnum' => $agent->agentnum,
+% 'classnum' => $pkg_class ? $pkg_class->classnum : ''
+% );
+% my $agent_pkg_class =
+% qsearchs( 'agent_pkg_class', \%agent_pkg_class )
+% || new FS::agent_pkg_class \%agent_pkg_class;
+% my $param = 'classnum'. $agent_pkg_class{classnum};
+
+ <TR>
+ <TD><% $agent_pkg_class->commission_percent || 0 %>%</TD>
+ <TD><% $pkg_class ? $pkg_class->classname : mt('(no package class)') |h %>
+ </TD>
+ </TR>
+
+% }
+
+ </TABLE>
+
+ </TD>
+
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
% foreach my $access_group (
% map $_->access_group,
% qsearch('access_groupagent', { 'agentnum' => $agent->agentnum })
diff --git a/httemplate/browse/cust_note_class.html b/httemplate/browse/cust_note_class.html
index f5d450b9f..7928199b3 100644
--- a/httemplate/browse/cust_note_class.html
+++ b/httemplate/browse/cust_note_class.html
@@ -3,7 +3,7 @@
'html_init' => $html_init,
'name' => 'customer note classes',
'disableable' => 1,
- 'disabled_statuspos' => 2,
+ 'disabled_statuspos' => 1,
'query' => { 'table' => 'cust_note_class',
'hashref' => {},
'order_by' => 'ORDER BY classnum',
diff --git a/httemplate/browse/part_export.cgi b/httemplate/browse/part_export.cgi
index 8e28f4fc6..b7ecc00a6 100755
--- a/httemplate/browse/part_export.cgi
+++ b/httemplate/browse/part_export.cgi
@@ -36,10 +36,9 @@ function part_export_areyousure(href) {
<TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="<% $p %>edit/part_export.cgi?<% $part_export->exportnum %>"><% $part_export->exportnum %></A></TD>
<TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
-% if( $part_export->exportname ) {
- <B><% $part_export->exportname %>:</B><BR>
-% }
-<% $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>
+ <% $part_export->label_html %>
+ (<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 CLASS="inv" BGCOLOR="<% $bgcolor %>">
<% itable() %>
diff --git a/httemplate/browse/part_svc.cgi b/httemplate/browse/part_svc.cgi
index 26d090a3d..a8f4a7c84 100755
--- a/httemplate/browse/part_svc.cgi
+++ b/httemplate/browse/part_svc.cgi
@@ -141,16 +141,7 @@ function part_export_areyousure(href) {
%
<TR>
- <TD><A HREF="<% $p %>edit/part_export.cgi?<% $part_export->exportnum %>">
-<% $part_export->exportnum %>:&nbsp;
-% if ($part_export->exportname) {
-<B><% $part_export->exportname %></B> (
-% }
-<% $part_export->exporttype %>&nbsp;to&nbsp;<% $part_export->machine %>
-% if ($part_export->exportname) {
-)
-% }
-</A></TD>
+ <TD><A HREF="<% $p %>edit/part_export.cgi?<% $part_export->exportnum %>"><% $part_export->label_html %></A></TD>
</TR>
% }
diff --git a/httemplate/browse/radius_group.html b/httemplate/browse/radius_group.html
index fbf6d3766..98e81ab86 100644
--- a/httemplate/browse/radius_group.html
+++ b/httemplate/browse/radius_group.html
@@ -5,15 +5,26 @@
'query' => { 'table' => 'radius_group' },
'count_query' => 'SELECT COUNT(*) FROM radius_group',
'header' => [ '#', 'RADIUS Group', 'Description', 'Priority',
- 'Check', 'Reply' ],
+ 'Check', 'Reply', 'Speed' ],
'fields' => [ 'groupnum',
'groupname',
'description',
'priority',
- $check_attr, $reply_attr
+ $check_attr, $reply_attr,
+ sub {
+ my $group = shift;
+ if ($group->speed_down and $group->speed_up) {
+ return join (' / ', $group->speed_down, $group->speed_up);
+ } elsif ( $group->speed_down ) {
+ return $group->speed_down . ' down';
+ } elsif ( $group->speed_up ) {
+ return $group->speed_up . ' up';
+ }
+ '';
+ },
],
- 'align' => 'lllcll',
- 'links' => [ $link, $link, '', '', '', '',
+ 'align' => 'lllcllc',
+ 'links' => [ $link, $link, '', '', '', '', ''
],
&>
<%init>
diff --git a/httemplate/browse/reason.html b/httemplate/browse/reason.html
index fe285be4a..14e97bf2f 100644
--- a/httemplate/browse/reason.html
+++ b/httemplate/browse/reason.html
@@ -17,14 +17,17 @@
'header' => [ '#',
ucfirst($classname) . ' Reason Type',
ucfirst($classname) . ' Reason',
+ ($class eq 'S' ? 'Unsuspension Fee' : ()),
],
'fields' => [ 'reasonnum',
sub { shift->reasontype->type },
'reason',
+ $unsuspend_pkg_comment,
],
'links' => [ $link,
$link,
'',
+ $unsuspend_pkg_link,
],
)
%>
@@ -50,4 +53,18 @@ my $count_query = 'SELECT COUNT(*) FROM reason LEFT JOIN reason_type on ' .
my $link = [ $p."edit/reason.html?class=$class&reasonnum=", 'reasonnum' ];
+my ($unsuspend_pkg_comment, $unsuspend_pkg_link);
+if ( $class eq 'S' ) {
+ $unsuspend_pkg_comment = sub {
+ my $pkgpart = shift->unsuspend_pkgpart or return '';
+ my $part_pkg = FS::part_pkg->by_key($pkgpart) or return '';
+ $part_pkg->pkg_comment;
+ };
+
+ my $unsuspend_pkg_link = sub {
+ my $pkgpart = shift->unsuspend_pkgpart or return '';
+ [ $p."edit/part_pkg.cgi?", $pkgpart ];
+ };
+}
+
</%init>
diff --git a/httemplate/config/config.cgi b/httemplate/config/config.cgi
index a4f9890a5..7960d7e38 100644
--- a/httemplate/config/config.cgi
+++ b/httemplate/config/config.cgi
@@ -304,7 +304,6 @@ Setting <b><% $key %></b>
%
% my %opt = ( 'element_name' => "$key$n",
% 'empty_label' => ' ',
-% 'showdisabled' => 1,
% );
% if ( $config_item->multiple ) {
% $opt{'multiple'} = 1 if $config_item->multiple;
diff --git a/httemplate/docs/license.html b/httemplate/docs/license.html
index fab8cd09f..e40b2436b 100644
--- a/httemplate/docs/license.html
+++ b/httemplate/docs/license.html
@@ -6,7 +6,7 @@
<P>
-Copyright &copy; 2005-2009 Freeside Internet Services, Inc.<BR>
+Copyright &copy; 2005-2012 Freeside Internet Services, Inc.<BR>
Copyright &copy; 2000-2005 Ivan Kohler<BR>
Copyright &copy; 1999 Silicon Interactive Software Design<BR>
All rights reserved<BR>
diff --git a/httemplate/edit/agent.cgi b/httemplate/edit/agent.cgi
index 6707d66a4..b043d1efe 100755
--- a/httemplate/edit/agent.cgi
+++ b/httemplate/edit/agent.cgi
@@ -19,9 +19,12 @@
</SCRIPT>
<INPUT TYPE="hidden" NAME="agentnum" VALUE="<% $agent->agentnum %>">
-Agent #<% $agent->agentnum ? $agent->agentnum : "(NEW)" %>
-<% &ntable("#cccccc", 2, '') %>
+<FONT CLASS="fsinnerbox-title">
+ Agent #<% $agent->agentnum ? $agent->agentnum : "(NEW)" %>
+</FONT>
+
+<TABLE CLASS="fsinnerbox">
<TR>
<TH ALIGN="right">Agent</TH>
@@ -117,8 +120,13 @@ Agent #<% $agent->agentnum ? $agent->agentnum : "(NEW)" %>
</TR>
% }
+</TABLE>
+<BR>
+
+<FONT CLASS="fsinnerbox-title"><% mt('Access Groups') |h %></FONT>
+<TABLE CLASS="fsinnerbox">
+
<TR>
- <TD ALIGN="right">Access Groups</TD>
<TD><% include('/elements/checkboxes-table.html',
'source_obj' => $agent,
'link_table' => 'access_groupagent',
@@ -131,6 +139,38 @@ Agent #<% $agent->agentnum ? $agent->agentnum : "(NEW)" %>
</TR>
</TABLE>
+<BR>
+
+<FONT CLASS="fsinnerbox-title"><% mt('Commissions') |h %></FONT>
+<TABLE CLASS="fsinnerbox">
+
+% #surprising amount of false laziness w/ edit/process/agent.cgi
+% my @pkg_class = qsearch('pkg_class', { 'disabled'=>'' });
+% foreach my $pkg_class ( '', @pkg_class ) {
+% my %agent_pkg_class = ( 'agentnum' => $agent->agentnum,
+% 'classnum' => $pkg_class ? $pkg_class->classnum : ''
+% );
+% my $agent_pkg_class =
+% qsearchs( 'agent_pkg_class', \%agent_pkg_class )
+% || new FS::agent_pkg_class \%agent_pkg_class;
+% my $param = 'classnum'. $agent_pkg_class{classnum};
+
+ <TR>
+ <TD><INPUT TYPE = "text"
+ NAME = "<% $param %>"
+ VALUE = "<% $cgi->param($param) || $agent_pkg_class->commission_percent |h %>"
+ SIZE = 6
+ MAXLENGTH = 7
+ >%
+ </TD>
+ <TD><% $pkg_class ? $pkg_class->classname : mt('(no package class)') |h %>
+ </TD>
+ </TR>
+
+% }
+
+</TABLE>
+
<BR>
<INPUT TYPE="submit" VALUE="<% $agent->agentnum ? "Apply changes" : "Add agent" %>">
diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi
index ef81ebab1..e3e812f19 100755
--- a/httemplate/edit/cust_main.cgi
+++ b/httemplate/edit/cust_main.cgi
@@ -28,8 +28,10 @@
<& cust_main/top_misc.html, $cust_main, 'custnum' => $custnum &>
%# birthdate
-% if ( $conf->exists('cust_main-enable_birthdate')
+% if ( $conf->config('national_id-country')
+% || $conf->exists('cust_main-enable_birthdate')
% || $conf->exists('cust_main-enable_spouse_birthdate')
+% || $conf->exists('cust_main-enable_anniversary_date')
% )
% {
<BR>
@@ -51,6 +53,7 @@
<& /elements/location.html,
object => $cust_main->bill_location,
prefix => 'bill_',
+ enable_coords => 1,
&>
<& cust_main/after_bill_location.html, $cust_main &>
</TABLE>
@@ -75,6 +78,7 @@
prefix => 'ship_',
enable_censustract => 1,
enable_district => 1,
+ enable_coords => 1,
&>
</TABLE>
<TABLE CLASS="fsinnerbox" ID="table_ship_location_blank"
@@ -246,6 +250,8 @@ if ( $cgi->param('error') ) {
$stateid = $cust_main->stateid; # don't mask an entered value on errors
$payinfo = $cust_main->payinfo; # don't mask an entered value on errors
+ $cust_main->national_id( $cgi->param('national_id1') || $cgi->param('national_id2') );
+
$prospectnum = $cgi->param('prospectnum') || '';
$pkgpart_svcpart = $cgi->param('pkgpart_svcpart') || '';
diff --git a/httemplate/edit/cust_main/billing.html b/httemplate/edit/cust_main/billing.html
index d7082f23a..2925ca87c 100644
--- a/httemplate/edit/cust_main/billing.html
+++ b/httemplate/edit/cust_main/billing.html
@@ -522,6 +522,17 @@
</TR>
% }
+% if ( $conf->exists('cust_main-select-prorate_day') ) {
+ <TR>
+ <TD ALIGN="right" WIDTH="200"><% mt('Prorate day (1-28)') |h %> </TD>
+ <TD>
+ <INPUT TYPE="text" NAME="prorate_day" VALUE="<% $cust_main->prorate_day %>" SIZE=3 MAXLENGTH=2>
+ </TD>
+ </TR>
+% } else {
+ <INPUT TYPE="hidden" NAME="prorate_day" VALUE="<% $cust_main->prorate_day %>">
+% }
+
<TR>
<TD ALIGN="right" WIDTH="200"><% mt('Invoice terms') |h %> </TD>
<TD WIDTH="408">
diff --git a/httemplate/edit/cust_main/birthdate.html b/httemplate/edit/cust_main/birthdate.html
index 5d6a123b1..e1adbd3bd 100644
--- a/httemplate/edit/cust_main/birthdate.html
+++ b/httemplate/edit/cust_main/birthdate.html
@@ -1,5 +1,38 @@
<% ntable("#cccccc", 2) %>
+
% # maybe put after the contact names?
+
+% my $id_country = $conf->config('national_id-country');
+% if ( $id_country ) {
+% if ( $id_country eq 'MY' ) {
+% my($old, $nric) = ( '', '');
+% if ( $cust_main->national_id =~ /^\d{6}\-\d{2}\-\d{4}$/ ) {
+% $nric = $cust_main->national_id;
+% } else { # elsif ( $cust_main->national_id =~ /^\w\d{9}$/ ) {
+% $old = $cust_main->national_id;
+% #} else {
+% # warn "unknown national_id format";
+%# <INPUT TYPE="hidden" NAME="national_id0" VALUE="<% $cust_main->national_id |h %>">
+% }
+
+ <% include( '/elements/tr-input-text.html',
+ 'field' => 'national_id1',
+ 'value' => $nric,
+ 'label' => 'NRIC',
+ )
+ %>
+ <% include( '/elements/tr-input-text.html',
+ 'field' => 'national_id2',
+ 'value' => $old,
+ 'label' => 'Old IC/Passport',
+ )
+ %>
+
+% } else {
+% warn "unknown national_id-country $id_country";
+% }
+% }
+
% if ( $conf->exists('cust_main-enable_birthdate') ) {
<% include( '/elements/tr-input-date-field.html', {
'name' => 'birthdate',
@@ -11,6 +44,7 @@
})
%>
% }
+
% if ( $conf->exists('cust_main-enable_spouse_birthdate') ) {
<% include( '/elements/tr-input-date-field.html', {
'name' => 'spouse_birthdate',
@@ -22,6 +56,19 @@
})
%>
% }
+
+% if ( $conf->exists('cust_main-enable_anniversary_date') ) {
+ <% include( '/elements/tr-input-date-field.html', {
+ 'name' => 'anniversary_date',
+ 'value' => $cust_main->anniversary_date,
+ 'label' => 'Anniversary Date',
+ 'format' => ( $conf->config('date_format') || "%m/%d/%Y" ),
+ 'usedatetime' => 1,
+ 'noinit' => $noinit++,
+ })
+ %>
+% }
+
</TABLE>
<%init>
diff --git a/httemplate/edit/cust_refund.cgi b/httemplate/edit/cust_refund.cgi
index ba9304066..1ef69fdae 100755
--- a/httemplate/edit/cust_refund.cgi
+++ b/httemplate/edit/cust_refund.cgi
@@ -141,7 +141,7 @@ my $reason = $cgi->param('reason');
my $link = $cgi->param('popup') ? 'popup' : '';
my @rights = ();
-push @rights, 'Post refund' if $payby =~ /^(BILL|CASH)$/;
+push @rights, 'Post refund' if $payby =~ /^(BILL|CASH|MCRD)$/;
push @rights, 'Post check refund' if $payby eq 'BILL';
push @rights, 'Post cash refund ' if $payby eq 'CASH';
push @rights, 'Refund payment' if $payby =~ /^(CARD|CHEK)$/;
diff --git a/httemplate/edit/discount.html b/httemplate/edit/discount.html
index b195eb37b..9bcd1e724 100644
--- a/httemplate/edit/discount.html
+++ b/httemplate/edit/discount.html
@@ -22,6 +22,7 @@
postfix => '<BR><FONT SIZE="-1"><I>(blank for non-expiring discount)</I></FONT>',
},
{ field => 'setup', type => 'checkbox', value=>'Y', },
+ #{ field => 'linked', type => 'checkbox', value=>'Y', },
],
'labels' => {
'discountnum' => 'Discount #',
@@ -32,6 +33,7 @@
'percent' => 'Percentage&nbsp;',
'months' => 'Duration (months)',
'setup' => 'Apply to setup fees',
+ #'linked' => 'Apply to add-on packages',
},
'viewall_dir' => 'browse',
'new_callback' => $new_callback,
@@ -114,6 +116,10 @@ my $javascript = <<END;
document.getElementById('percent_label').style.visibility = 'hidden';
document.getElementById('percent_input0').style.display = 'none';
document.getElementById('percent_input0').style.visibility = 'hidden';
+// document.getElementById('linked_label').style.display = 'none';
+// document.getElementById('linked_label').style.visibility = 'hidden';
+// document.getElementById('linked').style.display = 'none';
+// document.getElementById('linked').style.visibility = 'hidden';
} else if ( _type == 'Amount' ) {
document.getElementById('amount_label').style.display = '';
document.getElementById('amount_label').style.visibility = '';
@@ -123,6 +129,10 @@ my $javascript = <<END;
document.getElementById('percent_label').style.visibility = 'hidden';
document.getElementById('percent_input0').style.display = 'none';
document.getElementById('percent_input0').style.visibility = 'hidden';
+// document.getElementById('linked_label').style.display = 'none';
+// document.getElementById('linked_label').style.visibility = 'hidden';
+// document.getElementById('linked').style.display = 'none';
+// document.getElementById('linked').style.visibility = 'hidden';
} else if ( _type == 'Percentage' ) {
document.getElementById('amount_label').style.display = 'none';
document.getElementById('amount_label').style.visibility = 'hidden';
@@ -132,6 +142,10 @@ my $javascript = <<END;
document.getElementById('percent_label').style.visibility = '';
document.getElementById('percent_input0').style.display = '';
document.getElementById('percent_input0').style.visibility = '';
+// document.getElementById('linked_label').style.display = '';
+// document.getElementById('linked_label').style.visibility = '';
+// document.getElementById('linked').style.display = '';
+// document.getElementById('linked').style.visibility = '';
}
}
diff --git a/httemplate/edit/nas.html b/httemplate/edit/nas.html
index 2e66fc3be..8e6232cdb 100644
--- a/httemplate/edit/nas.html
+++ b/httemplate/edit/nas.html
@@ -49,7 +49,7 @@ sub html_bottom {
'link_table' => 'export_nas',
'target_table' => 'part_export',
'hashref' => { 'exporttype' =>
- { op => 'LIKE', value => '%sqlradius' }
+ { op => 'LIKE', value => '%sqlradius%' }
},
'name_callback' => sub { $_[0]->label },
'default' => 'yes',
diff --git a/httemplate/edit/part_export.cgi b/httemplate/edit/part_export.cgi
index d7219b74a..0407ee77b 100644
--- a/httemplate/edit/part_export.cgi
+++ b/httemplate/edit/part_export.cgi
@@ -13,12 +13,6 @@
</TD>
</TR>
<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 %>
@@ -63,7 +57,7 @@ my $widget = new HTML::Widgets::SelectLayers(
'options' => \%layers,
'form_name' => 'dummy',
'form_action' => 'process/part_export.cgi',
- 'form_text' => [qw( exportnum exportname machine )],
+ 'form_text' => [qw( exportnum exportname )],
# 'form_checkbox' => [qw()],
'html_between' => "</TD></TR></TABLE>\n",
'layer_callback' => sub {
@@ -71,9 +65,69 @@ my $widget = new HTML::Widgets::SelectLayers(
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;
+ if ( $layer ) {
+ $html .= '<TR><TD ALIGN="right">Description</TD><TD BGCOLOR=#ffffff>'.
+ $exports->{$layer}{notes}. '</TD></TR>';
+
+ if ( $exports->{$layer}{no_machine} ) {
+ $html .= '<INPUT TYPE="hidden" NAME="machine" VALUE="">'.
+ '<INPUT TYPE="hidden" NAME="svc_machine" VALUE=N">';
+ } else {
+ $html .= '<TR><TD ALIGN="right">Hostname or IP</TD><TD>';
+ my $machine = $part_export->machine;
+ if ( $exports->{$layer}{svc_machine} ) {
+ my( $N_CHK, $Y_CHK) = ( 'CHECKED', '' );
+ my( $machine_DISABLED, $pem_DISABLED) = ( '', 'DISABLED' );
+ my $part_export_machine = '';
+ if ( $cgi->param('svc_machine') eq 'Y'
+ || $machine eq '_SVC_MACHINE'
+ )
+ {
+ $Y_CHK = 'CHECKED';
+ $N_CHK = 'CHECKED';
+ $machine_DISABLED = 'DISABLED';
+ $pem_DISABLED = '';
+ $machine = '';
+ $part_export_machine =
+ $cgi->param('part_export_machine')
+ || join "\n",
+ map $_->machine,
+ grep ! $_->disabled,
+ $part_export->part_export_machine;
+ }
+ my $oc = qq(onChange="${layer}_svc_machine_changed(this)");
+ $html .= qq[
+ <INPUT TYPE="radio" NAME="svc_machine" VALUE="N" $N_CHK $oc>
+ <INPUT TYPE="text" NAME="machine" ID="${layer}_machine" VALUE="$machine" $machine_DISABLED>
+ <BR>
+ <INPUT TYPE="radio" NAME="svc_machine" VALUE="Y" $Y_CHK $oc>
+ Selected in each customer service from these choices
+ <TEXTAREA NAME="part_export_machine" ID="${layer}_part_export_machine" $pem_DISABLED>$part_export_machine</TEXTAREA>
+
+ <SCRIPT TYPE="text/javascript">
+ function ${layer}_svc_machine_changed (what) {
+ if ( what.checked ) {
+ var machine = document.getElementById("${layer}_machine");
+ var part_export_machine = document.getElementById("${layer}_part_export_machine");
+ if ( what.value == 'Y' ) {
+ machine.disabled = true;
+ part_export_machine.disabled = false;
+ } else if ( what.value == 'N' ) {
+ machine.disabled = false;
+ part_export_machine.disabled = true;
+ }
+ }
+ }
+ </SCRIPT>
+ ];
+ } else {
+ $html .= qq(<INPUT TYPE="text" NAME="machine" VALUE="$machine">).
+ '<INPUT TYPE="hidden" NAME="svc_machine" VALUE=N">';
+ }
+ $html .= "</TD></TR>";
+ }
+
+ }
foreach my $option ( keys %{$exports->{$layer}{options}} ) {
my $optinfo = $exports->{$layer}{options}{$option};
diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi
index cd0731370..f3ad8f52d 100755
--- a/httemplate/edit/part_pkg.cgi
+++ b/httemplate/edit/part_pkg.cgi
@@ -55,6 +55,7 @@
'svc_dst_pkgpart' => 'Include services of package',
'report_option' => 'Report classes',
'fcc_ds0s' => 'Voice-grade equivalents',
+ 'fcc_voip_class' => 'Category',
},
'fields' => [
@@ -196,6 +197,9 @@
{ type => 'tablebreak-tr-title',
value => 'FCC Form 477 information',
},
+ { field=>'fcc_voip_class',
+ type=>'select-voip_class',
+ },
{ field=>'fcc_ds0s', type=>'text', size=>6 },
)
: ()
diff --git a/httemplate/edit/part_svc.cgi b/httemplate/edit/part_svc.cgi
index 4bd083798..007c24629 100755
--- a/httemplate/edit/part_svc.cgi
+++ b/httemplate/edit/part_svc.cgi
@@ -144,12 +144,7 @@
% && qsearchs( 'export_svc', {
% exportnum => $part_export->exportnum,
% svcpart => $clone || $part_svc->svcpart });
-% $html .= '>'.$part_export->exportnum. ': ';
-% $html .= $part_export->exportname . '<DIV ALIGN="right"><FONT SIZE=-1>'
-% if ( $part_export->exportname );
-% $html .= $part_export->exporttype. ' to '. $part_export->machine;
-% $html .= '</FONT></DIV>' if ( $part_export->exportname );
-% $html .= '</TD>';
+% $html .= '>'. $part_export->label_html. '</TD>';
% $count++;
% $html .= '</TR><TR>' unless $count % $columns;
% }
diff --git a/httemplate/edit/payment_gateway.html b/httemplate/edit/payment_gateway.html
index e5897b035..dfe52f109 100644
--- a/httemplate/edit/payment_gateway.html
+++ b/httemplate/edit/payment_gateway.html
@@ -91,6 +91,7 @@ my %modules = (
'KeyBank' => 'Business::BatchPayment',
'Paymentech' => 'Business::BatchPayment',
+ 'TD_EFT' => 'Business::BatchPayment',
);
my %modules_for_namespace;
@@ -141,7 +142,7 @@ my $fields = [
{
field => 'gateway_options',
type => 'textarea',
- rows => '8',
+ rows => '12',
cols => '40',
curr_value_callback => sub { my($cgi, $object, $fref) = @_;
join("\r", $object->options );
diff --git a/httemplate/edit/prepay_credit.cgi b/httemplate/edit/prepay_credit.cgi
index c03bbf990..3f0d6ba1d 100644
--- a/httemplate/edit/prepay_credit.cgi
+++ b/httemplate/edit/prepay_credit.cgi
@@ -22,6 +22,7 @@ characters each
<& /elements/select-agent.html,
'empty_label' => '(any agent)',
+ 'curr_value' => $agentnum,
&>
<TABLE>
diff --git a/httemplate/edit/process/agent.cgi b/httemplate/edit/process/agent.cgi
index e776d281c..034c4cc50 100755
--- a/httemplate/edit/process/agent.cgi
+++ b/httemplate/edit/process/agent.cgi
@@ -1,11 +1,12 @@
<% include( 'elements/process.html',
- 'table' => 'agent',
- 'viewall_dir' => 'browse',
- 'viewall_ext' => 'cgi',
- 'process_m2m' => { 'link_table' => 'access_groupagent',
- 'target_table' => 'access_group',
- },
- 'edit_ext' => 'cgi',
+ 'table' => 'agent',
+ 'viewall_dir' => 'browse',
+ 'viewall_ext' => 'cgi',
+ 'process_m2m' => { 'link_table' => 'access_groupagent',
+ 'target_table' => 'access_group',
+ },
+ 'edit_ext' => 'cgi',
+ 'noerror_callback' => $process_agent_pkg_class,
)
%>
<%init>
@@ -18,4 +19,30 @@ if ( FS::Conf->new->exists('disable_acl_changes') ) {
die "shouldn't be reached";
}
+my $process_agent_pkg_class = sub {
+ my( $cgi, $agent ) = @_;
+
+ #surprising amount of false laziness w/ edit/agent.cgi
+ my @pkg_class = qsearch('pkg_class', { 'disabled'=>'' });
+ foreach my $pkg_class ( '', @pkg_class ) {
+ my %agent_pkg_class = ( 'agentnum' => $agent->agentnum,
+ 'classnum' => $pkg_class ? $pkg_class->classnum : ''
+ );
+ my $agent_pkg_class =
+ qsearchs( 'agent_pkg_class', \%agent_pkg_class )
+ || new FS::agent_pkg_class \%agent_pkg_class;
+
+ my $param = 'classnum'. $agent_pkg_class{classnum};
+
+ $agent_pkg_class->commission_percent( $cgi->param($param) );
+
+ my $method = $agent_pkg_class->agentpkgclassnum ? 'replace' : 'insert';
+
+ my $error = $agent_pkg_class->$method;
+ die $error if $error; #XXX push this down into agent.pm w/better/transactional error handling
+
+ }
+
+};
+
</%init>
diff --git a/httemplate/edit/process/cust_main.cgi b/httemplate/edit/process/cust_main.cgi
index 5ee553b32..31ec4ab12 100755
--- a/httemplate/edit/process/cust_main.cgi
+++ b/httemplate/edit/process/cust_main.cgi
@@ -110,11 +110,16 @@ if ( $cgi->param('no_credit_limit') ) {
$new->tagnum( [ $cgi->param('tagnum') ] );
+$error ||= $new->set_national_id_from_cgi( $cgi );
+
my %usedatetime = ( 'birthdate' => 1,
'spouse_birthdate' => 1,
+ 'anniversary_date' => 1,
);
-foreach my $dfield (qw( birthdate spouse_birthdate signupdate )) {
+foreach my $dfield (qw(
+ signupdate birthdate spouse_birthdate anniversary_date
+)) {
if ( $cgi->param($dfield) && $cgi->param($dfield) =~ /^([ 0-9\-\/]{0,10})$/) {
diff --git a/httemplate/edit/process/cust_pkg_discount.html b/httemplate/edit/process/cust_pkg_discount.html
index 6f97a791e..4a71f6975 100644
--- a/httemplate/edit/process/cust_pkg_discount.html
+++ b/httemplate/edit/process/cust_pkg_discount.html
@@ -39,7 +39,8 @@ my $cust_pkg_discount = new FS::cust_pkg_discount {
'amount' => scalar($cgi->param('discountnum_amount')),
'percent' => scalar($cgi->param('discountnum_percent')),
'months' => scalar($cgi->param('discountnum_months')),
- 'setup' => scalar($cgi->param('discountnum_setup')),
+ 'setup' => scalar($cgi->param('discountnum_setup')),
+ #'linked' => scalar($cgi->param('discountnum_linked')),
#'disabled' => $self->discountnum_disabled,
};
my $error = $cust_pkg_discount->insert;
diff --git a/httemplate/edit/process/cust_refund.cgi b/httemplate/edit/process/cust_refund.cgi
index f4cce6535..bde40727a 100755
--- a/httemplate/edit/process/cust_refund.cgi
+++ b/httemplate/edit/process/cust_refund.cgi
@@ -31,7 +31,7 @@ my $link = $cgi->param('popup') ? 'popup' : '';
my $payby = $cgi->param('payby');
my @rights = ();
-push @rights, 'Post refund' if $payby =~ /^(BILL|CASH)$/;
+push @rights, 'Post refund' if $payby =~ /^(BILL|CASH|MCRD)$/;
push @rights, 'Post check refund' if $payby eq 'BILL';
push @rights, 'Post cash refund ' if $payby eq 'CASH';
push @rights, 'Refund payment' if $payby =~ /^(CARD|CHEK)$/;
diff --git a/httemplate/edit/process/part_export.cgi b/httemplate/edit/process/part_export.cgi
index 21150ef67..6432d6b15 100644
--- a/httemplate/edit/process/part_export.cgi
+++ b/httemplate/edit/process/part_export.cgi
@@ -28,6 +28,11 @@ my $new = new FS::part_export ( {
} fields('part_export')
} );
+if ( $cgi->param('svc_machine') eq 'Y' ) {
+ $new->machine('_SVC_MACHINE');
+ $new->part_export_machine_textarea( $cgi->param('part_export_machine') );
+}
+
my $error;
if ( $exportnum ) {
#warn $old;
diff --git a/httemplate/edit/process/quick-cust_pkg.cgi b/httemplate/edit/process/quick-cust_pkg.cgi
index 306319869..2dadbccdc 100644
--- a/httemplate/edit/process/quick-cust_pkg.cgi
+++ b/httemplate/edit/process/quick-cust_pkg.cgi
@@ -2,19 +2,24 @@
% $cgi->param('error', $error);
<% $cgi->redirect(popurl(3). 'misc/order_pkg.html?'. $cgi->query_string ) %>
%} else {
-% my $frag = "cust_pkg". $cust_pkg->pkgnum;
% my $show = $curuser->default_customer_view =~ /^(jumbo|packages)$/
% ? ''
% : ';show=packages';
-% my $redir_url = popurl(3)
-% ."view/cust_main.cgi?custnum=$custnum$show;fragment=$frag#$frag";
+%
+% my $redir_url = popurl(3);
+% if ( $svcpart ) { # for going straight to service provisining after ordering
+% $redir_url .= 'edit/'.$part_svc->svcdb.'.cgi?'.
+% 'pkgnum='.$cust_pkg->pkgnum. ";svcpart=$svcpart";
+% $redir_url .= ";qualnum=$qualnum" if $qualnum;
+% } elsif ( $quotationnum ) {
+% $redir_url .= "view/quotation.html?quotationnum=$quotationnum";
+% } else {
+% my $custnum = $cust_main->custnum;
+% my $frag = "cust_pkg". $cust_pkg->pkgnum;
+% $redir_url .=
+% "view/cust_main.cgi?custnum=$custnum$show;fragment=$frag#$frag";
+% }
%
-% # for going right to a provision service after ordering a package
-% if ( $svcpart ) {
-% $redir_url = popurl(3)."edit/".$part_svc->svcdb.".cgi?".
-% "pkgnum=".$cust_pkg->pkgnum. ";svcpart=$svcpart";
-% $redir_url .= ";qualnum=$qualnum" if $qualnum;
-% }
<% header('Package ordered') %>
<SCRIPT TYPE="text/javascript">
// XXX fancy ajax rebuild table at some point, but a page reload will do for now
@@ -33,16 +38,27 @@ my $curuser = $FS::CurrentUser::CurrentUser;
die "access denied"
unless $curuser->access_right('Order customer package');
-#untaint custnum (probably not necessary, searching for it is escape enough)
-$cgi->param('custnum') =~ /^(\d+)$/
- or die 'illegal custnum '. $cgi->param('custnum');
-my $custnum = $1;
-my $cust_main = qsearchs({
- 'table' => 'cust_main',
- 'hashref' => { 'custnum' => $custnum },
- 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
-});
-die 'unknown custnum' unless $cust_main;
+my $cust_main;
+if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+ my $custnum = $1;
+ $cust_main = qsearchs({
+ 'table' => 'cust_main',
+ 'hashref' => { 'custnum' => $custnum },
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+ });
+}
+
+my $prospect_main;
+if ( $cgi->param('prospectnum') =~ /^(\d+)$/ ) {
+ my $prospectnum = $1;
+ $prospect_main = qsearchs({
+ 'table' => 'prospect_main',
+ 'hashref' => { 'prospectnum' => $prospectnum },
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+ });
+}
+
+die 'no custnum or prospectnum' unless $cust_main || $prospect_main;
#probably not necessary, taken care of by cust_pkg::check
$cgi->param('pkgpart') =~ /^(\d+)$/
@@ -72,47 +88,70 @@ if ( $cgi->param('svcpart') ) {
}
my $qualnum = '';
-if ( $cgi->param('qualnum') ) {
- $cgi->param('qualnum') =~ /^(\d+)$/ or die 'illegal qualnum';
+if ( $cgi->param('qualnum') =~ /^(\d+)$/ ) {
$qualnum = $1;
}
+my $quotationnum = '';
+if ( $cgi->param('quotationnum') =~ /^(\d+)$/ ) {
+ $quotationnum = $1;
+}
+# verify this quotation is visible to this user
+my $cust_pkg = '';
+my $quotation_pkg = '';
+my $error = '';
-my $cust_pkg = new FS::cust_pkg {
- 'custnum' => $custnum,
- 'pkgpart' => $pkgpart,
- 'quantity' => $quantity,
- 'start_date' => ( scalar($cgi->param('start_date'))
- ? parse_datetime($cgi->param('start_date'))
- : ''
- ),
- 'no_auto' => scalar($cgi->param('no_auto')),
- 'refnum' => $refnum,
- 'locationnum' => $locationnum,
- 'discountnum' => $discountnum,
- #for the create a new discount case
- 'discountnum__type' => scalar($cgi->param('discountnum__type')),
- 'discountnum_amount' => scalar($cgi->param('discountnum_amount')),
- 'discountnum_percent' => scalar($cgi->param('discountnum_percent')),
- 'discountnum_months' => scalar($cgi->param('discountnum_months')),
- 'discountnum_setup' => scalar($cgi->param('discountnum_setup')),
- 'contract_end' => ( scalar($cgi->param('contract_end'))
- ? parse_datetime($cgi->param('contract_end'))
- : ''
- ),
- 'waive_setup' => ( $cgi->param('waive_setup') eq 'Y' ? 'Y' : '' ),
-};
-
-my %opt = ( 'cust_pkg' => $cust_pkg );
-
-if ( $locationnum == -1 ) {
- my $cust_location = new FS::cust_location {
- map { $_ => scalar($cgi->param($_)) }
- qw( custnum address1 address2 city county state zip country geocode )
- };
- $opt{'cust_location'} = $cust_location;
-}
+my %hash = (
+ 'pkgpart' => $pkgpart,
+ 'quantity' => $quantity,
+ 'start_date' => ( scalar($cgi->param('start_date'))
+ ? parse_datetime($cgi->param('start_date'))
+ : ''
+ ),
+ 'refnum' => $refnum,
+ 'locationnum' => $locationnum,
+ 'discountnum' => $discountnum,
+ #for the create a new discount case
+ 'discountnum__type' => scalar($cgi->param('discountnum__type')),
+ 'discountnum_amount' => scalar($cgi->param('discountnum_amount')),
+ 'discountnum_percent' => scalar($cgi->param('discountnum_percent')),
+ 'discountnum_months' => scalar($cgi->param('discountnum_months')),
+ 'discountnum_setup' => scalar($cgi->param('discountnum_setup')),
+ 'contract_end' => ( scalar($cgi->param('contract_end'))
+ ? parse_datetime($cgi->param('contract_end'))
+ : ''
+ ),
+ 'waive_setup' => ( $cgi->param('waive_setup') eq 'Y' ? 'Y' : '' ),
+);
+$hash{'custnum'} = $cust_main->custnum if $cust_main;
+
+if ( $quotationnum ) {
+
+ $quotation_pkg = new FS::quotation_pkg \%hash;
+ $quotation_pkg->quotationnum($quotationnum);
+ $quotation_pkg->prospectnum($prospect_main->prospectnum) if $prospect_main;
-my $error = $cust_main->order_pkg( \%opt );
+ #XXX handle new location
+ $error = $quotation_pkg->insert;
+
+} else {
+
+ $cust_pkg = new FS::cust_pkg \%hash;
+
+ $cust_pkg->no_auto( scalar($cgi->param('no_auto')) );
+
+ my %opt = ( 'cust_pkg' => $cust_pkg );
+
+ if ( $locationnum == -1 ) {
+ my $cust_location = new FS::cust_location {
+ map { $_ => scalar($cgi->param($_)) }
+ qw( custnum address1 address2 city county state zip country geocode )
+ };
+ $opt{'cust_location'} = $cust_location;
+ }
+
+ $error = $cust_main->order_pkg( \%opt );
+
+}
</%init>
diff --git a/httemplate/edit/process/svc_acct.cgi b/httemplate/edit/process/svc_acct.cgi
index a7d5136fb..41aca65ee 100755
--- a/httemplate/edit/process/svc_acct.cgi
+++ b/httemplate/edit/process/svc_acct.cgi
@@ -56,13 +56,14 @@ my $new = new FS::svc_acct ( \%hash );
my $error = '';
+my $part_svc = $svcnum ?
+ $old->part_svc :
+ qsearchs( 'part_svc',
+ { 'svcpart' => $cgi->param('svcpart') }
+ );
+
# google captcha auth
if ( $cgi->param('captcha_response') ) {
- my $part_svc = $svcnum ?
- $old->part_svc :
- qsearchs( 'part_svc',
- { 'svcpart' => $cgi->param('svcpart') }
- );
my ($export) = $part_svc->part_export('acct_google');
if ( $export and
! $export->captcha_auth($cgi->param('captcha_response')) ) {
@@ -79,6 +80,18 @@ if ( $cgi->param('clear_password') eq '*HIDDEN*'
}
if ( ! $error ) {
+
+ my $export_info = FS::part_export::export_info();
+
+ my @svc_export_machine =
+ map FS::svc_export_machine->new({
+ 'svcnum' => $svcnum,
+ 'exportnum' => $_->exportnum,
+ 'machinenum' => scalar($cgi->param('exportnum'.$_->exportnum.'machinenum')),
+ }),
+ grep { $_->machine eq '_SVC_MACHINE' }
+ $part_svc->part_export;
+
if ( $svcnum ) {
foreach ( grep { $old->$_ != $new->$_ }
qw( seconds upbytes downbytes totalbytes )
@@ -92,9 +105,9 @@ if ( ! $error ) {
$error ||= $new->set_usage(\%hash); #unoverlimit and trigger radius changes
last; #once is enough
}
- $error ||= $new->replace($old);
+ $error ||= $new->replace($old, 'child_objects'=>\@svc_export_machine);
} else {
- $error ||= $new->insert;
+ $error ||= $new->insert('child_objects'=>\@svc_export_machine);
$svcnum = $new->svcnum;
}
}
diff --git a/httemplate/edit/process/svc_broadband.cgi b/httemplate/edit/process/svc_broadband.cgi
index 90eab4aad..25644e547 100644
--- a/httemplate/edit/process/svc_broadband.cgi
+++ b/httemplate/edit/process/svc_broadband.cgi
@@ -1,11 +1,10 @@
<& elements/svc_Common.html,
- table => 'svc_broadband',
- fields => [ fields('svc_broadband'), fields('nas'), 'usergroup' ],
+ table => 'svc_broadband',
+ fields => [ fields('svc_broadband'), fields('nas'), 'usergroup' ],
precheck_callback => \&precheck,
&>
<%init>
-# for historical reasons, process_m2m for usergroup tables is done
-# in the svc_x::insert/replace/delete methods, not here
+
my $curuser = $FS::CurrentUser::CurrentUser;
die "access denied"
diff --git a/httemplate/edit/radius_group.html b/httemplate/edit/radius_group.html
index 0c99b4c4c..d3ef40c5d 100644
--- a/httemplate/edit/radius_group.html
+++ b/httemplate/edit/radius_group.html
@@ -7,6 +7,8 @@
'description' => 'Description',
'attrnum' => 'Attribute',
'priority' => 'Priority',
+ 'speed_down' => 'Download speed',
+ 'speed_up' => 'Upload speed',
},
'viewall_dir' => 'browse',
'menubar' => \@menubar,
@@ -28,6 +30,16 @@
'size' => 2,
'colspan' => 6, # just to not interfere with radius_attr columns
},
+ { 'field' => 'speed_down',
+ 'type' => 'text',
+ 'size' => 8,
+ 'colspan' => 6,
+ },
+ { 'field' => 'speed_up',
+ 'type' => 'text',
+ 'size' => 8,
+ 'colspan' => 6,
+ },
{
'field' => 'attrnum',
'type' => 'radius_attr',
diff --git a/httemplate/edit/reason.html b/httemplate/edit/reason.html
index 620a2ea15..78d044755 100644
--- a/httemplate/edit/reason.html
+++ b/httemplate/edit/reason.html
@@ -1,50 +1,78 @@
-%
-% $cgi->param('class') =~ /^(\w)$/ or die "illegal class";
-% my $class=$1;
-%
-% my $classname = $FS::reason_type::class_name{$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',
- #XXX use something more sane than a hashref
- #then fix tr-select.html
- 'value' => { 'vcolumn' => 'typenum',
- 'ccolumn' => 'type',
- 'values' => \@types,
- },
- },
- 'reason',
- { 'field' => 'class',
- 'type' => 'hidden',
- 'value' => $class,
- },
- { 'field' => 'disabled',
- 'type' => 'checkbox',
- 'value' => 'Y'
- },
- ],
- 'viewall_url' => $p . "browse/reason.html?class=$class",
- )
-%>
+<& elements/edit.html,
+ 'menubar'=> [ "View all $classname Reasons" =>
+ $p.'browse/reason.html?class='.$class,
+ "View $classname Reason Types" =>
+ $p.'browse/reason_type.html?class='.$class,
+ ],
+ 'name' => ucfirst($classname) . ' Reason',
+
+ 'table' => 'reason',
+ 'labels' => {
+ 'reasonnum' => $classname . ' Reason',
+ 'reason_type' => $classname . ' Reason type',
+ 'reason' => $classname . ' Reason',
+ 'disabled' => 'Disabled',
+ 'class' => '',
+ 'unsuspend_pkgpart' => 'Unsuspension fee',
+ 'unsuspend_hold' => 'Delay until next bill',
+ },
+ 'fields' => \@fields,
+&>
<%init>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+$cgi->param('class') =~ /^(\w)$/ or die "illegal class";
+my $class=$1;
+
+my $classname = ucfirst($FS::reason_type::class_name{$class});
+
+my (@types) = qsearch( 'reason_type', { 'class' => $class } );
+
+unless (scalar(@types)) {
+ print $cgi->redirect( "reason_type.html?class=$class" );
+}
+
+my @fields = (
+ { 'field' => 'reason_type',
+ 'type' => 'select-table',
+ 'table' => 'reason_type',
+ 'name_col' => 'type',
+ 'value_col' => 'typenum',
+ 'hashref' => { 'class' => $class },
+ 'disable_empty' => 1,
+# #then fix tr-select.html
+#
+# 'value' => { 'vcolumn' => 'typenum',
+# 'ccolumn' => 'type',
+# 'values' => \@types,
+# },
+# # that wasn't so hard...did this do something else that I'm missing?
+ },
+ 'reason',
+ { 'field' => 'class',
+ 'type' => 'hidden',
+ 'value' => $class,
+ },
+ { 'field' => 'disabled',
+ 'type' => 'checkbox',
+ 'value' => 'Y'
+ },
+);
+
+push @fields,
+ { 'field' => 'unsuspend_pkgpart',
+ 'type' => 'select-part_pkg',
+ 'hashref' => { 'disabled' => '',
+ 'freq' => 0 }, # one-time charges only
+ },
+ { 'field' => 'unsuspend_hold',
+ 'type' => 'checkbox',
+ 'value' => 'Y',
+ },
+ if ( $class eq 'S' );
+
+
+
</%init>
diff --git a/httemplate/edit/svc_acct.cgi b/httemplate/edit/svc_acct.cgi
index 38567ef67..142c11150 100755
--- a/httemplate/edit/svc_acct.cgi
+++ b/httemplate/edit/svc_acct.cgi
@@ -173,6 +173,12 @@ function randomPass() {
<INPUT TYPE="hidden" NAME="sectornum" VALUE="<% $svc_acct->sectornum %>">
%}
+<& /elements/tr-svc_export_machine.html,
+ 'svc' => $svc_acct,
+ 'part_svc' => $part_svc,
+ 'cgi' => $cgi,
+&>
+
% #uid/gid
% foreach my $xid (qw( uid gid )) {
%
diff --git a/httemplate/elements/dashboard-toplist.html b/httemplate/elements/dashboard-toplist.html
index 72f596f4a..c6362e0c9 100644
--- a/httemplate/elements/dashboard-toplist.html
+++ b/httemplate/elements/dashboard-toplist.html
@@ -32,18 +32,21 @@
</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;
-
+% foreach my $priority ( @custom_priorities ) {
<TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="right">
- <% $ahref.$num %></A>
- </TD>
+% my $num = $num_tickets_by_priority{$priority}->{$custnum};
+% if ( $num ) {
+ <A HREF="<%
+ FS::TicketSystem->href_customer_tickets($custnum,$priority)
+ %>"><% $num %></A>
+% if ( $priority &&
+% exists($num_tickets_by_priority{''}{$custnum}) ) {
+% # decrement the customer's total by the number in
+% # this priority bin
+% $num_tickets_by_priority{''}{$custnum} -= $num;
+% }
+% }
+ </TD>
% }
</TR>
@@ -77,7 +80,7 @@
<TH CLASS="grid" BGCOLOR="#cccccc"><% $line %></TH>
<TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Lint') |h %></TH>
<TH CLASS="grid" BGCOLOR="#cccccc"></TH>
-% foreach my $priority ( @custom_priorities, '' ) {
+% foreach my $priority ( @custom_priorities ) {
<TH CLASS="grid" BGCOLOR="#cccccc">
<% $priority || '<i>(none)</i>'%>
</TH>
@@ -105,11 +108,83 @@ 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')
+my $custom_priority_field = $conf->config('ticket_system-custom_priority_field');
+if ( $custom_priority_field
&& @{[ $conf->config('ticket_system-custom_priority_field-values') ]} ) {
@custom_priorities =
$conf->config('ticket_system-custom_priority_field-values');
}
-
+push @custom_priorities, '';
+
+my %num_tickets_by_priority = map { $_ => {} } @custom_priorities;
+# "optimization" (i.e. "terrible hack") to avoid constructing
+# (@custom_priorities) x (cust_main) queries with a bazillion
+# joins each just to count tickets
+if ( $FS::TicketSystem::system eq 'RT_Internal'
+ and $conf->config('dashboard-toplist') )
+{
+ my $text = (driver_name =~ /^Pg/) ? 'text' : 'char';
+ # The RT API does not play nicely with aggregate queries,
+ # so we're going to go around it.
+ my $sql;
+ # optimization to keep this from taking a million years
+ my $cust_tickets =
+ "SELECT custnum, Tickets.Id, Tickets.Queue
+ FROM cust_main
+ JOIN Links ON (
+ Links.Target = 'freeside://freeside/cust_main/' || CAST(cust_main.custnum AS $text)
+ AND Links.Base LIKE '%rt://%/ticket/%'
+ AND Links.Type = 'MemberOf'
+ ) JOIN Tickets ON (Links.LocalBase = Tickets.Id)
+ UNION
+ SELECT custnum, Tickets.Id, Tickets.Queue
+ FROM cust_pkg JOIN cust_svc USING (pkgnum)
+ JOIN Links ON (
+ Links.Target = 'freeside://freeside/cust_svc/' || CAST(cust_svc.svcnum AS $text)
+ AND Links.Base LIKE '%rt://%/ticket/%'
+ AND Links.Type = 'MemberOf'
+ ) JOIN Tickets ON (Links.LocalBase = Tickets.Id)
+ ";
+
+ if ( $custom_priority_field ) {
+ $sql =
+ "SELECT cust_tickets.custnum AS custnum,
+ ObjectCustomFieldValues.Content as priority,
+ COUNT(DISTINCT cust_tickets.Id) AS num_tickets
+ FROM ($cust_tickets) AS cust_tickets
+ LEFT JOIN ObjectCustomFields ON (
+ ObjectCustomFields.ObjectId = '0' OR
+ ObjectCustomFields.ObjectId = cust_tickets.Queue
+ )
+ LEFT JOIN CustomFields ON (
+ ObjectCustomFields.CustomField = CustomFields.Id AND
+ CustomFields.Name = '$custom_priority_field'
+ )
+ LEFT JOIN ObjectCustomFieldValues ON (
+ ObjectCustomFieldValues.CustomField = CustomFields.Id AND
+ ObjectCustomFieldValues.ObjectType = 'RT::Ticket' AND
+ ObjectCustomFieldValues.Disabled = '0' AND
+ ObjectCustomFieldValues.ObjectId = cust_tickets.Id
+ )
+ GROUP BY cust_tickets.custnum, ObjectCustomFieldValues.Content";
+ #warn $sql."\n";
+ } else { # no custom_priority_field
+ $sql =
+ "SELECT cust_tickets.custnum,
+ '' as priority,
+ COUNT(DISTINCT cust_tickets.Id) AS num_tickets
+ FROM ($cust_tickets) AS cust_tickets
+ GROUP BY cust_tickets.custnum";
+ }
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+ while ( my $row = $sth->fetchrow_hashref ) {
+ #warn to_json($row)."\n";
+ $num_tickets_by_priority{ $row->{priority} }->{ $row->{custnum} } =
+ $row->{num_tickets};
+ }
+}
+#warn Dumper \%num_tickets_by_priority;
</%init>
diff --git a/httemplate/elements/location.html b/httemplate/elements/location.html
index 767231856..5c7c888de 100644
--- a/httemplate/elements/location.html
+++ b/httemplate/elements/location.html
@@ -11,8 +11,10 @@ Example:
'no_asterisks' => 0, #set true to disable the red asterisks next
#to required fields
'address1_label' => 'Address', #label for address
+ 'enable_coords' => 1, #show latitude/longitude fields
'enable_district' => 1, #show tax district field
'enable_censustract' => 1, #show censustract field
+
)
</%doc>
@@ -175,6 +177,7 @@ Example:
<TD COLSPAN=6><% include('/elements/select-country.html', %select_hash ) %></TD>
</TR>
+% if ( $opt{enable_coords} ) {
<TR>
<TD ALIGN="right"><% mt('Latitude') |h %></TH>
<TD COLSPAN=7>
@@ -195,6 +198,11 @@ Example:
>
</TD>
</TR>
+% } else {
+% foreach (qw(latitude longitude)) {
+<INPUT TYPE="hidden" NAME="<% $_ %>" VALUE="<% $object->get($_) |h%>">
+% }
+% }
<INPUT TYPE="hidden" NAME="<%$pre%>coord_auto" VALUE="<% $object->coord_auto %>">
<INPUT TYPE="hidden" NAME="<%$pre%>geocode" VALUE="<% $object->geocode %>">
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index 019afe94e..b2141e991 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -94,6 +94,11 @@ tie my %report_prospects, 'Tie::IxHash',
'Advanced prospect reports' => [ $fsurl. 'search/report_prospect_main.html', '' ],
;
+tie my %report_quotations, 'Tie::IxHash',
+ 'List quotations' => [ $fsurl. 'search/quotation.html', '' ],
+ 'Advanced quotation reports' => [ $fsurl. 'search/report_quotation.html', '' ],
+;
+
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', '' ],
@@ -256,6 +261,8 @@ tie my %report_inventory, 'Tie::IxHash',
tie my %report_rating, 'Tie::IxHash';
$report_rating{'RADIUS sessions'} = [ $fsurl.'search/sqlradius.html', '' ]
if $curuser->access_right("Usage: RADIUS sessions");
+$report_rating{'RADIUS data usage'} = [ $fsurl.'search/report_sqlradius_usage.html', '' ]
+ if $curuser->access_right("Usage: RADIUS sessions");
$report_rating{'Call Detail Records (CDRs)'} = [ $fsurl.'search/report_cdr.html', '' ]
if $curuser->access_right("Usage: Call Detail Records (CDRs)");
$report_rating{'Unrateable CDRs'} = [ $fsurl.'search/cdr.html?freesidestatus=failed;cdrbatchnum=_ALL_' ]
@@ -342,6 +349,8 @@ if($curuser->access_right('Financial reports')) {
tie my %report_menu, 'Tie::IxHash';
$report_menu{'Prospects'} = [ \%report_prospects, 'Prospect reports' ]
if $curuser->access_right('List prospects');
+$report_menu{'Quotations'} = [ \%report_quotations, 'Quotation reports' ]
+ if $curuser->access_right('List quotations');
$report_menu{'Customers'} = [ \%report_customers, 'Customer reports' ]
if $curuser->access_right('List customers');
$report_menu{'Invoices'} = [ \%report_invoices, 'Invoice reports' ]
diff --git a/httemplate/elements/select-rt-customfield.html b/httemplate/elements/select-rt-customfield.html
index 7a45bb14b..85758d585 100644
--- a/httemplate/elements/select-rt-customfield.html
+++ b/httemplate/elements/select-rt-customfield.html
@@ -3,9 +3,6 @@
<OPTION VALUE="<% shift @fields %>"><% shift @fields %></OPTION>
% }
</SELECT>
-<%once>
-RT::Init();
-</%once>
<%init>
my %opt = @_;
my $lookuptype = $opt{lookuptype};
diff --git a/httemplate/elements/select-table.html b/httemplate/elements/select-table.html
index 127028ee5..c0cd7a50b 100644
--- a/httemplate/elements/select-table.html
+++ b/httemplate/elements/select-table.html
@@ -181,24 +181,29 @@ if ( $opt{'records'} ) {
});
}
-unless ( $value < 1 # !$value #ignore negatives too
- or ref($value)
+if ( ref( $value ) eq 'ARRAY' ) {
+ $value = { map { $_ => 1 } @$value };
+}
+
+unless ( !ref($value) && $value < 1 # !$value #ignore negatives too
or ! exists( $opt{hashref}->{disabled} ) #??
- or grep { $value == $_->$key() } @records
+ #or grep { $value == $_->$key() } @records
) {
delete $opt{hashref}->{disabled};
- $opt{hashref}->{$key} = $value;
- my $record = qsearchs( {
- 'table' => $opt{table},
- 'addl_from' => $opt{'addl_from'},
- 'hashref' => $hashref,
- 'extra_sql' => $extra_sql,
- });
- push @records, $record if $record;
-}
-if ( ref( $value ) eq 'ARRAY' ) {
- $value = { map { $_ => 1 } @$value };
+ foreach my $v ( ref($value) ? keys %$value : ($value) ) {
+ next if grep { $v == $_->$key() } @records;
+
+ $opt{hashref}->{$key} = $v;
+ my $record = qsearchs( {
+ 'table' => $opt{table},
+ 'addl_from' => $opt{'addl_from'},
+ 'hashref' => $hashref,
+ 'extra_sql' => $extra_sql,
+ });
+ push @records, $record if $record;
+
+ }
}
my @pre_options = $opt{pre_options} ? @{ $opt{pre_options} } : ();
diff --git a/httemplate/elements/tr-amount_fee.html b/httemplate/elements/tr-amount_fee.html
index a1a9e3433..12488521a 100644
--- a/httemplate/elements/tr-amount_fee.html
+++ b/httemplate/elements/tr-amount_fee.html
@@ -90,7 +90,9 @@ if ( $amount > 0 ) {
$amount += $fee
if $fee && $fee_display eq 'subtract';
- &{ $opt{post_fee_callback} }( \$amount ) if $opt{post_fee_callback};
+ #&{ $opt{post_fee_callback} }( \$amount ) if $opt{post_fee_callback};
+ $amount += $amount * $opt{'surcharge_percentage'}/100
+ if $opt{'surcharge_percentage'} > 0;
$amount = sprintf("%.2f", $amount);
}
diff --git a/httemplate/elements/tr-select-cust_location.html b/httemplate/elements/tr-select-cust_location.html
index d9e3e9e27..b804f4548 100644
--- a/httemplate/elements/tr-select-cust_location.html
+++ b/httemplate/elements/tr-select-cust_location.html
@@ -216,6 +216,7 @@ Example:
'no_asterisks' => 1,
'no_bold' => $opt{'no_bold'},
'alt_format' => $opt{'alt_format'},
+ 'enable_coords'=> 1,
)
%>
<SCRIPT TYPE="text/javascript">
diff --git a/httemplate/elements/tr-select-discount.html b/httemplate/elements/tr-select-discount.html
index 30a60ec85..ee862519f 100644
--- a/httemplate/elements/tr-select-discount.html
+++ b/httemplate/elements/tr-select-discount.html
@@ -6,7 +6,7 @@
% } else {
<TR>
- <TD ALIGN="right" WIDTH="176"><% $opt{'label'} || '<B>'.emt('Discount').'</B>' %></TD>
+ <TD ALIGN="right" WIDTH="275"><% $opt{'label'} || '<B>'.emt('Discount').'</B>' %></TD>
<TD <% $colspan %>>
<% include( '/elements/select-discount.html',
'curr_value' => $discountnum,
@@ -74,6 +74,16 @@
)
%>
+%# <% include( '/elements/tr-checkbox.html',
+%# 'label' => '<B>Apply discount to add-on packages</B>',
+%# 'field' => $name.'_linked',
+%# 'id' => $name.'_linked',
+%# 'curr_value' => scalar($cgi->param($name.'_linked')),
+%# 'value' => 'Y',
+%# 'colspan' => $opt{'colspan'},
+%# )
+%# %>
+
<SCRIPT TYPE="text/javascript">
% my $ge = 'document.getElementById';
@@ -136,6 +146,10 @@
<% $ge %>('<% $name %>_percent_label0').style.visibility = 'hidden';
<% $ge %>('<% $name %>_percent_input0').style.display = 'none';
<% $ge %>('<% $name %>_percent_input0').style.visibility = 'hidden';
+// <% $ge %>('<% $name %>_linked_label0').style.display = 'none';
+// <% $ge %>('<% $name %>_linked_label0').style.visibility = 'hidden';
+// <% $ge %>('<% $name %>_linked').style.display = 'none';
+// <% $ge %>('<% $name %>_linked').style.visibility = 'hidden';
} else if ( <% $name %>__type == 'Amount' ) {
<% $ge %>('<% $name %>_amount_label0').style.display = '';
<% $ge %>('<% $name %>_amount_label0').style.visibility = '';
@@ -145,6 +159,11 @@
<% $ge %>('<% $name %>_percent_label0').style.visibility = 'hidden';
<% $ge %>('<% $name %>_percent_input0').style.display = 'none';
<% $ge %>('<% $name %>_percent_input0').style.visibility = 'hidden';
+ <% $ge %>('<% $name %>_percent_input0').style.visibility = 'hidden';
+// <% $ge %>('<% $name %>_linked_label0').style.display = 'none';
+// <% $ge %>('<% $name %>_linked_label0').style.visibility = 'hidden';
+// <% $ge %>('<% $name %>_linked').style.display = 'none';
+// <% $ge %>('<% $name %>_linked').style.visibility = 'hidden';
} else if ( <% $name %>__type == 'Percentage' ) {
<% $ge %>('<% $name %>_amount_label0').style.display = 'none';
<% $ge %>('<% $name %>_amount_label0').style.visibility = 'hidden';
@@ -154,6 +173,11 @@
<% $ge %>('<% $name %>_percent_label0').style.visibility = '';
<% $ge %>('<% $name %>_percent_input0').style.display = '';
<% $ge %>('<% $name %>_percent_input0').style.visibility = '';
+ <% $ge %>('<% $name %>_percent_input0').style.visibility = '';
+// <% $ge %>('<% $name %>_linked_label0').style.display = '';
+// <% $ge %>('<% $name %>_linked_label0').style.visibility = '';
+// <% $ge %>('<% $name %>_linked').style.display = '';
+// <% $ge %>('<% $name %>_linked').style.visibility = '';
}
}
diff --git a/httemplate/elements/tr-select-part_referral.html b/httemplate/elements/tr-select-part_referral.html
index 765aa8400..5041f7f73 100644
--- a/httemplate/elements/tr-select-part_referral.html
+++ b/httemplate/elements/tr-select-part_referral.html
@@ -14,13 +14,7 @@
<INPUT TYPE="hidden" NAME="<% $opt{'element_name'} || $opt{'field'} || 'refnum' %>" VALUE="<% $opt{'part_referrals'}->[0]->refnum %>">
% } else {
-
- <TR>
-% if ( $opt{'label'} ) {
- <TD ALIGN="right"><% $opt{'label'} %></TD>
-% } else {
- <TH ALIGN="right"><%$r%><% mt('Advertising source') |h %></TH>
-% }
+ <& /elements/tr-td-label.html, label => 'Advertising source', %opt &>
<TD COLSPAN="<% $colspan %>">
<& /elements/select-part_referral.html,
'curr_value' => $refnum,
diff --git a/httemplate/elements/tr-select-reason.html b/httemplate/elements/tr-select-reason.html
index 5a79d68ef..c1df10b94 100755
--- a/httemplate/elements/tr-select-reason.html
+++ b/httemplate/elements/tr-select-reason.html
@@ -32,8 +32,15 @@ Example:
<SCRIPT TYPE="text/javascript">
function sh_add<% $func_suffix %>()
{
+ var hints = <% encode_json(\@hints) %>;
+ var select_reason = document.getElementById('<% $id %>');
- if (document.getElementById('<% $id %>').selectedIndex == 0){
+% if ( $class eq 'S' ) {
+ document.getElementById('<% $id %>_hint').innerHTML =
+ hints[select_reason.selectedIndex];
+% }
+
+ if (select_reason.selectedIndex == 0){
<% $controlledbutton ? $controlledbutton.'.disabled = true;' : ';' %>
}else{
<% $controlledbutton ? $controlledbutton.'.disabled = false;' : ';' %>
@@ -41,8 +48,8 @@ Example:
%if ($curuser->access_right($add_access_right)){
- if (document.getElementById('<% $id %>').selectedIndex ==
- (document.getElementById('<% $id %>').length - 1)) {
+ if (select_reason.selectedIndex ==
+ (select_reason.length - 1)) {
document.getElementById('new<% $id %>').disabled = false;
document.getElementById('new<% $id %>').style.display = 'inline';
document.getElementById('new<% $id %>Label').style.display = 'inline';
@@ -113,6 +120,13 @@ Example:
</TR>
% }
+% if ( $class eq 'S' ) {
+<TR>
+ <TD COLSPAN=2 ALIGN="center" id="<% $id %>_hint">
+ </TD>
+</TR>
+% }
+
<TR>
<TD ALIGN="right">
<P id="new<% $id %>Label" style="display:<% $display %>"><% mt('New Reason') |h %></P>
@@ -184,6 +198,43 @@ my @reasons = qsearch({
order_by => 'ORDER BY reason_type.type ASC, reason.reason ASC',
});
+my @hints;
+if ( $class eq 'S' ) {
+ my $conf = FS::Conf->new;
+ @hints = ( '' );
+ foreach my $reason (@reasons) {
+ if ( $reason->unsuspend_pkgpart ) {
+ my $part_pkg = FS::part_pkg->by_key($reason->unsuspend_pkgpart);
+ if ( $part_pkg ) {
+ if ( $part_pkg->option('setup_fee',1) > 0 and
+ $part_pkg->option('recur_fee',1) == 0 ) {
+ # the usual case
+ push @hints,
+ mt('A [_1] unsuspension fee will apply.',
+ ($conf->config('money_char') || '$') .
+ sprintf('%.2f', $part_pkg->option('setup_fee'))
+ );
+ } else {
+ # oddball cases--not really supported
+ push @hints,
+ mt('An unsuspension package will apply: [_1]',
+ $part_pkg->price_info
+ );
+ }
+ } else { #no $part_pkg
+ push @hints,
+ '<FONT COLOR="#ff0000">Unsuspend pkg #'.$reason->unsuspend_pkgpart.
+ ' not found.</FONT>';
+ }
+ } else { #no unsuspend_pkgpart
+ push @hints, '';
+ }
+ }
+ push @hints, ''; # for the "new reason" case
+ @hints = map {'<FONT SIZE="-1">'.$_.'</FONT>'} @hints;
+}
+
+
my $curuser = $FS::CurrentUser::CurrentUser;
</%init>
diff --git a/httemplate/elements/tr-select-voip_class.html b/httemplate/elements/tr-select-voip_class.html
new file mode 100644
index 000000000..dcc1487cc
--- /dev/null
+++ b/httemplate/elements/tr-select-voip_class.html
@@ -0,0 +1,24 @@
+<& tr-td-label.html, label => 'Category', @_ &>
+<TD>
+<SELECT NAME="<% $opt{'field'} %>">
+% while(@options) {
+% my $value = shift @options;
+% my $selected = ($value eq $opt{'curr_value'}) ? 'SELECTED' : '';
+ <OPTION VALUE="<% $value %>" <% $selected %>><% shift @options %></OPTION>
+% }
+</SELECT>
+</TD></TR>
+<%init>
+my %opt = (
+ field => 'fcc_voip_class',
+ label => 'Category',
+ @_
+);
+my @options = (
+ '' => '',
+ 1 => 'VoIP without Broadband',
+ 2 => 'VoIP with Broadband',
+ 3 => 'Wholesale VoIP'
+);
+
+</%init>
diff --git a/httemplate/elements/tr-svc_export_machine.html b/httemplate/elements/tr-svc_export_machine.html
new file mode 100644
index 000000000..92b6ac1d7
--- /dev/null
+++ b/httemplate/elements/tr-svc_export_machine.html
@@ -0,0 +1,37 @@
+% foreach my $part_export (@part_export) {
+% my $label = ( $part_export->exportname
+% ? $part_export->exportname
+% : $part_export->label
+% ).
+% ' hostname';
+%
+% my $element = 'exportnum'. $part_export->exportnum. 'machinenum';
+% my $machinenum = $opt{cgi}->param($element);
+% if ( ! $machinenum && $opt{svc}->svcnum ) {
+% my $svc_export_machine = qsearchs('svc_export_machine', {
+% 'svcnum' => $opt{svc}->svcnum,
+% 'exportnum' => $part_export->exportnum,
+% });
+% $machinenum = $svc_export_machine->machinenum if $svc_export_machine;
+% }
+
+ <& /elements/tr-select-table.html,
+ 'label' => $label,
+ 'element_name' => 'exportnum'. $part_export->exportnum. 'machinenum',
+ 'table' => 'part_export_machine',
+ 'name_col' => 'machine',
+ 'hashref' => { 'exportnum' => $part_export->exportnum,
+ 'disabled' => '',
+ },
+ 'curr_value' => $machinenum,
+ 'empty_label' => 'Select export hostname',
+ &>
+% }
+<%init>
+
+my %opt = @_;
+
+my @part_export = grep { $_->machine eq '_SVC_MACHINE' }
+ $opt{part_svc}->part_export;
+
+</%init>
diff --git a/httemplate/graph/cust_bill_pkg.cgi b/httemplate/graph/cust_bill_pkg.cgi
index e7a3bd27e..c334ae9e7 100644
--- a/httemplate/graph/cust_bill_pkg.cgi
+++ b/httemplate/graph/cust_bill_pkg.cgi
@@ -8,6 +8,7 @@
'graph_labels' => \@labels,
'colors' => \@colors,
'links' => \@links,
+ 'no_graph' => \@no_graph,
'remove_empty' => 1,
'bottom_total' => 1,
'bottom_link' => $bottom_link,
@@ -118,6 +119,7 @@ my @params = ();
my @labels = ();
my @colors = ();
my @links = ();
+my @no_graph;
my @components = ( 'SRU' );
# split/omit components as appropriate
@@ -134,6 +136,11 @@ elsif ( $use_usage == 2 ) {
$components[-1] =~ s/U//;
}
+# Categorization of line items goes
+# Agent -> Referral -> Package class -> Component (setup/recur/usage)
+# If per-agent totals are enabled, they go under the Agent level.
+# There aren't any other kinds of subtotals.
+
foreach my $agent ( $all_agent || $sel_agent || qsearch('agent', { 'disabled' => '' } ) ) {
my $col_scheme = Color::Scheme->new
@@ -146,7 +153,11 @@ foreach my $agent ( $all_agent || $sel_agent || qsearch('agent', { 'disabled' =>
### fixup the color handling for package classes...
### and usage
- foreach my $part_referral ( $all_part_referral || $sel_part_referral || qsearch('part_referral', { 'disabled' => '' } ) ) {
+ foreach my $part_referral (
+ $all_part_referral ||
+ $sel_part_referral ||
+ qsearch('part_referral', { 'disabled' => '' } )
+ ) {
foreach my $pkg_class ( @pkg_class ) {
foreach my $component ( @components ) {
@@ -186,9 +197,46 @@ foreach my $agent ( $all_agent || $sel_agent || qsearch('agent', { 'disabled' =>
@onetime_colors = ($col_scheme->colors)[2,6,10,3,7,11]
unless @onetime_colors;
push @colors, shift @recur_colors;
-
- }
+ push @no_graph, 0;
+
+ } #foreach $component
+ } #foreach $pkg_class
+ } #foreach $part_referral
+
+ if ( $cgi->param('agent_totals') and !$all_agent ) {
+ my $row_agentnum = $agent->agentnum;
+ # Include all components that are anywhere on this report
+ my $component = join('', @components);
+
+ my @row_params = ( 'agentnum' => $row_agentnum,
+ 'use_override' => $use_override,
+ 'average_per_cust_pkg' => $average_per_cust_pkg,
+ 'distribute' => $distribute,
+ 'charges' => $component,
+ );
+ my $row_link = "$link;".
+ "agentnum=$row_agentnum;".
+ "distribute=$distribute;".
+ "charges=$component";
+
+ # Also apply any refnum/classnum filters
+ if ( !$all_class and scalar(@pkg_class) == 1 ) {
+ # then a specific class has been chosen, but it may be the empty class
+ my $row_classnum = ref($pkg_class[0]) ? $pkg_class[0]->classnum : 0;
+ push @row_params, 'classnum' => $row_classnum;
+ $row_link .= ";classnum=$row_classnum";
}
+ if ( $sel_part_referral ) {
+ push @row_params, 'refnum' => $sel_part_referral->refnum;
+ $row_link .= ";refnum=".$sel_part_referral->refnum;
+ }
+
+ push @items, 'cust_bill_pkg';
+ push @labels, mt('[_1] - Subtotal', $agent->agent);
+ push @params, \@row_params;
+ push @links, $row_link;
+ push @colors, '000000'; # better idea?
+ push @no_graph, 1;
}
$hue += $hue_increment;
diff --git a/httemplate/graph/elements/monthly.html b/httemplate/graph/elements/monthly.html
index 839a3873e..c736de696 100644
--- a/httemplate/graph/elements/monthly.html
+++ b/httemplate/graph/elements/monthly.html
@@ -20,6 +20,7 @@ Example:
'link_fromparam' => 'param_from', #defaults to 'begin'
'link_toparam' => 'param_to', #defaults to 'end'
'daily' => 1, # omit for monthly granularity
+ 'no_graph' => \@no_graph, # items to leave off the graph (subtotals)
#optional, pulled from CGI params if not specified
'start_month' => $smonth,
@@ -49,18 +50,19 @@ Example:
'items' => $data->{'items'},
'data' => $data->{'data'},
'row_labels' => $data->{'item_labels'},
- 'graph_labels' => $opt{'graph_labels'} || $data->{'item_labels'},
+ 'graph_labels' => \@graph_labels,
'col_labels' => $col_labels,
'axis_labels' => $data->{label},
- 'colors' => $data->{colors},
+ 'colors' => \@colors,
'links' => \@links,
+ 'no_graph' => \@no_graph,
'bottom_link' => \@bottom_link,
'transpose' => $opt{'daily'},
- map { $_, $opt{$_} } (qw(title
- nototal
- graph_type
- bottom_total
- sprintf
+ map { $_, $opt{$_} } (qw(title
+ nototal
+ graph_type
+ bottom_total
+ sprintf
disable_money
chart_options)),
) %>
@@ -103,7 +105,7 @@ if ( $opt{'daily'} ) { # daily granularity
my %reportopts = (
'items' => \@items,
'params' => $opt{'params'},
- 'item_labels' => ( $cgi->param('_type') =~ /^(png)$/
+ 'item_labels' => ( $cgi->param('_type') =~ /^(png)$/
? $opt{'graph_labels'}
: $opt{'labels'}
),
@@ -140,12 +142,20 @@ my $col_labels = [ map { my $m = $_; $m =~ s/^(\d+)\//$mon[$1-1] / ; $m }
@{$data->{label}} ];
$col_labels = $data->{label} if $opt{'daily'};
+my @colors;
+my @graph_labels;
+my @no_graph;
if ( $opt{'remove_empty'} ) {
- # need to filter out series labels for collapsed rows
- $opt{'graph_labels'} = [
- map { $opt{'graph_labels'}[$_] }
- @{ $data->{indices} }
- ];
+ # then filter out per-item things for collapsed rows
+ foreach my $i (@{ $data->{'indices'} }) {
+ push @colors, $opt{'colors'}[$i];
+ push @graph_labels, $opt{'graph_labels'}[$i];
+ push @no_graph, $opt{'no_graph'}[$i];
+ }
+} else {
+ @colors = @{ $opt{'colors'} };
+ @graph_labels = @{ $opt{'graph_labels'} };
+ @no_graph = @{ $opt{'no_graph'} || [] };
}
my @links;
diff --git a/httemplate/graph/elements/report.html b/httemplate/graph/elements/report.html
index f7746165a..98b477826 100644
--- a/httemplate/graph/elements/report.html
+++ b/httemplate/graph/elements/report.html
@@ -14,6 +14,7 @@ Example:
'graph_labels' => \@graph_labels, #defaults to row_labels
'links' => \@links, #optional
+ 'no_graph' => \@no_graph, #optional
#these run parallel to the elements of each @item
'col_labels' => \@col_labels, #required
@@ -128,7 +129,19 @@ any delimiter and linked from the elements in @data.
%
<% $output %>
% } elsif ( $cgi->param('_type') eq 'png' ) {
-%
+% # delete any items that shouldn't be on the graph
+% if ( my $no_graph = $opt{'no_graph'} ) {
+% my $i = 0;
+% while (@$no_graph) {
+% if ( shift @$no_graph ) {
+% splice @data, $i, 1;
+% splice @{$opt{'graph_labels'}}, $i, 1;
+% splice @{$opt{'colors'}}, $i, 1;
+% $i--; # because everything is shifted down
+% }
+% $i++;
+% }
+% }
% my $graph_type = 'LinesPoints';
% if ( $opt{'graph_type'} =~ /^(LinesPoints|Mountain|Bars)$/ ) {
% $graph_type = $1;
diff --git a/httemplate/graph/report_cust_bill_pkg.html b/httemplate/graph/report_cust_bill_pkg.html
index 4cedcef17..31792e8dd 100644
--- a/httemplate/graph/report_cust_bill_pkg.html
+++ b/httemplate/graph/report_cust_bill_pkg.html
@@ -11,28 +11,45 @@
<TD>Show projected data for future months</TD>
</TR>
-<% include('/elements/tr-select-agent.html',
- 'label' => 'Agent ',
- 'disable_empty' => 0,
- 'pre_options' => [ 'all' => 'all (aggregate)' ],
- 'empty_label' => 'all (breakdown)',
- )
-%>
-
-<% include('/elements/tr-select-part_referral.html',
- 'label' => 'Advertising source ',
- 'disable_empty' => 0,
- 'pre_options' => [ 'all' => 'all (aggregate)' ],
- 'empty_label' => 'all (breakdown)',
- )
-%>
-
-<% include('/elements/tr-select-pkg_class.html',
- 'pre_options' => [ 'all' => 'all (aggregate)',
- '0' => 'all (breakdown)' ],
- 'empty_label' => '(empty class)',
- )
-%>
+<SCRIPT TYPE="text/javascript">
+function enable_agent_totals(obj) {
+%# enable it iff we are breaking down by agent AND something else
+ obj.form.agent_totals.disabled = !(
+ obj.form.agentnum.value == '' && (
+ obj.form.refnum.value == '' ||
+ obj.form.classnum.value == 0 ||
+ obj.form.use_setup.value == 1 ||
+ obj.form.use_usage.value == 1
+ )
+ );
+}
+</SCRIPT>
+
+<& /elements/tr-select-agent.html,
+ 'field' => 'agentnum',
+ 'label' => 'Agent ',
+ 'disable_empty' => 0,
+ 'pre_options' => [ 'all' => 'all (aggregate)' ],
+ 'empty_label' => 'all (breakdown)',
+ 'onchange' => 'enable_agent_totals',
+&>
+
+<& /elements/tr-select-part_referral.html,
+ 'field' => 'refnum',
+ 'label' => 'Advertising source ',
+ 'disable_empty' => 0,
+ 'pre_options' => [ 'all' => 'all (aggregate)' ],
+ 'empty_label' => 'all (breakdown)',
+ 'onchange' => 'enable_agent_totals'
+&>
+
+<& /elements/tr-select-pkg_class.html,
+ 'field' => 'classnum',
+ 'pre_options' => [ 'all' => 'all (aggregate)',
+ '0' => 'all (breakdown)' ],
+ 'empty_label' => '(empty class)',
+ 'onchange' => 'enable_agent_totals',
+&>
<!--
<TR>
@@ -47,10 +64,16 @@
'field' => 'use_'.lc($_),
'options' => [ 0, 1, 2 ],
'labels' => { 0 => 'Combine', 1 => 'Separate', 2 => 'Do not show' },
+ 'onchange'=> 'enable_agent_totals',
&>
% }
<TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="agent_totals" VALUE="1" DISABLED="1"></TD>
+ <TD>Show per-agent subtotals</TD>
+</TR>
+
+<TR>
<TD ALIGN="right"><INPUT TYPE="checkbox" NAME="use_override" VALUE="1"></TD>
<TD>Separate sub-packages from parents</TD>
</TR>
diff --git a/httemplate/index.html b/httemplate/index.html
index ae1509610..71926aa4e 100644
--- a/httemplate/index.html
+++ b/httemplate/index.html
@@ -1,9 +1,18 @@
+<%init>my $debug = $cgi->param('debug');</%init>
+% warn time.": header.html\n" if $debug;
+%
<& /elements/header.html, mt('Billing Main') &>
+% warn time.": dashboard-install_welcome.html\n" if $debug;
+%
<& /elements/dashboard-install_welcome.html &>
+% warn time.": dashboard-toplist.html\n" if $debug;
+%
<& /elements/dashboard-toplist.html &>
+% warn time.": fetching recently changed customers\n" if $debug;
+%
% 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 )
@@ -20,6 +29,7 @@
% @custnums = splice(@custnums, 0, 10);
%
% if ( @custnums ) {
+% warn time.": displaying recently changed customers\n" if $debug;
<& /elements/table-grid.html &>
diff --git a/httemplate/misc/cust_main-import.cgi b/httemplate/misc/cust_main-import.cgi
index 74f9b4c89..d56feaccf 100644
--- a/httemplate/misc/cust_main-import.cgi
+++ b/httemplate/misc/cust_main-import.cgi
@@ -36,6 +36,7 @@ Import a file containing customer records.
<OPTION VALUE="svc_external">External service
<OPTION VALUE="svc_external_svc_phone">External service and phone service
<OPTION VALUE="birthdates-acct_phone_hardware">Birthdates and account, phone and hardware services
+ <OPTION VALUE="national_id-acct_phone">National ID, plus account and phone services
</SELECT>
</TD>
</TR>
@@ -110,6 +111,9 @@ Uploaded files can be CSV (comma-separated value) files or Excel spreadsheets.
<b>Birthdates and account, phone and hardware services</b> format has the following field order: <i>agent_custid, refnum<%$req%>, last<%$req%>, first<%$req%>, company, address1<%$req%>, address2, city<%$req%>, state<%$req%>, zip<%$req%>, country, daytime, night, ship_last, ship_first, ship_company, ship_address1, ship_address2, ship_city, ship_state, ship_zip, ship_country, birthdate, spouse_birthdate, payinfo, paycvv, paydate, invoicing_list, pkgpart, next_bill_date, username, _password, countrycode, phonenum, sip_password, pin, typenum, ip_addr, hw_addr, serial</i>
<BR><BR>
+<b>National ID, plus account and phone services</b> format has the following field order: <i>agent_custid, refnum<%$req%>, last<%$req%>, first<%$req%>, company, address1<%$req%>, address2, city<%$req%>, state<%$req%>, zip<%$req%>, country, daytime, night, ship_last, ship_first, ship_company, ship_address1, ship_address2, ship_city, ship_state, ship_zip, ship_country, national_id, payinfo, paycvv, paydate, invoicing_list, pkgpart, next_bill_date, username, _password, slipip, countrycode, phonenum, sip_password, pin</i>
+<BR><BR>
+
<%$req%> Required fields
<BR><BR>
@@ -135,6 +139,8 @@ advertising source table.
<li><i>username</i> and <i>_password</i> are required if <i>pkgpart</i> is specified. (Extended and Extended plus company formats)
+ <li><i>slipip</i>: IP address
+
<li><i>id</i>: External service id, integer
<li><i>title</i>: External service identifier, text
diff --git a/httemplate/misc/order_pkg.html b/httemplate/misc/order_pkg.html
index 7aa024a34..c5f4509ab 100644
--- a/httemplate/misc/order_pkg.html
+++ b/httemplate/misc/order_pkg.html
@@ -1,4 +1,6 @@
-<& /elements/header-popup.html, mt('Order new package') &>
+<& /elements/header-popup.html, $quotationnum ? mt('Add package to quotation')
+ : mt('Order new package')
+&>
<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>
@@ -11,8 +13,10 @@
<FORM NAME="OrderPkgForm" ACTION="<% $p %>edit/process/quick-cust_pkg.cgi" METHOD="POST">
-<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $cust_main->custnum %>">
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $cust_main ? $cust_main->custnum : '' %>">
+<INPUT TYPE="hidden" NAME="prospectnum" VALUE="<% $prospect_main ? $prospect_main->prospectnum : '' %>">
<INPUT TYPE="hidden" NAME="qualnum" VALUE="<% scalar($cgi->param('qualnum')) |h %>">
+<INPUT TYPE="hidden" NAME="quotationnum" VALUE="<% $quotationnum %>">
% if ( $svcpart ) {
<INPUT TYPE="hidden" NAME="svcpart" VALUE="<% $svcpart %>">
% }
@@ -26,9 +30,10 @@
</TR>
% } else {
<& /elements/tr-select-cust-part_pkg.html,
- 'curr_value' => $pkgpart,
- 'classnum' => -1,
- 'cust_main' => $cust_main,
+ 'curr_value' => $pkgpart,
+ 'classnum' => -1,
+ 'cust_main' => $cust_main,
+ 'prospect_main' => $prospect_main,
&>
% }
@@ -39,6 +44,8 @@
<INPUT TYPE="text" NAME="quantity" SIZE=4 VALUE="<% $quantity %>">
</TD>
</TR>
+% } else {
+ <INPUT TYPE="hidden" NAME="quantity" VALUE="1">
% }
<TR>
@@ -54,7 +61,7 @@
</TD>
</TR>
-% if ( $cust_main->payby =~ /^(CARD|CHEK)$/ ) {
+% if ( $cust_main && $cust_main->payby =~ /^(CARD|CHEK)$/ ) {
% my $what = lc(FS::payby->shortname($cust_main->payby));
<TR>
<TH ALIGN="right"><% mt("Disable automatic $what charge") |h %> </TH>
@@ -97,8 +104,9 @@
% } else {
<& /elements/tr-select-cust_location.html,
- 'cgi' => $cgi,
- 'cust_main' => $cust_main,
+ 'cgi' => $cgi,
+ 'cust_main' => $cust_main,
+ 'prospect_main' => $prospect_main,
&>
% }
@@ -152,20 +160,42 @@ die "access denied"
my $conf = new FS::Conf;
my $date_format = $conf->config('date_format') || '%m/%d/%Y';
-$cgi->param('custnum') =~ /^(\d+)$/ or die "no custnum";
-my $custnum = $1;
-my $cust_main = qsearchs({
- 'table' => 'cust_main',
- 'hashref' => { 'custnum' => $custnum },
- 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
-});
+my $cust_main = '';
+if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+ my $custnum = $1;
+ $cust_main = qsearchs({
+ 'table' => 'cust_main',
+ 'hashref' => { 'custnum' => $custnum },
+ 'extra_sql' => ' AND '. $curuser->agentnums_sql,
+ });
+}
+
+my $prospect_main = '';
+if ( $cgi->param('prospectnum') =~ /^(\d+)$/ ) {
+ my $prospectnum = $1;
+ $prospect_main = qsearchs({
+ 'table' => 'prospect_main',
+ 'hashref' => { 'prospectnum' => $prospectnum },
+ 'extra_sql' => ' AND '. $curuser->agentnums_sql,
+ });
+}
+
+my $quotationnum = '';
+if ( $cgi->param('quotationnum') =~ /^(\d+)$/ ) {
+ $quotationnum = $1;
+}
+
+die 'no custnum or prospectnum' unless $cust_main || $prospect_main;
my $part_pkg = '';
if ( $cgi->param('lock_pkgpart') ) {
$part_pkg = qsearchs({
'table' => 'part_pkg',
'hashref' => { 'pkgpart' => scalar($cgi->param('lock_pkgpart')) },
- 'extra_sql' => ' AND '. FS::part_pkg->agent_pkgs_sql( $cust_main->agent ),
+ 'extra_sql' => ' AND '. FS::part_pkg->agent_pkgs_sql(
+ $cust_main ? $cust_main->agent
+ : $prospect_main->agent
+ ),
})
or die "unknown pkgpart ". $cgi->param('lock_pkgpart');
}
@@ -179,7 +209,7 @@ if ( $cgi->param('quantity') =~ /^\s*(\d+)\s*$/ ) {
my $format = $date_format. ' %T %z (%Z)'; #false laziness w/REAL_cust_pkg.cgi?
my $start_date = '';
-if( ! $conf->exists('order_pkg-no_start_date') ) {
+if( ! $conf->exists('order_pkg-no_start_date') && $cust_main ) {
$start_date = $cust_main->next_bill_date;
$start_date = $start_date ? time2str($format, $start_date) : '';
}
diff --git a/httemplate/misc/payment.cgi b/httemplate/misc/payment.cgi
index 1ae15b930..5b9f63dc0 100644
--- a/httemplate/misc/payment.cgi
+++ b/httemplate/misc/payment.cgi
@@ -12,11 +12,12 @@
<& /elements/tr-amount_fee.html,
'amount' => $amount,
- 'process-pkgpart' => scalar($conf->config('manual_process-pkgpart')),
+ 'process-pkgpart' =>
+ scalar($conf->config('manual_process-pkgpart', $cust_main->agentnum)),
'process-display' => scalar($conf->config('manual_process-display')),
- 'process-skip-first' => $conf->exists('manual_process-skip_first'),
+ 'process-skip_first' => $conf->exists('manual_process-skip_first'),
'num_payments' => scalar($cust_main->cust_pay),
- 'post_fee_callback' => $post_fee_callback,
+ 'surcharge_percentage' => scalar($conf->config('credit-card-surcharge-percentage')),
&>
<& /elements/tr-select-discount_term.html,
@@ -78,7 +79,7 @@
</TR>
<& /elements/location.html,
- 'object' => $cust_main, #XXX errors???
+ 'object' => $cust_main->bill_location,
'no_asterisks' => 1,
'address1_label' => emt('Card billing address'),
&>
@@ -251,6 +252,10 @@ my $custnum = $1;
my $cust_main = qsearchs( 'cust_main', { 'custnum'=>$custnum } );
die "unknown custnum $custnum" unless $cust_main;
+my $location = $cust_main->bill_location;
+# no proper error handling on this anyway, but when we have it,
+# remember to repopulate fields in $location
+
my $balance = $cust_main->balance;
my $payinfo = '';
@@ -269,19 +274,6 @@ if ( $balance > 0 ) {
$amount = $balance;
}
-my $post_fee_callback = sub {
- my( $amountref ) = @_;
-
- return unless $$amountref > 0;
-
- my $conf = new FS::Conf;
-
- my $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage');
- $$amountref += $$amountref * $cc_surcharge_pct/100 if $cc_surcharge_pct > 0;
-
- $$amountref = sprintf("%.2f", $$amountref);
-};
-
my $payunique = "webui-payment-". time. "-$$-". rand() * 2**32;
</%init>
diff --git a/httemplate/misc/process/void-cust_bill.html b/httemplate/misc/process/void-cust_bill.html
new file mode 100755
index 000000000..899901a50
--- /dev/null
+++ b/httemplate/misc/process/void-cust_bill.html
@@ -0,0 +1,26 @@
+%if ( $error ) {
+% $cgi->param('error', $error);
+<% $cgi->redirect(popurl(1). "void-cust_bill.html?". $cgi->query_string ) %>
+%} else {
+<& /elements/header-popup.html, 'Invoice voided' &>
+<SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+</SCRIPT>
+</BODY></HTML>
+%}
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Void invoices');
+
+#untaint invnum
+$cgi->param('invnum') =~ /^(\d+)$/ || die "Illegal invnum";
+my $invnum = $1;
+
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+
+my $custnum = $cust_bill->custnum;
+
+my $error = $cust_bill->void( $cgi->param('reason') );
+
+</%init>
diff --git a/httemplate/misc/timeworked.html b/httemplate/misc/timeworked.html
index 672fad8d6..e4392825c 100755
--- a/httemplate/misc/timeworked.html
+++ b/httemplate/misc/timeworked.html
@@ -99,8 +99,6 @@ my(%ticketmap, %ticket, %customers);
my $title = 'Assign Time Worked';
tie %ticketmap, 'Tie::IxHash';
-RT::Init();
-
my $CurrentUser = RT::CurrentUser->new();
$CurrentUser->LoadByName($FS::CurrentUser::CurrentUser->username);
diff --git a/httemplate/misc/unvoid-cust_bill_void.html b/httemplate/misc/unvoid-cust_bill_void.html
new file mode 100755
index 000000000..f61416549
--- /dev/null
+++ b/httemplate/misc/unvoid-cust_bill_void.html
@@ -0,0 +1,25 @@
+%if ( $error ) {
+% errorpage($error);
+%} else {
+% my $show = $curuser->default_customer_view =~ /^(jumbo|payment_history)$/
+% ? ''
+% : ';show=payment_history';
+<% $cgi->redirect($p. "view/cust_main.cgi?custnum=$custnum$show" ) %>
+%}
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('Unvoid invoices');
+
+#untaint invnum
+$cgi->param('invnum') =~ /^(\d+)$/ || die "Illegal invnum";
+my $invnum = $1;
+
+my $cust_bill_void = qsearchs('cust_bill_void', { 'invnum' => $invnum } );
+my $custnum = $cust_bill_void->custnum;
+
+my $error = $cust_bill_void->unvoid;
+
+</%init>
diff --git a/httemplate/misc/unvoid-cust_pay_void.cgi b/httemplate/misc/unvoid-cust_pay_void.cgi
index 91fe1c223..4726ee576 100755
--- a/httemplate/misc/unvoid-cust_pay_void.cgi
+++ b/httemplate/misc/unvoid-cust_pay_void.cgi
@@ -6,7 +6,7 @@
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Unvoid');
+ unless $FS::CurrentUser::CurrentUser->access_right('Unvoid payments');
#untaint paynum
my($query) = $cgi->keywords;
diff --git a/httemplate/misc/void-cust_bill.html b/httemplate/misc/void-cust_bill.html
new file mode 100644
index 000000000..1608fd051
--- /dev/null
+++ b/httemplate/misc/void-cust_bill.html
@@ -0,0 +1,45 @@
+<& /elements/header-popup.html, mt('Void invoice') &>
+
+<% include('/elements/error.html') %>
+
+<% emt('Are you sure you want to void this invoice?') %>
+<BR><BR>
+
+<% emt("Invoice #[_1] ([_2])",$cust_bill->display_invnum, $money_char. $cust_bill->owed) %>
+<BR><BR>
+
+<FORM METHOD="POST" ACTION="process/void-cust_bill.html">
+<INPUT TYPE="hidden" NAME="invnum" VALUE="<% $invnum %>">
+
+<% ntable("#cccccc", 2) %>
+<TR>
+ <TD ALIGN="right">Reason</TD>
+ <TD><INPUT TYPE="text" NAME="reason" VALUE="<% $cgi->param('reason') |h %>"></TD>
+</TR>
+
+</TABLE>
+
+<BR>
+<CENTER>
+<BUTTON TYPE="submit">Yes, void invoice</BUTTON>&nbsp;&nbsp;&nbsp;\
+<BUTTON TYPE="button" onClick="parent.cClick();">No, do not void invoice</BUTTON>
+</CENTER>
+
+</FORM>
+</BODY>
+</HTML>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Void invoices');
+
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
+
+#untaint invnum
+$cgi->param('invnum') =~ /^(\d+)$/ || die "Illegal invnum";
+my $invnum = $1;
+
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+
+</%init>
diff --git a/httemplate/misc/void-cust_pay.cgi b/httemplate/misc/void-cust_pay.cgi
index 7b484e93e..31b7a6201 100755
--- a/httemplate/misc/void-cust_pay.cgi
+++ b/httemplate/misc/void-cust_pay.cgi
@@ -12,7 +12,7 @@ my $paynum = $1;
my $cust_pay = qsearchs('cust_pay',{'paynum'=>$paynum});
-my $right = 'Regular void';
+my $right = 'Void payments';
$right = 'Credit card void' if $cust_pay->payby eq 'CARD';
$right = 'Echeck void' if $cust_pay->payby eq 'CHEK';
diff --git a/httemplate/misc/xmlhttp-cust_main-search.cgi b/httemplate/misc/xmlhttp-cust_main-search.cgi
index 16f7cd2bc..acf7e70e2 100644
--- a/httemplate/misc/xmlhttp-cust_main-search.cgi
+++ b/httemplate/misc/xmlhttp-cust_main-search.cgi
@@ -10,7 +10,7 @@
%
% my $string = $cgi->param('arg');
% my @cust_main = smart_search( 'search' => $string,
-% 'no_fuzzy_on_exact' => 1, #pref?
+% 'no_fuzzy_on_exact' => ! $FS::CurrentUser::CurrentUser->option('enable_fuzzy_on_exact'),
% );
% my $return = [ map [ $_->custnum,
% $_->name,
diff --git a/httemplate/pref/pref-process.html b/httemplate/pref/pref-process.html
index 932cf1a0a..c4fef0311 100644
--- a/httemplate/pref/pref-process.html
+++ b/httemplate/pref/pref-process.html
@@ -50,6 +50,7 @@ unless ( $error ) { # if ($access_user) {
#XXX autogen
my @paramlist = qw( locale menu_position default_customer_view
spreadsheet_format mobile_menu
+ enable_fuzzy_on_exact
disable_html_editor disable_enter_submit_onetimecharge
email_address
snom-ip snom-username snom-password
diff --git a/httemplate/pref/pref.html b/httemplate/pref/pref.html
index 9ebf2f1ba..575b8045b 100644
--- a/httemplate/pref/pref.html
+++ b/httemplate/pref/pref.html
@@ -90,7 +90,14 @@ Interface
</SELECT>
</TD>
</TR>
-
+
+ <TR>
+ <TH ALIGN="right" COLSPAN=1>Enable approximate customer searching even when an exact match is found: </TH>
+ <TD ALIGN="left" COLSPAN=2>
+ <INPUT TYPE="checkbox" NAME="enable_fuzzy_on_exact" VALUE="1" <% $curuser->option('enable_fuzzy_on_exact') ? 'CHECKED' : '' %>>
+ </TD>
+ </TR>
+
<TR>
<TH ALIGN="right" COLSPAN=1>Disable HTML editor for customer notes: </TH>
<TD ALIGN="left" COLSPAN=2>
diff --git a/httemplate/search/477.html b/httemplate/search/477.html
index 250e71811..6f5fcdf3b 100755
--- a/httemplate/search/477.html
+++ b/httemplate/search/477.html
@@ -1,33 +1,24 @@
-% unless ( $type eq 'xml' ) {
-<% include( '/elements/header.html', 'FCC Form 477 Results') %>
-%}else{
+% if ( $type eq 'xml' ) {
<?xml version="1.0" encoding="ISO-8859-1"?>
<Form_477_submission xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://specialreports.fcc.gov/wcb/Form477/XMLSchema-instance/form_477_upload_Schema.xsd" >
-%}
-% if ( $type eq 'html' || $type eq 'html-print' ) {
+% } else { #html
+<& /elements/header.html, "FCC Form 477 Results - $state" &>
<TABLE WIDTH="100%">
- <TR><TD></TD>
-%}elsif ( $type eq 'xml' ) {
-%}
-% unless ( $type eq 'html-print' || $type eq 'xml' ) {
+ <TR>
+ <TD></TD>
+ <TD ALIGN="right" CLASS="noprint">
+ Download full results<BR>
+% $cgi->param('_type', 'xml');
+ as <A HREF="<% $cgi->self_url %>">XML file</A><BR>
- <TD ALIGN="right">
+% $cgi->param('_type', 'html-print');
+ as <A HREF="<% $cgi->self_url %>">printable copy</A>
- Download full results<BR>
-% $cgi->param('_type', 'xml');
- as <A HREF="<% $cgi->self_url %>">XML file</A><BR>
-
-% $cgi->param('_type', 'html-print');
- as <A HREF="<% $cgi->self_url %>">printable copy</A>
-
- </TD>
-% $cgi->param('_type', $type );
-% }
-% if ( $type eq 'html' || $type eq 'html-print' ) {
+ </TD>
+% $cgi->param('_type', $type );
</TR>
</TABLE>
-%}elsif ( $type eq 'xml' ) {
-%}
+% } #html
% foreach my $part ( @parts ) {
% if ( $part{$part} ) {
%
@@ -47,8 +38,8 @@
% if ( $type eq 'xml' ) {
<<% 'Part_IA_'. chr(65 + $tech) %>>
% }
-<% include( "477part${part}_summary.html", 'tech_code' => $tech, 'url' => $url ) %>
-<% include( "477part${part}_detail.html", 'tech_code' => $tech, 'url' => $url ) %>
+<& "477part${part}_summary.html", 'tech_code' => $tech, 'url' => $url &>
+<& "477part${part}_detail.html", 'tech_code' => $tech, 'url' => $url &>
% if ( $type eq 'xml' ) {
</<% 'Part_IA_'. chr(65 + $tech) %>>
% }
@@ -58,7 +49,7 @@
<<% 'Part_'. $part %>>
% }
% my $url = &{$url_mangler}($part);
-<% include( "477part${part}.html", 'url' => $url ) %>
+<& "477part${part}.html", 'url' => $url &>
% if ( $type eq 'xml' ) {
</<% 'Part_'. $part %>>
% }
@@ -66,11 +57,11 @@
% }
% }
%
-% if ( $type eq 'html' || $type eq 'html-print' ) {
-<% include( '/elements/footer.html') %>
-%}elsif ( $type eq 'xml' ) {
+% if ( $type eq 'xml' ) {
</Form_477_submission>
-%}
+% } else {
+<& /elements/footer.html &>
+% }
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
@@ -78,6 +69,9 @@ my $curuser = $FS::CurrentUser::CurrentUser;
die "access denied"
unless $curuser->access_right('List packages');
+my $state = uc($cgi->param('state'));
+$state =~ /^[A-Z]{2}$/ or die "illegal state: $state";
+
my %part = map { $_ => 1 } grep { /^\w+$/ } $cgi->param('part');
my $type = $cgi->param('_type') || 'html';
my $xlsname = '477report';
diff --git a/httemplate/search/477partIA_detail.html b/httemplate/search/477partIA_detail.html
index 2eca1072b..66f3a8651 100755
--- a/httemplate/search/477partIA_detail.html
+++ b/httemplate/search/477partIA_detail.html
@@ -23,9 +23,10 @@ die "access denied"
my %opt = @_;
my %search_hash = ();
-for ( qw(agentnum magic) ) {
+for ( qw(agentnum magic state) ) {
$search_hash{$_} = $cgi->param($_) if $cgi->param($_);
}
+$search_hash{'country'} = 'US';
$search_hash{'classnum'} = [ $cgi->param('classnum') ];
diff --git a/httemplate/search/477partIA_summary.html b/httemplate/search/477partIA_summary.html
index ecacaefad..f5c2bc251 100755
--- a/httemplate/search/477partIA_summary.html
+++ b/httemplate/search/477partIA_summary.html
@@ -40,9 +40,10 @@ die "access denied"
my %opt = @_;
my %search_hash = ();
-for ( qw(agentnum magic) ) {
+for ( qw(agentnum magic state) ) {
$search_hash{$_} = $cgi->param($_) if $cgi->param($_);
}
+$search_hash{'country'} = 'US';
$search_hash{'classnum'} = [ $cgi->param('classnum') ];
my @column_option = grep { /^\d+$/ } $cgi->param('part1_column_option')
diff --git a/httemplate/search/477partIIA.html b/httemplate/search/477partIIA.html
index 9b363ad5e..d2cc8c3e9 100755
--- a/httemplate/search/477partIIA.html
+++ b/httemplate/search/477partIIA.html
@@ -22,9 +22,10 @@ die "access denied"
my $html_init = '<H2>Part IIA</H2>';
my %search_hash = ();
-for ( qw(agentnum magic) ) {
+for ( qw(agentnum magic state) ) {
$search_hash{$_} = $cgi->param($_) if $cgi->param($_);
}
+$search_hash{'country'} = 'US';
$search_hash{'classnum'} = [ $cgi->param('classnum') ];
my @row_option = grep { /^\d+$/ } $cgi->param('part2a_row_option')
diff --git a/httemplate/search/477partIIB.html b/httemplate/search/477partIIB.html
index 94aa818fb..c58310d36 100755
--- a/httemplate/search/477partIIB.html
+++ b/httemplate/search/477partIIB.html
@@ -1,17 +1,44 @@
-<% include( 'elements/search.html',
- 'html_init' => $html_init,
- 'name' => 'lines',
- 'query' => $query,
- 'count_query' => 'SELECT 11',
- 'really_disable_download' => 1,
- 'disable_download' => 1,
- 'nohtmlheader' => 1,
- 'disable_total' => 1,
- 'header' => [ @headers ],
- 'xml_elements' => [ @xml_elements ],
- 'fields' => [ @fields ],
- )
-%>
+% if ( $cgi->param('_type') eq 'xml' ) {
+% my @cols = qw(a b c);
+% for ( my $row = 0; $row < scalar(@rows); $row++ ) {
+% for my $col (0..2) {
+% if ( exists($data[$col][$row]) ) {
+<PartII_<% $row %><% $cols[$col] %>>
+% }
+</PartII_<% $row %><% $cols[$col] %>>
+% } #for $col
+% } #for $row
+% } else { # HTML mode
+% # fake up the search-html.html header
+<H2>Part IIB</H2>
+<TABLE>
+ <TR><TD VALIGN="bottom"><BR></TD></TR>
+ <TR><TD COLSPAN=2>
+ <TABLE CLASS="grid" CELLSPACING=0 STYLE="border: 1px solid #cccccc;" BGCOLOR="#cccccc">
+ <TR>
+% foreach (@headers) {
+ <TH class="grid"><% $_ %></TH>
+% }
+ </TR>
+% my @bgcolor = ('eeeeee','ffffff');
+% my $row = 0;
+% foreach my $rowhead (@rows) {
+ <TR>
+ <TD CLASS="grid" BGCOLOR="#<% $bgcolor[$row % 2] %>"><% $rowhead %></TD>
+% for my $col (0..2) {
+ <TD CLASS="grid" BGCOLOR="#<% $bgcolor[$row % 2] %>">
+% if ( exists($data[$col][$row]) ) {
+ <% $data[$col][$row] %>
+% }
+ </TD>
+% } # for $col
+ </TR>
+% $row++;
+% } #for $rowhead
+ </TABLE>
+ </TD></TR>
+</TABLE>
+% } #XML/HTML
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
@@ -19,67 +46,89 @@ my $curuser = $FS::CurrentUser::CurrentUser;
die "access denied"
unless $curuser->access_right('List packages');
-my $html_init = '<H2>Part IIB</H2>';
my %search_hash = ();
-
-for ( qw(agentnum magic) ) {
- $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
-}
-$search_hash{'classnum'} = [ $cgi->param('classnum') ];
-
-my @row_option = grep { /^\d+$/ } $cgi->param('part2b_row_option')
- if $cgi->param('part2b_row_option');
-
-# fudge in 2nd row
-unshift @row_option, $row_option[0];
-
-my $query = 'SELECT '. join(' UNION SELECT ', 1..8);
-
-my $total_count = 0;
-my $column_value = sub {
- my $row = shift;
-
- my @report_option = ( $row_option[$row - 1] || '' );
- my $sql_query = FS::cust_pkg->search(
- { %search_hash, 'report_option' => join(',', @report_option) }
- );
-
- my $count_sql = delete($sql_query->{'count_query'});
- if ( $row == 2 ) {
- $count_sql =~ s/COUNT\(\*\) FROM/sum(COALESCE(CASE WHEN cust_main.company IS NULL OR cust_main.company = '' THEN CASE WHEN part_pkg.fcc_ds0s IS NOT NULL AND part_pkg.fcc_ds0s > 0 THEN part_pkg.fcc_ds0s WHEN pkg_class.fcc_ds0s IS NOT NULL AND pkg_class.fcc_ds0s > 0 THEN pkg_class.fcc_ds0s ELSE 0 END ELSE 0 END, 0) ) FROM/
- or die "couldn't parse count_sql";
- } else {
- $count_sql =~ s/COUNT\(\*\) FROM/sum(COALESCE(CASE WHEN part_pkg.fcc_ds0s IS NOT NULL AND part_pkg.fcc_ds0s > 0 THEN part_pkg.fcc_ds0s WHEN pkg_class.fcc_ds0s IS NOT NULL AND pkg_class.fcc_ds0s > 0 THEN pkg_class.fcc_ds0s ELSE 0 END, 0)) FROM/
- or die "couldn't parse count_sql";
- }
-
- my $count_sth = dbh->prepare($count_sql)
- or die "Error preparing $count_sql: ". dbh->errstr;
- $count_sth->execute
- or die "Error executing $count_sql: ". $count_sth->errstr;
- my $count_arrayref = $count_sth->fetchrow_arrayref;
- my $count = $count_arrayref->[0];
+$search_hash{'agentnum'} = $cgi->param('agentnum');
+$search_hash{'state'} = $cgi->param('state');
+$search_hash{'classnum'} = [ $cgi->param('classnum') ];
+$search_hash{'status'} = 'active';
- $total_count = $count if $row == 1;
- $count = sprintf('%.2f', $total_count ? 100*$count/$total_count : 0)
- if $row != 1;
+my @row_option;
+foreach ($cgi->param('part2b_row_option')) {
+ push @row_option, (/^\d+$/ ? $_ : undef);
+}
- return "$count";
+my $is_residential = "AND COALESCE(cust_main.company, '') = ''";
+my $has_report_option = sub {
+ map {
+ defined($row_option[$_]) ?
+ "AND EXISTS(
+ SELECT 1 FROM part_pkg_option
+ WHERE part_pkg_option.pkgpart = part_pkg.pkgpart
+ AND optionname = 'report_option_" . $row_option[$_]."'
+ AND optionvalue = '1'
+ )" : 'AND FALSE'
+ } @_
};
-my @headers = (
- '',
- 'without broadband',
- 'with broadband',
- 'wholesale',
+# an arrayref for each column
+my @data;
+# get the skeleton of the query
+my $sql_query = FS::cust_pkg->search(\%search_hash);
+my $from_where = $sql_query->{'count_query'};
+$from_where =~ s/^SELECT COUNT\(\*\) //;
+# columns 1 and 2
+my $query_ds0 = "SELECT SUM(COALESCE(part_pkg.fcc_ds0s, pkg_class.fcc_ds0s, 0))
+ $from_where";
+# column 3
+my $query_custnum = "SELECT COUNT(DISTINCT cust_pkg.custnum) $from_where";
+
+my @base_queries = ($query_ds0, $query_ds0, $query_custnum);
+my @col_conds = (
+ # column 1
+ [
+ '',
+ $is_residential,
+ $has_report_option->(0), # nomadic
+ ],
+ # column 2
+ [
+ '',
+ $is_residential,
+ $has_report_option->(0..5),
+ ],
+ # column 3
+ [
+ ''
+ ]
);
-my @xml_elements = (
- sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}a" },
- sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}b" },
- sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}c" },
-);
+my $col = 0;
+foreach (@col_conds) {
+ my @col_data;
+ my $row = 0;
+ foreach my $cond (@{ $col_conds[$col] }) {
+ # three parts: the select expression, the VoIP class (column selection),
+ # and the row selection
+ my $query = $base_queries[$col] .
+ " AND part_pkg.fcc_voip_class = '".($col+1)."'
+ $cond";
+ my $count = FS::Record->scalar_sql($query) || 0;
+ if ( $row == 0 ) {
+ $col_data[$row] = $count; # the raw count
+ } else {
+ if ( $col_data[0] == 0 ) {
+ $col_data[$row] = ''; # show nothing in this row, then
+ } else {
+ $col_data[$row] = sprintf('%.2f', 100 * $count / $col_data[0]) . '%';
+ }
+ } #if $row == 0
+ $row++;
+ }
+ $data[$col] = \@col_data;
+ $col++;
+}
+
my @rows = (
'total number',
@@ -92,12 +141,11 @@ my @rows = (
'% other broadband',
);
-my @fields = (
- sub { my $row = shift; $rows[$row->[0] - 1]; },
- sub { 0; },
- sub { my $row = shift; &{$column_value}($row->[0]); },
- sub { 0; },
+my @headers = (
+ '',
+ 'without broadband',
+ 'with broadband',
+ 'wholesale',
);
-shift @fields if $cgi->param('_type') eq 'xml';
</%init>
diff --git a/httemplate/search/477partV.html b/httemplate/search/477partV.html
index 0987fea44..2fd5119d1 100755
--- a/httemplate/search/477partV.html
+++ b/httemplate/search/477partV.html
@@ -10,7 +10,7 @@
'no_field_elements' => 1,
'fields' => [ 'zip' ],
'url' => $opt{url} || '',
- 'disable_download' => 1,
+ 'really_disable_download' => 1,
)
%>
@@ -27,9 +27,10 @@ my %search_hash = ();
my @sql_query = ();
my @count_query = ();
-for ( qw(agentnum magic) ) {
+for ( qw(agentnum magic state) ) {
$search_hash{$_} = $cgi->param($_) if $cgi->param($_);
}
+$search_hash{'country'} = 'US';
$search_hash{'classnum'} = [ $cgi->param('classnum') ];
$search_hash{report_option} = $cgi->param('partv_report_option')
if $cgi->param('partv_report_option');
diff --git a/httemplate/search/477partVI_census.html b/httemplate/search/477partVI_census.html
index 4d1fb2136..8425c4b48 100755
--- a/httemplate/search/477partVI_census.html
+++ b/httemplate/search/477partVI_census.html
@@ -23,6 +23,7 @@
'links' => \@links,
'url' => $opt{url} || '',
'xml_row_element' => 'Datarow',
+ 'really_disable_download' => 1,
)
%>
<%init>
@@ -80,9 +81,10 @@ push @fields,
my %search_hash = ();
my @sql_query = ();
-for ( qw(agentnum magic) ) {
+for ( qw(agentnum magic state) ) {
$search_hash{$_} = $cgi->param($_) if $cgi->param($_);
}
+$search_hash{'country'} = 'US';
$search_hash{'classnum'} = [ $cgi->param('classnum') ]
if grep { $_ eq 'classnum' } $cgi->param;
@@ -115,10 +117,10 @@ foreach my $row ( @row_option ) {
);
my $extracolumns = "$rowcount AS upload, $columncount AS download, $tech_code as technology_code";
my $percent = "CASE WHEN count(*) > 0 THEN 100-100*cast(count(cust_main.company) as numeric)/cast(count(*) as numeric) ELSE cast(0 as numeric) END AS residential";
- $sql_query->{select} = "count(*) AS quantity, $extracolumns, censustract, $percent";
+ $sql_query->{select} = "count(*) AS quantity, $extracolumns, cust_location.censustract, $percent";
$sql_query->{order_by} =~ /^(.*)(ORDER BY pkgnum)(.*)$/s
or die "couldn't parse order_by";
- $sql_query->{order_by} = "$1 GROUP BY censustract $3";
+ $sql_query->{order_by} = "$1 GROUP BY cust_location.censustract $3";
push @sql_query, $sql_query;
}
$columncount++;
@@ -131,7 +133,8 @@ my $count_query = 'SELECT count(*) FROM ( ('.
map { my $addl_from = $_->{addl_from};
my $extra_sql = $_->{extra_sql};
my $order_by = $_->{order_by};
- "SELECT censustract from cust_pkg $addl_from $extra_sql $order_by";
+ "SELECT cust_location.censustract from cust_pkg $addl_from
+ $extra_sql $order_by";
}
@sql_query
). ') ) AS foo';
diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi
index 1a46b0097..4c0fa4a56 100644
--- a/httemplate/search/cust_bill_pkg.cgi
+++ b/httemplate/search/cust_bill_pkg.cgi
@@ -3,25 +3,10 @@
'name' => emt('line items'),
'query' => $query,
'count_query' => $count_query,
- 'count_addl' => [ $money_char. '%.2f total',
- $unearned ? ( $money_char. '%.2f unearned revenue' ) : (),
- ],
+ 'count_addl' => \@total_desc,
'header' => [
emt('Description'),
- ( $unearned
- ? ( emt('Unearned'),
- emt('Owed'), # useful in 'paid' mode?
- emt('Payment date') )
- : ( emt('Setup charge') )
- ),
- ( $use_usage eq 'usage'
- ? emt('Usage charge')
- : emt('Recurring charge')
- ),
- ( $unearned
- ? ( emt('Charge start'), emt('Charge end') )
- : ()
- ),
+ @peritem_desc,
emt('Invoice'),
emt('Date'),
FS::UI::Web::cust_header(),
@@ -33,65 +18,21 @@
},
#strikethrough or "N/A ($amount)" or something these when
# they're not applicable to pkg_tax search
- sub { my $cust_bill_pkg = shift;
- if ( $unearned ) {
-
- sprintf($money_char.'%.2f',
- $cust_bill_pkg->unearned_revenue)
-
- } else {
- sprintf($money_char.'%.2f', $cust_bill_pkg->setup );
- }
- },
- ( $unearned
- ? ( $owed_sub, $payment_date_sub, )
- : ()
- ),
- sub { my $row = shift;
- my $value = 0;
- if ( $use_usage eq 'recurring' or $unearned ) {
- $value = $row->recur - $row->usage;
- } elsif ( $use_usage eq 'usage' ) {
- $value = $row->usage;
- } else {
- $value = $row->recur;
- }
- sprintf($money_char.'%.2f', $value );
- },
- ( $unearned
- ? ( sub { time2str('%b %d %Y', shift->sdate ) },
- # shift edate back a day
- # 82799 = 3600*23 - 1
- # (to avoid skipping a day during DST)
- sub { time2str('%b %d %Y', shift->edate - 82799 ) },
- )
- : ()
- ),
+ @peritem_sub,
'invnum',
sub { time2str('%b %d %Y', shift->_date ) },
\&FS::UI::Web::cust_fields,
],
'sort_fields' => [
'',
- 'setup', #broken in $unearned case i guess
- ( $unearned ? ('', '') : () ),
- ( $use_usage eq 'recurring' or $unearned
- ? 'recur - usage' :
- $use_usage eq 'usage'
- ? 'usage'
- : 'recur'
- ),
- ( $unearned ? ('sdate', 'edate') : () ),
+ @peritem,
'invnum',
'_date',
],
'links' => [
#'',
'',
- '',
- ( $unearned ? ( '', '' ) : () ),
- '',
- ( $unearned ? ( '', '' ) : () ),
+ @peritem_null,
$ilink,
$ilink,
( map { $_ ne 'Cust. Status' ? $clink : '' }
@@ -99,19 +40,14 @@
),
],
#'align' => 'rlrrrc'.FS::UI::Web::cust_aligns(),
- 'align' => 'lr'.
- ( $unearned ? 'rc' : '' ).
- 'r'.
- ( $unearned ? 'cc' : '' ).
+ 'align' => 'l'.
+ $peritem_align.
'rc'.
FS::UI::Web::cust_aligns(),
'color' => [
#'',
'',
- '',
- ( $unearned ? ( '', '' ) : () ),
- '',
- ( $unearned ? ( '', '' ) : () ),
+ @peritem_null,
'',
'',
FS::UI::Web::cust_colors(),
@@ -119,44 +55,126 @@
'style' => [
#'',
'',
- '',
- ( $unearned ? ( '', '' ) : () ),
- '',
- ( $unearned ? ( '', '' ) : () ),
+ @peritem_null,
'',
'',
FS::UI::Web::cust_styles(),
],
&>
-<%init>
+<%doc>
+
+Output parameters:
+- distribute: Boolean. If true, recurring fees will be "prorated" for the
+ portion of the package date range (sdate-edate) that falls within the date
+ range of the report. Line items will be limited to those for which this
+ portion is > 0. This disables filtering on invoice date.
+
+- use_usage: Separate usage (cust_bill_pkg_detail records) from
+ recurring charges. If set to "usage", will show usage instead of
+ recurring charges. If set to "recurring", will deduct usage and only
+ show the flat rate charge. If not passed, the "recurring charge" column
+ will include usage charges also.
+
+Filtering parameters:
+- begin, end: Date range. Applies to invoice date, not necessarily package
+ date range. But see "distribute".
+
+- status: Customer status (active, suspended, etc.). This will filter on
+ _current_ customer status, not status at the time the invoice was generated.
+
+- agentnum: Filter on customer agent.
+
+- refnum: Filter on customer reference source.
+
+- classnum: Filter on package class.
+
+- use_override: Apply "classnum" and "taxclass" filtering based on the
+ override (bundle) pkgpart, rather than always using the true pkgpart.
+
+- nottax: Limit to items that are not taxes (pkgnum > 0).
+
+- istax: Limit to items that are taxes (pkgnum == 0).
+
+- taxnum: Limit to items whose tax definition matches this taxnum.
+ With "nottax" that means items that are subject to that tax;
+ with "istax" it's the tax charges themselves. Can be specified
+ more than once to include multiple taxes.
+
+- country, state, county, city: Limit to items whose tax location
+ matches these fields. If "nottax" it's the tax location of the package;
+ if "istax" the location of the tax.
+
+- taxname, taxnameNULL: With "nottax", limit to items whose tax location
+ matches a tax with this name. With "istax", limit to items that have
+ this tax name. taxnameNULL is equivalent to "taxname = '' OR taxname
+ = 'Tax'".
+
+- out: With "nottax", limit to items that don't match any tax definition.
+ With "istax", find tax items that are unlinked to their tax definitions.
+ Current Freeside (> July 2012) always creates tax links, but unlinked
+ items may result from an incomplete upgrade of legacy data.
+
+- locationtaxid: With "nottax", limit to packages matching this
+ tax_rate_location ID; with "tax", limit to taxes generated from that
+ location.
+
+- taxclass: Filter on package taxclass.
+
+- taxclassNULL: With "nottax", limit to items that would be subject to the
+ tax with taxclass = NULL. This doesn't necessarily mean part_pkg.taxclass
+ is NULL; it also includes taxclasses that don't have a tax in this region.
+
+- itemdesc: Limit to line items with this description. Note that non-tax
+ packages usually have a description of NULL. (Deprecated.)
-#LOTS of false laziness below w/cust_credit_bill_pkg.cgi
+- report_group: Can contain '=' or '!=' followed by a string to limit to
+ line items where itemdesc starts with, or doesn't start with, the string.
+
+- cust_tax: Limit to customers who are tax-exempt. If "taxname" is also
+ specified, limit to customers who are also specifically exempt from that
+ tax.
+
+- pkg_tax: Limit to packages that are tax-exempt, and only include the
+ exempt portion (setup, recurring, or both) when calculating totals.
+
+- taxable: Limit to packages that are subject to tax, i.e. where a
+ cust_bill_pkg_tax_location record exists.
+
+- credit: Limit to line items that received a credit application. The
+ amount of the credit will also be shown.
+
+</%doc>
+<%init>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
my $conf = new FS::Conf;
-
-my $unearned = '';
-my $unearned_mode = '';
-my $unearned_base = '';
-my $unearned_sql = '';
+my $money_char = $conf->config('money_char') || '$';
my @select = ( 'cust_bill_pkg.*', 'cust_bill._date' );
+my @total = ( 'COUNT(*)', 'SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)');
+my @total_desc = ( '%d line items', $money_char.'%.2f total' ); # sprintf strings
+my @peritem = ( 'setup', 'recur' );
+my @peritem_desc = ( 'Setup charge', 'Recurring charge' );
my ($join_cust, $join_pkg ) = ('', '');
+my $use_usage;
+
+# valid in both the tax and non-tax cases
+$join_cust =
+ " LEFT JOIN cust_bill USING (invnum)
+ LEFT JOIN cust_main USING (custnum)
+ ";
-#here is the agent virtualization
+#agent virtualization
my $agentnums_sql =
$FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' );
my @where = ( $agentnums_sql );
+# date range
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
-if ( $cgi->param('status') =~ /^([a-z]+)$/ ) {
- push @where, FS::cust_main->cust_status_sql . " = '$1'";
-}
-
if ( $cgi->param('distribute') == 1 ) {
push @where, "sdate <= $ending",
"edate > $beginning",
@@ -167,456 +185,371 @@ else {
"cust_bill._date <= $ending";
}
+# status
+if ( $cgi->param('status') =~ /^([a-z]+)$/ ) {
+ push @where, FS::cust_main->cust_status_sql . " = '$1'";
+}
+
+# agentnum
if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
push @where, "cust_main.agentnum = $1";
}
+# refnum
if ( $cgi->param('refnum') =~ /^(\d+)$/ ) {
push @where, "cust_main.refnum = $1";
}
-#classnum
-# not specified: all classes
-# 0: empty class
-# N: classnum
-my $use_override = $cgi->param('use_override');
-if ( $cgi->param('classnum') =~ /^(\d+)$/ ) {
- my $comparison = '';
- if ( $1 == 0 ) {
- $comparison = "IS NULL";
- } else {
- $comparison = "= $1";
- }
-
- if ( $use_override ) {
- push @where, "(
- part_pkg.classnum $comparison AND pkgpart_override IS NULL OR
- override.classnum $comparison AND pkgpart_override IS NOT NULL
- )";
- } else {
- push @where, "part_pkg.classnum $comparison";
- }
-}
+# the non-tax case
+if ( $cgi->param('nottax') ) {
-if ( $cgi->param('taxclass')
- && ! $cgi->param('istax') #no part_pkg.taxclass in this case
- #(should we save a taxclass or a link to taxnum
- # in cust_bill_pkg or something like
- # cust_bill_pkg_tax_location?)
- )
-{
-
- #override taxclass when use_override is specified? probably
- #if ( $use_override ) {
- #
- # push @where,
- # ' ( '. join(' OR ',
- # map {
- # ' ( part_pkg.taxclass = '. dbh->quote($_).
- # ' AND pkgpart_override IS NULL '.
- # ' OR '.
- # ' override.taxclass = '. dbh->quote($_).
- # ' AND pkgpart_override IS NOT NULL '.
- # ' ) '
- # }
- # $cgi->param('taxclass')
- # ).
- # ' ) ';
- #
- #} else {
-
- push @where, ' part_pkg.taxclass IN ( '.
- join(', ', map dbh->quote($_), $cgi->param('taxclass') ).
- ' ) ';
-
- #}
+ push @where, 'cust_bill_pkg.pkgnum > 0';
-}
+ # then we want the package and its definition
+ $join_pkg =
+' LEFT JOIN cust_pkg USING (pkgnum)
+ LEFT JOIN part_pkg USING (pkgpart)';
-my @loc_param = qw( district city county state country );
+ my $part_pkg = 'part_pkg';
+ if ( $cgi->param('use_override') ) {
+ # still need the real part_pkg for tax applicability,
+ # so alias this one
+ $join_pkg .= " LEFT JOIN part_pkg AS override ON (
+ COALESCE(cust_bill_pkg.pkgpart_override, cust_pkg.pkgpart, 0) = part_pkg.pkgpart
+ )";
+ $part_pkg = 'override';
+ }
+ push @select, 'part_pkg.pkg'; # or should this use override?
-if ( $cgi->param('out') ) {
+ my @tax_where; # will go into a subquery
+ my @exempt_where; # will also go into a subquery
- my ( $loc_sql, @param ) = FS::cust_pkg->location_sql( 'ornull' => 1 );
- while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution
- $loc_sql =~ s/\?/'cust_main_county.'.shift(@param)/e;
+ # classnum (of override pkgpart if applicable)
+ # not specified: all classes
+ # 0: empty class
+ # N: classnum
+ if ( $cgi->param('classnum') =~ /^(\d+)$/ ) {
+ push @where, "COALESCE($part_pkg.classnum, 0) = $1";
}
- $loc_sql =~ s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g
- if $cgi->param('istax');
-
- push @where, "
- 0 = (
- SELECT COUNT(*) FROM cust_main_county
- WHERE cust_main_county.tax > 0
- AND $loc_sql
- )
- ";
+ # taxclass
+ if ( $cgi->param('taxclassNULL') ) {
+ # a little different from 'taxclass' in that it applies to the
+ # effective taxclass, not the real one
+ push @tax_where, 'cust_main_county.taxclass IS NULL'
+ } elsif ( $cgi->param('taxclass') ) {
+ push @tax_where, "$part_pkg.taxclass IN (" .
+ join(', ', map {dbh->quote($_)} $cgi->param('taxclass') ).
+ ')';
+ }
- #not linked to by anything, but useful for debugging "out of taxable region"
- if ( grep $cgi->param($_), @loc_param ) {
+ if ( $cgi->param('exempt_cust') eq 'Y' ) {
+ # tax-exempt customers
+ push @exempt_where, "(exempt_cust = 'Y' OR exempt_cust_taxname = 'Y')";
- my %ph = map { $_ => dbh->quote( scalar($cgi->param($_)) ) } @loc_param;
+ } elsif ( $cgi->param('exempt_pkg') eq 'Y' ) { # non-taxable package
+ # non-taxable package charges
+ push @exempt_where, "(exempt_setup = 'Y' OR exempt_recur = 'Y')";
+ }
+ # we don't handle exempt_monthly here
+
+ if ( $cgi->param('taxname') ) { # specific taxname
+ push @tax_where, 'cust_main_county.taxname = '.
+ dbh->quote($cgi->param('taxname'));
+ } elsif ( $cgi->param('taxnameNULL') ) {
+ push @tax_where, 'cust_main_county.taxname IS NULL OR '.
+ 'cust_main_county.taxname = \'Tax\'';
+ }
- my ( $loc_sql, @param ) = FS::cust_pkg->location_sql;
- while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution
- $loc_sql =~ s/\?/$ph{shift(@param)}/e;
+ # country:state:county:city:district (may be repeated)
+ # You can also pass a big list of taxnums but that leads to huge URLs.
+ # Note that this means "packages whose tax is in this region", not
+ # "packages in this region". It's meant for links from the tax report.
+ if ( $cgi->param('region') ) {
+ my @orwhere;
+ foreach ( $cgi->param('region') ) {
+ my %loc;
+ @loc{qw(country state county city district)} =
+ split(':', $cgi->param('region'));
+ my $string = join(' AND ',
+ map {
+ if ( $loc{$_} ) {
+ "$_ = ".dbh->quote($loc{$_});
+ } else {
+ "$_ IS NULL";
+ }
+ } keys(%loc)
+ );
+ push @orwhere, "($string)";
}
+ push @tax_where, '(' . join(' OR ', @orwhere) . ')' if @orwhere;
+ }
- push @where, $loc_sql;
-
+ # specific taxnums
+ if ( $cgi->param('taxnum') ) {
+ my $taxnum_in = join(',',
+ grep /^\d+$/, $cgi->param('taxnum')
+ );
+ push @tax_where, "cust_main_county.taxnum IN ($taxnum_in)"
+ if $taxnum_in;
}
-} elsif ( $cgi->param('country') ) {
+ # If we're showing exempt items, we need to find those with
+ # cust_tax_exempt_pkg records matching the selected taxes.
+ # If we're showing taxable items, we need to find those with
+ # cust_bill_pkg_tax_location records. We also need to find the
+ # exemption records so that we can show the taxable amount.
+ # If we're showing all items, we need the union of those.
+ # If we're showing 'out' (items that aren't region/class taxable),
+ # then we need the set of all items minus the union of those.
- my @counties = $cgi->param('county');
-
- if ( scalar(@counties) > 1 ) {
+ my $exempt_sub;
- #hacky, could be more efficient. care if it is ever used for more than the
- # tax-report_groups filtering kludge
+ if ( @exempt_where or @tax_where
+ or $cgi->param('taxable') or $cgi->param('out') )
+ {
+ # process exemption restrictions, including @tax_where
+ my $exempt_sub = 'SELECT SUM(amount) as exempt_amount, billpkgnum
+ FROM cust_tax_exempt_pkg JOIN cust_main_county USING (taxnum)';
- my $locs_sql =
- ' ( '. join(' OR ', map {
+ $exempt_sub .= ' WHERE '.join(' AND ', @tax_where, @exempt_where)
+ if (@tax_where or @exempt_where);
- my %ph = ( 'county' => dbh->quote($_),
- map { $_ => dbh->quote( $cgi->param($_) ) }
- qw( district city state country )
- );
+ $exempt_sub .= ' GROUP BY billpkgnum';
- my ( $loc_sql, @param ) = FS::cust_pkg->location_sql;
- while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution
- $loc_sql =~ s/\?/$ph{shift(@param)}/e;
- }
+ $join_pkg .= " LEFT JOIN ($exempt_sub) AS item_exempt
+ USING (billpkgnum)";
+ }
+
+ if ( @tax_where or $cgi->param('taxable') or $cgi->param('out') ) {
+ # process tax restrictions
+ unshift @tax_where,
+ 'cust_main_county.tax > 0';
+
+ my $tax_sub = "SELECT invnum, cust_bill_pkg_tax_location.pkgnum
+ FROM cust_bill_pkg_tax_location
+ JOIN cust_bill_pkg AS tax_item USING (billpkgnum)
+ JOIN cust_main_county USING (taxnum)
+ WHERE ". join(' AND ', @tax_where).
+ " GROUP BY invnum, cust_bill_pkg_tax_location.pkgnum";
+
+ $join_pkg .= " LEFT JOIN ($tax_sub) AS item_tax
+ ON (item_tax.invnum = cust_bill_pkg.invnum AND
+ item_tax.pkgnum = cust_bill_pkg.pkgnum)";
+ }
- $loc_sql;
+ # now do something with that
+ if ( @exempt_where ) {
- } @counties
+ push @where, 'item_exempt.billpkgnum IS NOT NULL';
+ push @select, 'item_exempt.exempt_amount';
+ push @peritem, 'exempt_amount';
+ push @peritem_desc, 'Exempt';
+ push @total, 'SUM(exempt_amount)';
+ push @total_desc, "$money_char%.2f tax-exempt";
- ). ' ) ';
+ } elsif ( $cgi->param('taxable') ) {
- push @where, $locs_sql;
+ my $taxable = 'cust_bill_pkg.setup + cust_bill_pkg.recur '.
+ '- COALESCE(item_exempt.exempt_amount, 0)';
- } else {
+ push @where, 'item_tax.invnum IS NOT NULL';
+ push @select, "($taxable) AS taxable_amount";
+ push @peritem, 'taxable_amount';
+ push @peritem_desc, 'Taxable';
+ push @total, "SUM($taxable)";
+ push @total_desc, "$money_char%.2f taxable";
- my %ph = map { $_ => dbh->quote( scalar($cgi->param($_)) ) } @loc_param;
+ } elsif ( $cgi->param('out') ) {
+
+ push @where, 'item_tax.invnum IS NULL',
+ 'item_exempt.billpkgnum IS NULL';
- my ( $loc_sql, @param ) = FS::cust_pkg->location_sql;
- while ( $loc_sql =~ /\?/ ) { #easier to do our own substitution
- $loc_sql =~ s/\?/$ph{shift(@param)}/e;
- }
+ } elsif ( @tax_where ) {
- push @where, $loc_sql;
+ # union of taxable + all exempt_ cases
+ push @where,
+ '(item_tax.invnum IS NOT NULL OR item_exempt.billpkgnum IS NOT NULL)';
}
-
- if ( $cgi->param('istax') ) {
- if ( $cgi->param('taxname') ) {
- push @where, 'itemdesc = '. dbh->quote( $cgi->param('taxname') );
- #} elsif ( $cgi->param('taxnameNULL') {
- } else {
- push @where, "( itemdesc IS NULL OR itemdesc = '' OR itemdesc = 'Tax' )";
- }
- } elsif ( $cgi->param('nottax') ) {
- #what can we usefully do with "taxname" ???? look up a class???
- } else {
- #warn "neither nottax nor istax parameters specified";
- }
- if ( $cgi->param('taxclassNULL')
- && ! $cgi->param('istax') #no part_pkg.taxclass in this case
- #(see comment above?)
- )
- {
- my %hash = ( 'country' => scalar($cgi->param('country')) );
- foreach (qw( state county )) {
- $hash{$_} = scalar($cgi->param($_)) if $cgi->param($_);
- }
- my $cust_main_county = qsearchs('cust_main_county', \%hash);
- die "unknown base region for empty taxclass" unless $cust_main_county;
+ # recur/usage separation
+ $use_usage = $cgi->param('usage');
+ if ( $use_usage eq 'recurring' ) {
- my $same_sql = $cust_main_county->sql_taxclass_sameregion;
- $same_sql =~ s/taxclass/part_pkg.taxclass/g;
- push @where, $same_sql if $same_sql;
+ my $recur_no_usage = FS::cust_bill_pkg->charged_sql('', '', no_usage => 1);
+ push @select, "($recur_no_usage) AS recur_no_usage";
+ $peritem[1] = 'recur_no_usage';
+ $total[1] = "SUM(cust_bill_pkg.setup + $recur_no_usage)";
+ $total_desc[1] .= ' (excluding usage)';
+
+ } elsif ( $use_usage eq 'usage' ) {
+ my $usage = FS::cust_bill_pkg->usage_sql();
+ push @select, "($usage) AS _usage";
+ # there's already a method named 'usage'
+ $peritem[1] = '_usage';
+ $peritem_desc[1] = 'Usage charge';
+ $total[1] = "SUM($usage)";
+ $total_desc[1] .= ' usage charges';
}
-} elsif ( scalar( grep( /locationtaxid/, $cgi->param ) ) ) {
+} elsif ( $cgi->param('istax') ) {
- push @where, FS::tax_rate_location->location_sql(
- map { $_ => (scalar($cgi->param($_)) || '') }
- qw( district city county state locationtaxid )
- );
+ @peritem = ( 'setup' ); # taxes only have setup
+ @peritem_desc = ( 'Tax charge' );
-}
+ push @where, 'cust_bill_pkg.pkgnum = 0';
-# unearned revenue mode
-if ( $cgi->param('unearned_now') =~ /^(\d+)$/ ) {
+ # tax location when using tax_rate_location
+ if ( scalar( grep( /locationtaxid/, $cgi->param ) ) ) {
- $unearned = $1;
- $unearned_mode = $cgi->param('mode');
+ $join_pkg .= ' LEFT JOIN cust_bill_pkg_tax_rate_location USING ( billpkgnum ) '.
+ ' LEFT JOIN tax_rate_location USING ( taxratelocationnum )';
+ push @where, FS::tax_rate_location->location_sql(
+ map { $_ => (scalar($cgi->param($_)) || '') }
+ qw( district city county state locationtaxid )
+ );
- push @where, "cust_bill_pkg.sdate < $unearned",
- "cust_bill_pkg.edate > $unearned",
- "cust_bill_pkg.recur != 0",
- "part_pkg.freq != '0'";
+ $total[1] = 'SUM(
+ COALESCE(cust_bill_pkg_tax_rate_location.amount,
+ cust_bill_pkg.setup + cust_bill_pkg.recur)
+ )';
- if ( !$cgi->param('include_monthly') ) {
- push @where,
- "part_pkg.freq != '1'",
- "part_pkg.freq NOT LIKE '%h'",
- "part_pkg.freq NOT LIKE '%d'",
- "part_pkg.freq NOT LIKE '%w'";
- }
+ } elsif ( $cgi->param('out') ) {
- my $usage_sql = FS::cust_bill_pkg->usage_sql;
- push @select, "($usage_sql) AS usage"; # we need this
- my $paid_sql = 'GREATEST(' .
- FS::cust_bill_pkg->paid_sql($unearned, '', setuprecur => 'recur') .
- " - $usage_sql, 0)";
+ $join_pkg = '
+ LEFT JOIN cust_bill_pkg_tax_location USING (billpkgnum)
+ ';
+ push @where, 'cust_bill_pkg_tax_location.billpkgnum IS NULL';
- push @select, "$paid_sql AS paid_no_usage"; # need this either way
+ # each billpkgnum should appear only once
+ $total[0] = 'COUNT(*)';
+ $total[1] = 'SUM(cust_bill_pkg.setup)';
- if ( $unearned_mode eq 'paid' ) {
- # then use the amount paid, minus usage charges
- $unearned_base = $paid_sql;
- }
- else {
- # use the amount billed, minus usage charges and credits
- $unearned_base = "GREATEST( cust_bill_pkg.recur - ".
- FS::cust_bill_pkg->credited_sql($unearned, '', setuprecur => 'recur') .
- " - $usage_sql, 0)";
- # include only rows that have some non-usage, non-credited portion
- }
- # whatever we're using as the base, only show rows where it's positive
- push @where, "$unearned_base > 0";
-
- my $period = "CAST(cust_bill_pkg.edate - cust_bill_pkg.sdate AS REAL)";
- my $elapsed = "GREATEST( $unearned - cust_bill_pkg.sdate, 0 )";
- my $remaining = "(1 - $elapsed/$period)";
-
- $unearned_sql = "CAST( $unearned_base * $remaining AS DECIMAL(10,2) )";
- push @select, "$unearned_sql AS unearned_revenue";
-
- # last payment/credit date
- my %t = (pay => 'cust_bill_pay', credit => 'cust_credit_bill');
- foreach my $x (qw(pay credit)) {
- my $table = $t{$x};
- my $link = $table.'_pkg';
- my $pkey = dbdef->table($table)->primary_key;
- my $last_date_sql = "SELECT MAX(_date)
- FROM $table JOIN $link USING ($pkey)
- WHERE $link.billpkgnum = cust_bill_pkg.billpkgnum
- AND $table._date <= $unearned";
- push @select, "($last_date_sql) AS last_$x";
- }
+ } else { # not locationtaxid or 'out'--the normal case
-}
+ $join_pkg = '
+ LEFT JOIN cust_bill_pkg_tax_location USING (billpkgnum)
+ JOIN cust_main_county USING (taxnum)
+ ';
-if ( $cgi->param('itemdesc') ) {
- if ( $cgi->param('itemdesc') eq 'Tax' ) {
- push @where, "(itemdesc='Tax' OR itemdesc is null)";
- } else {
- push @where, 'itemdesc='. dbh->quote($cgi->param('itemdesc'));
+ # don't double-count the components of consolidated taxes
+ $total[0] = 'COUNT(DISTINCT cust_bill_pkg.billpkgnum)';
+ $total[1] = 'SUM(cust_bill_pkg_tax_location.amount)';
}
-}
-if ( $cgi->param('report_group') =~ /^(=|!=) (.*)$/ && $cgi->param('istax') ) {
- my ( $group_op, $group_value ) = ( $1, $2 );
- if ( $group_op eq '=' ) {
- #push @where, 'itemdesc LIKE '. dbh->quote($group_value.'%');
- push @where, 'itemdesc = '. dbh->quote($group_value);
- } elsif ( $group_op eq '!=' ) {
- push @where, '( itemdesc != '. dbh->quote($group_value) .' OR itemdesc IS NULL )';
- } else {
- die "guru meditation #00de: group_op $group_op\n";
+ # taxclass
+ if ( $cgi->param('taxclassNULL') ) {
+ push @where, 'cust_main_county.taxclass IS NULL';
}
-
-}
-push @where, 'cust_bill_pkg.pkgnum != 0' if $cgi->param('nottax');
-push @where, 'cust_bill_pkg.pkgnum = 0' if $cgi->param('istax');
-
-if ( $cgi->param('cust_tax') ) {
- #false laziness -ish w/report_tax.cgi
- my $cust_exempt;
- if ( $cgi->param('taxname') ) {
- my $q_taxname = dbh->quote($cgi->param('taxname'));
- $cust_exempt =
- "( tax = 'Y'
- OR EXISTS ( SELECT 1 FROM cust_main_exemption
- WHERE cust_main_exemption.custnum = cust_main.custnum
- AND cust_main_exemption.taxname = $q_taxname )
- )
- ";
- } else {
- $cust_exempt = " tax = 'Y' ";
+ # taxname
+ if ( $cgi->param('taxnameNULL') ) {
+ push @where, 'cust_main_county.taxname IS NULL OR '.
+ 'cust_main_county.taxname = \'Tax\'';
+ } elsif ( $cgi->param('taxname') ) {
+ push @where, 'cust_main_county.taxname = '.
+ dbh->quote($cgi->param('taxname'));
}
- push @where, $cust_exempt;
-}
-
-my $use_usage = $cgi->param('use_usage');
-
-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
- )
- )
- ";
-
- push @where, "( ( part_pkg.setuptax = 'Y' AND cust_bill_pkg.setup > 0 )
- OR ( part_pkg.recurtax = 'Y' AND cust_bill_pkg.recur > 0 ) )",
- "( tax != 'Y' OR tax IS NULL )";
-
-} elsif ( $cgi->param('taxable') ) {
-
- my $setup_taxable = "(
- CASE WHEN part_pkg.setuptax = 'Y'
- THEN 0
- ELSE cust_bill_pkg.setup
- END
- )";
-
- my $recur_taxable = "(
- CASE WHEN part_pkg.recurtax = 'Y'
- THEN 0
- ELSE cust_bill_pkg.recur
- END
- )";
-
- my $exempt = "(
- SELECT COALESCE( SUM(amount), 0 ) FROM cust_tax_exempt_pkg
- WHERE cust_tax_exempt_pkg.billpkgnum = cust_bill_pkg.billpkgnum
- )";
-
- $count_query =
- "SELECT COUNT(*), SUM( $setup_taxable + $recur_taxable - $exempt )";
-
- push @where,
- #not tax-exempt package (setup or recur)
- "(
- ( ( part_pkg.setuptax != 'Y' OR part_pkg.setuptax IS NULL )
- AND cust_bill_pkg.setup > 0 )
- OR
- ( ( part_pkg.recurtax != 'Y' OR part_pkg.recurtax IS NULL )
- AND cust_bill_pkg.recur > 0 )
- )",
- #not a tax_exempt customer
- "( tax != 'Y' OR tax IS NULL )";
- #not covered in full by a monthly tax exemption (texas tax)
- "0 < ( $setup_taxable + $recur_taxable - $exempt )",
-
-} else {
-
- if ( $use_usage ) {
- $count_query = "SELECT COUNT(*), ";
- } else {
- $count_query = "SELECT COUNT(DISTINCT billpkgnum), ";
+ # specific taxnums
+ if ( $cgi->param('taxnum') ) {
+ my $taxnum_in = join(',',
+ grep /^\d+$/, $cgi->param('taxnum')
+ );
+ push @where, "cust_main_county.taxnum IN ($taxnum_in)"
+ if $taxnum_in;
}
- if ( $unearned ) {
- $count_query .= "SUM( $unearned_base ), SUM( $unearned_sql )";
- } elsif ( $use_usage eq 'recurring' ) {
- $count_query .= "SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - usage)";
- } elsif ( $use_usage eq 'usage' ) {
- $count_query .= "SUM(usage)";
- } elsif ( scalar( grep( /locationtaxid/, $cgi->param ) ) ) {
- $count_query .= "SUM( COALESCE(cust_bill_pkg_tax_rate_location.amount, cust_bill_pkg.setup + cust_bill_pkg.recur))";
- } elsif ( $cgi->param('iscredit') eq 'rate') {
- $count_query .= "SUM( cust_credit_bill_pkg.amount )";
- } else {
- $count_query .= "SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)";
+ # report group (itemdesc)
+ if ( $cgi->param('report_group') =~ /^(=|!=) (.*)$/ ) {
+ my ( $group_op, $group_value ) = ( $1, $2 );
+ if ( $group_op eq '=' ) {
+ #push @where, 'itemdesc LIKE '. dbh->quote($group_value.'%');
+ push @where, 'itemdesc = '. dbh->quote($group_value);
+ } elsif ( $group_op eq '!=' ) {
+ push @where, '( itemdesc != '. dbh->quote($group_value) .' OR itemdesc IS NULL )';
+ } else {
+ die "guru meditation #00de: group_op $group_op\n";
+ }
}
-}
-
-$join_cust = ' JOIN cust_bill USING ( invnum )
- LEFT JOIN cust_main USING ( custnum ) ';
-
-if ( $cgi->param('nottax') ) {
-
- $join_pkg .= ' LEFT JOIN cust_pkg USING ( pkgnum )
- LEFT JOIN part_pkg USING ( pkgpart )
- LEFT JOIN part_pkg AS override
- ON pkgpart_override = override.pkgpart ';
- $join_pkg .= ' LEFT JOIN cust_location USING ( locationnum ) '
- if $conf->exists('tax-pkg_address');
-
-} elsif ( $cgi->param('istax') ) {
-
- #false laziness w/report_tax.cgi $taxfromwhere
- if ( scalar( grep( /locationtaxid/, $cgi->param ) ) ||
- $cgi->param('iscredit') eq 'rate') {
+ # itemdesc, for some reason
+ if ( $cgi->param('itemdesc') ) {
+ if ( $cgi->param('itemdesc') eq 'Tax' ) {
+ push @where, "(itemdesc='Tax' OR itemdesc is null)";
+ } else {
+ push @where, 'itemdesc='. dbh->quote($cgi->param('itemdesc'));
+ }
+ }
- $join_pkg .=
- ' LEFT JOIN cust_bill_pkg_tax_rate_location USING ( billpkgnum ) '.
- ' LEFT JOIN tax_rate_location USING ( taxratelocationnum ) ';
+} # nottax / istax
- } elsif ( $conf->exists('tax-pkg_address') ) {
+# credit
+if ( $cgi->param('credit') ) {
- $join_pkg .= ' LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum )
- LEFT JOIN cust_location USING ( locationnum ) ';
+ my $credit_sub;
- #quelle kludge, somewhat false laziness w/report_tax.cgi
- s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g for @where;
- }
+ if ( $cgi->param('istax') ) {
+ # then we need to group/join by billpkgtaxlocationnum, to get only the
+ # relevant part of partial taxes
+ my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount,
+ reason.reason as reason_text, access_user.username AS username_text,
+ billpkgtaxlocationnum, billpkgnum
+ FROM cust_credit_bill_pkg
+ JOIN cust_credit_bill USING (creditbillnum)
+ JOIN cust_credit USING (crednum)
+ LEFT JOIN reason USING (reasonnum)
+ LEFT JOIN access_user USING (usernum)
+ GROUP BY billpkgnum, billpkgtaxlocationnum, reason.reason,
+ access_user.username";
+
+ if ( $cgi->param('out') ) {
+
+ # find credits that are applied to the line items, but not to
+ # a cust_bill_pkg_tax_location link
+ $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit
+ USING (billpkgnum)";
+ push @where, 'item_credit.billpkgtaxlocationnum IS NULL';
- if ( $cgi->param('iscredit') ) {
- $join_pkg .= ' JOIN cust_credit_bill_pkg USING ( billpkgnum';
- if ( $cgi->param('iscredit') eq 'rate' ) {
- $join_pkg .= ', billpkgtaxratelocationnum )';
- } elsif ( $conf->exists('tax-pkg_address') ) {
- $join_pkg .= ', billpkgtaxlocationnum )';
- push @where, "billpkgtaxratelocationnum IS NULL";
} else {
- $join_pkg .= ' )';
- push @where, "billpkgtaxratelocationnum IS NULL";
- }
- }
-} else {
+ # find credits that are applied to the CBPTL links that are
+ # considered "interesting" by the report criteria
+ $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit
+ USING (billpkgtaxlocationnum)";
- #die?
- warn "neiether nottax nor istax parameters specified";
- #same as before?
- $join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum )
- LEFT JOIN part_pkg USING ( pkgpart ) ';
+ }
-}
+ } else {
+ # then only group by billpkgnum
+ my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount,
+ reason.reason as reason_text, access_user.username AS username_text,
+ billpkgnum
+ FROM cust_credit_bill_pkg
+ JOIN cust_credit_bill USING (creditbillnum)
+ JOIN cust_credit USING (crednum)
+ LEFT JOIN reason USING (reasonnum)
+ LEFT JOIN access_user USING (usernum)
+ GROUP BY billpkgnum, reason.reason, access_user.username";
+ $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit USING (billpkgnum)";
+ }
-my $where = ' WHERE '. join(' AND ', @where);
-
-if ($use_usage) {
- $count_query .=
- " FROM (SELECT cust_bill_pkg.setup, cust_bill_pkg.recur,
- ( SELECT COALESCE( SUM(amount), 0 ) FROM cust_bill_pkg_detail
- WHERE cust_bill_pkg.billpkgnum = cust_bill_pkg_detail.billpkgnum
- ) AS usage FROM cust_bill_pkg $join_cust $join_pkg $where
- ) AS countquery";
-} else {
- $count_query .= " FROM cust_bill_pkg $join_cust $join_pkg $where";
-}
+ push @where, 'item_credit.billpkgnum IS NOT NULL';
+ push @select, 'item_credit.credit_amount',
+ 'item_credit.username_text',
+ 'item_credit.reason_text';
+ push @peritem, 'credit_amount', 'username_text', 'reason_text';
+ push @peritem_desc, 'Credited', 'By', 'Reason';
+ push @total, 'SUM(credit_amount)';
+ push @total_desc, "$money_char%.2f credited";
+} # if credit
-push @select, 'part_pkg.pkg',
- 'part_pkg.freq',
- unless $cgi->param('istax');
+push @select, 'cust_main.custnum', FS::UI::Web::cust_sql_fields();
-push @select, 'cust_main.custnum',
- FS::UI::Web::cust_sql_fields();
+my $where = join(' AND ', @where);
+$where &&= "WHERE $where";
my $query = {
'table' => 'cust_bill_pkg',
@@ -624,25 +557,31 @@ my $query = {
'hashref' => {},
'select' => join(",\n", @select ),
'extra_sql' => $where,
- 'order_by' => 'ORDER BY cust_bill._date, billpkgnum',
+ 'order_by' => 'ORDER BY cust_bill._date, cust_bill_pkg.billpkgnum',
};
-my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ];
-my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
+my $count_query =
+ 'SELECT ' . join(',', @total) .
+ " FROM cust_bill_pkg $join_cust $join_pkg
+ $where";
-my $conf = new FS::Conf;
-my $money_char = $conf->config('money_char') || '$';
+shift @total_desc; #the first one is implicit
-my $owed_sub = sub {
- $money_char . shift->get('owed') # owed_recur is not correct here
-};
-my $payment_date_sub = sub {
- #my $cust_bill_pkg = shift;
- my @cust_pay = sort { $a->_date <=> $b->_date }
- map $_->cust_bill_pay->cust_pay,
- shift->cust_bill_pay_pkg('recur') #recur :/
- or return '';
- time2str('%b %d %Y', $cust_pay[-1]->_date );
-};
+@peritem_desc = map {emt($_)} @peritem_desc;
+my @peritem_sub = map {
+ my $field = $_;
+ if ($field =~ /_text$/) { # kludge for credit reason/username fields
+ sub {$_[0]->get($field)};
+ } else {
+ sub { sprintf($money_char.'%.2f', $_[0]->get($field)) }
+ }
+} @peritem;
+my @peritem_null = map { '' } @peritem; # placeholders
+my $peritem_align = 'r' x scalar(@peritem);
+
+my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ];
+my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
+warn "\n\nQUERY:\n".Dumper($query)."\n\nCOUNT_QUERY:\n$count_query\n\n"
+ if $cgi->param('debug');
</%init>
diff --git a/httemplate/search/cust_bill_pkg_referral.html b/httemplate/search/cust_bill_pkg_referral.html
index 3cb434caa..77b486021 100644
--- a/httemplate/search/cust_bill_pkg_referral.html
+++ b/httemplate/search/cust_bill_pkg_referral.html
@@ -146,6 +146,16 @@ if ( @status_where ) {
') IN (' . join(',', @status_where) .')';
}
+my @refnum;
+foreach my $refnum ($cgi->param('refnum')) {
+ if ( $refnum =~ /^\d+$/ ) {
+ push @refnum, $refnum;
+ }
+}
+if ( @refnum ) {
+ push @where, 'cust_main.refnum IN ('.join(',', @refnum).')';
+}
+
if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
push @where, "cust_main.agentnum = $1";
}
diff --git a/httemplate/search/cust_main-zip.html b/httemplate/search/cust_main-zip.html
index c317dc36f..08800d431 100644
--- a/httemplate/search/cust_main-zip.html
+++ b/httemplate/search/cust_main-zip.html
@@ -4,8 +4,8 @@
'query' => $sql_query,
'count_query' => $count_sql,
'header' => [ 'Zip code', 'Customers', ],
- #'fields' => [ 'zip', 'num_cust', ],
- #'links' => [ '', sub { 'somewhere'; } ],
+ 'fields' => [ 0, 1 ],
+ 'links' => [ '', $link ],
)
%>
<%init>
@@ -63,48 +63,36 @@ sub strip_plus4 {
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';
- }
+$cgi->param('column') =~ /^(bill|ship)$/;
+my $location = $1 || 'bill';
+$location .= '_locationnum';
+my $zip;
+if ( $cgi->param('ignore_plus4') ) {
+ $zip = strip_plus4('cust_location.zip');
} else {
-
- $czip = 'zip';
-
- if ( $cgi->param('ignore_plus4') ) {
- $zip = strip_plus4('zip');
- } else {
- $zip = fieldorempty('zip');
- }
-
+ $zip = fieldorempty('cust_location.zip');
}
# construct the queries and send 'em off
+my $join = "JOIN cust_location ON (cust_main.$location = cust_location.locationnum)";
+
my $sql_query =
"SELECT $zip AS zipcode,
COUNT(*) AS num_cust
FROM cust_main
+ $join
$where
GROUP BY zipcode
- ORDER BY num_cust DESC
+ ORDER BY num_cust DESC, $zip ASC
";
-my $count_sql = "select count(distinct $czip) from cust_main $where";
+my $count_sql =
+ "SELECT COUNT(DISTINCT cust_location.zip)
+ FROM cust_main $join $where";
-# XXX should link...
+my $link = [ $p.'search/cust_main.html?zip=',
+ sub { $_[0]->[0] } ];
</%init>
diff --git a/httemplate/search/cust_main.cgi b/httemplate/search/cust_main.cgi
index 859ef04e6..7c3ad3384 100755
--- a/httemplate/search/cust_main.cgi
+++ b/httemplate/search/cust_main.cgi
@@ -81,13 +81,8 @@
<TR>
<TH CLASS="grid" BGCOLOR="#cccccc"><% mt('#') |h %></TH>
<TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Status') |h %></TH>
- <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('(bill) name') |h %></TH>
- <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('company') |h %></TH>
-
-%if ( defined dbdef->table('cust_main')->column('ship_last') ) {
- <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('(service) name') |h %></TH>
- <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('company') |h %></TH>
-%}
+ <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Name') |h %></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Company') |h %></TH>
%foreach my $addl_header ( @addl_headers ) {
<TH CLASS="grid" BGCOLOR="#cccccc"><% $addl_header %></TH>
@@ -172,25 +167,6 @@
<% $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 %>>
- <A HREF="<% $view %>"><FONT SIZE=-1><% "$ship_last, $ship_first" %></FONT></A>
- </TD>
- <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN=<% $rowspan %>>
- <% $pship_company %></A>
- </TD>
-% }
-%
% foreach my $addl_col ( @addl_cols ) {
% if ( $addl_col eq 'tickets' ) {
% if ( @custom_priorities ) {
@@ -492,9 +468,10 @@ if ( $cgi->param('browse')
if ( $cgi->param('search_cust') ) {
$sortby = \*company_sort;
$orderby = "ORDER BY LOWER(company || ' ' || last || ' ' || first )";
- push @cust_main, smart_search( 'search' => scalar($cgi->param('search_cust')),
- 'no_fuzzy_on_exact' => 1, #pref?
- );
+ push @cust_main, smart_search(
+ 'search' => scalar($cgi->param('search_cust')),
+ 'no_fuzzy_on_exact' => ! $curuser->option('enable_fuzzy_on_exact'),
+ );
}
@cust_main = grep { $_->ncancelled_pkgs || ! $_->all_pkgs } @cust_main
diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html
index e164b98f4..fa79b4dfb 100755
--- a/httemplate/search/cust_main.html
+++ b/httemplate/search/cust_main.html
@@ -41,7 +41,7 @@ my %search_hash = ();
#scalars
my @scalars = qw (
- agentnum status address paydate_year paydate_month invoice_terms
+ agentnum status address zip paydate_year paydate_month invoice_terms
no_censustract with_geocode custbatch usernum
cancelled_pkgs
cust_fields flattened_pkgs
@@ -61,7 +61,7 @@ for my $param (qw( classnum refnum payby tagnum )) {
# parse dates
###
-foreach my $field (qw( signupdate birthdate spouse_birthdate )) {
+foreach my $field (qw( signupdate birthdate spouse_birthdate anniversary_date )) {
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field);
diff --git a/httemplate/search/cust_pay_pending.html b/httemplate/search/cust_pay_pending.html
index 8b7350853..2afce0ce9 100755
--- a/httemplate/search/cust_pay_pending.html
+++ b/httemplate/search/cust_pay_pending.html
@@ -5,7 +5,6 @@
'name_verb' => 'pending',
'disable_link' => 1,
'disable_by' => 1, #add otaker to cust_pay_pending?
- 'html_init' => include('/elements/init_overlib.html'),
'addl_header' => [ 'Time', 'Payment Status', ],
'addl_fields' => [ sub { time2str('%r', shift->_date ) },
$status_sub,
diff --git a/httemplate/search/cust_tax_exempt_pkg.cgi b/httemplate/search/cust_tax_exempt_pkg.cgi
index 3a5155ae8..1b767f846 100644
--- a/httemplate/search/cust_tax_exempt_pkg.cgi
+++ b/httemplate/search/cust_tax_exempt_pkg.cgi
@@ -103,7 +103,7 @@ my $join = "
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('View customer tax exemptions');
-my @where = ();
+my @where = ("exempt_monthly = 'Y'");
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
if ( $beginning || $ending ) {
@@ -121,6 +121,7 @@ if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
}
if ( $cgi->param('out') ) {
+ # wtf? how would you ever get exemptions on a non-taxable package location?
push @where, "
0 = (
@@ -151,6 +152,11 @@ if ( $cgi->param('out') ) {
push @where, 'taxclass = '. dbh->quote( $cgi->param('taxclass') )
if $cgi->param('taxclass');
+} elsif ( $cgi->param('taxnum') ) {
+
+ my $taxnum_in = join(',', grep /^\d+$/, $cgi->param('taxnum') );
+ push @where, "taxnum IN ($taxnum_in)" if $taxnum_in;
+
}
my $where = scalar(@where) ? 'WHERE '.join(' AND ', @where) : '';
diff --git a/httemplate/search/elements/cust_pay_batch_top.html b/httemplate/search/elements/cust_pay_batch_top.html
index 739e65b50..1dcc37ac1 100644
--- a/httemplate/search/elements/cust_pay_batch_top.html
+++ b/httemplate/search/elements/cust_pay_batch_top.html
@@ -33,6 +33,7 @@ Download batch in format <SELECT NAME="format">
'action' => "${p}misc/upload-batch.cgi",
'num_files' => 1,
'fields' => [ 'batchnum', 'format', 'gatewaynum' ],
+ 'url' => $cgi->self_url,
'message' => 'Batch results uploaded.',
) %>
Upload results<BR></TR>
@@ -87,7 +88,7 @@ Batch is <% $statustext{$status} %><BR>
<%def .select_gateway>
% if ( $show_gateways ) {
- or from gateway
+ or for gateway
<& /elements/select-table.html,
empty_label => ' ',
field => 'gatewaynum',
diff --git a/httemplate/search/elements/search-csv.html b/httemplate/search/elements/search-csv.html
index 9eb1b66d1..90230e6dc 100644
--- a/httemplate/search/elements/search-csv.html
+++ b/httemplate/search/elements/search-csv.html
@@ -27,10 +27,21 @@
% $csv->combine(@$row); #or die $csv->status;
% }
%
-%
<% $csv->string %>\
%
% }
+%
+% if ( $opt{'footer'} and !$opt{'no_csv_header'} ) {
+% my @footer;
+% foreach my $item (@{ $opt{'footer'} }) {
+% if ( ref($item) eq 'CODE' ) {
+% $item = &{$item}();
+% }
+% push @footer, $item;
+% }
+% $csv->combine(@footer);
+<% $csv->string %>\
+% }
<%init>
my %args = @_;
diff --git a/httemplate/search/elements/search-html.html b/httemplate/search/elements/search-html.html
index 53167c26e..d7e81282b 100644
--- a/httemplate/search/elements/search-html.html
+++ b/httemplate/search/elements/search-html.html
@@ -134,9 +134,9 @@
% and !$opt{'disable_download'}
% and $type ne 'html-print' ) {
- <TD ALIGN="right">
+ <TD ALIGN="right" CLASS="noprint">
- Download full results<BR>
+ <% $opt{'download_label'} || 'Download full results' %><BR>
% $cgi->param('_type', "$xlsname.xls" );
as <A HREF="<% "$self_url?". $cgi->query_string %>">Excel spreadsheet</A><BR>
@@ -337,6 +337,11 @@
% map {
% if ( ref($_) eq 'CODE' ) {
% &{$_}($row);
+% } elsif ( ref($row) eq 'ARRAY' and
+% $_ =~ /^\d+$/ ) {
+% # for the 'straight SQL' case: specify fields
+% # by position
+% $row->[$_];
% } else {
% $row->$_();
% }
@@ -345,7 +350,8 @@
%
% ) {
%
-% my $class = ( $field =~ /^<TABLE/i ) ? 'inv' : 'grid';
+%# my $class = ( $field =~ /^<TABLE/i ) ? 'inv' : 'grid';
+% my $class = 'grid';
%
% my $align = $aligns ? shift @$aligns : '';
% $align = " ALIGN=$align" if $align;
diff --git a/httemplate/search/elements/search-xls.html b/httemplate/search/elements/search-xls.html
index 09dbe46e0..94d88b096 100644
--- a/httemplate/search/elements/search-xls.html
+++ b/httemplate/search/elements/search-xls.html
@@ -55,6 +55,10 @@ my $writer = sub {
# Wrapper for $worksheet->write.
# Do any massaging of the value/format here.
my ($r, $c, $value, $format) = @_;
+ # convert HTML entities
+ # both Spreadsheet::WriteExcel and Excel::Writer::XLSX accept UTF-8 strings
+ $value = decode_entities($value);
+
if ( $value =~ /^\Q$money_char\E(-?\d+\.?\d*)$/ ) {
# Currency: strip the symbol, clone the requested format,
# and format it for currency
@@ -130,6 +134,17 @@ foreach my $row ( @$rows ) {
}
+if ( $opt{'footer'} ) {
+ $r++;
+ $c = 0;
+ foreach my $item (@{ $opt{'footer'} }) {
+ if ( ref($item) eq 'CODE' ) {
+ $item = &{$item}();
+ }
+ $writer->( $r, $c++, $item, $header_format );
+ }
+}
+
$workbook->close();# or die "Error creating .xls file: $!";
http_header('Content-Length' => length($data) );
diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html
index 9bc66b6fa..eca68a2f8 100644
--- a/httemplate/search/elements/search.html
+++ b/httemplate/search/elements/search.html
@@ -162,7 +162,11 @@ Example:
# Excel-specific listref of ( hashrefs or coderefs )
# each hashref: http://search.cpan.org/dist/Spreadsheet-WriteExcel/lib/Spreadsheet/WriteExcel.pm#Format_methods_and_Format_properties
'xls_format' => => [],
-
+
+
+ # miscellany
+ 'download_label' => 'Download this report',
+ # defaults to 'Download full results'
&>
</%doc>
diff --git a/httemplate/search/quotation.html b/httemplate/search/quotation.html
new file mode 100755
index 000000000..259c85c22
--- /dev/null
+++ b/httemplate/search/quotation.html
@@ -0,0 +1,268 @@
+<& elements/search.html,
+ 'title' => emt('Quotation Search Results'),
+ 'html_init' => $html_init,
+ 'menubar' => $menubar,
+ 'name' => 'quotations',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'count_addl' => $count_addl,
+ 'redirect' => $link,
+ 'header' => [ emt('Quotation #'),
+ emt('Setup'),
+ emt('Recurring'),
+ emt('Date'),
+ emt('Prospect'),
+ emt('Customer'),
+ ],
+ 'fields' => [
+ 'quotationnum',
+ sub { $money_char. shift->total_setup },
+ sub { $money_char. shift->total_recur },
+ sub { time2str('%b %d %Y', shift->_date ) },
+ sub { my $prospect_main = shift->prospect_main;
+ $prospect_main ? $prospect_main->name : '';
+ },
+ sub { my $cust_main = shift->cust_main;
+ $cust_main ? $cust_main->name : '';
+ },
+ #\&FS::UI::Web::cust_fields,
+ ],
+ 'sort_fields' => [
+ 'quotationnum',
+ '', #FS::quotation->total_setup_sql,
+ '', #FS::quotation->total_recur_sql,
+ '_date',
+ '',
+ '',
+ ],
+ 'align' => 'rrrrll', #.FS::UI::Web::cust_aligns(),
+ 'links' => [
+ $link,
+ $link,
+ $link,
+ $link,
+ $prospect_link,
+ $cust_link,
+ #( map { $_ ne 'Cust. Status' ? $clink : '' }
+ # FS::UI::Web::cust_header()
+ #),
+ ],
+# 'color' => [
+# '',
+# '',
+# '',
+# '',
+# '',
+# FS::UI::Web::cust_colors(),
+# ],
+# 'style' => [
+# '',
+# '',
+# '',
+# '',
+# '',
+# FS::UI::Web::cust_styles(),
+# ],
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('List quotations');
+
+my $join_prospect_main = 'LEFT JOIN prospect_main USING ( prospectnum )';
+my $join_cust_main = 'LEFT JOIN cust_main ON ( quotation.custnum = cust_main.custnum )';
+
+#here is the agent virtualization
+my $agentnums_sql = ' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
+ ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
+ ' ) ';
+
+my( $count_query, $sql_query );
+my $count_addl = '';
+my %search;
+
+#if ( $cgi->param('quotationnum') =~ /^\s*(FS-)?(\d+)\s*$/ ) {
+#
+# my $where = "WHERE quotationnum = $2 AND $agentnums_sql";
+#
+# $count_query = "SELECT COUNT(*) FROM quotation $join_prospect_main $join_cust_main $where";
+#
+# $sql_query = {
+# 'table' => 'quotation',
+# 'addl_from' => "$join_prospect_main $join_cust_main",
+# 'hashref' => {},
+# 'extra_sql' => $where,
+# };
+#
+#} else {
+
+ #some false laziness w/cust_bill::re_X
+ my $orderby = 'ORDER BY quotation._date';
+
+ if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $search{'agentnum'} = $1;
+ }
+
+# if ( $cgi->param('refnum') =~ /^(\d+)$/ ) {
+# $search{'refnum'} = $1;
+# }
+
+ if ( $cgi->param('prospectnum') =~ /^(\d+)$/ ) {
+ $search{'prospectnum'} = $1;
+ }
+
+ if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+ $search{'custnum'} = $1;
+ }
+
+ # begin/end/beginning/ending
+ my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, '');
+ $search{'_date'} = [ $beginning, $ending ]
+ unless $beginning == 0 && $ending == 4294967295;
+
+ if ( $cgi->param('quotationnum_min') =~ /^\s*(\d+)\s*$/ ) {
+ $search{'quotationnum_min'} = $1;
+ }
+ if ( $cgi->param('quotationnum_max') =~ /^\s*(\d+)\s*$/ ) {
+ $search{'quotationnum_max'} = $1;
+ }
+
+ #amounts
+ $search{$_} = [ FS::UI::Web::parse_lt_gt($cgi, $_) ]
+ foreach qw( total_setup total_recur );
+
+# my($query) = $cgi->keywords;
+# if ( $query =~ /^(OPEN(\d*)_)?(invnum|date|custnum)$/ ) {
+# $search{'open'} = 1 if $1;
+# ($search{'days'}, my $field) = ($2, $3);
+# $field = "_date" if $field eq 'date';
+# $orderby = "ORDER BY cust_bill.$field";
+# }
+
+# if ( $cgi->param('newest_percust') ) {
+# $search{'newest_percust'} = 1;
+# $count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
+# }
+
+ my $extra_sql = ' WHERE '. FS::quotation->search_sql_where( \%search );
+
+ unless ( $count_query ) {
+ $count_query = 'SELECT COUNT(*)';
+ }
+ $count_query .= " FROM quotation $join_prospect_main $join_cust_main $extra_sql";
+
+ $sql_query = {
+ 'table' => 'quotation',
+ 'addl_from' => "$join_prospect_main $join_cust_main",
+ 'hashref' => {},
+ 'select' => join(', ',
+ 'quotation.*',
+ #( map "cust_main.$_", qw(custnum last first company) ),
+ 'prospect_main.prospectnum as prospect_main_prospectnum',
+ 'cust_main.custnum as cust_main_custnum',
+ #FS::UI::Web::cust_sql_fields(),
+ ),
+ 'extra_sql' => $extra_sql,
+ 'order_by' => $orderby,
+ };
+
+#}
+
+my $link = [ "${p}view/quotation.html?", 'quotationnum', ];
+my $prospect_link = sub {
+ my $quotation = shift;
+ $quotation->prospect_main_prospectnum
+ ? [ "${p}view/prospect_main.html?", 'prospectnum' ]
+ : '';
+};
+
+my $cust_link = sub {
+ my $quotation = shift;
+ $quotation->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',
+ [ keys %search ],
+ "../misc/${_}invoices.cgi",
+ { 'message' => "Invoices re-${action}ed" }, #would be nice to show the number of them, but...
+ $_, #key
+ ),
+ qq!<FORM NAME="${_}form">!,
+ ( map { my $f = $_;
+ my @values = ref($search{$f}) ? @{ $search{$f} } : $search{$f};
+ map qq!<INPUT TYPE="hidden" NAME="$f" VALUE="$_">!, @values;
+ }
+ keys %search
+ ),
+ qq!</FORM>!
+} qw( print_ email_ fax_ ftp_ spool_ ) ).
+
+'<SCRIPT TYPE="text/javascript">
+
+function confirm_print_process() {
+ if ( ! confirm('.js_mt("Are you sure you want to reprint these invoices?").') ) {
+ return;
+ }
+ print_process();
+}
+function confirm_email_process() {
+ if ( ! confirm('.js_mt("Are you sure you want to re-email these invoices?").') ) {
+ return;
+ }
+ email_process();
+}
+function confirm_fax_process() {
+ if ( ! confirm('.js_mt("Are you sure you want to re-fax these invoices?").') ) {
+ return;
+ }
+ fax_process();
+}
+function confirm_ftp_process() {
+ if ( ! confirm('.js_mt("Are you sure you want to re-FTP these invoices?").') ) {
+ return;
+ }
+ ftp_process();
+}
+function confirm_spool_process() {
+ if ( ! confirm('.js_mt("Are you sure you want to re-spool these invoices?").') ) {
+ return;
+ }
+ spool_process();
+}
+
+</SCRIPT>';
+
+my $menubar = [];
+
+#if ( $curuser->access_right('Resend quotations') ) {
+#
+# push @$menubar, emt('Print these invoices') =>
+# "javascript:confirm_print_process()",
+# emt('Email these invoices') =>
+# "javascript:confirm_email_process()";
+#
+# push @$menubar, emt('Fax these invoices') =>
+# "javascript:confirm_fax_process()"
+# if $conf->exists('hylafax');
+#
+# push @$menubar, emt('FTP these invoices') =>
+# "javascript:confirm_ftp_process()"
+# if $conf->exists('cust_bill-ftpformat');
+#
+# push @$menubar, emt('Spool these invoices') =>
+# "javascript:confirm_spool_process()"
+# if $conf->exists('cust_bill-spoolformat');
+#
+#}
+
+</%init>
diff --git a/httemplate/search/report_477.html b/httemplate/search/report_477.html
index c9d97c5eb..f593a94d8 100755
--- a/httemplate/search/report_477.html
+++ b/httemplate/search/report_477.html
@@ -17,6 +17,18 @@
)
%>
+% # not tr-select-state, we only want to choose from among those that
+% # have customers
+ <& /elements/tr-select-table.html,
+ 'label' => 'State',
+ 'field' => 'state',
+ 'table' => 'cust_location',
+ 'name_col' => 'state',
+ 'value_col' => 'state',
+ 'disable_empty' => 1,
+ 'records' => \@states,
+ &>
+
<% include( '/elements/tr-select-pkg_class.html',
'multiple' => 1,
'empty_label' => '(empty class)',
@@ -252,4 +264,10 @@
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('List packages');
+my @states = qsearch({
+ 'table' => 'cust_location',
+ 'select' => 'DISTINCT(state)',
+ 'hashref' => { 'country' => 'US' }, # 477 report isn't relevant elsewhere
+});
+
</%init>
diff --git a/httemplate/search/report_cdr.html b/httemplate/search/report_cdr.html
index e3418a7d4..0e1693b9c 100644
--- a/httemplate/search/report_cdr.html
+++ b/httemplate/search/report_cdr.html
@@ -24,9 +24,12 @@
<SELECT NAME="freesidestatus">
<OPTION VALUE="">(all)</OPTION>
<OPTION VALUE="NULL">unprocessed</OPTION>
+%# <OPTION VALUE="processing-tiered">processing</OPTION>
<OPTION VALUE="rated">prerated
- <OPTION VALUE="done">processed</OPTION>
- <OPTION VALUE="failed">skipped</OPTION>
+ <OPTION VALUE="no-charge">processed (included)</OPTION>
+ <OPTION VALUE="done">processed (billed)</OPTION>
+ <OPTION VALUE="skipped">skipped</OPTION>
+ <OPTION VALUE="failed">failed</OPTION>
</SELECT>
</TD>
</TR>
diff --git a/httemplate/search/report_cust_bill_pkg_referral.html b/httemplate/search/report_cust_bill_pkg_referral.html
index ff2caa1fa..b4716d4fc 100644
--- a/httemplate/search/report_cust_bill_pkg_referral.html
+++ b/httemplate/search/report_cust_bill_pkg_referral.html
@@ -18,6 +18,11 @@
'disable_empty' => 1,
&>
+<& /elements/tr-select-part_referral.html,
+ 'multiple' => 1,
+ 'disable_empty' => 1,
+&>
+
<& /elements/tr-select-pkg_class.html,
'pre_options' => [ '' => 'all', '0' => '(empty class)' ],
'disable_empty' => 1,
diff --git a/httemplate/search/report_cust_main-zip.html b/httemplate/search/report_cust_main-zip.html
index 00cb9ed2c..8bad332a9 100644
--- a/httemplate/search/report_cust_main-zip.html
+++ b/httemplate/search/report_cust_main-zip.html
@@ -8,8 +8,8 @@
<TD ALIGN="right">Billing or service zip</TD>
<TD>
<SELECT NAME="column">
- <OPTION VALUE="zip">Billing zip
- <OPTION VALUE="ship_zip">Service zip
+ <OPTION VALUE="bill">Billing zip
+ <OPTION VALUE="ship">Service zip
</SELECT>
</TD>
</TR>
diff --git a/httemplate/search/report_cust_main.html b/httemplate/search/report_cust_main.html
index 39cf695d8..3e7181d4f 100755
--- a/httemplate/search/report_cust_main.html
+++ b/httemplate/search/report_cust_main.html
@@ -28,13 +28,19 @@
<& /elements/tr-select-part_referral.html,
'label' => emt('Advertising Source'),
'multiple' => 1,
- 'all_selected' => 1,
+ #no, causes customers with disabled ones to disappear
+ #'all_selected' => 1,
&>
<TR>
<TD ALIGN="right" VALIGN="center"><% mt('Address') |h %></TD>
<TD><INPUT TYPE="text" NAME="address" SIZE=54></TD>
</TR>
+
+ <TR>
+ <TD ALIGN="right" VALIGN="center"><% mt('Zip') |h %></TD>
+ <TD><INPUT TYPE="text" NAME="zip" SIZE=12></TD>
+ </TR>
<TR>
<TD ALIGN="right" VALIGN="center"><% mt('Signup date') |h %></TD>
@@ -76,6 +82,20 @@
</TR>
% }
+% if ( $conf->exists('cust_main-enable_anniversary_date') ) {
+ <TR>
+ <TD ALIGN="right" VALIGN="center"><% mt('Anniversary Date') |h %></TD>
+ <TD>
+ <TABLE>
+ <& /elements/tr-input-beginning_ending.html,
+ prefix => 'anniversary_date',
+ layout => 'horiz',
+ &>
+ </TABLE>
+ </TD>
+ </TR>
+% }
+
<& /elements/tr-select-cust_tag.html,
'cgi' => $cgi,
'is_report' => 1,
diff --git a/httemplate/search/report_quotation.html b/httemplate/search/report_quotation.html
new file mode 100644
index 000000000..1be904dc3
--- /dev/null
+++ b/httemplate/search/report_quotation.html
@@ -0,0 +1,75 @@
+<& /elements/header.html, mt($title, @title_arg) &>
+
+<FORM ACTION="quotation.html" METHOD="GET">
+<INPUT TYPE="hidden" NAME="magic" VALUE="_date">
+<INPUT TYPE="hidden" NAME="prospectnum" VALUE="<% $prospectnum %>">
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
+
+<TABLE BGCOLOR="#cccccc" CELLSPACING=0
+
+% unless ( $custnum ) {
+ <& /elements/tr-select-agent.html,
+ 'curr_value' => scalar( $cgi->param('agentnum') ),
+ 'label' => emt('Quotations for agent: '),
+ 'disable_empty' => 0,
+ &>
+% }
+
+ <& /elements/tr-input-beginning_ending.html &>
+
+ <& /elements/tr-input-lessthan_greaterthan.html,
+ label => emt('Setup'),
+ field => 'total_setup',
+ &>
+
+ <& /elements/tr-input-lessthan_greaterthan.html,
+ label => emt('Recurring'),
+ field => 'total_recur',
+ &>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="<% mt('Get Report') |h %>">
+
+</FORM>
+
+<& /elements/footer.html &>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List quotations');
+
+my $conf = new FS::Conf;
+
+my $title = 'Quotation Report';
+#false laziness w/report_cust_pkg.html
+my @title_arg = ();
+
+my $prospectnum = '';
+my $prospect_main = '';
+if ( $cgi->param('prospectnum') =~ /^(\d+)$/ ) {
+ $prospectnum = $1;
+ $prospect_main = qsearchs({
+ 'table' => 'prospect_main',
+ 'hashref' => { 'prospectnum' => $prospectnum },
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+ }) or die "unknown prospectnum $prospectnum";
+ $title .= ': [_1]';
+ push @title_arg, $prospect_main->name;
+}
+
+my $custnum = '';
+my $cust_main = '';
+if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+ $custnum = $1;
+ $cust_main = qsearchs({
+ 'table' => 'cust_main',
+ 'hashref' => { 'custnum' => $custnum },
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+ }) or die "unknown custnum $custnum";
+ $title .= ': [_1]';
+ push @title_arg, $cust_main->name;
+}
+
+</%init>
diff --git a/httemplate/search/report_rt_ticket.html b/httemplate/search/report_rt_ticket.html
index f0d7a4200..a4ceaa6a4 100644
--- a/httemplate/search/report_rt_ticket.html
+++ b/httemplate/search/report_rt_ticket.html
@@ -59,7 +59,6 @@ if ( @pkgparts ) {
}
# get a list of TimeValue-type custom fields
-RT::Init();
my $CurrentUser = RT::CurrentUser->new();
$CurrentUser->LoadByName($FS::CurrentUser::CurrentUser->username);
die "RT not configured" unless $CurrentUser->id;
diff --git a/httemplate/search/report_sqlradius_usage.html b/httemplate/search/report_sqlradius_usage.html
new file mode 100644
index 000000000..01215e834
--- /dev/null
+++ b/httemplate/search/report_sqlradius_usage.html
@@ -0,0 +1,40 @@
+<& /elements/header.html, mt($title) &>
+
+<FORM ACTION="sqlradius_usage.html" METHOD="GET">
+
+<TABLE BGCOLOR="#cccccc" CELLSPACING=0
+
+<& /elements/tr-select-agent.html,
+ 'empty_label' => 'all',
+&>
+
+% my @exporttypes = map { "'$_'" } qw(sqlradius broadband_sqlradius);
+<& /elements/tr-select-table.html,
+ 'label' => 'Export',
+ 'table' => 'part_export',
+ 'name_col' => 'label',
+ 'hashref' => {},
+ 'extra_sql' => ' WHERE exporttype IN('.join(',', @exporttypes).')',
+ 'disable_empty' => 1,
+ 'order_by' => 'ORDER BY exportnum',
+&>
+
+<& /elements/tr-input-beginning_ending.html &>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="<% mt('Get Report') |h %>">
+
+</FORM>
+
+<& /elements/footer.html &>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Usage: RADIUS sessions');
+ # yes?
+
+my $title = 'Data Usage Report';
+
+</%init>
diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi
index 2786f571b..42a52d154 100755
--- a/httemplate/search/report_tax.cgi
+++ b/httemplate/search/report_tax.cgi
@@ -60,9 +60,9 @@ as <A HREF="<% $p.'search/report_tax-xls.cgi?'.$cgi->query_string%>">Excel sprea
% my $link = '';
% if ( $region->{'label'} eq $out ) {
% $link = ';out=1';
-% } else {
-% $link = ';'. $region->{'url_param'}
-% if $region->{'url_param'};
+% } elsif ( $region->{'taxnums'} ) {
+% # might be nicer to specify this as country:state:city
+% $link = ';'.join(';', map { "taxnum=$_" } @{ $region->{'taxnums'} });
% }
%
% if ( $bgcolor eq $bgcolor1 ) {
@@ -71,15 +71,12 @@ as <A HREF="<% $p.'search/report_tax-xls.cgi?'.$cgi->query_string%>">Excel sprea
% $bgcolor = $bgcolor1;
% }
%
-% #my $diff = 0;
% my $hicolor = $bgcolor;
% unless ( $cgi->param('show_taxclasses') ) {
% my $diff = abs( sprintf( '%.2f', $region->{'owed'} )
% - sprintf( '%.2f', $region->{'tax'} )
% );
% if ( $diff > 0.02 ) {
-% # $hicolor = $hicolor eq '#eeeeee' ? '#eeee66' : '#ffff99';
-% #} elsif ( $diff ) {
% $hicolor = $hicolor eq '#eeeeee' ? '#eeee99' : '#ffffcc';
% }
% }
@@ -94,16 +91,19 @@ as <A HREF="<% $p.'search/report_tax-xls.cgi?'.$cgi->query_string%>">Excel sprea
<<%$td%>><% $region->{'label'} %></TD>
<<%$td%> ALIGN="right">
<A HREF="<% $baselink. $link %>;nottax=1"
- ><% &$money_sprintf( $region->{'total'} ) %></A>
+ ><% &$money_sprintf( $region->{'sales'} ) %></A>
</TD>
+% if ( $region->{'label'} eq $out ) {
+ <<%$td%> COLSPAN=12></TD>
+% } else { #not $out
<<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD>
<<%$td%> ALIGN="right">
- <A HREF="<% $baselink. $link %>;nottax=1;cust_tax=Y"
+ <A HREF="<% $baselink. $link %>;nottax=1;exempt_cust=Y"
><% &$money_sprintf( $region->{'exempt_cust'} ) %></A>
</TD>
<<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD>
<<%$td%> ALIGN="right">
- <A HREF="<% $baselink. $link %>;nottax=1;pkg_tax=Y"
+ <A HREF="<% $baselink. $link %>;nottax=1;exempt_pkg=Y"
><% &$money_sprintf( $region->{'exempt_pkg'} ) %></A>
</TD>
<<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD>
@@ -122,12 +122,24 @@ as <A HREF="<% $p.'search/report_tax-xls.cgi?'.$cgi->query_string%>">Excel sprea
<<%$tdh%> ALIGN="right">
<% &$money_sprintf( $region->{'owed'} ) %>
</TD>
-
-% unless ( $cgi->param('show_taxclasses') ) {
+% } # if !$out
+% unless ( $cgi->param('show_taxclasses') ) {
% my $invlink = $region->{'url_param_inv'}
% ? ';'. $region->{'url_param_inv'}
% : $link;
+% if ( $region->{'label'} eq $out ) {
+ <<%$td%> ALIGN="right">
+ <A HREF="<% $baselink. $invlink %>;istax=1"
+ ><% &$money_sprintf_nonzero( $region->{'tax'} ) %></A>
+ </TD>
+ <<%$td%>></TD>
+ <<%$td%> ALIGN="right">
+ <A HREF="<% $creditlink. $invlink %>;istax=1"
+ ><% &$money_sprintf_nonzero( $region->{'credit'} ) %></A>
+ </TD>
+ <<%$td%> COLSPAN=2></TD>
+% } else { #not $out
<<%$tdh%> ALIGN="right">
<A HREF="<% $baselink. $invlink %>;istax=1"
><% &$money_sprintf( $region->{'tax'} ) %></A>
@@ -141,7 +153,8 @@ as <A HREF="<% $p.'search/report_tax-xls.cgi?'.$cgi->query_string%>">Excel sprea
<<%$tdh%> ALIGN="right">
<% &$money_sprintf( $region->{'tax'} - $region->{'credit'} ) %>
</TD>
-% }
+% }
+% } # not $out
</TR>
% }
@@ -190,6 +203,18 @@ as <A HREF="<% $p.'search/report_tax-xls.cgi?'.$cgi->query_string%>">Excel sprea
<TR>
<<%$td%>><% $region->{'label'} %></TD>
+% if ( $region->{'label'} eq $out ) {
+ <<%$td%> ALIGN="right">
+ <A HREF="<% $baselink. $invlink %>;istax=1"
+ ><% &$money_sprintf_nonzero( $region->{'tax'} ) %></A>
+ </TD>
+ <<%$td%>></TD>
+ <<%$td%> ALIGN="right">
+ <A HREF="<% $creditlink. $invlink %>;istax=1"
+ ><% &$money_sprintf_nonzero( $region->{'credit'} ) %></A>
+ </TD>
+ <<%$td%> COLSPAN=2></TD>
+% } else { #not $out
<<%$td%> ALIGN="right">
<A HREF="<% $baselink. $link %>;istax=1"
><% &$money_sprintf( $region->{'tax'} ) %></A>
@@ -204,70 +229,52 @@ as <A HREF="<% $p.'search/report_tax-xls.cgi?'.$cgi->query_string%>">Excel sprea
<% &$money_sprintf( $region->{'tax'} - $region->{'credit'} ) %>
</TD>
</TR>
-
-% }
-
-% if ( $bgcolor eq $bgcolor1 ) {
-% $bgcolor = $bgcolor2;
-% } else {
-% $bgcolor = $bgcolor1;
-% }
-% my $td = qq(TD CLASS="grid" BGCOLOR="$bgcolor");
-
- <TR>
- <<%$td%>>Total</TD>
- <<%$td%> ALIGN="right">
- <A HREF="<% $baselink %>;istax=1"
- ><% &$money_sprintf( $tot_tax ) %></A>
- </TD>
- <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD>
- <<%$td%> ALIGN="right">
- <A HREF="<% $creditlink %>;istax=1"
- ><% &$money_sprintf( $tot_credit ) %></A>
- </TD>
- <<%$td%>><FONT SIZE="+1"><B> = </B></FONT></TD>
- <<%$td%> ALIGN="right">
- <% &$money_sprintf( $tot_tax - $tot_credit ) %>
- </TD>
- </TR>
+% } # if $out
+% } #foreach $region
</TABLE>
-% }
+% } # if show_taxclasses
<% include('/elements/footer.html') %>
<%init>
-my $DEBUG = $cgi->param('debug') || 0;
-
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+my $DEBUG = $cgi->param('debug') || 0;
+
my $conf = new FS::Conf;
-my $user = getotaker;
+my $out = 'Out of taxable region(s)';
+
+my %label_opt = ( out => 1 ); #enable 'Out of Taxable Region' label
+$label_opt{no_city} = 1 unless $cgi->param('show_cities');
+$label_opt{no_taxclass} = 1 unless $cgi->param('show_taxclasses');
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_cust_pkg = $join_cust.
' LEFT JOIN cust_pkg USING ( pkgnum )
- LEFT JOIN part_pkg USING ( pkgpart )
- LEFT JOIN cust_location
- ON ( cust_location.locationnum = ' .
- FS::cust_pkg->tax_locationnum_sql . ' )';
+ LEFT JOIN part_pkg USING ( pkgpart ) ';
my $from_join_cust_pkg = " FROM cust_bill_pkg $join_cust_pkg ";
-my $where = "WHERE _date >= $beginning AND _date <= $ending ";
+# either or both of these can be used to link cust_bill_pkg to cust_main_county
+my $pkg_tax = "SELECT SUM(amount) as tax_amount, invnum, taxnum, ".
+ "cust_bill_pkg_tax_location.pkgnum ".
+ "FROM cust_bill_pkg_tax_location JOIN cust_bill_pkg USING (billpkgnum) ".
+ "GROUP BY billpkgnum, invnum, taxnum, cust_bill_pkg_tax_location.pkgnum";
-# this query will be run once per cust_main_county,
-# or maybe once per country/state/city tuple,
-# or maybe once per country/state...it's hard to say.
-my ($location_sql, @base_param) = FS::cust_location->in_county_sql(param => 1);
-$where .= " AND $location_sql ";
+my $pkg_tax_exempt = "SELECT SUM(amount) AS exempt_charged, billpkgnum, taxnum ".
+ "FROM cust_tax_exempt_pkg EXEMPT_WHERE GROUP BY billpkgnum, taxnum";
+
+my $where = "WHERE _date >= $beginning AND _date <= $ending ";
+my $group = "GROUP BY cust_main_county.taxnum";
my $agentname = '';
if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
@@ -277,270 +284,188 @@ if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
$where .= ' AND cust_main.agentnum = '. $agent->agentnum;
}
-sub gotcust {
- my $table = shift;
- my $prefix = @_ ? shift : '';
- "
- ( $table.district = cust_main_county.district
- OR cust_main_county.district = ''
- OR cust_main_county.district IS NULL )
- AND ( $table.${prefix}city = cust_main_county.city
- OR cust_main_county.city = ''
- OR cust_main_county.city IS NULL )
- AND ( $table.${prefix}county = cust_main_county.county
- OR cust_main_county.county = ''
- OR cust_main_county.county IS NULL )
- AND ( $table.${prefix}state = cust_main_county.state
- OR cust_main_county.state = ''
- OR cust_main_county.state IS NULL )
- AND ( $table.${prefix}country = cust_main_county.country )
- ";
-}
-
-#non-parameterized form
-my $location_in_county = FS::cust_location->in_county_sql;
-my $gotcust = "WHERE EXISTS(
- SELECT 1 FROM cust_location WHERE $location_in_county AND disabled IS NULL
+my $nottax = 'cust_bill_pkg.pkgnum != 0';
+
+# one query for each column of the report
+# plus separate queries for the totals row
+my (%sql, %all_sql);
+
+# general form
+my $exempt = "SELECT cust_main_county.taxnum, SUM(exempt_charged)
+ FROM cust_main_county
+ JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
+ USING (taxnum)
+ JOIN cust_bill_pkg USING (billpkgnum)
+ $join_cust $where AND $nottax $group";
+
+my $all_exempt = "SELECT SUM(exempt_charged)
+ FROM cust_main_county
+ JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
+ USING (taxnum)
+ JOIN cust_bill_pkg USING (billpkgnum)
+ $join_cust $where AND $nottax";
+
+# sales to tax-exempt customers
+$sql{exempt_cust} = $exempt;
+$sql{exempt_cust} =~ s/EXEMPT_WHERE/WHERE exempt_cust = 'Y' OR exempt_cust_taxname = 'Y'/;
+$all_sql{exempt_cust} = $all_exempt;
+$all_sql{exempt_cust} =~ s/EXEMPT_WHERE/WHERE exempt_cust = 'Y' OR exempt_cust_taxname = 'Y'/;
+
+# sales of tax-exempt packages
+$sql{exempt_pkg} = $exempt;
+$sql{exempt_pkg} =~ s/EXEMPT_WHERE/WHERE exempt_setup = 'Y' OR exempt_recur = 'Y'/;
+$all_sql{exempt_pkg} = $all_exempt;
+$all_sql{exempt_pkg} =~ s/EXEMPT_WHERE/WHERE exempt_setup = 'Y' OR exempt_recur = 'Y'/;
+
+# monthly per-customer exemptions
+$sql{exempt_monthly} = $exempt;
+$sql{exempt_monthly} =~ s/EXEMPT_WHERE/WHERE exempt_monthly = 'Y'/;
+$all_sql{exempt_monthly} = $all_exempt;
+$all_sql{exempt_monthly} =~ s/EXEMPT_WHERE/WHERE exempt_monthly = 'Y'/;
+
+# taxable sales
+$sql{taxable} = "SELECT cust_main_county.taxnum,
+ SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(exempt_charged, 0))
+ FROM cust_main_county
+ JOIN ($pkg_tax) AS pkg_tax USING (taxnum)
+ JOIN cust_bill_pkg USING (invnum, pkgnum)
+ LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
+ ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum
+ AND pkg_tax_exempt.taxnum = cust_main_county.taxnum)
+ $join_cust $where AND $nottax $group";
+
+# Here we're going to sum all line items that are taxable _at all_,
+# under any tax. exempt_charged is the sum of all exemptions for a
+# particular billpkgnum + taxnum; we take the taxnum that has the
+# smallest sum of exemptions and subtract that from the charged amount.
+$all_sql{taxable} = "SELECT
+ SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(min_exempt, 0))
+ FROM cust_bill_pkg
+ JOIN (
+ SELECT invnum, pkgnum, MIN(exempt_charged) AS min_exempt
+ FROM ($pkg_tax) AS pkg_tax
+ JOIN cust_bill_pkg USING (invnum, pkgnum)
+ LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt USING (billpkgnum, taxnum)
+ GROUP BY invnum, pkgnum
+ ) AS pkg_is_taxable
+ USING (invnum, pkgnum)
+ $join_cust $where AND $nottax";
+ # we don't join pkg_tax_exempt.taxnum here, because
+
+$sql{taxable} =~ s/EXEMPT_WHERE//; # unrestricted
+$all_sql{taxable} =~ s/EXEMPT_WHERE//;
+
+# there isn't one for 'sales', because we calculate sales by adding up
+# the taxable and exempt columns.
+
+# sum of billed tax:
+# join cust_bill_pkg to cust_main_county via cust_bill_pkg_tax_location
+my $taxfrom = " FROM cust_bill_pkg
+ $join_cust
+ LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum )
+ LEFT JOIN cust_main_county USING ( taxnum )";
+
+my $istax = "cust_bill_pkg.pkgnum = 0";
+my $named_tax = "(
+ taxname = itemdesc
+ OR ( taxname IS NULL
+ AND ( itemdesc IS NULL OR itemdesc = '' OR itemdesc = 'Tax' )
+ )
)";
-my $out = 'Out of taxable region(s)';
-# these are actually tax labels, not regions
-my %regions = ();
-
-# Phase 1: Taxable and exempt sales
-# Collect for each cust_main_county, and assign to a bin based on label.
-# Note that "label" includes city if show_cities is on, and taxclass if
-# show_taxclasses is on.
-foreach my $r ( qsearch({ 'table' => 'cust_main_county',
- 'extra_sql' => $gotcust,
- 'debug' => $DEBUG,
- })
- )
-{
- warn $r->county. ' '. $r->state. ' '. $r->country. "\n" if $DEBUG > 1;
-
- # set up a %regions entry for this region's tax label
- my $label = getlabel($r);
- $regions{$label}->{'label'} = $label;
-
- $regions{$label}->{$_} = $r->$_() for (qw( county state country )); #taxname?
-
- my @url_param = qw( county state country taxname );
- push @url_param, 'city' if $cgi->param('show_cities') && $r->city();
-
- $regions{$label}->{'url_param'} =
- join(';', map "$_=".uri_escape($r->$_()), @url_param );
-
- my @param = @base_param;
- my $mywhere = $where;
-
- if ( $r->taxclass ) {
-
- $mywhere .= " AND taxclass = ? ";
- push @param, 'taxclass';
- $regions{$label}->{'url_param'} .= ';taxclass='. uri_escape($r->taxclass);
- #no, always# if $cgi->param('show_taxclasses');
-
- $regions{$label}->{'taxclass'} = $r->taxclass;
-
- } else {
-
- # SQL for "taxclass doesn't match any other tax in the region"
- my $same_sql = $r->sql_taxclass_sameregion;
- $mywhere .= " AND $same_sql" if $same_sql;
-
- $regions{$label}->{'url_param'} .= ';taxclassNULL=1'
- if $cgi->param('show_taxclasses')
- || $same_sql;
-
- }
-
- # FROM cust_bill_pkg JOIN (whatever is needed to determine tax location)
- # WHERE (matches tax location and agentnum and taxclass)
- # takes parameters in @base_param, plus taxclass if there is one
- my $fromwhere = "$from_join_cust_pkg $mywhere"; # AND payby != 'COMP' ";
-
- my $nottax = 'pkgnum != 0';
-
- ## calculate total of sales (non-tax line items) for this region
-
- my $t_sql =
- "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) $fromwhere AND $nottax";
- my $t = scalar_sql($r, \@param, $t_sql);
- $regions{$label}->{'total'} += $t;
-
- #$regions{$label}->{subtotals}->{$r->taxnum} = $t; #useful debug
-
- ## calculate customer-exemption for this region
-
- #false laziness -ish w/report_tax.cgi
- my $cust_exempt;
- if ( $r->taxname ) {
- my $q_taxname = dbh->quote($r->taxname);
- $cust_exempt =
- "( tax = 'Y'
- OR EXISTS ( SELECT 1 FROM cust_main_exemption
- WHERE cust_main_exemption.custnum = cust_main.custnum
- AND cust_main_exemption.taxname = $q_taxname
- )
- )
- ";
- } else {
- $cust_exempt = " tax = 'Y' ";
- }
-
- my $x_cust = scalar_sql($r, \@param,
- "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur)
- $fromwhere AND $nottax AND $cust_exempt "
- );
-
- $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 )
- "
- );
- $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_pkg
- $mywhere"
- );
- $regions{$label}->{'exempt_monthly'} += $x_monthly;
-
- my $taxable = $t - $x_cust - $x_pkg - $x_monthly;
- $regions{$label}->{'taxable'} += $taxable;
-
- $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.'%';
- }
+$sql{tax} = "SELECT cust_main_county.taxnum,
+ SUM(cust_bill_pkg_tax_location.amount)
+ $taxfrom
+ $where AND $istax AND $named_tax
+ $group";
+
+$all_sql{tax} = "SELECT SUM(cust_bill_pkg.setup)
+ FROM cust_bill_pkg
+ $join_cust
+ $where AND $istax";
+
+# sum of credits applied against billed tax
+my $creditfrom = $taxfrom .
+ ' JOIN cust_credit_bill_pkg USING (billpkgtaxlocationnum)';
+my $creditfromwhere = $where .
+ ' AND billpkgtaxratelocationnum IS NULL';
+
+$sql{credit} = "SELECT cust_main_county.taxnum,
+ SUM(cust_credit_bill_pkg.amount)
+ $creditfrom
+ $creditfromwhere AND $istax AND $named_tax
+ $group";
+
+$all_sql{credit} = "SELECT SUM(cust_credit_bill_pkg.amount)
+ FROM cust_credit_bill_pkg
+ JOIN cust_bill_pkg USING (billpkgnum)
+ $join_cust
+ $where AND $istax";
+
+my %data;
+my %total = (owed => 0);
+foreach my $k (keys(%sql)) {
+ my $stmt = $sql{$k};
+ warn "\n".uc($k).":\n".$stmt."\n" if $DEBUG;
+ my $sth = dbh->prepare($stmt);
+ # two columns => key/value
+ $sth->execute
+ or die "failed to execute $k query: ".$sth->errstr;
+ $data{$k} = +{ map { @$_ } @{ $sth->fetchall_arrayref([]) } };
+
+ warn "\n".$all_sql{$k}."\n" if $DEBUG;
+ $total{$k} = FS::Record->scalar_sql( $all_sql{$k} );
+ warn Dumper($data{$k}) if $DEBUG > 1;
}
-warn Dumper(\%regions) if $DEBUG > 1;
-# $regions{$label} now contains 'total', 'exempt_cust', 'exempt_pkg',
-# 'exempt_monthly', summed over each set of regions with the same label.
-
-my $distinct = "country, state, county, city, district,
- CASE WHEN taxname IS NULL THEN '' ELSE taxname END AS taxname";
-my $taxclass_distinct =
- #a little bit unsure of this part... test?
- #ah, it looks like it winds up being irrelevant as ->{'tax'}
- # from $regions is not displayed when show_taxclasses is on
- ( $cgi->param('show_taxclasses')
- ? " CASE WHEN taxclass IS NULL THEN '' ELSE taxclass END "
- : " '' "
- )." AS taxclass";
-
-
-# Phase 2: invoiced/credited tax items
-# Collect this data for each country/state/city/district/taxname(/taxclass).
-my %qsearch = (
- 'select' => "DISTINCT $distinct, $taxclass_distinct",
- 'table' => 'cust_main_county',
- 'hashref' => {},
- 'extra_sql' => $gotcust,
- 'debug' => $DEBUG,
+# so $data{tax}, for example, is now a hash with one entry
+# for each taxnum, containing the tax billed on that taxnum.
+
+# oddball cases:
+# "out of taxable region" sales
+my %out;
+my $out_sales_sql =
+ "SELECT SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)
+ FROM (cust_bill_pkg $join_cust)
+ LEFT JOIN ($pkg_tax) AS pkg_tax USING (invnum, pkgnum)
+ LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt USING (billpkgnum)
+ $where AND $nottax
+ AND pkg_tax.taxnum IS NULL AND pkg_tax_exempt.taxnum IS NULL"
+;
+
+$out_sales_sql =~ s/EXEMPT_WHERE//;
+
+$out{sales} = FS::Record->scalar_sql($out_sales_sql);
+
+# unlinked tax collected (for diagnostics)
+my $out_tax_sql =
+ "SELECT SUM(cust_bill_pkg.setup)
+ FROM (cust_bill_pkg $join_cust)
+ LEFT JOIN cust_bill_pkg_tax_location USING (billpkgnum)
+ $where AND $istax AND cust_bill_pkg_tax_location.billpkgnum IS NULL"
+;
+$out{tax} = FS::Record->scalar_sql($out_tax_sql);
+# unlinked tax credited (for diagnostics)
+my $out_credit_sql =
+ "SELECT SUM(cust_credit_bill_pkg.amount)
+ FROM cust_credit_bill_pkg
+ JOIN cust_bill_pkg USING (billpkgnum)
+ $join_cust
+ $where AND $istax AND cust_credit_bill_pkg.billpkgtaxlocationnum IS NULL"
+;
+$out{credit} = FS::Record->scalar_sql($out_credit_sql);
+
+# all sales
+$total{sales} = FS::Record->scalar_sql(
+ "SELECT SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)
+ FROM cust_bill_pkg $join_cust $where AND $nottax"
);
-# Join to cust_main the same as before (we need agentnum)
-# but not to cust_pkg (because tax line items don't have a package)
-# and then to cust_location via cust_bill_pkg_tax_location
-my $taxfromwhere = "FROM cust_bill_pkg $join_cust
- LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum )
- LEFT JOIN cust_location USING ( locationnum )
- ";
-my $taxwhere = $where;
-
-my $creditfromwhere = $taxfromwhere.
- " JOIN cust_credit_bill_pkg USING (billpkgnum, billpkgtaxlocationnum)";
-
-$taxfromwhere .= " $taxwhere "; #AND payby != 'COMP' ";
-$creditfromwhere .= " $taxwhere AND billpkgtaxratelocationnum IS NULL"; #AND payby != 'COMP' ";
-
-#should i be a cust_main_county method or something
-# yes. yes, you should.
-
-# $taxfromwhere: Most of a query to find cust_bill_pkg records linked to a
-# customer matching a given state/county/city/district (and within the date
-# range for the report).
-# @base_param: A list of the fields from cust_main_county to use as parameters.
-
-# $_taxamount_sub: Takes a cust_main_county and returns the sum of taxes billed
-# within the report period for all customers located in that county. If
-# the cust_main_county has a taxname, limits to taxes with that name; otherwise
-# includes all line items with pkgnum = 0 and description either 'Tax' or empty.
-
-my $_taxamount_sub = sub {
- my $r = shift;
-
- #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 $sql = "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) ".
- " $taxfromwhere AND cust_bill_pkg.pkgnum = 0 $named_tax";
-
- scalar_sql($r, [ @base_param ], $sql );
-};
-
-# $_creditamount_sub: As above, but returns the sum of credits applied
-
-my $_creditamount_sub = sub {
- my $r = shift;
-
- #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 $sql = "SELECT SUM(cust_credit_bill_pkg.amount) ".
- " $creditfromwhere AND cust_bill_pkg.pkgnum = 0 $named_tax";
-
- scalar_sql($r, [ @base_param ], $sql );
-};
-
#tax-report_groups filtering
my($group_op, $group_value) = ( '', '' );
if ( $cgi->param('report_group') =~ /^(=|!=) (.*)$/ ) {
( $group_op, $group_value ) = ( $1, $2 );
}
-my $group_test = sub {
+my $group_test = sub { # to be applied to a tax label
my $label = shift;
return 1 unless $group_op; #in case we get called inadvertantly
if ( $label eq $out ) { #don't display "out of taxable region" in this case
@@ -554,90 +479,83 @@ my $group_test = sub {
}
};
+# if show_taxclasses is on, %base_regions will contain the same data
+# as %regions, but with taxclasses merged together (and ignoring report_group
+# filtering).
+my (%regions, %base_regions);
my $tot_tax = 0;
my $tot_credit = 0;
-#foreach my $label ( keys %regions ) {
-foreach my $r ( qsearch(\%qsearch) ) {
- #warn join('-', map { $r->$_() } qw( country state county taxname ) )."\n";
+my @loc_params = qw(country state county);
+push @loc_params, qw(city district) if $cgi->param('show_cities');
- my $label = getlabel($r);
- if ( $group_op ) {
- next unless &{$group_test}($label);
+foreach my $r ( qsearch({ 'table' => 'cust_main_county', })) {
+ my $taxnum = $r->taxnum;
+ # set up a %regions entry for this region's tax label
+ my $label = $r->label(%label_opt);
+ next if $label eq $out;
+ $regions{$label} ||= { label => $label };
+
+ $regions{$label}->{$_} = $r->get($_) foreach @loc_params;
+ $regions{$label}->{taxnums} ||= [];
+ push @{ $regions{$label}->{taxnums} }, $r->taxnum;
+
+ my %x; # keys are data items (like 'tax', 'exempt_cust', etc.)
+ foreach my $k (keys %data) {
+ next unless exists($data{$k}->{$taxnum});
+ $x{$k} = $data{$k}->{$taxnum};
+ $regions{$label}->{$k} += $x{$k};
+ if ( $k eq 'taxable' or $k =~ /^exempt/ ) {
+ $regions{$label}->{'sales'} += $x{$k};
+ }
}
- #my $fromwhere = $join_pkg. $where. " AND payby != 'COMP' ";
- #my @param = @base_param;
+ my $owed = $data{'taxable'}->{$taxnum} * ($r->tax/100);
+ $regions{$label}->{'owed'} += $owed;
+ $total{'owed'} += $owed;
- my $x = &{$_taxamount_sub}($r);
-
- $regions{$label}->{'tax'} += $x;
- $tot_tax += $x unless $cgi->param('show_taxclasses');
-
- ## calculate credit for this region
-
- $x = &{$_creditamount_sub}($r);
-
- $regions{$label}->{'credit'} += $x;
- $tot_credit += $x unless $cgi->param('show_taxclasses');
-
-}
-
-# Phase 3: Non-taxclassed totals for invoiced/credited tax
-# (If show_taxclasses is not in use, this was phase 2, but it
-# displays somewhere different.)
-# Don't filter by report_groups.
-my %base_regions = ();
-if ( $cgi->param('show_taxclasses') ) {
-
- $qsearch{'select'} = "DISTINCT $distinct";
- foreach my $r ( qsearch(\%qsearch) ) {
-
- my $x = &{$_taxamount_sub}($r);
-
- my $base_label = getlabel($r, 'no_taxclass'=>1 );
- $base_regions{$base_label}->{'label'} = $base_label;
-
- $base_regions{$base_label}->{'url_param'} =
- join(';', map "$_=". uri_escape($r->$_()),
- qw( county state country taxname )
- );
-
- $base_regions{$base_label}->{'tax'} += $x;
- $tot_tax += $x;
-
- ## calculate credit for this region
-
- $x = &{$_creditamount_sub}($r);
-
- $base_regions{$base_label}->{'credit'} += $x;
- $tot_credit += $x;
+ if ( defined($regions{$label}->{'rate'})
+ && $regions{$label}->{'rate'} != $r->tax.'%' ) {
+ $regions{$label}->{'rate'} = 'variable';
+ } else {
+ $regions{$label}->{'rate'} = $r->tax.'%';
+ }
+ if ( $cgi->param('show_taxclasses') ) {
+ my $base_label = $r->label(%label_opt, 'no_taxclass' => 1);
+ $base_regions{$base_label} ||=
+ {
+ label => $base_label,
+ tax => 0,
+ credit => 0,
+ };
+ $base_regions{$base_label}->{tax} += $x{tax};
+ $base_regions{$base_label}->{credit} += $x{credit};
}
}
-my @regions = keys %regions;
+my @regions = map { $_->{label} }
+ sort {
+ ($b eq $out) <=> ($a eq $out)
+ or $a->{country} cmp $b->{country}
+ or $a->{state} cmp $b->{state}
+ or $a->{county} cmp $b->{county}
+ or $a->{city} cmp $b->{city}
+ }
+ grep { $_->{sales} > 0 or $_->{tax} > 0 or $_->{credit} > 0 }
+ values %regions;
#tax-report_groups filtering
@regions = grep &{$group_test}($_), @regions
if $group_op;
#calculate totals
-my( $total, $tot_taxable, $tot_owed ) = ( 0, 0, 0 );
-my( $exempt_cust, $exempt_pkg, $exempt_monthly, $tot_credit ) = ( 0, 0, 0, 0 );
my %taxclasses = ();
my %county = ();
my %state = ();
my %country = ();
-foreach (@regions) {
- $total += $regions{$_}->{'total'};
- $tot_taxable += $regions{$_}->{'taxable'};
- $tot_owed += $regions{$_}->{'owed'};
- $exempt_cust += $regions{$_}->{'exempt_cust'};
- $exempt_pkg += $regions{$_}->{'exempt_pkg'};
- $exempt_monthly += $regions{$_}->{'exempt_monthly'};
- $tot_credit += $regions{$_}->{'credit'};
+foreach my $label (@regions) {
$taxclasses{$regions{$_}->{'taxclass'}} = 1
if $regions{$_}->{'taxclass'};
$county{$regions{$_}->{'county'}} = 1;
@@ -672,29 +590,27 @@ if ( $group_op ) {
#ordering
@regions =
map $regions{$_},
- sort { ( ($a eq $out) cmp ($b eq $out) ) || ($b cmp $a) }
+ sort { $a cmp $b }
@regions;
my @base_regions =
map $base_regions{$_},
- sort { ( ($a eq $out) cmp ($b eq $out) ) || ($b cmp $a) }
+ sort { $a cmp $b }
keys %base_regions;
-#add total line
-push @regions, {
- 'label' => 'Total',
- 'url_param' => $total_url_param,
- 'url_param_inv' => $total_url_param_invoiced,
- 'total' => $total,
- 'exempt_cust' => $exempt_cust,
- 'exempt_pkg' => $exempt_pkg,
- 'exempt_monthly' => $exempt_monthly,
- 'taxable' => $tot_taxable,
- 'rate' => '',
- 'owed' => $tot_owed,
- 'tax' => $tot_tax,
- 'credit' => $tot_credit,
-};
+#add "Out of taxable" and total lines
+%out = ( %out,
+ 'label' => $out,
+ 'rate' => ''
+);
+%total = ( %total,
+ 'label' => 'Total',
+ 'url_param' => $total_url_param,
+ 'url_param_inv' => $total_url_param_invoiced,
+ 'rate' => '',
+);
+push @regions, \%out, \%total;
+push @base_regions, \%out, \%total;
#--
@@ -702,69 +618,15 @@ my $money_char = $conf->config('money_char') || '$';
my $money_sprintf = sub {
$money_char. sprintf('%.2f', shift );
};
-
-sub getlabel {
- my $r = shift;
- my %opt = @_;
-
- my $label;
- if (
- $r->tax == 0
- && ! scalar( qsearch('cust_main_county', { 'district'=> $r->district,
- 'city' => $r->city,
- 'county' => $r->county,
- 'state' => $r->state,
- 'country' => $r->country,
- 'tax' => { op=>'>', value=>0 },
- }
- )
- )
-
- ) {
- #kludge to avoid "will not stay shared" warning
- my $out = 'Out of taxable region(s)';
- $label = $out;
- } else {
- $label = $r->country;
- $label = $r->state.", $label" if $r->state;
- $label = $r->county." county, $label" if $r->county;
- $label = $r->city. ", $label" if $r->city && $cgi->param('show_cities');
- $label = "$label (". $r->taxclass. ")"
- if $r->taxclass
- && $cgi->param('show_taxclasses')
- && ! $opt{'no_taxclass'};
- $label = $r->taxname. " ($label)" if $r->taxname;
- }
- return $label;
-}
-
-#my %count_taxname = (); #cache
-#sub count_taxname {
-# my $taxname = shift;
-# return $count_taxname{$taxname} if exists $count_taxname{$taxname};
-# my $sql = 'SELECT COUNT(*) FROM cust_main_county WHERE taxname = ?';
-# my $sth = dbh->prepare($sql) or die dbh->errstr;
-# $sth->execute( $taxname )
-# or die "Unexpected error executing statement $sql: ". $sth->errstr;
-# $count_taxname{$taxname} = $sth->fetchrow_arrayref->[0];
-#}
-
-#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 $money_sprintf_nonzero = sub {
+ $_[0] == 0 ? '' : &$money_sprintf($_[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";
-my $creditlink = $p. "search/cust_credit_bill_pkg.html?$dateagentlink";
+my $creditlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;credit=1";
</%init>
diff --git a/httemplate/search/sqlradius_usage.html b/httemplate/search/sqlradius_usage.html
new file mode 100644
index 000000000..29ef4c0e8
--- /dev/null
+++ b/httemplate/search/sqlradius_usage.html
@@ -0,0 +1,201 @@
+% if ( @include_agents ) {
+% # jumbo report
+<& /elements/header.html, $title &>
+% foreach my $agent ( @include_agents ) {
+% $cgi->param('agentnum', $agent->agentnum); #for download links
+<DIV WIDTH="100%" STYLE="page-break-after: always">
+<FONT SIZE=6><% $agent->agent %></FONT><BR><BR>
+ <& sqlradius_usage.html,
+ export => $export,
+ agentnum => $agent->agentnum,
+ nohtmlheader => 1,
+ usage_by_username => \%usage_by_username,
+ download_label => 'Download this section',
+ &>
+</DIV>
+<BR><BR>
+% }
+<& /elements/footer.html &>
+% } else {
+<& elements/search.html,
+ 'title' => $title,
+ 'name' => 'services',
+ 'query' => $sql_query,
+ 'count_query' => $sql_query->{'count_query'},
+ 'header' => [ #FS::UI::Web::cust_header(),
+ '#',
+ 'Customer',
+ 'Package',
+ @svc_header,
+ 'Upload (GB)',
+ 'Download (GB)',
+ 'Total (GB)',
+ ],
+ 'footer' => \@footer,
+ 'fields' => [ #\&FS::UI::Web::cust_fields,
+ 'display_custnum',
+ 'name',
+ 'pkg',
+ @svc_fields,
+ @svc_usage,
+ ],
+ 'links' => [ #( map { $_ ne 'Cust. Status' ? $link_cust : '' }
+ # FS::UI::Web::cust_header() ),
+ $link_cust,
+ $link_cust,
+ '', #package
+ ( map { $link_svc } @svc_header ),
+ '',
+ '',
+ '',
+ ],
+ 'align' => #FS::UI::Web::cust_aligns() .
+ 'rlc' . ('l' x scalar(@svc_header)) . 'rrr' ,
+ 'nohtmlheader' => ($opt{'nohtmlheader'} || 0),
+ 'download_label' => $opt{'download_label'},
+&>
+% }
+<%init>
+
+my %opt = @_;
+
+die "access denied" unless
+ $FS::CurrentUser::CurrentUser->access_right('List services');
+
+my $title = 'Data Usage Report - ';
+my $agentnum;
+my @include_agents;
+
+if ( $opt{'agentnum'} ) {
+ $agentnum = $opt{'agentnum'};
+} elsif ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+}
+
+if ( $agentnum ) {
+ my $agent = FS::agent->by_key($agentnum);
+ $title = $agent->agent." $title";
+} else {
+ @include_agents = qsearch('agent', {});
+}
+
+# usage query params
+my( $beginning, $ending ) = FS::UI::Web::parse_beginning_ending($cgi);
+
+if ( $beginning ) {
+ $title .= time2str('%h %o %Y ', $beginning);
+}
+$title .= 'through ';
+if ( $ending == 4294967295 ) {
+ $title .= 'now';
+} else {
+ $title .= time2str('%h %o %Y', $ending);
+}
+
+my $export;
+my %usage_by_username;
+if ( exists($opt{usage_by_username}) ) {
+ # There's no agent separation in the radacct data. So in the jumbo report
+ # do this procedure once, and pass the hash into all the per-agent sections.
+ %usage_by_username = %{ $opt{usage_by_username} };
+ $export = $opt{export};
+} else {
+
+ $cgi->param('exportnum') =~ /^(\d+)$/
+ or die "illegal export: '".$cgi->param('exportnum')."'";
+ $export = FS::part_export->by_key($1)
+ or die "exportnum $1 not found";
+ $export->exporttype =~ /sqlradius/
+ or die "exportnum ".$export->exportnum." is type ".$export->exporttype.
+ ", not sqlradius";
+
+ my $usage = $export->usage_sessions( {
+ stoptime_start => $beginning,
+ stoptime_end => $ending,
+ summarize => 1
+ } );
+ # arrayref of hashrefs of
+ # (username, acctsessiontime, acctinputoctets, acctoutputoctets)
+ # (XXX needs to include 'realm' for sqlradius_withdomain)
+ # rearrange to be indexed by username.
+
+ foreach (@$usage) {
+ my $username = $_->{'username'};
+ my @row = (
+ $_->{'acctinputoctets'},
+ $_->{'acctoutputoctets'},
+ $_->{'acctinputoctets'} + $_->{'acctoutputoctets'}
+ );
+ $usage_by_username{$username} = \@row;
+ }
+}
+
+#warn Dumper(\%usage_by_username);
+my @total_usage = (0, 0, 0, 0); # session time, input, output, input + output
+my @svc_usage = map {
+ my $i = $_;
+ sub {
+ my $username = $export->export_username(shift);
+ return '' if !exists($usage_by_username{$username});
+ my $value = $usage_by_username{ $username }->[$i];
+ $total_usage[$i] += $value;
+ # for now, always show in GB, rounded to 3 digits
+ bytes_to_gb($value);
+ }
+} (0,1,2);
+
+# set up svcdb-specific stuff
+my $export_username = sub {
+ $export->export_username(shift); # countrycode + phone, formatted MAC, etc.
+};
+
+my %svc_header = (
+ svc_acct => [ 'Username' ],
+ svc_broadband => [ 'MAC address', 'IP address' ],
+# svc_phone => [ 'Phone' ], #not yet supported, no search method
+ # (not sure input/output octets is relevant)
+);
+my %svc_fields = (
+ svc_acct => [ $export_username ],
+ svc_broadband => [ $export_username, 'ip_addr' ],
+# svc_phone => [ $export_username ],
+);
+
+# what kind of service we're operating on
+my $svcdb = FS::part_export::export_info()->{$export->exporttype}->{'svc'};
+my $class = "FS::$svcdb";
+my @svc_header = @{ $svc_header{$svcdb} };
+my @svc_fields = @{ $svc_fields{$svcdb} };
+
+# svc_x search params
+my %search_hash = ( 'agentnum' => $agentnum,
+ 'exportnum' => $export->exportnum );
+
+my $sql_query = $class->search(\%search_hash);
+$sql_query->{'select'} .= ', part_pkg.pkg';
+$sql_query->{'addl_from'} .= ' LEFT JOIN part_pkg USING (pkgpart)';
+
+my $link_svc = [ $p.'view/cust_svc.cgi?', 'svcnum' ];
+
+my $link_cust = [ $p.'view/cust_main.cgi?', 'custnum' ];
+
+# columns between the customer name and the usage fields
+my $skip_cols = 1 + scalar(@svc_header);
+
+my @footer = (
+ '',
+ FS::Record->scalar_sql($sql_query->{count_query}) . ' services',
+ ('') x $skip_cols,
+ map {
+ my $i = $_;
+ sub { # defer this until the rows have been processed
+ bytes_to_gb($total_usage[$i])
+ }
+ } (0,1,2)
+);
+
+sub bytes_to_gb {
+ $_[0] ? sprintf('%.3f', $_[0] / (1024*1024*1024.0)) : '';
+}
+
+</%init>
diff --git a/httemplate/view/cust_bill.cgi b/httemplate/view/cust_bill.cgi
index a8b4ac15c..95ce60b1d 100755
--- a/httemplate/view/cust_bill.cgi
+++ b/httemplate/view/cust_bill.cgi
@@ -166,8 +166,6 @@ die "Invoice #$invnum not found!" unless $cust_bill;
my $custnum = $cust_bill->custnum;
my $display_custnum = $cust_bill->cust_main->display_custnum;
-#my $printed = $cust_bill->printed;
-
my $link = "invnum=$invnum";
$link .= ';template='. uri_escape($template) if $template;
$link .= ';notice_name='. $notice_name if $notice_name;
diff --git a/httemplate/view/cust_bill_void.html b/httemplate/view/cust_bill_void.html
new file mode 100755
index 000000000..2c526747b
--- /dev/null
+++ b/httemplate/view/cust_bill_void.html
@@ -0,0 +1,79 @@
+<& /elements/header.html, mt('Voided Invoice'), menubar(
+ emt("View this customer (#[_1])",$display_custnum) => "${p}view/cust_main.cgi?$custnum",
+) &>
+
+<SCRIPT TYPE="text/javascript">
+function areyousure(href, message) {
+ if (confirm(message) == true)
+ window.location.href = href;
+}
+</SCRIPT>
+<% areyousure_link("${p}misc/unvoid-cust_bill_void.html?invnum=". $cust_bill_void->invnum,
+ emt('Are you sure you want to unvoid this invoice?'),
+ emt('Unvoid this invoice'), #tooltip
+ emt('Unvoid this invoice') #link
+ )
+%>
+<BR><BR>
+
+% #voided PDFs?
+% #if ( $conf->exists('invoice_latex') ) {
+%#
+%# <A HREF="<% $p %>view/cust_bill-pdf.cgi?<% $link %>"><% mt('View typeset invoice PDF') |h %></A>
+%# <BR><BR>
+% #}
+
+%#something very big and obvious showing its voided...
+<DIV STYLE="color:#FF0000; font-size:1000%; font-weight:bold; z-index:100;
+ position: absolute; top: 300px; left: 130px;
+ zoom: 1; filter: alpha(opacity=25); opacity: 0.25;
+">VOID</DIV>
+
+% if ( $conf->exists('invoice_html') ) {
+ <% join('', $cust_bill_void->print_html(\%opt) ) %>
+% } else {
+ <PRE><% join('', $cust_bill_void->print_text(\%opt) ) %></PRE>
+% }
+
+<& /elements/footer.html &>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('View invoices');
+
+my $invnum;
+my($query) = $cgi->keywords;
+if ( $query =~ /^(\d+)$/ ) {
+ $invnum = $1;
+} else {
+ $invnum = $cgi->param('invnum');
+}
+
+my $conf = new FS::Conf;
+
+my %opt = (
+ 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
+);
+
+my $cust_bill_void = qsearchs({
+ 'select' => 'cust_bill_void.*',
+ 'table' => 'cust_bill_void',
+ #'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => { 'invnum' => $invnum },
+ #'extra_sql' => ' AND '. $curuser->agentnums_sql,
+});
+die "Voided invoice #$invnum not found!" unless $cust_bill_void;
+
+my $custnum = $cust_bill_void->custnum;
+my $display_custnum = $cust_bill_void->cust_main->display_custnum;
+
+#my $link = "invnum=$invnum";
+
+sub areyousure_link {
+ my ($url,$msg,$title,$label) = (shift,shift,shift,shift);
+ '<A HREF="javascript:areyousure(\''.$url.'\',\''.$msg.'\')" TITLE="'.$title.'">'.$label.'</A>';
+}
+
+</%init>
diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html
index b2a0efdef..5c46803d2 100644
--- a/httemplate/view/cust_main/billing.html
+++ b/httemplate/view/cust_main/billing.html
@@ -23,6 +23,14 @@
<TD BGCOLOR="#ffffff"><B><% $balance %></B></TD>
</TR>
+% if ( $conf->exists('cust_main-select-prorate_day') ) {
+<TR>
+ <TD ALIGN="right"><% mt('Prorate day of month') |h %></TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->prorate_day %>
+ </TD>
+</TR>
+% }
+
% if ( $conf->exists('cust_main-select-billday')
% && ($cust_main->payby eq 'CARD' || $cust_main->payby eq 'CHEK') ) {
<TR>
diff --git a/httemplate/view/cust_main/contacts.html b/httemplate/view/cust_main/contacts.html
index 9c6069182..d65af669a 100644
--- a/httemplate/view/cust_main/contacts.html
+++ b/httemplate/view/cust_main/contacts.html
@@ -1,13 +1,18 @@
% my %addr_label = ('bill' => 'Billing address', 'ship' => 'Service address');
%# Locations (possibly break this out)
-% my @which = ('bill');
-% push @which, 'ship' if $cust_main->has_ship_address;
+% my @which = ('bill', 'ship');
% while (@which) {
% my $this = shift @which;
% my $method = $this.'_location';
% my $location = $cust_main->$method;
-<FONT CLASS="fsinnerbox-title"><% mt( $addr_label{$this} ) |h %></FONT>
+<FONT CLASS="fsinnerbox-title"><% mt( $addr_label{$this} ) |h %>
+% if ( $this eq 'ship' and
+% $cust_main->bill_locationnum == $cust_main->ship_locationnum )
+% {
+ (<% mt('same as billing') %>)
+% }
+</FONT>
<TABLE CLASS="fsinnerbox">
% if ( $this eq 'bill' ) {
diff --git a/httemplate/view/cust_main/custom_content/.birthdate.html.swp b/httemplate/view/cust_main/custom_content/.birthdate.html.swp
deleted file mode 100644
index 9571d22cf..000000000
--- a/httemplate/view/cust_main/custom_content/.birthdate.html.swp
+++ /dev/null
Binary files differ
diff --git a/httemplate/view/cust_main/custom_content/.small_custview.html.swp b/httemplate/view/cust_main/custom_content/.small_custview.html.swp
deleted file mode 100644
index a39f52dde..000000000
--- a/httemplate/view/cust_main/custom_content/.small_custview.html.swp
+++ /dev/null
Binary files differ
diff --git a/httemplate/view/cust_main/custom_content/.spouse_birthdate.html.swp b/httemplate/view/cust_main/custom_content/.spouse_birthdate.html.swp
deleted file mode 100644
index 0042012f7..000000000
--- a/httemplate/view/cust_main/custom_content/.spouse_birthdate.html.swp
+++ /dev/null
Binary files differ
diff --git a/httemplate/view/cust_main/custom_content/.svc_Common.html.swp b/httemplate/view/cust_main/custom_content/.svc_Common.html.swp
deleted file mode 100644
index 15591b96d..000000000
--- a/httemplate/view/cust_main/custom_content/.svc_Common.html.swp
+++ /dev/null
Binary files differ
diff --git a/httemplate/view/cust_main/custom_content/.svc_acct.html.swp b/httemplate/view/cust_main/custom_content/.svc_acct.html.swp
deleted file mode 100644
index e2db6d5d1..000000000
--- a/httemplate/view/cust_main/custom_content/.svc_acct.html.swp
+++ /dev/null
Binary files differ
diff --git a/httemplate/view/cust_main/custom_content/.svc_hardware.html.swp b/httemplate/view/cust_main/custom_content/.svc_hardware.html.swp
deleted file mode 100644
index 1106f9ed5..000000000
--- a/httemplate/view/cust_main/custom_content/.svc_hardware.html.swp
+++ /dev/null
Binary files differ
diff --git a/httemplate/view/cust_main/custom_content/.svc_phone.html.swp b/httemplate/view/cust_main/custom_content/.svc_phone.html.swp
deleted file mode 100644
index 79b8185e1..000000000
--- a/httemplate/view/cust_main/custom_content/.svc_phone.html.swp
+++ /dev/null
Binary files differ
diff --git a/httemplate/view/cust_main/misc.html b/httemplate/view/cust_main/misc.html
index a0ab403e8..263c266e7 100644
--- a/httemplate/view/cust_main/misc.html
+++ b/httemplate/view/cust_main/misc.html
@@ -102,6 +102,26 @@
<TD BGCOLOR="#ffffff"><% $cust_main->signupdate ? time2str($date_format, $cust_main->signupdate) : '' %></TD>
</TR>
+% my $id_country = $conf->config('national_id-country');
+% if ( $id_country ) {
+% if ( $id_country eq 'MY' ) {
+ <TR>
+% my($old, $nric) = ( '', '');
+% if ( $cust_main->national_id =~ /^\d{6}\-\d{2}\-\d{4}$/ ) {
+ <TD ALIGN="right"><% mt('NRIC') |h %></TD>
+% } else { # elsif ( $cust_main->national_id =~ /^\w\d{9}$/ ) {
+ <TD ALIGN="right"><% mt('Old IC/Passport') |h %></TD>
+% #} else {
+% # warn "unknown national_id format";
+%# <TD ALIGN="right"></TD>
+% }
+ <TD BGCOLOR="#ffffff"><% $cust_main->national_id |h %></TD>
+ </TR>
+% } else {
+% warn "unknown national_id-country $id_country";
+% }
+% }
+
% if ( $conf->exists('cust_main-enable_birthdate') ) {
% my $dt = $cust_main->birthdate ne ''
% ? DateTime->from_epoch( 'epoch' => $cust_main->birthdate,
@@ -130,6 +150,20 @@
% }
+% if ( $conf->exists('cust_main-enable_anniversary_date') ) {
+% my $dt = $cust_main->anniversary_date ne ''
+% ? DateTime->from_epoch( 'epoch' => $cust_main->anniversary_date,
+% 'time_zone' =>'floating',
+% )
+% : '';
+
+ <TR>
+ <TD ALIGN="right"><% mt('Anniversary Date') |h %></TD>
+ <TD BGCOLOR="#ffffff"><% $dt ? $dt->strftime($date_format) : '' %></TD>
+ </TR>
+
+% }
+
% if ( $conf->exists('cust_main-require_censustract') ) {
<TR>
diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html
index 9e08c0c5d..166addbf4 100644
--- a/httemplate/view/cust_main/payment_history.html
+++ b/httemplate/view/cust_main/payment_history.html
@@ -277,7 +277,9 @@
% ? sprintf("$money_char\%.2f", $item->{'charge'})
% : exists($item->{'charge_nobal'})
% ? sprintf("$money_char\%.2f", $item->{'charge_nobal'})
-% : '';
+% : exists($item->{'void_charge'})
+% ? sprintf("<DEL>$money_char\%.2f</DEL>", $item->{'void_charge'})
+% : '';
%
% my $payment = exists($item->{'payment'})
% ? sprintf("-&nbsp;$money_char\%.2f", $item->{'payment'})
@@ -428,6 +430,15 @@ foreach my $cust_bill ($cust_main->cust_bill) {
$num_cust_bill++;
}
+#voided invoices
+foreach my $cust_bill_void ($cust_main->cust_bill_void) {
+ push @history, {
+ 'date' => $cust_bill_void->_date,
+ 'desc' => include('payment_history/voided_invoice.html', $cust_bill_void, %opt ),
+ 'void_charge' => $cust_bill_void->charged,
+ };
+}
+
#statements
foreach my $cust_statement ($cust_main->cust_statement) {
push @history, {
diff --git a/httemplate/view/cust_main/payment_history/invoice.html b/httemplate/view/cust_main/payment_history/invoice.html
index 3028f0f69..96a9f5456 100644
--- a/httemplate/view/cust_main/payment_history/invoice.html
+++ b/httemplate/view/cust_main/payment_history/invoice.html
@@ -1,4 +1,4 @@
-<% $link %><% $invoice %><% $link ? '</A>' : '' %><% $delete %><% $under %>
+<% $link %><% $invoice %><% $link ? '</A>' : '' %><% "$void$delete$under" %>
<%init>
my( $cust_bill, %opt ) = @_;
@@ -26,6 +26,18 @@ my $link = $curuser->access_right('View invoices')
? qq!<A HREF="${p}view/cust_bill.cgi?$invnum">!
: '';
+my $void = '';
+if ( $cust_bill->closed !~ /^Y/i && $curuser->access_right('Void invoices') ) {
+ $void =
+ ' ('. include('/elements/popup_link.html',
+ 'label' => emt('void'),
+ 'action' => "${p}misc/void-cust_bill.html?;invnum=".
+ $cust_bill->invnum,
+ 'actionlabel' => emt('Void Invoice'),
+ ).
+ ')';
+}
+
my $delete = '';
$delete = areyousure_link("${p}misc/delete-cust_bill.html?$invnum",
emt('Are you sure you want to delete this invoice?'),
diff --git a/httemplate/view/cust_main/payment_history/payment.html b/httemplate/view/cust_main/payment_history/payment.html
index d7322a2d6..ff269bfaf 100644
--- a/httemplate/view/cust_main/payment_history/payment.html
+++ b/httemplate/view/cust_main/payment_history/payment.html
@@ -181,7 +181,7 @@ $void = areyousure_link("${p}misc/void-cust_pay.cgi?".$cust_pay->paynum,
&& $curuser->access_right('Echeck void')
)
|| ( $cust_pay->payby !~ /^(CARD|CHEK)$/
- && $curuser->access_right('Regular void')
+ && $curuser->access_right('Void payments')
)
)
);
diff --git a/httemplate/view/cust_main/payment_history/voided_invoice.html b/httemplate/view/cust_main/payment_history/voided_invoice.html
new file mode 100644
index 000000000..15393cbf5
--- /dev/null
+++ b/httemplate/view/cust_main/payment_history/voided_invoice.html
@@ -0,0 +1,57 @@
+<DEL><% $link %><% $invoice %><% $link ? '</A>' : '' %></DEL>
+<I><% mt("voided [_1]", time2str($date_format, $cust_bill_void->void_date) ) |h %>
+% my $void_user = $cust_bill_void->void_access_user;
+% if ($void_user) {
+ by <% $void_user->username %></I>
+% }
+<% "$unvoid$delete$under" %>
+<%init>
+
+my( $cust_bill_void, %opt ) = @_;
+
+my $date_format = $opt{'date_format'} || '%m/%d/%Y';
+
+my $conf = new FS::Conf;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+my $invoice = emt("Invoice #[_1] (Balance [_2])",$cust_bill_void->display_invnum, $cust_bill_void->charged);
+
+my $under = '';
+
+my $invnum = $cust_bill_void->invnum;
+
+my $link = $curuser->access_right('View invoices')
+ ? qq!<A HREF="${p}view/cust_bill_void.html?$invnum">!
+ : '';
+
+my $unvoid = '';
+$unvoid = areyousure_link("${p}misc/unvoid-cust_bill_void.html?invnum=". $cust_bill_void->invnum,
+ emt('Are you sure you want to unvoid this invoice?'),
+ emt('Unvoid this invoice'),
+ emt('unvoid')
+ )
+ if $cust_bill_void->closed !~ /^Y/ && $curuser->access_right('Unvoid invoices');
+
+my $delete = '';
+$delete = areyousure_link("${p}misc/delete-cust_bill.html?$invnum",
+ emt('Are you sure you want to delete this invoice?'),
+ emt('Delete this invoice from the database completely'),
+ emt('delete')
+ )
+ if ( $opt{'deleteinvoices'} && $curuser->access_right('Delete invoices') );
+
+my $events = '';
+#1.9
+if ( $cust_bill_void->num_cust_event
+ && ( $curuser->access_right('Billing event reports')
+ || $curuser->access_right('View customer billing events')
+ )
+ ) {
+ $under .=
+ qq!<BR><A HREF="${p}search/cust_event.html?invnum=$invnum">( !.
+ emt('View invoice events').' )</A>';
+}
+$under = '<FONT SIZE="-1">'.$under.'</FONT>' if length($under);
+
+</%init>
diff --git a/httemplate/view/cust_main/payment_history/voided_payment.html b/httemplate/view/cust_main/payment_history/voided_payment.html
index 2f038be41..88b5e0a84 100644
--- a/httemplate/view/cust_main/payment_history/voided_payment.html
+++ b/httemplate/view/cust_main/payment_history/voided_payment.html
@@ -31,6 +31,6 @@ $unvoid = areyousure_link("${p}misc/unvoid-cust_pay_void.cgi?".$cust_pay_void->p
emt('Unvoid this payment from the database') . $unvoidmsg,
emt('unvoid')
)
- if ( $cust_pay_void->closed !~ /^Y/i && $curuser->access_right('Unvoid') );
+ if ( $cust_pay_void->closed !~ /^Y/i && $curuser->access_right('Unvoid payments') );
</%init>
diff --git a/httemplate/view/elements/tr-svc_export_machine.html b/httemplate/view/elements/tr-svc_export_machine.html
new file mode 100644
index 000000000..1ba8d74a1
--- /dev/null
+++ b/httemplate/view/elements/tr-svc_export_machine.html
@@ -0,0 +1,27 @@
+% foreach my $part_export (@part_export) {
+% my $label = ( $part_export->exportname
+% ? $part_export->exportname
+% : $part_export->label
+% ).
+% ' hostname';
+%
+% my $svc_export_machine = qsearchs('svc_export_machine', {
+% 'svcnum' => $opt{svc}->svcnum,
+% 'exportnum' => $part_export->exportnum,
+% });
+
+ <& tr.html,
+ 'label' => $label,
+ 'value' => $svc_export_machine
+ ? $svc_export_machine->part_export_machine->machine
+ : '',
+ &>
+% }
+<%init>
+
+my %opt = @_;
+
+my @part_export = grep { $_->machine eq '_SVC_MACHINE' }
+ $opt{part_svc}->part_export;
+
+</%init>
diff --git a/httemplate/view/quotation.html b/httemplate/view/quotation.html
index 461b5dfb6..a88acf82b 100755
--- a/httemplate/view/quotation.html
+++ b/httemplate/view/quotation.html
@@ -44,8 +44,6 @@ XXX resending quotations
% }
% #plaintext quotations? <PRE><% join('', $quotation->print_text() ) %></PRE>
-</%doc>
-
<& /elements/footer.html &>
<%init>
diff --git a/httemplate/view/svc_acct/basics.html b/httemplate/view/svc_acct/basics.html
index bcd84696e..1cdf77615 100644
--- a/httemplate/view/svc_acct/basics.html
+++ b/httemplate/view/svc_acct/basics.html
@@ -56,6 +56,11 @@
&>
% }
+<& /view/elements/tr-svc_export_machine.html,
+ 'svc' => $svc_acct,
+ 'part_svc' => $part_svc,
+&>
+
% if ($svc_acct->uid ne '') {
<& /view/elements/tr.html, label=>mt('UID'), value=>$svc_acct->uid &>
% }
diff --git a/init.d/insserv-override-apache2 b/init.d/insserv-override-apache2
new file mode 100644
index 000000000..1b333e806
--- /dev/null
+++ b/init.d/insserv-override-apache2
@@ -0,0 +1,11 @@
+### BEGIN INIT INFO
+# Provides: apache2
+# Required-Start: $local_fs $remote_fs $network $syslog $named
+# Required-Stop: $local_fs $remote_fs $network $syslog $named
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# X-Interactive: true
+# Short-Description: Start/stop apache2 web server
+# Should-Start: postgresql mysql
+# Should-Stop: postgresql mysql
+### END INIT INFO
diff --git a/rt/bin/rt b/rt/bin/rt
index 32f459a7e..89873f5d6 100755
--- a/rt/bin/rt
+++ b/rt/bin/rt
@@ -420,7 +420,7 @@ sub show {
}
elsif (my $spec = is_object_spec($_, $type)) {
push @objects, $spec;
- $rawprint = 1 if $_ =~ /\/content$/ or $_ !~ /^ticket/;
+ $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/;
}
else {
my $datum = /^-/ ? "option" : "argument";
diff --git a/rt/bin/rt.in b/rt/bin/rt.in
index e54a07add..2a9f643c8 100644
--- a/rt/bin/rt.in
+++ b/rt/bin/rt.in
@@ -420,7 +420,7 @@ sub show {
}
elsif (my $spec = is_object_spec($_, $type)) {
push @objects, $spec;
- $rawprint = 1 if $_ =~ /\/content$/ or $_ !~ /^ticket/;
+ $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/;
}
else {
my $datum = /^-/ ? "option" : "argument";
diff --git a/rt/configure b/rt/configure
index 1862c5fe6..76ef85b92 100755
--- a/rt/configure
+++ b/rt/configure
@@ -1,7 +1,7 @@
#! /bin/sh
# From configure.ac Revision.
# Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.67 for RT rt-4.0.6.
+# Generated by GNU Autoconf 2.68 for RT rt-4.0.7.
#
# Report bugs to <rt-bugs@bestpractical.com>.
#
@@ -92,6 +92,7 @@ fi
IFS=" "" $as_nl"
# Find who we are. Look in the path if we contain no directory separator.
+as_myself=
case $0 in #((
*[\\/]* ) as_myself=$0 ;;
*) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
@@ -216,11 +217,18 @@ IFS=$as_save_IFS
# We cannot yet assume a decent shell, so we have to provide a
# neutralization value for shells without unset; and this also
# works around shells that cannot unset nonexistent variables.
+ # Preserve -v and -x to the replacement shell.
BASH_ENV=/dev/null
ENV=/dev/null
(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV
export CONFIG_SHELL
- exec "$CONFIG_SHELL" "$as_myself" ${1+"$@"}
+ case $- in # ((((
+ *v*x* | *x*v* ) as_opts=-vx ;;
+ *v* ) as_opts=-v ;;
+ *x* ) as_opts=-x ;;
+ * ) as_opts= ;;
+ esac
+ exec "$CONFIG_SHELL" $as_opts "$as_myself" ${1+"$@"}
fi
if test x$as_have_required = xno; then :
@@ -552,8 +560,8 @@ MAKEFLAGS=
# Identity of this package.
PACKAGE_NAME='RT'
PACKAGE_TARNAME='rt'
-PACKAGE_VERSION='rt-4.0.6'
-PACKAGE_STRING='RT rt-4.0.6'
+PACKAGE_VERSION='rt-4.0.7'
+PACKAGE_STRING='RT rt-4.0.7'
PACKAGE_BUGREPORT='rt-bugs@bestpractical.com'
PACKAGE_URL=''
@@ -1165,7 +1173,7 @@ Try \`$0 --help' for more information"
$as_echo "$as_me: WARNING: you should use --build, --host, --target" >&2
expr "x$ac_option" : ".*[^-._$as_cr_alnum]" >/dev/null &&
$as_echo "$as_me: WARNING: invalid host type: $ac_option" >&2
- : ${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}
+ : "${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}"
;;
esac
@@ -1303,7 +1311,7 @@ if test "$ac_init_help" = "long"; then
# Omit some internal or obsolete options to make the list less imposing.
# This message is too long to be a string in the A/UX 3.1 sh.
cat <<_ACEOF
-\`configure' configures RT rt-4.0.6 to adapt to many kinds of systems.
+\`configure' configures RT rt-4.0.7 to adapt to many kinds of systems.
Usage: $0 [OPTION]... [VAR=VALUE]...
@@ -1364,7 +1372,7 @@ fi
if test -n "$ac_init_help"; then
case $ac_init_help in
- short | recursive ) echo "Configuration of RT rt-4.0.6:";;
+ short | recursive ) echo "Configuration of RT rt-4.0.7:";;
esac
cat <<\_ACEOF
@@ -1488,8 +1496,8 @@ fi
test -n "$ac_init_help" && exit $ac_status
if $ac_init_version; then
cat <<\_ACEOF
-RT configure rt-4.0.6
-generated by GNU Autoconf 2.67
+RT configure rt-4.0.7
+generated by GNU Autoconf 2.68
Copyright (C) 2010 Free Software Foundation, Inc.
This configure script is free software; the Free Software Foundation
@@ -1535,7 +1543,7 @@ sed 's/^/| /' conftest.$ac_ext >&5
ac_retval=1
fi
- eval $as_lineno_stack; test "x$as_lineno_stack" = x && { as_lineno=; unset as_lineno;}
+ eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
as_fn_set_status $ac_retval
} # ac_fn_c_try_compile
@@ -1581,7 +1589,7 @@ fi
# interfere with the next link command; also delete a directory that is
# left behind by Apple's compiler. We do this before executing the actions.
rm -rf conftest.dSYM conftest_ipa8_conftest.oo
- eval $as_lineno_stack; test "x$as_lineno_stack" = x && { as_lineno=; unset as_lineno;}
+ eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
as_fn_set_status $ac_retval
} # ac_fn_c_try_link
@@ -1589,8 +1597,8 @@ cat >config.log <<_ACEOF
This file contains any messages produced by compilers while
running configure, to aid debugging if configure makes a mistake.
-It was created by RT $as_me rt-4.0.6, which was
-generated by GNU Autoconf 2.67. Invocation command line was
+It was created by RT $as_me rt-4.0.7, which was
+generated by GNU Autoconf 2.68. Invocation command line was
$ $0 $@
@@ -1848,7 +1856,7 @@ $as_echo "$as_me: loading site script $ac_site_file" >&6;}
|| { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
as_fn_error $? "failed to load site script $ac_site_file
-See \`config.log' for more details" "$LINENO" 5 ; }
+See \`config.log' for more details" "$LINENO" 5; }
fi
done
@@ -1946,7 +1954,7 @@ rt_version_major=4
rt_version_minor=0
-rt_version_patch=6
+rt_version_patch=7
test "x$rt_version_major" = 'x' && rt_version_major=0
test "x$rt_version_minor" = 'x' && rt_version_minor=0
@@ -1998,7 +2006,7 @@ ac_configure="$SHELL $ac_aux_dir/configure" # Please don't use this var.
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install" >&5
$as_echo_n "checking for a BSD-compatible install... " >&6; }
if test -z "$INSTALL"; then
-if test "${ac_cv_path_install+set}" = set; then :
+if ${ac_cv_path_install+:} false; then :
$as_echo_n "(cached) " >&6
else
as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
@@ -2079,7 +2087,7 @@ test -z "$INSTALL_DATA" && INSTALL_DATA='${INSTALL} -m 644'
set dummy perl; ac_word=$2
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
$as_echo_n "checking for $ac_word... " >&6; }
-if test "${ac_cv_path_PERL+set}" = set; then :
+if ${ac_cv_path_PERL+:} false; then :
$as_echo_n "(cached) " >&6
else
case $PERL in
@@ -2800,7 +2808,7 @@ if test -n "$ac_tool_prefix"; then
set dummy ${ac_tool_prefix}gcc; ac_word=$2
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
$as_echo_n "checking for $ac_word... " >&6; }
-if test "${ac_cv_prog_CC+set}" = set; then :
+if ${ac_cv_prog_CC+:} false; then :
$as_echo_n "(cached) " >&6
else
if test -n "$CC"; then
@@ -2840,7 +2848,7 @@ if test -z "$ac_cv_prog_CC"; then
set dummy gcc; ac_word=$2
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
$as_echo_n "checking for $ac_word... " >&6; }
-if test "${ac_cv_prog_ac_ct_CC+set}" = set; then :
+if ${ac_cv_prog_ac_ct_CC+:} false; then :
$as_echo_n "(cached) " >&6
else
if test -n "$ac_ct_CC"; then
@@ -2893,7 +2901,7 @@ if test -z "$CC"; then
set dummy ${ac_tool_prefix}cc; ac_word=$2
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
$as_echo_n "checking for $ac_word... " >&6; }
-if test "${ac_cv_prog_CC+set}" = set; then :
+if ${ac_cv_prog_CC+:} false; then :
$as_echo_n "(cached) " >&6
else
if test -n "$CC"; then
@@ -2933,7 +2941,7 @@ if test -z "$CC"; then
set dummy cc; ac_word=$2
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
$as_echo_n "checking for $ac_word... " >&6; }
-if test "${ac_cv_prog_CC+set}" = set; then :
+if ${ac_cv_prog_CC+:} false; then :
$as_echo_n "(cached) " >&6
else
if test -n "$CC"; then
@@ -2992,7 +3000,7 @@ if test -z "$CC"; then
set dummy $ac_tool_prefix$ac_prog; ac_word=$2
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
$as_echo_n "checking for $ac_word... " >&6; }
-if test "${ac_cv_prog_CC+set}" = set; then :
+if ${ac_cv_prog_CC+:} false; then :
$as_echo_n "(cached) " >&6
else
if test -n "$CC"; then
@@ -3036,7 +3044,7 @@ do
set dummy $ac_prog; ac_word=$2
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
$as_echo_n "checking for $ac_word... " >&6; }
-if test "${ac_cv_prog_ac_ct_CC+set}" = set; then :
+if ${ac_cv_prog_ac_ct_CC+:} false; then :
$as_echo_n "(cached) " >&6
else
if test -n "$ac_ct_CC"; then
@@ -3091,7 +3099,7 @@ fi
test -z "$CC" && { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
as_fn_error $? "no acceptable C compiler found in \$PATH
-See \`config.log' for more details" "$LINENO" 5 ; }
+See \`config.log' for more details" "$LINENO" 5; }
# Provide some information about the compiler.
$as_echo "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5
@@ -3206,7 +3214,7 @@ sed 's/^/| /' conftest.$ac_ext >&5
{ { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
as_fn_error 77 "C compiler cannot create executables
-See \`config.log' for more details" "$LINENO" 5 ; }
+See \`config.log' for more details" "$LINENO" 5; }
else
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
$as_echo "yes" >&6; }
@@ -3249,7 +3257,7 @@ else
{ { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
as_fn_error $? "cannot compute suffix of executables: cannot compile and link
-See \`config.log' for more details" "$LINENO" 5 ; }
+See \`config.log' for more details" "$LINENO" 5; }
fi
rm -f conftest conftest$ac_cv_exeext
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext" >&5
@@ -3308,7 +3316,7 @@ $as_echo "$ac_try_echo"; } >&5
$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
as_fn_error $? "cannot run C compiled programs.
If you meant to cross compile, use \`--host'.
-See \`config.log' for more details" "$LINENO" 5 ; }
+See \`config.log' for more details" "$LINENO" 5; }
fi
fi
fi
@@ -3319,7 +3327,7 @@ rm -f conftest.$ac_ext conftest$ac_cv_exeext conftest.out
ac_clean_files=$ac_clean_files_save
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for suffix of object files" >&5
$as_echo_n "checking for suffix of object files... " >&6; }
-if test "${ac_cv_objext+set}" = set; then :
+if ${ac_cv_objext+:} false; then :
$as_echo_n "(cached) " >&6
else
cat confdefs.h - <<_ACEOF >conftest.$ac_ext
@@ -3360,7 +3368,7 @@ sed 's/^/| /' conftest.$ac_ext >&5
{ { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
as_fn_error $? "cannot compute suffix of object files: cannot compile
-See \`config.log' for more details" "$LINENO" 5 ; }
+See \`config.log' for more details" "$LINENO" 5; }
fi
rm -f conftest.$ac_cv_objext conftest.$ac_ext
fi
@@ -3370,7 +3378,7 @@ OBJEXT=$ac_cv_objext
ac_objext=$OBJEXT
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether we are using the GNU C compiler" >&5
$as_echo_n "checking whether we are using the GNU C compiler... " >&6; }
-if test "${ac_cv_c_compiler_gnu+set}" = set; then :
+if ${ac_cv_c_compiler_gnu+:} false; then :
$as_echo_n "(cached) " >&6
else
cat confdefs.h - <<_ACEOF >conftest.$ac_ext
@@ -3407,7 +3415,7 @@ ac_test_CFLAGS=${CFLAGS+set}
ac_save_CFLAGS=$CFLAGS
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5
$as_echo_n "checking whether $CC accepts -g... " >&6; }
-if test "${ac_cv_prog_cc_g+set}" = set; then :
+if ${ac_cv_prog_cc_g+:} false; then :
$as_echo_n "(cached) " >&6
else
ac_save_c_werror_flag=$ac_c_werror_flag
@@ -3485,7 +3493,7 @@ else
fi
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $CC option to accept ISO C89" >&5
$as_echo_n "checking for $CC option to accept ISO C89... " >&6; }
-if test "${ac_cv_prog_cc_c89+set}" = set; then :
+if ${ac_cv_prog_cc_c89+:} false; then :
$as_echo_n "(cached) " >&6
else
ac_cv_prog_cc_c89=no
@@ -3583,7 +3591,7 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for aginitlib in -lgraph" >&5
$as_echo_n "checking for aginitlib in -lgraph... " >&6; }
-if test "${ac_cv_lib_graph_aginitlib+set}" = set; then :
+if ${ac_cv_lib_graph_aginitlib+:} false; then :
$as_echo_n "(cached) " >&6
else
ac_check_lib_save_LIBS=$LIBS
@@ -3617,7 +3625,7 @@ LIBS=$ac_check_lib_save_LIBS
fi
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_graph_aginitlib" >&5
$as_echo "$ac_cv_lib_graph_aginitlib" >&6; }
-if test "x$ac_cv_lib_graph_aginitlib" = x""yes; then :
+if test "x$ac_cv_lib_graph_aginitlib" = xyes; then :
RT_GRAPHVIZ="1"
fi
@@ -3643,7 +3651,7 @@ fi
set dummy gdlib-config; ac_word=$2
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
$as_echo_n "checking for $ac_word... " >&6; }
-if test "${ac_cv_prog_RT_GD+set}" = set; then :
+if ${ac_cv_prog_RT_GD+:} false; then :
$as_echo_n "(cached) " >&6
else
if test -n "$RT_GD"; then
@@ -3699,7 +3707,7 @@ fi
set dummy gpg; ac_word=$2
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
$as_echo_n "checking for $ac_word... " >&6; }
-if test "${ac_cv_prog_RT_GPG+set}" = set; then :
+if ${ac_cv_prog_RT_GPG+:} false; then :
$as_echo_n "(cached) " >&6
else
if test -n "$RT_GPG"; then
@@ -3984,10 +3992,21 @@ $as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;;
:end' >>confcache
if diff "$cache_file" confcache >/dev/null 2>&1; then :; else
if test -w "$cache_file"; then
- test "x$cache_file" != "x/dev/null" &&
+ if test "x$cache_file" != "x/dev/null"; then
{ $as_echo "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5
$as_echo "$as_me: updating cache $cache_file" >&6;}
- cat confcache >$cache_file
+ if test ! -f "$cache_file" || test -h "$cache_file"; then
+ cat confcache >"$cache_file"
+ else
+ case $cache_file in #(
+ */* | ?:*)
+ mv -f confcache "$cache_file"$$ &&
+ mv -f "$cache_file"$$ "$cache_file" ;; #(
+ *)
+ mv -f confcache "$cache_file" ;;
+ esac
+ fi
+ fi
else
{ $as_echo "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5
$as_echo "$as_me: not updating unwritable cache $cache_file" >&6;}
@@ -4055,7 +4074,7 @@ LTLIBOBJS=$ac_ltlibobjs
-: ${CONFIG_STATUS=./config.status}
+: "${CONFIG_STATUS=./config.status}"
ac_write_fail=0
ac_clean_files_save=$ac_clean_files
ac_clean_files="$ac_clean_files $CONFIG_STATUS"
@@ -4156,6 +4175,7 @@ fi
IFS=" "" $as_nl"
# Find who we are. Look in the path if we contain no directory separator.
+as_myself=
case $0 in #((
*[\\/]* ) as_myself=$0 ;;
*) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
@@ -4462,8 +4482,8 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
# report actual input values of CONFIG_FILES etc. instead of their
# values after options handling.
ac_log="
-This file was extended by RT $as_me rt-4.0.6, which was
-generated by GNU Autoconf 2.67. Invocation command line was
+This file was extended by RT $as_me rt-4.0.7, which was
+generated by GNU Autoconf 2.68. Invocation command line was
CONFIG_FILES = $CONFIG_FILES
CONFIG_HEADERS = $CONFIG_HEADERS
@@ -4515,8 +4535,8 @@ _ACEOF
cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`"
ac_cs_version="\\
-RT config.status rt-4.0.6
-configured by $0, generated by GNU Autoconf 2.67,
+RT config.status rt-4.0.7
+configured by $0, generated by GNU Autoconf 2.68,
with options \\"\$ac_cs_config\\"
Copyright (C) 2010 Free Software Foundation, Inc.
@@ -4658,7 +4678,7 @@ do
"t/data/configs/apache2.2+mod_perl.conf") CONFIG_FILES="$CONFIG_FILES t/data/configs/apache2.2+mod_perl.conf" ;;
"t/data/configs/apache2.2+fastcgi.conf") CONFIG_FILES="$CONFIG_FILES t/data/configs/apache2.2+fastcgi.conf" ;;
- *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5 ;;
+ *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;;
esac
done
@@ -4679,9 +4699,10 @@ fi
# after its creation but before its name has been assigned to `$tmp'.
$debug ||
{
- tmp=
+ tmp= ac_tmp=
trap 'exit_status=$?
- { test -z "$tmp" || test ! -d "$tmp" || rm -fr "$tmp"; } && exit $exit_status
+ : "${ac_tmp:=$tmp}"
+ { test ! -d "$ac_tmp" || rm -fr "$ac_tmp"; } && exit $exit_status
' 0
trap 'as_fn_exit 1' 1 2 13 15
}
@@ -4689,12 +4710,13 @@ $debug ||
{
tmp=`(umask 077 && mktemp -d "./confXXXXXX") 2>/dev/null` &&
- test -n "$tmp" && test -d "$tmp"
+ test -d "$tmp"
} ||
{
tmp=./conf$$-$RANDOM
(umask 077 && mkdir "$tmp")
} || as_fn_error $? "cannot create a temporary directory in ." "$LINENO" 5
+ac_tmp=$tmp
# Set up the scripts for CONFIG_FILES section.
# No need to generate them if there are no CONFIG_FILES.
@@ -4716,7 +4738,7 @@ else
ac_cs_awk_cr=$ac_cr
fi
-echo 'BEGIN {' >"$tmp/subs1.awk" &&
+echo 'BEGIN {' >"$ac_tmp/subs1.awk" &&
_ACEOF
@@ -4744,7 +4766,7 @@ done
rm -f conf$$subs.sh
cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
-cat >>"\$tmp/subs1.awk" <<\\_ACAWK &&
+cat >>"\$ac_tmp/subs1.awk" <<\\_ACAWK &&
_ACEOF
sed -n '
h
@@ -4792,7 +4814,7 @@ t delim
rm -f conf$$subs.awk
cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
_ACAWK
-cat >>"\$tmp/subs1.awk" <<_ACAWK &&
+cat >>"\$ac_tmp/subs1.awk" <<_ACAWK &&
for (key in S) S_is_set[key] = 1
FS = ""
@@ -4824,7 +4846,7 @@ if sed "s/$ac_cr//" < /dev/null > /dev/null 2>&1; then
sed "s/$ac_cr\$//; s/$ac_cr/$ac_cs_awk_cr/g"
else
cat
-fi < "$tmp/subs1.awk" > "$tmp/subs.awk" \
+fi < "$ac_tmp/subs1.awk" > "$ac_tmp/subs.awk" \
|| as_fn_error $? "could not setup config files machinery" "$LINENO" 5
_ACEOF
@@ -4864,7 +4886,7 @@ do
esac
case $ac_mode$ac_tag in
:[FHL]*:*);;
- :L* | :C*:*) as_fn_error $? "invalid tag \`$ac_tag'" "$LINENO" 5 ;;
+ :L* | :C*:*) as_fn_error $? "invalid tag \`$ac_tag'" "$LINENO" 5;;
:[FH]-) ac_tag=-:-;;
:[FH]*) ac_tag=$ac_tag:$ac_tag.in;;
esac
@@ -4883,7 +4905,7 @@ do
for ac_f
do
case $ac_f in
- -) ac_f="$tmp/stdin";;
+ -) ac_f="$ac_tmp/stdin";;
*) # Look for the file first in the build tree, then in the source tree
# (if the path is not absolute). The absolute path cannot be DOS-style,
# because $ac_f cannot contain `:'.
@@ -4892,7 +4914,7 @@ do
[\\/$]*) false;;
*) test -f "$srcdir/$ac_f" && ac_f="$srcdir/$ac_f";;
esac ||
- as_fn_error 1 "cannot find input file: \`$ac_f'" "$LINENO" 5 ;;
+ as_fn_error 1 "cannot find input file: \`$ac_f'" "$LINENO" 5;;
esac
case $ac_f in *\'*) ac_f=`$as_echo "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac
as_fn_append ac_file_inputs " '$ac_f'"
@@ -4918,8 +4940,8 @@ $as_echo "$as_me: creating $ac_file" >&6;}
esac
case $ac_tag in
- *:-:* | *:-) cat >"$tmp/stdin" \
- || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;;
+ *:-:* | *:-) cat >"$ac_tmp/stdin" \
+ || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;;
esac
;;
esac
@@ -5049,21 +5071,22 @@ s&@abs_top_builddir@&$ac_abs_top_builddir&;t t
s&@INSTALL@&$ac_INSTALL&;t t
$ac_datarootdir_hack
"
-eval sed \"\$ac_sed_extra\" "$ac_file_inputs" | $AWK -f "$tmp/subs.awk" >$tmp/out \
- || as_fn_error $? "could not create $ac_file" "$LINENO" 5
+eval sed \"\$ac_sed_extra\" "$ac_file_inputs" | $AWK -f "$ac_tmp/subs.awk" \
+ >$ac_tmp/out || as_fn_error $? "could not create $ac_file" "$LINENO" 5
test -z "$ac_datarootdir_hack$ac_datarootdir_seen" &&
- { ac_out=`sed -n '/\${datarootdir}/p' "$tmp/out"`; test -n "$ac_out"; } &&
- { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' "$tmp/out"`; test -z "$ac_out"; } &&
+ { ac_out=`sed -n '/\${datarootdir}/p' "$ac_tmp/out"`; test -n "$ac_out"; } &&
+ { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' \
+ "$ac_tmp/out"`; test -z "$ac_out"; } &&
{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \`datarootdir'
which seems to be undefined. Please make sure it is defined" >&5
$as_echo "$as_me: WARNING: $ac_file contains a reference to the variable \`datarootdir'
which seems to be undefined. Please make sure it is defined" >&2;}
- rm -f "$tmp/stdin"
+ rm -f "$ac_tmp/stdin"
case $ac_file in
- -) cat "$tmp/out" && rm -f "$tmp/out";;
- *) rm -f "$ac_file" && mv "$tmp/out" "$ac_file";;
+ -) cat "$ac_tmp/out" && rm -f "$ac_tmp/out";;
+ *) rm -f "$ac_file" && mv "$ac_tmp/out" "$ac_file";;
esac \
|| as_fn_error $? "could not create $ac_file" "$LINENO" 5
;;
diff --git a/rt/docs/web_deployment.pod b/rt/docs/web_deployment.pod
index 4c3f73fb5..5d2cd4c00 100644
--- a/rt/docs/web_deployment.pod
+++ b/rt/docs/web_deployment.pod
@@ -23,7 +23,7 @@ to use L<Starman>, a high performance preforking server:
/opt/rt4/sbin/rt-server --server Starman --port 8080
B<NOTICE>: After you run the standalone server as root, you will need to
-remove your C<var/mason> directory, or the non-standalone servers
+remove your C<var/mason_data> directory, or the non-standalone servers
(Apache, etc), which run as a non-privileged user, will not be able to
write to it and will not work.
diff --git a/rt/etc/RT_Config.pm.in b/rt/etc/RT_Config.pm.in
index bd48b6efd..169182033 100644
--- a/rt/etc/RT_Config.pm.in
+++ b/rt/etc/RT_Config.pm.in
@@ -640,6 +640,9 @@ Set($NotifyActor, 0);
By default, RT records each message it sends out to its own internal
database. To change this behavior, set C<$RecordOutgoingEmail> to 0
+If this is disabled, users' digest mail delivery preferences
+(i.e. EmailFrequency) will also be ignored.
+
=cut
Set($RecordOutgoingEmail, 1);
@@ -897,8 +900,8 @@ Set(@JSFiles, qw/
jquery-1.4.2.min.js
jquery_noconflict.js
jquery-ui-1.8.4.custom.min.js
+ jquery-ui-timepicker-addon.js
jquery-ui-patch-datepicker.js
- ui.timepickr.js
titlebox-state.js
util.js
userautocomplete.js
@@ -1826,6 +1829,16 @@ If the "RT has detected a possible cross-site request forgery" error is triggere
by a host:port sent by your browser that you believe should be valid, you can copy
the host:port from the error message into this list.
+Simple wildcards, similar to SSL certificates, are allowed. For example:
+
+ *.example.com:80 # matches foo.example.com
+ # but not example.com
+ # or foo.bar.example.com
+
+ www*.example.com:80 # matches www3.example.com
+ # and www-test.example.com
+ # and www.example.com
+
=cut
Set(@ReferrerWhitelist, qw());
@@ -2279,10 +2292,11 @@ all possible transitions in each lifecycle using the following format:
=head3 Statuses available during ticket creation
-By default users can create tickets with any status, except
-deleted. If you want to restrict statuses available during creation
-then describe transition from '' (empty string), like in the example
-above.
+By default users can create tickets with a status of new,
+open, or resolved, but cannot create tickets with a status of
+rejected, stalled, or deleted. If you want to change the statuses
+available during creation, update the transition from '' (empty
+string), like in the example above.
=head3 Protecting status changes with rights
diff --git a/rt/etc/RT_SiteConfig.pm b/rt/etc/RT_SiteConfig.pm
index 29a7d0231..4a397fa16 100644
--- a/rt/etc/RT_SiteConfig.pm
+++ b/rt/etc/RT_SiteConfig.pm
@@ -49,7 +49,7 @@ Set($MessageBoxWidth, 80);
Set($MessageBoxRichTextHeight, 368);
#redirects to ticket display on quick create
-#Set($QuickCreateRedirect, 1);
+#Set($DisplayTicketAfterQuickCreate, 1);
#Set(@Plugins,(qw(Extension::QuickDelete RT::FM)));
diff --git a/rt/etc/initialdata b/rt/etc/initialdata
index cc07cec59..7ab746db1 100644
--- a/rt/etc/initialdata
+++ b/rt/etc/initialdata
@@ -1,4 +1,4 @@
-# Initial data for a fresh RT3 Installation.
+# Initial data for a fresh RT installation.
@Users = (
{ Name => 'root',
diff --git a/rt/etc/schema.SQLite b/rt/etc/schema.SQLite
index 138971cfb..6897be2d6 100644
--- a/rt/etc/schema.SQLite
+++ b/rt/etc/schema.SQLite
@@ -3,7 +3,7 @@
CREATE TABLE Attachments (
id INTEGER PRIMARY KEY ,
TransactionId INTEGER ,
- Parent integer NULL ,
+ Parent integer NULL DEFAULT 0 ,
MessageId varchar(160) NULL ,
Subject varchar(255) NULL ,
Filename varchar(255) NULL ,
@@ -11,7 +11,7 @@ CREATE TABLE Attachments (
ContentEncoding varchar(80) NULL ,
Content LONGTEXT NULL ,
Headers LONGTEXT NULL ,
- Creator integer NULL ,
+ Creator integer NULL DEFAULT 0 ,
Created DATETIME NULL
) ;
@@ -30,12 +30,12 @@ CREATE TABLE Queues (
CommentAddress varchar(120) NULL ,
Lifecycle varchar(32) NULL ,
SubjectTag varchar(120) NULL ,
- InitialPriority integer NULL ,
- FinalPriority integer NULL ,
- DefaultDueIn integer NULL ,
- Creator integer NULL ,
+ InitialPriority integer NULL DEFAULT 0 ,
+ FinalPriority integer NULL DEFAULT 0 ,
+ DefaultDueIn integer NULL DEFAULT 0 ,
+ Creator integer NULL DEFAULT 0 ,
Created DATETIME NULL ,
- LastUpdatedBy integer NULL ,
+ LastUpdatedBy integer NULL DEFAULT 0 ,
LastUpdated DATETIME NULL ,
Disabled int2 NOT NULL DEFAULT 0
@@ -51,11 +51,11 @@ CREATE TABLE Links (
Base varchar(240) NULL ,
Target varchar(240) NULL ,
Type varchar(20) NOT NULL ,
- LocalTarget integer NULL ,
- LocalBase integer NULL ,
- LastUpdatedBy integer NULL ,
+ LocalTarget integer NULL DEFAULT 0 ,
+ LocalBase integer NULL DEFAULT 0 ,
+ LastUpdatedBy integer NULL DEFAULT 0 ,
LastUpdated DATETIME NULL ,
- Creator integer NULL ,
+ Creator integer NULL DEFAULT 0 ,
Created DATETIME NULL
) ;
@@ -106,9 +106,9 @@ CREATE TABLE ScripConditions (
Argument varchar(255) NULL ,
ApplicableTransTypes varchar(60) NULL ,
- Creator integer NULL ,
+ Creator integer NULL DEFAULT 0 ,
Created DATETIME NULL ,
- LastUpdatedBy integer NULL ,
+ LastUpdatedBy integer NULL DEFAULT 0 ,
LastUpdated DATETIME NULL
) ;
@@ -119,8 +119,8 @@ CREATE TABLE ScripConditions (
CREATE TABLE Transactions (
id INTEGER PRIMARY KEY ,
ObjectType varchar(255) NULL ,
- ObjectId integer NULL ,
- TimeTaken integer NULL ,
+ ObjectId integer NULL DEFAULT 0 ,
+ TimeTaken integer NULL DEFAULT 0 ,
Type varchar(20) NULL ,
Field varchar(40) NULL ,
OldValue varchar(255) NULL ,
@@ -130,7 +130,7 @@ CREATE TABLE Transactions (
NewReference integer NULL ,
Data varchar(255) NULL ,
- Creator integer NULL ,
+ Creator integer NULL DEFAULT 0 ,
Created DATETIME NULL
) ;
@@ -143,19 +143,19 @@ CREATE INDEX Transactions1 ON Transactions (ObjectType, ObjectId);
CREATE TABLE Scrips (
id INTEGER PRIMARY KEY ,
Description varchar(255),
- ScripCondition integer NULL ,
- ScripAction integer NULL ,
+ ScripCondition integer NULL DEFAULT 0 ,
+ ScripAction integer NULL DEFAULT 0 ,
ConditionRules text NULL ,
ActionRules text NULL ,
CustomIsApplicableCode text NULL ,
CustomPrepareCode text NULL ,
CustomCommitCode text NULL ,
Stage varchar(32) NULL ,
- Queue integer NULL ,
- Template integer NULL ,
- Creator integer NULL ,
+ Queue integer NULL DEFAULT 0 ,
+ Template integer NULL DEFAULT 0 ,
+ Creator integer NULL DEFAULT 0 ,
Created DATETIME NULL ,
- LastUpdatedBy integer NULL ,
+ LastUpdatedBy integer NULL DEFAULT 0 ,
LastUpdated DATETIME NULL
) ;
@@ -167,7 +167,7 @@ CREATE TABLE ACL (
id INTEGER PRIMARY KEY ,
PrincipalType varchar(25) NOT NULL,
- PrincipalId INTEGER,
+ PrincipalId INTEGER DEFAULT 0,
RightName varchar(25) NOT NULL ,
ObjectType varchar(25) NOT NULL ,
ObjectId INTEGER default 0,
@@ -185,8 +185,8 @@ CREATE TABLE ACL (
CREATE TABLE GroupMembers (
id INTEGER PRIMARY KEY ,
- GroupId integer NULL,
- MemberId integer NULL,
+ GroupId integer NULL DEFAULT 0,
+ MemberId integer NULL DEFAULT 0,
Creator integer NOT NULL DEFAULT 0 ,
Created DATETIME NULL ,
LastUpdatedBy integer NOT NULL DEFAULT 0 ,
@@ -250,9 +250,9 @@ CREATE TABLE Users (
Timezone char(50) NULL ,
PGPKey text NULL,
- Creator integer NULL ,
+ Creator integer NULL DEFAULT 0 ,
Created DATETIME NULL ,
- LastUpdatedBy integer NULL ,
+ LastUpdatedBy integer NULL DEFAULT 0 ,
LastUpdated DATETIME NULL
) ;
@@ -270,20 +270,20 @@ CREATE INDEX Users4 ON Users (EmailAddress);
CREATE TABLE Tickets (
id INTEGER PRIMARY KEY ,
- EffectiveId integer NULL ,
- Queue integer NULL ,
+ EffectiveId integer NULL DEFAULT 0 ,
+ Queue integer NULL DEFAULT 0 ,
Type varchar(16) NULL ,
- IssueStatement integer NULL ,
- Resolution integer NULL ,
- Owner integer NULL ,
+ IssueStatement integer NULL DEFAULT 0 ,
+ Resolution integer NULL DEFAULT 0 ,
+ Owner integer NULL DEFAULT 0 ,
Subject varchar(200) NULL DEFAULT '[no subject]' ,
- InitialPriority integer NULL ,
- FinalPriority integer NULL ,
- Priority integer NULL ,
- TimeEstimated integer NULL ,
- TimeWorked integer NULL ,
+ InitialPriority integer NULL DEFAULT 0 ,
+ FinalPriority integer NULL DEFAULt 0 ,
+ Priority integer NULL DEFAULT 0 ,
+ TimeEstimated integer NULL DEFAULT 0 ,
+ TimeWorked integer NULL DEFAULT 0 ,
Status varchar(64) NULL ,
- TimeLeft integer NULL ,
+ TimeLeft integer NULL DEFAULT 0 ,
Told DATETIME NULL ,
Starts DATETIME NULL ,
Started DATETIME NULL ,
@@ -291,9 +291,9 @@ CREATE TABLE Tickets (
Resolved DATETIME NULL ,
- LastUpdatedBy integer NULL ,
+ LastUpdatedBy integer NULL DEFAULT 0 ,
LastUpdated DATETIME NULL ,
- Creator integer NULL ,
+ Creator integer NULL DEFAULT 0 ,
Created DATETIME NULL ,
Disabled int2 NOT NULL DEFAULT 0
@@ -315,9 +315,9 @@ CREATE TABLE ScripActions (
Description varchar(255) NULL ,
ExecModule varchar(60) NULL ,
Argument varchar(255) NULL ,
- Creator integer NULL ,
+ Creator integer NULL DEFAULT 0 ,
Created DATETIME NULL ,
- LastUpdatedBy integer NULL ,
+ LastUpdatedBy integer NULL DEFAULT 0 ,
LastUpdated DATETIME NULL
) ;
@@ -333,11 +333,11 @@ CREATE TABLE Templates (
Description varchar(255) NULL ,
Type varchar(16) NULL ,
Language varchar(16) NULL ,
- TranslationOf integer NULL ,
+ TranslationOf integer NULL DEFAULT 0 ,
Content blob NULL ,
LastUpdated DATETIME NULL ,
- LastUpdatedBy integer NULL ,
- Creator integer NULL ,
+ LastUpdatedBy integer NULL DEFAULT 0 ,
+ Creator integer NULL DEFAULT 0 ,
Created DATETIME NULL
) ;
@@ -437,10 +437,10 @@ CREATE TABLE Attributes (
Content LONGTEXT NULL ,
ContentType varchar(16),
ObjectType varchar(25) NOT NULL ,
- ObjectId INTEGER default 0,
- Creator integer NULL ,
+ ObjectId INTEGER ,
+ Creator integer NULL DEFAULT 0 ,
Created DATETIME NULL ,
- LastUpdatedBy integer NULL ,
+ LastUpdatedBy integer NULL DEFAULT 0 ,
LastUpdated DATETIME NULL
) ;
@@ -483,22 +483,22 @@ Parent integer NOT NULL DEFAULT 0,
Name varchar(255) NOT NULL DEFAULT '',
Description varchar(255) NOT NULL DEFAULT '',
ObjectType varchar(64) NOT NULL DEFAULT '',
-ObjectId integer NOT NULL
+ObjectId integer NOT NULL DEFAULT 0
);
CREATE TABLE ObjectTopics (
id INTEGER PRIMARY KEY,
-Topic integer NOT NULL,
+Topic integer NOT NULL DEFAULT 0,
ObjectType varchar(64) NOT NULL DEFAULT '',
-ObjectId integer NOT NULL
+ObjectId integer NOT NULL DEFAULT 0
);
CREATE TABLE ObjectClasses (
id INTEGER PRIMARY KEY,
-Class integer NOT NULL,
+Class integer NOT NULL DEFAULT 0,
ObjectType varchar(64) NOT NULL DEFAULT '',
-ObjectId integer NOT NULL,
+ObjectId integer NOT NULL DEFAULT 0,
Creator integer NOT NULL DEFAULT 0,
Created TIMESTAMP NULL,
LastUpdatedBy integer NOT NULL DEFAULT 0,
diff --git a/rt/etc/upgrade/3.3.0/schema.mysql b/rt/etc/upgrade/3.3.0/schema.mysql
index f6998363e..d8b04991e 100644
--- a/rt/etc/upgrade/3.3.0/schema.mysql
+++ b/rt/etc/upgrade/3.3.0/schema.mysql
@@ -1,37 +1,32 @@
-alter Table Transactions ADD Column (ObjectType varchar(64) not null);
-update Transactions set ObjectType = 'RT::Ticket';
-alter table Transactions drop column EffectiveTicket;
-alter table Transactions add column ReferenceType varchar(255) NULL;
-alter table Transactions add column OldReference integer NULL;
-alter table Transactions add column NewReference integer NULL;
-alter table Transactions drop index transactions1;
-alter table Transactions change Ticket ObjectId integer NOT NULL DEFAULT 0 ;
+drop index transactions1 ON Transactions;
-CREATE INDEX Transactions1 ON Transactions (ObjectType, ObjectId);
-
-alter table TicketCustomFieldValues rename ObjectCustomFieldValues;
+alter Table Transactions
+ ADD COLUMN (ObjectType varchar(64) not null),
+ DROP COLUMN EffectiveTicket,
+ ADD COLUMN ReferenceType varchar(255) NULL,
+ ADD COLUMN OldReference integer NULL,
+ ADD COLUMN NewReference integer NULL,
+ CHANGE Ticket ObjectId integer NOT NULL DEFAULT 0;
-alter table ObjectCustomFieldValues change Ticket ObjectId integer NOT NULL DEFAULT 0 ;
+UPDATE Transactions set ObjectType = 'RT::Ticket';
+CREATE INDEX Transactions1 ON Transactions (ObjectType, ObjectId);
-alter table ObjectCustomFieldValues add column ObjectType varchar(255) not null;
+alter table TicketCustomFieldValues rename ObjectCustomFieldValues,
+ change Ticket ObjectId integer NOT NULL DEFAULT 0 ,
+ add column ObjectType varchar(255) not null,
+ add column Current bool default 1,
+ add column LargeContent LONGTEXT NULL,
+ add column ContentType varchar(80) NULL,
+ add column ContentEncoding varchar(80) NULL;
update ObjectCustomFieldValues set ObjectType = 'RT::Ticket';
-alter table ObjectCustomFieldValues add column Current bool default 1;
-
-alter table ObjectCustomFieldValues add column LargeContent LONGTEXT NULL;
-
-alter table ObjectCustomFieldValues add column ContentType varchar(80) NULL;
-
-alter table ObjectCustomFieldValues add column ContentEncoding varchar(80) NULL;
-
# These could fail if there's no such index and there's no "drop index if exists" syntax
#alter table ObjectCustomFieldValues drop index ticketcustomfieldvalues1;
#alter table ObjectCustomFieldValues drop index ticketcustomfieldvalues2;
-alter table ObjectCustomFieldValues add index ObjectCustomFieldValues1 (Content);
-
-alter table ObjectCustomFieldValues add index ObjectCustomFieldValues2 (CustomField,ObjectType,ObjectId);
+alter table ObjectCustomFieldValues add index ObjectCustomFieldValues1 (Content),
+ add index ObjectCustomFieldValues2 (CustomField,ObjectType,ObjectId);
CREATE TABLE ObjectCustomFields (
@@ -50,10 +45,10 @@ CREATE TABLE ObjectCustomFields (
INSERT into ObjectCustomFields (id, CustomField, ObjectId, SortOrder, Creator, LastUpdatedBy) SELECT null, id, Queue, SortOrder, Creator, LastUpdatedBy from CustomFields;
-alter table CustomFields add column LookupType varchar(255) NOT NULL;
-alter table CustomFields add column Repeated int2 NOT NULL DEFAULT 0 ;
-alter table CustomFields add column Pattern varchar(255) NULL;
-alter table CustomFields add column MaxValues integer;
+alter table CustomFields add column LookupType varchar(255) NOT NULL,
+ add column Repeated int2 NOT NULL DEFAULT 0 ,
+ add column Pattern varchar(255) NULL,
+ add column MaxValues integer;
# See above
# alter table CustomFields drop index CustomFields1;
@@ -62,4 +57,4 @@ UPDATE CustomFields SET MaxValues = 1 WHERE Type LIKE '%Single';
UPDATE CustomFields SET Type = 'Select' WHERE Type LIKE 'Select%';
UPDATE CustomFields SET Type = 'Freeform' WHERE Type LIKE 'Freeform%';
UPDATE CustomFields Set LookupType = 'RT::Queue-RT::Ticket';
-alter table CustomFields drop column Queue;
+alter table CustomFields drop column Queue;
diff --git a/rt/etc/upgrade/3.3.11/schema.mysql b/rt/etc/upgrade/3.3.11/schema.mysql
index cc35d40f2..eff84783f 100644
--- a/rt/etc/upgrade/3.3.11/schema.mysql
+++ b/rt/etc/upgrade/3.3.11/schema.mysql
@@ -1,5 +1,5 @@
-ALTER TABLE ObjectCustomFieldValues ADD COLUMN SortOrder INTEGER NOT NULL DEFAULT 0;
-ALTER TABLE ObjectCustomFieldValues ADD COLUMN Disabled int2 NOT NULL DEFAULT 0;
+ALTER TABLE ObjectCustomFieldValues ADD COLUMN SortOrder INTEGER NOT NULL DEFAULT 0,
+ ADD COLUMN Disabled int2 NOT NULL DEFAULT 0;
UPDATE ObjectCustomFieldValues SET Disabled = 1 WHERE Current = 0;
ALTER TABLE ObjectCustomFieldValues DROP COLUMN Current;
diff --git a/rt/etc/upgrade/3.9.5/schema.mysql b/rt/etc/upgrade/3.9.5/schema.mysql
index 4bd0907c0..fe5018c78 100644
--- a/rt/etc/upgrade/3.9.5/schema.mysql
+++ b/rt/etc/upgrade/3.9.5/schema.mysql
@@ -6,15 +6,15 @@ AND CustomFieldValues.id = Attributes.ObjectId);
DELETE FROM Attributes WHERE Name = 'Category' AND ObjectType = 'RT::CustomFieldValue';
-ALTER TABLE Groups ADD COLUMN Creator integer NOT NULL DEFAULT 0;
-ALTER TABLE Groups ADD COLUMN Created DATETIME NULL;
-ALTER TABLE Groups ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0;
-ALTER TABLE Groups ADD COLUMN LastUpdated DATETIME NULL;
-ALTER TABLE GroupMembers ADD COLUMN Creator integer NOT NULL DEFAULT 0;
-ALTER TABLE GroupMembers ADD COLUMN Created DATETIME NULL;
-ALTER TABLE GroupMembers ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0;
-ALTER TABLE GroupMembers ADD COLUMN LastUpdated DATETIME NULL;
-ALTER TABLE ACL ADD COLUMN Creator integer NOT NULL DEFAULT 0;
-ALTER TABLE ACL ADD COLUMN Created DATETIME NULL;
-ALTER TABLE ACL ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0;
-ALTER TABLE ACL ADD COLUMN LastUpdated DATETIME NULL;
+ALTER TABLE Groups ADD COLUMN Creator integer NOT NULL DEFAULT 0,
+ ADD COLUMN Created DATETIME NULL,
+ ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0,
+ ADD COLUMN LastUpdated DATETIME NULL;
+ALTER TABLE GroupMembers ADD COLUMN Creator integer NOT NULL DEFAULT 0,
+ ADD COLUMN Created DATETIME NULL,
+ ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0,
+ ADD COLUMN LastUpdated DATETIME NULL;
+ALTER TABLE ACL ADD COLUMN Creator integer NOT NULL DEFAULT 0,
+ ADD COLUMN Created DATETIME NULL,
+ ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0,
+ ADD COLUMN LastUpdated DATETIME NULL;
diff --git a/rt/etc/upgrade/3.9.7/schema.mysql b/rt/etc/upgrade/3.9.7/schema.mysql
index 1be165647..4cbed6cc7 100644
--- a/rt/etc/upgrade/3.9.7/schema.mysql
+++ b/rt/etc/upgrade/3.9.7/schema.mysql
@@ -1,6 +1,6 @@
ALTER TABLE Users ADD COLUMN AuthToken VARCHAR(16) CHARACTER SET ascii NULL;
-ALTER TABLE CustomFields ADD COLUMN BasedOn INTEGER NULL;
-ALTER TABLE CustomFields ADD COLUMN RenderType VARCHAR(64) NULL;
-ALTER TABLE CustomFields ADD COLUMN ValuesClass VARCHAR(64) CHARACTER SET ascii NULL;
-ALTER TABLE Queues ADD COLUMN SubjectTag VARCHAR(120) NULL;
-ALTER TABLE Queues ADD COLUMN Lifecycle VARCHAR(32) NULL;
+ALTER TABLE CustomFields ADD COLUMN BasedOn INTEGER NULL,
+ ADD COLUMN RenderType VARCHAR(64) NULL,
+ ADD COLUMN ValuesClass VARCHAR(64) CHARACTER SET ascii NULL;
+ALTER TABLE Queues ADD COLUMN SubjectTag VARCHAR(120) NULL,
+ ADD COLUMN Lifecycle VARCHAR(32) NULL;
diff --git a/rt/lib/RT.pm b/rt/lib/RT.pm
index 063f7f719..4372a564d 100644
--- a/rt/lib/RT.pm
+++ b/rt/lib/RT.pm
@@ -138,6 +138,8 @@ up logging|/InitLogging>, and L<loads plugins|/InitPlugins>.
sub Init {
+ my @arg = @_;
+
CheckPerlRequirements();
InitPluginPaths();
@@ -146,7 +148,7 @@ sub Init {
ConnectToDatabase();
InitSystemObjects();
InitClasses();
- InitLogging();
+ InitLogging(@arg);
InitPlugins();
RT::I18N->Init;
RT->Config->PostLoadCheck;
@@ -174,6 +176,8 @@ Create the Logger object and set up signal handlers.
sub InitLogging {
+ my %arg = @_;
+
# We have to set the record separator ($, man perlvar)
# or Log::Dispatch starts getting
# really pissy, as some other module we use unsets it.
@@ -309,11 +313,14 @@ sub InitLogging {
));
}
}
- InitSignalHandlers();
+ InitSignalHandlers(%arg);
}
sub InitSignalHandlers {
+ my %arg = @_;
+ return if $arg{'NoSignalHandlers'};
+
# Signal handlers
## This is the default handling of warnings and die'ings in the code
## (including other used modules - maybe except for errors catched by
diff --git a/rt/lib/RT.pm.in b/rt/lib/RT.pm.in
deleted file mode 100644
index fafd2b778..000000000
--- a/rt/lib/RT.pm.in
+++ /dev/null
@@ -1,719 +0,0 @@
-# BEGIN BPS TAGGED BLOCK {{{
-#
-# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
-# <sales@bestpractical.com>
-#
-# (Except where explicitly superseded by other copyright notices)
-#
-#
-# LICENSE:
-#
-# This work is made available to you under the terms of Version 2 of
-# the GNU General Public License. A copy of that license should have
-# been provided with this software, but in any event can be snarfed
-# from www.gnu.org.
-#
-# This work is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-# 02110-1301 or visit their web page on the internet at
-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
-#
-#
-# CONTRIBUTION SUBMISSION POLICY:
-#
-# (The following paragraph is not intended to limit the rights granted
-# to you to modify and distribute this software under the terms of
-# the GNU General Public License and is only of importance to you if
-# you choose to contribute your changes and enhancements to the
-# community by submitting them to Best Practical Solutions, LLC.)
-#
-# By intentionally submitting any modifications, corrections or
-# derivatives to this work, or any other work intended for use with
-# Request Tracker, to Best Practical Solutions, LLC, you confirm that
-# you are the copyright holder for those contributions and you grant
-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
-# royalty-free, perpetual, license to use, copy, create derivative
-# works based on those contributions, and sublicense and distribute
-# those contributions and any derivatives thereof.
-#
-# END BPS TAGGED BLOCK }}}
-
-use strict;
-use warnings;
-
-package RT;
-
-
-use File::Spec ();
-use Cwd ();
-
-use vars qw($Config $System $SystemUser $Nobody $Handle $Logger $_INSTALL_MODE);
-
-our $VERSION = '@RT_VERSION_MAJOR@.@RT_VERSION_MINOR@.@RT_VERSION_PATCH@';
-
-@DATABASE_ENV_PREF@
-
-our $BasePath = '@RT_PATH@';
-our $EtcPath = '@RT_ETC_PATH@';
-our $BinPath = '@RT_BIN_PATH@';
-our $SbinPath = '@RT_SBIN_PATH@';
-our $VarPath = '@RT_VAR_PATH@';
-our $PluginPath = '@RT_PLUGIN_PATH@';
-our $LocalPath = '@RT_LOCAL_PATH@';
-our $LocalEtcPath = '@LOCAL_ETC_PATH@';
-our $LocalLibPath = '@LOCAL_LIB_PATH@';
-our $LocalLexiconPath = '@LOCAL_LEXICON_PATH@';
-our $LocalPluginPath = $LocalPath."/plugins";
-
-
-# $MasonComponentRoot is where your rt instance keeps its mason html files
-
-our $MasonComponentRoot = '@MASON_HTML_PATH@';
-
-# $MasonLocalComponentRoot is where your rt instance keeps its site-local
-# mason html files.
-
-our $MasonLocalComponentRoot = '@MASON_LOCAL_HTML_PATH@';
-
-# $MasonDataDir Where mason keeps its datafiles
-
-our $MasonDataDir = '@MASON_DATA_PATH@';
-
-# RT needs to put session data (for preserving state between connections
-# via the web interface)
-our $MasonSessionDir = '@MASON_SESSION_PATH@';
-
-unless ( File::Spec->file_name_is_absolute($EtcPath) ) {
-
-# if BasePath exists and is absolute, we won't infer it from $INC{'RT.pm'}.
-# otherwise RT.pm will make src dir(where we configure RT) be the BasePath
-# instead of the --prefix one
- unless ( -d $BasePath && File::Spec->file_name_is_absolute($BasePath) ) {
- my $pm_path = ( File::Spec->splitpath( $INC{'RT.pm'} ) )[1];
-
- # need rel2abs here is to make sure path is absolute, since $INC{'RT.pm'}
- # is not always absolute
- $BasePath =
- File::Spec->rel2abs(
- File::Spec->catdir( $pm_path, File::Spec->updir ) );
- }
-
- $BasePath = Cwd::realpath( $BasePath );
-
- for my $path ( qw/EtcPath BinPath SbinPath VarPath LocalPath LocalEtcPath
- LocalLibPath LocalLexiconPath PluginPath LocalPluginPath
- MasonComponentRoot MasonLocalComponentRoot MasonDataDir
- MasonSessionDir/ ) {
- no strict 'refs';
- # just change relative ones
- $$path = File::Spec->catfile( $BasePath, $$path )
- unless File::Spec->file_name_is_absolute( $$path );
- }
-}
-
-
-=head1 NAME
-
-RT - Request Tracker
-
-=head1 SYNOPSIS
-
-A fully featured request tracker package
-
-=head1 DESCRIPTION
-
-=head2 INITIALIZATION
-
-=head2 LoadConfig
-
-Load RT's config file. First, the site configuration file
-(F<RT_SiteConfig.pm>) is loaded, in order to establish overall site
-settings like hostname and name of RT instance. Then, the core
-configuration file (F<RT_Config.pm>) is loaded to set fallback values
-for all settings; it bases some values on settings from the site
-configuration file.
-
-In order for the core configuration to not override the site's
-settings, the function C<Set> is used; it only sets values if they
-have not been set already.
-
-=cut
-
-sub LoadConfig {
- require RT::Config;
- $Config = new RT::Config;
- $Config->LoadConfigs;
- require RT::I18N;
-
- # RT::Essentials mistakenly recommends that WebPath be set to '/'.
- # If the user does that, do what they mean.
- $RT::WebPath = '' if ($RT::WebPath eq '/');
-
- # fix relative LogDir and GnuPG homedir
- unless ( File::Spec->file_name_is_absolute( $Config->Get('LogDir') ) ) {
- $Config->Set( LogDir =>
- File::Spec->catfile( $BasePath, $Config->Get('LogDir') ) );
- }
-
- my $gpgopts = $Config->Get('GnuPGOptions');
- unless ( File::Spec->file_name_is_absolute( $gpgopts->{homedir} ) ) {
- $gpgopts->{homedir} = File::Spec->catfile( $BasePath, $gpgopts->{homedir} );
- }
-
- RT::I18N->Init;
-}
-
-=head2 Init
-
-L<Connect to the database /ConnectToDatabase>, L<initilizes system objects /InitSystemObjects>,
-L<preloads classes /InitClasses> and L<set up logging /InitLogging>.
-
-=cut
-
-sub Init {
-
- my @arg = @_;
-
- CheckPerlRequirements();
-
- InitPluginPaths();
-
- #Get a database connection
- ConnectToDatabase();
- InitSystemObjects();
- InitClasses();
- InitLogging(@arg);
- InitPlugins();
- RT->Config->PostLoadCheck;
-
-}
-
-=head2 ConnectToDatabase
-
-Get a database connection. See also </Handle>.
-
-=cut
-
-sub ConnectToDatabase {
- require RT::Handle;
- $Handle = new RT::Handle unless $Handle;
- $Handle->Connect;
- return $Handle;
-}
-
-=head2 InitLogging
-
-Create the Logger object and set up signal handlers.
-
-=cut
-
-sub InitLogging {
-
- my %arg = @_;
-
- # We have to set the record separator ($, man perlvar)
- # or Log::Dispatch starts getting
- # really pissy, as some other module we use unsets it.
- $, = '';
- use Log::Dispatch 1.6;
-
- my %level_to_num = (
- map( { $_ => } 0..7 ),
- debug => 0,
- info => 1,
- notice => 2,
- warning => 3,
- error => 4, 'err' => 4,
- critical => 5, crit => 5,
- alert => 6,
- emergency => 7, emerg => 7,
- );
-
- unless ( $RT::Logger ) {
-
- $RT::Logger = Log::Dispatch->new;
-
- my $stack_from_level;
- if ( $stack_from_level = RT->Config->Get('LogStackTraces') ) {
- # if option has old style '\d'(true) value
- $stack_from_level = 0 if $stack_from_level =~ /^\d+$/;
- $stack_from_level = $level_to_num{ $stack_from_level } || 0;
- } else {
- $stack_from_level = 99; # don't log
- }
-
- my $simple_cb = sub {
- # if this code throw any warning we can get segfault
- no warnings;
- my %p = @_;
-
- # skip Log::* stack frames
- my $frame = 0;
- $frame++ while caller($frame) && caller($frame) =~ /^Log::/;
- my ($package, $filename, $line) = caller($frame);
-
- $p{'message'} =~ s/(?:\r*\n)+$//;
- return "[". gmtime(time) ."] [". $p{'level'} ."]: "
- . $p{'message'} ." ($filename:$line)\n";
- };
-
- my $syslog_cb = sub {
- # if this code throw any warning we can get segfault
- no warnings;
- my %p = @_;
-
- my $frame = 0; # stack frame index
- # skip Log::* stack frames
- $frame++ while caller($frame) && caller($frame) =~ /^Log::/;
- my ($package, $filename, $line) = caller($frame);
-
- # syswrite() cannot take utf8; turn it off here.
- Encode::_utf8_off($p{message});
-
- $p{message} =~ s/(?:\r*\n)+$//;
- if ($p{level} eq 'debug') {
- return "$p{message}\n";
- } else {
- return "$p{message} ($filename:$line)\n";
- }
- };
-
- my $stack_cb = sub {
- no warnings;
- my %p = @_;
- return $p{'message'} unless $level_to_num{ $p{'level'} } >= $stack_from_level;
-
- require Devel::StackTrace;
- my $trace = Devel::StackTrace->new( ignore_class => [ 'Log::Dispatch', 'Log::Dispatch::Base' ] );
- return $p{'message'} . $trace->as_string;
-
- # skip calling of the Log::* subroutins
- my $frame = 0;
- $frame++ while caller($frame) && caller($frame) =~ /^Log::/;
- $frame++ while caller($frame) && (caller($frame))[3] =~ /^Log::/;
-
- $p{'message'} .= "\nStack trace:\n";
- while( my ($package, $filename, $line, $sub) = caller($frame++) ) {
- $p{'message'} .= "\t$sub(...) called at $filename:$line\n";
- }
- return $p{'message'};
- };
-
- if ( $Config->Get('LogToFile') ) {
- my ($filename, $logdir) = (
- $Config->Get('LogToFileNamed') || 'rt.log',
- $Config->Get('LogDir') || File::Spec->catdir( $VarPath, 'log' ),
- );
- if ( $filename =~ m![/\\]! ) { # looks like an absolute path.
- ($logdir) = $filename =~ m{^(.*[/\\])};
- }
- else {
- $filename = File::Spec->catfile( $logdir, $filename );
- }
-
- unless ( -d $logdir && ( ( -f $filename && -w $filename ) || -w $logdir ) ) {
- # localizing here would be hard when we don't have a current user yet
- die "Log file '$filename' couldn't be written or created.\n RT can't run.";
- }
-
- require Log::Dispatch::File;
- $RT::Logger->add( Log::Dispatch::File->new
- ( name=>'file',
- min_level=> $Config->Get('LogToFile'),
- filename=> $filename,
- mode=>'append',
- callbacks => [ $simple_cb, $stack_cb ],
- ));
- }
- if ( $Config->Get('LogToScreen') ) {
- require Log::Dispatch::Screen;
- $RT::Logger->add( Log::Dispatch::Screen->new
- ( name => 'screen',
- min_level => $Config->Get('LogToScreen'),
- callbacks => [ $simple_cb, $stack_cb ],
- stderr => 1,
- ));
- }
- if ( $Config->Get('LogToSyslog') ) {
- require Log::Dispatch::Syslog;
- $RT::Logger->add(Log::Dispatch::Syslog->new
- ( name => 'syslog',
- ident => 'RT',
- min_level => $Config->Get('LogToSyslog'),
- callbacks => [ $syslog_cb, $stack_cb ],
- stderr => 1,
- $Config->Get('LogToSyslogConf'),
- ));
- }
- }
- InitSignalHandlers(%arg);
-}
-
-sub InitSignalHandlers {
-
- my %arg = @_;
-
-# Signal handlers
-## This is the default handling of warnings and die'ings in the code
-## (including other used modules - maybe except for errors catched by
-## Mason). It will log all problems through the standard logging
-## mechanism (see above).
-
- unless ( $arg{'NoSignalHandlers'} ) {
-
- $SIG{__WARN__} = sub {
- # The 'wide character' warnings has to be silenced for now, at least
- # until HTML::Mason offers a sane way to process both raw output and
- # unicode strings.
- # use 'goto &foo' syntax to hide ANON sub from stack
- if( index($_[0], 'Wide character in ') != 0 ) {
- unshift @_, $RT::Logger, qw(level warning message);
- goto &Log::Dispatch::log;
- }
- };
-
- #When we call die, trap it and log->crit with the value of the die.
-
- $SIG{__DIE__} = sub {
- # if we are not in eval and perl is not parsing code
- # then rollback transactions and log RT error
- unless ($^S || !defined $^S ) {
- $RT::Handle->Rollback(1) if $RT::Handle;
- $RT::Logger->crit("$_[0]") if $RT::Logger;
- }
- die $_[0];
- };
-
- }
-}
-
-
-sub CheckPerlRequirements {
- if ($^V < 5.008003) {
- die sprintf "RT requires Perl v5.8.3 or newer. Your current Perl is v%vd\n", $^V;
- }
-
- # use $error here so the following "die" can still affect the global $@
- my $error;
- {
- local $@;
- eval {
- my $x = '';
- my $y = \$x;
- require Scalar::Util;
- Scalar::Util::weaken($y);
- };
- $error = $@;
- }
-
- if ($error) {
- die <<"EOF";
-
-RT requires the Scalar::Util module be built with support for the 'weaken'
-function.
-
-It is sometimes the case that operating system upgrades will replace
-a working Scalar::Util with a non-working one. If your system was working
-correctly up until now, this is likely the cause of the problem.
-
-Please reinstall Scalar::Util, being careful to let it build with your C
-compiler. Ususally this is as simple as running the following command as
-root.
-
- perl -MCPAN -e'install Scalar::Util'
-
-EOF
-
- }
-}
-
-=head2 InitClasses
-
-Load all modules that define base classes.
-
-=cut
-
-sub InitClasses {
- shift if @_%2; # so we can call it as a function or method
- my %args = (@_);
- require RT::Tickets;
- require RT::Transactions;
- require RT::Attachments;
- require RT::Users;
- require RT::Principals;
- require RT::CurrentUser;
- require RT::Templates;
- require RT::Queues;
- require RT::ScripActions;
- require RT::ScripConditions;
- require RT::Scrips;
- require RT::Groups;
- require RT::GroupMembers;
- require RT::CustomFields;
- require RT::CustomFieldValues;
- require RT::ObjectCustomFields;
- require RT::ObjectCustomFieldValues;
- require RT::Attributes;
- require RT::Dashboard;
- require RT::Approval;
-
- # on a cold server (just after restart) people could have an object
- # in the session, as we deserialize it so we never call constructor
- # of the class, so the list of accessible fields is empty and we die
- # with "Method xxx is not implemented in RT::SomeClass"
- $_->_BuildTableAttributes foreach qw(
- RT::Ticket
- RT::Transaction
- RT::Attachment
- RT::User
- RT::Principal
- RT::Template
- RT::Queue
- RT::ScripAction
- RT::ScripCondition
- RT::Scrip
- RT::Group
- RT::GroupMember
- RT::CustomField
- RT::CustomFieldValue
- RT::ObjectCustomField
- RT::ObjectCustomFieldValue
- RT::Attribute
- );
-
- if ( $args{'Heavy'} ) {
- # load scrips' modules
- my $scrips = RT::Scrips->new($RT::SystemUser);
- $scrips->Limit( FIELD => 'Stage', OPERATOR => '!=', VALUE => 'Disabled' );
- while ( my $scrip = $scrips->Next ) {
- local $@;
- eval { $scrip->LoadModules } or
- $RT::Logger->error("Invalid Scrip ".$scrip->Id.". Unable to load the Action or Condition. ".
- "You should delete or repair this Scrip in the admin UI.\n$@\n");
- }
-
- foreach my $class ( grep $_, RT->Config->Get('CustomFieldValuesSources') ) {
- local $@;
- eval "require $class; 1" or $RT::Logger->error(
- "Class '$class' is listed in CustomFieldValuesSources option"
- ." in the config, but we failed to load it:\n$@\n"
- );
- }
-
- RT::I18N->LoadLexicons;
- }
-}
-
-=head2 InitSystemObjects
-
-Initializes system objects: C<$RT::System>, C<$RT::SystemUser>
-and C<$RT::Nobody>.
-
-=cut
-
-sub InitSystemObjects {
-
- #RT's system user is a genuine database user. its id lives here
- require RT::CurrentUser;
- $SystemUser = new RT::CurrentUser;
- $SystemUser->LoadByName('RT_System');
-
- #RT's "nobody user" is a genuine database user. its ID lives here.
- $Nobody = new RT::CurrentUser;
- $Nobody->LoadByName('Nobody');
-
- require RT::System;
- $System = RT::System->new( $SystemUser );
-}
-
-=head1 CLASS METHODS
-
-=head2 Config
-
-Returns the current L<config object RT::Config>, but note that
-you must L<load config /LoadConfig> first otherwise this method
-returns undef.
-
-Method can be called as class method.
-
-=cut
-
-sub Config { return $Config }
-
-=head2 DatabaseHandle
-
-Returns the current L<database handle object RT::Handle>.
-
-See also L</ConnectToDatabase>.
-
-=cut
-
-sub DatabaseHandle { return $Handle }
-
-=head2 Logger
-
-Returns the logger. See also L</InitLogging>.
-
-=cut
-
-sub Logger { return $Logger }
-
-=head2 System
-
-Returns the current L<system object RT::System>. See also
-L</InitSystemObjects>.
-
-=cut
-
-sub System { return $System }
-
-=head2 SystemUser
-
-Returns the system user's object, it's object of
-L<RT::CurrentUser> class that represents the system. See also
-L</InitSystemObjects>.
-
-=cut
-
-sub SystemUser { return $SystemUser }
-
-=head2 Nobody
-
-Returns object of Nobody. It's object of L<RT::CurrentUser> class
-that represents a user who can own ticket and nothing else. See
-also L</InitSystemObjects>.
-
-=cut
-
-sub Nobody { return $Nobody }
-
-=head2 Plugins
-
-Returns a listref of all Plugins currently configured for this RT instance.
-You can define plugins by adding them to the @Plugins list in your RT_SiteConfig
-
-=cut
-
-our @PLUGINS = ();
-sub Plugins {
- my $self = shift;
- unless (@PLUGINS) {
- $self->InitPluginPaths;
- @PLUGINS = $self->InitPlugins;
- }
- return \@PLUGINS;
-}
-
-=head2 PluginDirs
-
-Takes optional subdir (e.g. po, lib, etc.) and return plugins' dirs that exist.
-
-This code chacke plugins names or anything else and required when main config
-is loaded to load plugins' configs.
-
-=cut
-
-sub PluginDirs {
- my $self = shift;
- my $subdir = shift;
-
- require RT::Plugin;
-
- my @res;
- foreach my $plugin (grep $_, RT->Config->Get('Plugins')) {
- my $path = RT::Plugin->new( name => $plugin )->Path( $subdir );
- next unless -d $path;
- push @res, $path;
- }
- return @res;
-}
-
-=head2 InitPluginPaths
-
-Push plugins' lib paths into @INC right after F<local/lib>.
-In case F<local/lib> isn't in @INC, append them to @INC
-
-=cut
-
-sub InitPluginPaths {
- my $self = shift || __PACKAGE__;
-
- my @lib_dirs = $self->PluginDirs('lib');
-
- my @tmp_inc;
- my $added;
- for (@INC) {
- if ( Cwd::realpath($_) eq $RT::LocalLibPath) {
- push @tmp_inc, $_, @lib_dirs;
- $added = 1;
- } else {
- push @tmp_inc, $_;
- }
- }
-
- # append @lib_dirs in case $RT::LocalLibPath isn't in @INC
- push @tmp_inc, @lib_dirs unless $added;
-
- my %seen;
- @INC = grep !$seen{$_}++, @tmp_inc;
-}
-
-=head2 InitPlugins
-
-Initialze all Plugins found in the RT configuration file, setting up their lib and HTML::Mason component roots.
-
-=cut
-
-sub InitPlugins {
- my $self = shift;
- my @plugins;
- require RT::Plugin;
- foreach my $plugin (grep $_, RT->Config->Get('Plugins')) {
- $plugin->require;
- die $UNIVERSAL::require::ERROR if ($UNIVERSAL::require::ERROR);
- push @plugins, RT::Plugin->new(name =>$plugin);
- }
- return @plugins;
-}
-
-
-sub InstallMode {
- my $self = shift;
- if (@_) {
- $_INSTALL_MODE = shift;
- if($_INSTALL_MODE) {
- require RT::CurrentUser;
- $SystemUser = RT::CurrentUser->new();
- }
- }
- return $_INSTALL_MODE;
-}
-
-
-=head1 BUGS
-
-Please report them to rt-bugs@bestpractical.com, if you know what's
-broken and have at least some idea of what needs to be fixed.
-
-If you're not sure what's going on, report them rt-devel@lists.bestpractical.com.
-
-=head1 SEE ALSO
-
-L<RT::StyleGuide>
-L<DBIx::SearchBuilder>
-
-
-=cut
-
-require RT::Base;
-RT::Base->_ImportOverlays();
-
-1;
diff --git a/rt/lib/RT/Action/CreateTickets.pm b/rt/lib/RT/Action/CreateTickets.pm
index 31489c8ff..efd2bdaf6 100644
--- a/rt/lib/RT/Action/CreateTickets.pm
+++ b/rt/lib/RT/Action/CreateTickets.pm
@@ -567,7 +567,8 @@ sub Parse {
$self->_ParseMultilineTemplate(%args);
} elsif ( $args{'Content'} =~ /(?:\t|,)/i ) {
$self->_ParseXSVTemplate(%args);
-
+ } else {
+ RT->Logger->error("Invalid Template Content (Couldn't find ===, and is not a csv/tsv template) - unable to parse: $args{Content}");
}
}
diff --git a/rt/lib/RT/Articles.pm b/rt/lib/RT/Articles.pm
index 8dd661d2e..47d0ebea2 100644
--- a/rt/lib/RT/Articles.pm
+++ b/rt/lib/RT/Articles.pm
@@ -360,6 +360,7 @@ sub LimitCustomField {
QUOTEVALUE => $args{'QUOTEVALUE'},
ENTRYAGGREGATOR => 'AND', #$args{'ENTRYAGGREGATOR'},
SUBCLAUSE => $clause,
+ CASESENSITIVE => 0,
);
$self->SUPER::Limit(
ALIAS => $ObjectValuesAlias,
@@ -380,6 +381,7 @@ sub LimitCustomField {
QUOTEVALUE => $args{'QUOTEVALUE'},
ENTRYAGGREGATOR => $args{'ENTRYAGGREGATOR'},
SUBCLAUSE => $clause,
+ CASESENSITIVE => 0,
);
$self->SUPER::Limit(
ALIAS => $ObjectValuesAlias,
@@ -389,6 +391,7 @@ sub LimitCustomField {
QUOTEVALUE => $args{'QUOTEVALUE'},
ENTRYAGGREGATOR => $args{'ENTRYAGGREGATOR'},
SUBCLAUSE => $clause,
+ CASESENSITIVE => 0,
);
}
}
diff --git a/rt/lib/RT/Config.pm b/rt/lib/RT/Config.pm
index f87ef84c9..014c76468 100644
--- a/rt/lib/RT/Config.pm
+++ b/rt/lib/RT/Config.pm
@@ -411,8 +411,8 @@ our %META = (
Description => q|What tickets to display in the 'More about requestor' box|, #loc
Values => [qw(Active Inactive All None)],
ValuesLabel => {
- Active => "Show the Requestor's 10 highest priority open tickets", #loc
- Inactive => "Show the Requestor's 10 highest priority closed tickets", #loc
+ Active => "Show the Requestor's 10 highest priority active tickets", #loc
+ Inactive => "Show the Requestor's 10 highest priority inactive tickets", #loc
All => "Show the Requestor's 10 highest priority tickets", #loc
None => "Show no tickets for the Requestor", #loc
},
@@ -749,7 +749,7 @@ our %META = (
my %seen;
foreach my $encoding ( grep defined && length, splice @$value ) {
- next if $seen{ $encoding }++;
+ next if $seen{ $encoding };
if ( $encoding eq '*' ) {
unshift @$value, '*';
next;
diff --git a/rt/lib/RT/Crypt/GnuPG.pm b/rt/lib/RT/Crypt/GnuPG.pm
index ab444d068..c5fb12bef 100644
--- a/rt/lib/RT/Crypt/GnuPG.pm
+++ b/rt/lib/RT/Crypt/GnuPG.pm
@@ -1683,6 +1683,7 @@ my %ignore_keyword = map { $_ => 1 } qw(
BEGIN_ENCRYPTION SIG_ID VALIDSIG
ENC_TO BEGIN_DECRYPTION END_DECRYPTION GOODMDC
TRUST_UNDEFINED TRUST_NEVER TRUST_MARGINAL TRUST_FULLY TRUST_ULTIMATE
+ DECRYPTION_INFO
);
sub ParseStatus {
diff --git a/rt/lib/RT/Dashboard.pm b/rt/lib/RT/Dashboard.pm
index 14ffa6ad3..2e2bbc489 100644
--- a/rt/lib/RT/Dashboard.pm
+++ b/rt/lib/RT/Dashboard.pm
@@ -454,6 +454,36 @@ sub CurrentUserCanCreateAny {
return 0;
}
+=head2 Delete
+
+Deletes the dashboard and related subscriptions.
+Returns a tuple of status and message, where status is true upon success.
+
+=cut
+
+sub Delete {
+ my $self = shift;
+ my $id = $self->id;
+ my ( $status, $msg ) = $self->SUPER::Delete(@_);
+ if ( $status ) {
+ # delete all the subscriptions
+ my $subscriptions = RT::Attributes->new( RT->SystemUser );
+ $subscriptions->Limit(
+ FIELD => 'Name',
+ VALUE => 'Subscription',
+ );
+ $subscriptions->Limit(
+ FIELD => 'Description',
+ VALUE => "Subscription to dashboard $id",
+ );
+ while ( my $subscription = $subscriptions->Next ) {
+ $subscription->Delete();
+ }
+ }
+
+ return ( $status, $msg );
+}
+
RT::Base->_ImportOverlays();
1;
diff --git a/rt/lib/RT/Generated.pm b/rt/lib/RT/Generated.pm
index 2abcf3b6e..9fd946f5b 100644
--- a/rt/lib/RT/Generated.pm
+++ b/rt/lib/RT/Generated.pm
@@ -50,7 +50,7 @@ package RT;
use warnings;
use strict;
-our $VERSION = '4.0.6';
+our $VERSION = '4.0.7';
diff --git a/rt/lib/RT/I18N.pm b/rt/lib/RT/I18N.pm
index cadf7cc7c..e453cfa04 100644
--- a/rt/lib/RT/I18N.pm
+++ b/rt/lib/RT/I18N.pm
@@ -227,7 +227,7 @@ sub SetMIMEEntityToEncoding {
my $body = $entity->bodyhandle;
- if ( $enc ne $charset && $body ) {
+ if ( $body && ($enc ne $charset || $enc =~ /^utf-?8(?:-strict)?$/i) ) {
my $string = $body->as_string or return;
$RT::Logger->debug( "Converting '$charset' to '$enc' for "
@@ -335,7 +335,7 @@ sub DecodeMIMEWordsToEncoding {
}
# now we have got a decoded subject, try to convert into the encoding
- unless ( $charset eq $to_charset ) {
+ if ( $charset ne $to_charset || $charset =~ /^utf-?8(?:-strict)?$/i ) {
Encode::from_to( $enc_str, $charset, $to_charset );
}
@@ -537,7 +537,7 @@ sub SetMIMEHeadToEncoding {
my @values = $head->get_all($tag);
$head->delete($tag);
foreach my $value (@values) {
- if ( $charset ne $enc ) {
+ if ( $charset ne $enc || $enc =~ /^utf-?8(?:-strict)?$/i ) {
Encode::_utf8_off($value);
Encode::from_to( $value, $charset => $enc );
}
diff --git a/rt/lib/RT/Interface/Email.pm b/rt/lib/RT/Interface/Email.pm
index 02a1ec0c0..4c3ee9986 100755
--- a/rt/lib/RT/Interface/Email.pm
+++ b/rt/lib/RT/Interface/Email.pm
@@ -787,7 +787,7 @@ sub GetForwardFrom {
my $ticket = $args{Ticket} || $txn->Object;
if ( RT->Config->Get('ForwardFromUser') ) {
- return ( $txn || $ticket )->CurrentUser->UserObj->EmailAddress;
+ return ( $txn || $ticket )->CurrentUser->EmailAddress;
}
else {
return $ticket->QueueObj->CorrespondAddress
@@ -1221,8 +1221,16 @@ sub SetInReplyTo {
if @references > 10;
my $mail = $args{'Message'};
- $mail->head->set( 'In-Reply-To' => join ' ', @rtid? (@rtid) : (@id) ) if @id || @rtid;
- $mail->head->set( 'References' => join ' ', @references );
+ $mail->head->set( 'In-Reply-To' => Encode::encode_utf8(join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid;
+ $mail->head->set( 'References' => Encode::encode_utf8(join ' ', @references) );
+}
+
+sub ExtractTicketId {
+ my $entity = shift;
+
+ my $subject = $entity->head->get('Subject') || '';
+ chomp $subject;
+ return ParseTicketId( $subject );
}
sub ParseTicketId {
@@ -1448,7 +1456,7 @@ sub Gateway {
}
# }}}
- $args{'ticket'} ||= ParseTicketId( $Subject );
+ $args{'ticket'} ||= ExtractTicketId( $Message );
$SystemTicket = RT::Ticket->new( RT->SystemUser );
$SystemTicket->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ;
@@ -1704,17 +1712,20 @@ sub _RunUnsafeAction {
return ( 0, "Ticket not taken" );
}
} elsif ( $args{'Action'} =~ /^resolve$/i ) {
- my ( $status, $msg ) = $args{'Ticket'}->SetStatus('resolved');
- unless ($status) {
+ my $new_status = $args{'Ticket'}->FirstInactiveStatus;
+ if ($new_status) {
+ my ( $status, $msg ) = $args{'Ticket'}->SetStatus($new_status);
+ unless ($status) {
- #Warn the sender that we couldn't actually submit the comment.
- MailError(
- To => $args{'ErrorsTo'},
- Subject => "Ticket not resolved",
- Explanation => $msg,
- MIMEObj => $args{'Message'}
- );
- return ( 0, "Ticket not resolved" );
+ #Warn the sender that we couldn't actually submit the comment.
+ MailError(
+ To => $args{'ErrorsTo'},
+ Subject => "Ticket not resolved",
+ Explanation => $msg,
+ MIMEObj => $args{'Message'}
+ );
+ return ( 0, "Ticket not resolved" );
+ }
}
} else {
return ( 0, "Not supported unsafe action $args{'Action'}", $args{'Ticket'} );
diff --git a/rt/lib/RT/Interface/Web.pm b/rt/lib/RT/Interface/Web.pm
index 94da3072d..1aae7581e 100644
--- a/rt/lib/RT/Interface/Web.pm
+++ b/rt/lib/RT/Interface/Web.pm
@@ -261,7 +261,15 @@ sub HandleRequest {
$HTML::Mason::Commands::m->comp( '/Elements/SetupSessionCookie', %$ARGS );
SendSessionCookie();
- $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new() unless _UserLoggedIn();
+
+ if ( _UserLoggedIn() ) {
+ # make user info up to date
+ $HTML::Mason::Commands::session{'CurrentUser'}
+ ->Load( $HTML::Mason::Commands::session{'CurrentUser'}->id );
+ }
+ else {
+ $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
+ }
# Process session-related callbacks before any auth attempts
$HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Session', CallbackPage => '/autohandler' );
@@ -287,7 +295,7 @@ sub HandleRequest {
my $m = $HTML::Mason::Commands::m;
# REST urls get a special 401 response
- if ($m->request_comp->path =~ '^/REST/\d+\.\d+/') {
+ if ($m->request_comp->path =~ m{^/REST/\d+\.\d+/}) {
$HTML::Mason::Commands::r->content_type("text/plain");
$m->error_format("text");
$m->out("RT/$RT::VERSION 401 Credentials required\n");
@@ -457,7 +465,7 @@ sub MaybeShowInstallModePage {
my $m = $HTML::Mason::Commands::m;
if ( $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex') ) {
$m->call_next();
- } elsif ( $m->request_comp->path !~ '^(/+)Install/' ) {
+ } elsif ( $m->request_comp->path !~ m{^(/+)Install/} ) {
RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "Install/index.html" );
} else {
$m->call_next();
@@ -557,7 +565,7 @@ sub ShowRequestedPage {
unless ( $HTML::Mason::Commands::session{'CurrentUser'}->Privileged ) {
# if the user is trying to access a ticket, redirect them
- if ( $m->request_comp->path =~ '^(/+)Ticket/Display.html' && $ARGS->{'id'} ) {
+ if ( $m->request_comp->path =~ m{^(/+)Ticket/Display.html} && $ARGS->{'id'} ) {
RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "SelfService/Display.html?id=" . $ARGS->{'id'} );
}
@@ -659,7 +667,7 @@ sub AttemptExternalAuth {
delete $HTML::Mason::Commands::session{'CurrentUser'};
$user = $orig_user;
- if ( RT->Config->Get('WebExternalOnly') ) {
+ unless ( RT->Config->Get('WebFallbackToInternalAuth') ) {
TangentForLoginWithError('You are not an authorized user');
}
}
@@ -970,7 +978,7 @@ sub MobileClient {
my $self = shift;
-if (($ENV{'HTTP_USER_AGENT'} || '') =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS|S60)/io && !$HTML::Mason::Commands::session{'NotMobile'}) {
+if (($ENV{'HTTP_USER_AGENT'} || '') =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS|S60|Mobile)/io && !$HTML::Mason::Commands::session{'NotMobile'}) {
return 1;
} else {
return undef;
@@ -1183,6 +1191,14 @@ our %is_whitelisted_component = (
# information for the search. Because it's a straight-up read, in
# addition to embedding its own auth, it's fine.
'/NoAuth/rss/dhandler' => 1,
+
+ # While these can be used for denial-of-service against RT
+ # (construct a very inefficient query and trick lots of users into
+ # running them against RT) it's incredibly useful to be able to link
+ # to a search result or bookmark a result page.
+ '/Search/Results.html' => 1,
+ '/Search/Simple.html' => 1,
+ '/m/tickets/search' => 1,
);
sub IsCompCSRFWhitelisted {
@@ -1237,7 +1253,19 @@ sub IsRefererCSRFWhitelisted {
my $configs;
for my $config ( $base_url, RT->Config->Get('ReferrerWhitelist') ) {
push @$configs,$config;
- return 1 if $referer->host_port eq $config;
+
+ my $host_port = $referer->host_port;
+ if ($config =~ /\*/) {
+ # Turn a literal * into a domain component or partial component match.
+ # Refer to http://tools.ietf.org/html/rfc2818#page-5
+ my $regex = join "[a-zA-Z0-9\-]*",
+ map { quotemeta($_) }
+ split /\*/, $config;
+
+ return 1 if $host_port =~ /^$regex$/i;
+ } else {
+ return 1 if $host_port eq $config;
+ }
}
return (0,$referer,$configs);
@@ -1962,7 +1990,7 @@ sub MakeMIMEEntity {
);
my $Message = MIME::Entity->build(
Type => 'multipart/mixed',
- "Message-Id" => RT::Interface::Email::GenMessageId,
+ "Message-Id" => Encode::encode_utf8( RT::Interface::Email::GenMessageId ),
map { $_ => Encode::encode_utf8( $args{ $_} ) }
grep defined $args{$_}, qw(Subject From Cc)
);
diff --git a/rt/lib/RT/Record.pm b/rt/lib/RT/Record.pm
index e134178be..fd238de16 100755
--- a/rt/lib/RT/Record.pm
+++ b/rt/lib/RT/Record.pm
@@ -639,6 +639,8 @@ sub __Value {
my $value = $self->SUPER::__Value($field);
+ return undef if (!defined $value);
+
if ( $args{'decode_utf8'} ) {
if ( !utf8::is_utf8($value) ) {
utf8::decode($value);
@@ -1675,7 +1677,7 @@ sub _AddCustomFieldValue {
0,
$self->loc(
"Custom field [_1] does not apply to this object",
- $args{'Field'}
+ ref $args{'Field'} ? $args{'Field'}->id : $args{'Field'}
)
);
}
diff --git a/rt/lib/RT/Scrip.pm b/rt/lib/RT/Scrip.pm
index 950661624..8f97e747f 100755
--- a/rt/lib/RT/Scrip.pm
+++ b/rt/lib/RT/Scrip.pm
@@ -545,7 +545,7 @@ sub _Set {
}
}
- return $self->__Set(@_);
+ return $self->SUPER::_Set(@_);
}
diff --git a/rt/lib/RT/Scrips.pm b/rt/lib/RT/Scrips.pm
index 13a4b7d7d..fa33f7ec7 100755
--- a/rt/lib/RT/Scrips.pm
+++ b/rt/lib/RT/Scrips.pm
@@ -178,16 +178,6 @@ Commit all of this object's prepared scrips
sub Commit {
my $self = shift;
- # RT::Scrips->_SetupSourceObjects will clobber
- # the CurrentUser, but we need to keep this ticket
- # so that the _TransactionBatch cache is maintained
- # and doesn't run twice. sigh.
- $self->_StashCurrentUser( TicketObj => $self->{TicketObj} ) if $self->{TicketObj};
-
- #We're really going to need a non-acled ticket for the scrips to work
- $self->_SetupSourceObjects( TicketObj => $self->{'TicketObj'},
- TransactionObj => $self->{'TransactionObj'} );
-
foreach my $scrip (@{$self->Prepared}) {
$RT::Logger->debug(
"Committing scrip #". $scrip->id
@@ -199,8 +189,6 @@ sub Commit {
TransactionObj => $self->{'TransactionObj'} );
}
- # Apply the bandaid.
- $self->_RestoreCurrentUser( TicketObj => $self->{TicketObj} ) if $self->{TicketObj};
}
@@ -221,12 +209,6 @@ sub Prepare {
Type => undef,
@_ );
- # RT::Scrips->_SetupSourceObjects will clobber
- # the CurrentUser, but we need to keep this ticket
- # so that the _TransactionBatch cache is maintained
- # and doesn't run twice. sigh.
- $self->_StashCurrentUser( TicketObj => $args{TicketObj} ) if $args{TicketObj};
-
#We're really going to need a non-acled ticket for the scrips to work
$self->_SetupSourceObjects( TicketObj => $args{'TicketObj'},
Ticket => $args{'Ticket'},
@@ -259,10 +241,6 @@ sub Prepare {
}
- # Apply the bandaid.
- $self->_RestoreCurrentUser( TicketObj => $args{TicketObj} ) if $args{TicketObj};
-
-
return (@{$self->Prepared});
};
@@ -279,40 +257,6 @@ sub Prepared {
return ($self->{'prepared_scrips'} || []);
}
-=head2 _StashCurrentUser TicketObj => RT::Ticket
-
-Saves aside the current user of the original ticket that was passed to these scrips.
-This is used to make sure that we don't accidentally leak the RT_System current user
-back to the calling code.
-
-=cut
-
-sub _StashCurrentUser {
- my $self = shift;
- my %args = @_;
-
- $self->{_TicketCurrentUser} = $args{TicketObj}->CurrentUser;
-}
-
-=head2 _RestoreCurrentUser TicketObj => RT::Ticket
-
-Uses the current user saved by _StashCurrentUser to reset a Ticket object
-back to the caller's current user and avoid leaking an RT_System ticket to
-calling code.
-
-=cut
-
-sub _RestoreCurrentUser {
- my $self = shift;
- my %args = @_;
- unless ( $self->{_TicketCurrentUser} ) {
- RT->Logger->debug("Called _RestoreCurrentUser without a stashed current user object");
- return;
- }
- $args{TicketObj}->CurrentUser($self->{_TicketCurrentUser});
-
-}
-
=head2 _SetupSourceObjects { TicketObj , Ticket, Transaction, TransactionObj }
Setup a ticket and transaction for this Scrip collection to work with as it runs through the
@@ -334,14 +278,22 @@ sub _SetupSourceObjects {
@_ );
- if ( $self->{'TicketObj'} = $args{'TicketObj'} ) {
- # This clobbers the passed in TicketObj by turning it into one
- # whose current user is RT_System. Anywhere in the Web UI
- # currently calling into this is thus susceptable to a privilege
- # leak; the only current call site is ->Apply, which bandaids
- # over the top of this by re-asserting the CurrentUser
- # afterwards.
- $self->{'TicketObj'}->CurrentUser( $self->CurrentUser );
+ if ( $args{'TicketObj'} ) {
+ # This loads a clean copy of the Ticket object to ensure that we
+ # don't accidentally escalate the privileges of the passed in
+ # ticket (this function can be invoked from the UI).
+ # We copy the TransactionBatch transactions so that Scrips
+ # running against the new Ticket will have access to them. We
+ # use RanTransactionBatch to guard against running
+ # TransactionBatch Scrips more than once.
+ $self->{'TicketObj'} = RT::Ticket->new( $self->CurrentUser );
+ $self->{'TicketObj'}->Load( $args{'TicketObj'}->Id );
+ if ( $args{'TicketObj'}->TransactionBatch ) {
+ # try to ensure that we won't infinite loop if something dies, triggering DESTROY while
+ # we have the _TransactionBatch objects;
+ $self->{'TicketObj'}->RanTransactionBatch(1);
+ $self->{'TicketObj'}->{'_TransactionBatch'} = $args{'TicketObj'}->{'_TransactionBatch'};
+ }
}
else {
$self->{'TicketObj'} = RT::Ticket->new( $self->CurrentUser );
diff --git a/rt/lib/RT/Search/Googleish.pm b/rt/lib/RT/Search/Googleish.pm
index a1254836a..1b4071f4d 100644
--- a/rt/lib/RT/Search/Googleish.pm
+++ b/rt/lib/RT/Search/Googleish.pm
@@ -110,7 +110,7 @@ sub QueryToSQL {
(\w+) # A straight word
(?:\. # With an optional .foo
($RE{delimited}{-delim=>q['"]}
- |\w+
+ |[\w-]+ # Allow \w + dashes
) # Which could be ."foo bar", too
)?
)
@@ -225,6 +225,11 @@ sub GuessType {
return "default";
}
+# $_[0] is $self
+# $_[1] is escaped value without surrounding single quotes
+# $_[2] is a boolean of "was quoted by the user?"
+# ensure this is false before you do smart matching like $_[1] eq "me"
+# $_[3] is escaped subkey, if any (see HandleCf)
sub HandleDefault { return subject => "Subject LIKE '$_[1]'"; }
sub HandleSubject { return subject => "Subject LIKE '$_[1]'"; }
sub HandleFulltext { return content => "Content LIKE '$_[1]'"; }
@@ -242,7 +247,14 @@ sub HandleStatus {
}
}
sub HandleOwner {
- return owner => (!$_[2] and $_[1] eq "me") ? "Owner.id = '__CurrentUser__'" : "Owner = '$_[1]'";
+ if (!$_[2] and $_[1] eq "me") {
+ return owner => "Owner.id = '__CurrentUser__'";
+ }
+ elsif (!$_[2] and $_[1] =~ /\w+@\w+/) {
+ return owner => "Owner.EmailAddress = '$_[1]'";
+ } else {
+ return owner => "Owner = '$_[1]'";
+ }
}
sub HandleWatcher {
return watcher => (!$_[2] and $_[1] eq "me") ? "Watcher.id = '__CurrentUser__'" : "Watcher = '$_[1]'";
diff --git a/rt/lib/RT/SearchBuilder.pm b/rt/lib/RT/SearchBuilder.pm
index 3e9855110..4278f7587 100644
--- a/rt/lib/RT/SearchBuilder.pm
+++ b/rt/lib/RT/SearchBuilder.pm
@@ -211,29 +211,35 @@ sub LimitCustomField {
@_ );
my $alias = $self->Join(
- TYPE => 'left',
- ALIAS1 => 'main',
- FIELD1 => 'id',
- TABLE2 => 'ObjectCustomFieldValues',
- FIELD2 => 'ObjectId'
+ TYPE => 'left',
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'ObjectCustomFieldValues',
+ FIELD2 => 'ObjectId'
);
$self->Limit(
- ALIAS => $alias,
- FIELD => 'CustomField',
- OPERATOR => '=',
- VALUE => $args{'CUSTOMFIELD'},
+ ALIAS => $alias,
+ FIELD => 'CustomField',
+ OPERATOR => '=',
+ VALUE => $args{'CUSTOMFIELD'},
) if ($args{'CUSTOMFIELD'});
$self->Limit(
- ALIAS => $alias,
- FIELD => 'ObjectType',
- OPERATOR => '=',
- VALUE => $self->_SingularClass,
+ ALIAS => $alias,
+ FIELD => 'ObjectType',
+ OPERATOR => '=',
+ VALUE => $self->_SingularClass,
);
$self->Limit(
- ALIAS => $alias,
- FIELD => 'Content',
- OPERATOR => $args{'OPERATOR'},
- VALUE => $args{'VALUE'},
+ ALIAS => $alias,
+ FIELD => 'Content',
+ OPERATOR => $args{'OPERATOR'},
+ VALUE => $args{'VALUE'},
+ );
+ $self->Limit(
+ ALIAS => $alias,
+ FIELD => 'Disabled',
+ OPERATOR => '=',
+ VALUE => 0,
);
}
diff --git a/rt/lib/RT/Shredder.pm b/rt/lib/RT/Shredder.pm
index 40c73b36d..4f96e162d 100644
--- a/rt/lib/RT/Shredder.pm
+++ b/rt/lib/RT/Shredder.pm
@@ -539,9 +539,9 @@ sub WipeoutAll
{
my $self = $_[0];
- while ( my ($k, $v) = each %{ $self->{'cache'} } ) {
- next if $v->{'State'} & (WIPED | IN_WIPING);
- $self->Wipeout( Object => $v->{'Object'} );
+ foreach my $cache_val ( values %{ $self->{'cache'} } ) {
+ next if $cache_val->{'State'} & (WIPED | IN_WIPING);
+ $self->Wipeout( Object => $cache_val->{'Object'} );
}
}
diff --git a/rt/lib/RT/Test.pm b/rt/lib/RT/Test.pm
index 7d69dd60d..3e7c910ec 100644
--- a/rt/lib/RT/Test.pm
+++ b/rt/lib/RT/Test.pm
@@ -131,14 +131,14 @@ sub import {
if (RT->Config->Get('DevelMode')) { require Module::Refresh; }
- $class->bootstrap_db( %args );
-
RT::InitPluginPaths();
+ RT::InitClasses();
+
+ $class->bootstrap_db( %args );
__reconnect_rt()
unless $args{nodb};
- RT::InitClasses();
RT::InitLogging();
RT->Plugins;
diff --git a/rt/lib/RT/Ticket.pm b/rt/lib/RT/Ticket.pm
index 00f88b657..577c44429 100755
--- a/rt/lib/RT/Ticket.pm
+++ b/rt/lib/RT/Ticket.pm
@@ -1124,7 +1124,7 @@ sub AddWatcher {
return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
unless $addr;
- if ( lc $self->CurrentUser->UserObj->EmailAddress
+ if ( lc $self->CurrentUser->EmailAddress
eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
{
$args{'PrincipalId'} = $self->CurrentUser->id;
@@ -1305,7 +1305,7 @@ sub DeleteWatcher {
}
}
else {
- $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
+ $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
return ( 0,
$self->loc('Error in parameters to Ticket->DeleteWatcher') );
}
@@ -1989,6 +1989,31 @@ sub FirstActiveStatus {
return $next;
}
+=head2 FirstInactiveStatus
+
+Returns the first inactive status that the ticket could transition to,
+according to its current Queue's lifecycle. May return undef if there
+is no such possible status to transition to, or we are already in it.
+This is used in resolve action in UnsafeEmailCommands, for instance.
+
+=cut
+
+sub FirstInactiveStatus {
+ my $self = shift;
+
+ my $lifecycle = $self->QueueObj->Lifecycle;
+ my $status = $self->Status;
+ my @inactive = $lifecycle->Inactive;
+ # no change if no inactive statuses in the lifecycle
+ return undef unless @inactive;
+
+ # no change if the ticket is already has first status from the list of inactive
+ return undef if lc $status eq lc $inactive[0];
+
+ my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
+ return $next;
+}
+
=head2 SetStarted
Takes a date in ISO format or undef
@@ -2315,7 +2340,9 @@ sub _RecordNote {
my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
$args{'MIMEObj'}->head->set(
- 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
+ 'RT-Message-ID' => Encode::encode_utf8(
+ RT::Interface::Email::GenMessageId( Ticket => $self )
+ )
);
}
@@ -3340,6 +3367,28 @@ sub SeenUpTo {
return $txns->First;
}
+=head2 RanTransactionBatch
+
+Acts as a guard around running TransactionBatch scrips.
+
+Should be false until you enter the code that runs TransactionBatch scrips
+
+Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
+
+=cut
+
+sub RanTransactionBatch {
+ my $self = shift;
+ my $val = shift;
+
+ if ( defined $val ) {
+ return $self->{_RanTransactionBatch} = $val;
+ } else {
+ return $self->{_RanTransactionBatch};
+ }
+
+}
+
=head2 TransactionBatch
@@ -3376,6 +3425,22 @@ sub ApplyTransactionBatch {
sub _ApplyTransactionBatch {
my $self = shift;
+
+ return if $self->RanTransactionBatch;
+ $self->RanTransactionBatch(1);
+
+ my $still_exists = RT::Ticket->new( RT->SystemUser );
+ $still_exists->Load( $self->Id );
+ if (not $still_exists->Id) {
+ # The ticket has been removed from the database, but we still
+ # have pending TransactionBatch txns for it. Unfortunately,
+ # because it isn't in the DB anymore, attempting to run scrips
+ # on it may produce unpredictable results; simply drop the
+ # batched transactions.
+ $RT::Logger->warning("TransactionBatch was fired on a ticket that no longer exists; unable to run scrips! Call ->ApplyTransactionBatch before shredding the ticket, for consistent results.");
+ return;
+ }
+
my $batch = $self->TransactionBatch;
my %seen;
@@ -3423,10 +3488,7 @@ sub DESTROY {
return;
}
- my $batch = $self->TransactionBatch;
- return unless $batch && @$batch;
-
- return $self->_ApplyTransactionBatch;
+ return $self->ApplyTransactionBatch;
}
diff --git a/rt/lib/RT/Tickets.pm b/rt/lib/RT/Tickets.pm
index 485d7df53..c9986f41e 100755
--- a/rt/lib/RT/Tickets.pm
+++ b/rt/lib/RT/Tickets.pm
@@ -436,6 +436,10 @@ sub _LinkLimit {
my $is_null = 0;
$is_null = 1 if !$value || $value =~ /^null$/io;
+ unless ($is_null) {
+ $value = RT::URI->new( $sb->CurrentUser )->CanonicalizeURI( $value );
+ }
+
my $direction = $meta->[1] || '';
my ($matchfield, $linkfield) = ('', '');
if ( $direction eq 'To' ) {
@@ -1651,6 +1655,7 @@ sub _CustomFieldLimit {
FIELD => $column,
OPERATOR => $op,
VALUE => $value,
+ CASESENSITIVE => 0,
%rest
) );
$self->_CloseParen;
@@ -1713,6 +1718,7 @@ sub _CustomFieldLimit {
FIELD => 'Content',
OPERATOR => $op,
VALUE => $value,
+ CASESENSITIVE => 0,
%rest
);
}
@@ -1739,6 +1745,7 @@ sub _CustomFieldLimit {
OPERATOR => $op,
VALUE => $value,
ENTRYAGGREGATOR => 'AND',
+ CASESENSITIVE => 0,
) );
}
}
@@ -1748,6 +1755,7 @@ sub _CustomFieldLimit {
FIELD => 'Content',
OPERATOR => $op,
VALUE => $value,
+ CASESENSITIVE => 0,
%rest
);
@@ -1774,6 +1782,7 @@ sub _CustomFieldLimit {
OPERATOR => $op,
VALUE => $value,
ENTRYAGGREGATOR => 'AND',
+ CASESENSITIVE => 0,
) );
$self->_CloseParen;
}
@@ -1830,6 +1839,7 @@ sub _CustomFieldLimit {
FIELD => $column,
OPERATOR => $op,
VALUE => $value,
+ CASESENSITIVE => 0,
) );
}
else {
@@ -1839,6 +1849,7 @@ sub _CustomFieldLimit {
FIELD => 'Content',
OPERATOR => $op,
VALUE => $value,
+ CASESENSITIVE => 0,
);
}
$self->_SQLLimit(
diff --git a/rt/lib/RT/Transaction.pm b/rt/lib/RT/Transaction.pm
index bd4d83546..3344687da 100755
--- a/rt/lib/RT/Transaction.pm
+++ b/rt/lib/RT/Transaction.pm
@@ -133,12 +133,6 @@ sub Create {
return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
}
-
- # Set up any custom fields passed at creation. Has to happen
- # before scrips.
-
- $self->UpdateCustomFields(%{ $args{'CustomFields'} });
-
#lets create our transaction
my %params = (
Type => $args{'Type'},
@@ -169,6 +163,11 @@ sub Create {
}
}
+ # Set up any custom fields passed at creation. Has to happen
+ # before scrips.
+
+ $self->UpdateCustomFields(%{ $args{'CustomFields'} });
+
$self->AddAttribute(
Name => 'SquelchMailTo',
Content => RT::User->CanonicalizeEmailAddress($_)
diff --git a/rt/lib/RT/URI.pm b/rt/lib/RT/URI.pm
index fce04598a..284a75ee0 100644
--- a/rt/lib/RT/URI.pm
+++ b/rt/lib/RT/URI.pm
@@ -91,7 +91,26 @@ sub new {
return ($self);
}
+=head2 CanonicalizeURI <URI>
+Returns the canonical form of the given URI by calling L</FromURI> and then L</URI>.
+
+If the URI is unparseable by FromURI the passed in URI is simply returned untouched.
+
+=cut
+
+sub CanonicalizeURI {
+ my $self = shift;
+ my $uri = shift;
+ if ($self->FromURI($uri)) {
+ my $canonical = $self->URI;
+ if ($canonical and $uri ne $canonical) {
+ RT->Logger->debug("Canonicalizing URI '$uri' to '$canonical'");
+ $uri = $canonical;
+ }
+ }
+ return $uri;
+}
=head2 FromObject <Object>
diff --git a/rt/lib/RT/User.pm b/rt/lib/RT/User.pm
index 9b4a82683..e7f7c2ad6 100755
--- a/rt/lib/RT/User.pm
+++ b/rt/lib/RT/User.pm
@@ -932,7 +932,7 @@ sub IsPassword {
# crypt() output
return 0 unless crypt(encode_utf8($value), $stored) eq $stored;
} else {
- $RT::Logger->warn("Unknown password form");
+ $RT::Logger->warning("Unknown password form");
return 0;
}
diff --git a/rt/sbin/rt-server.fcgi.in b/rt/sbin/rt-server.fcgi.in
index 45c377088..f84f6c103 100644
--- a/rt/sbin/rt-server.fcgi.in
+++ b/rt/sbin/rt-server.fcgi.in
@@ -172,7 +172,7 @@ if (caller) {
require Plack::Runner;
my $is_fastcgi = $0 =~ m/fcgi$/;
-my $r = Plack::Runner->new( $0 =~ 'standalone' ? ( server => 'Standalone' ) :
+my $r = Plack::Runner->new( $0 =~ /standalone/ ? ( server => 'Standalone' ) :
$is_fastcgi ? ( server => 'FCGI' )
: (),
env => 'deployment' );
diff --git a/rt/sbin/rt-server.in b/rt/sbin/rt-server.in
index 45c377088..f84f6c103 100644
--- a/rt/sbin/rt-server.in
+++ b/rt/sbin/rt-server.in
@@ -172,7 +172,7 @@ if (caller) {
require Plack::Runner;
my $is_fastcgi = $0 =~ m/fcgi$/;
-my $r = Plack::Runner->new( $0 =~ 'standalone' ? ( server => 'Standalone' ) :
+my $r = Plack::Runner->new( $0 =~ /standalone/ ? ( server => 'Standalone' ) :
$is_fastcgi ? ( server => 'FCGI' )
: (),
env => 'deployment' );
diff --git a/rt/sbin/rt-test-dependencies.in b/rt/sbin/rt-test-dependencies.in
index 37ef32f64..960d640c3 100644
--- a/rt/sbin/rt-test-dependencies.in
+++ b/rt/sbin/rt-test-dependencies.in
@@ -56,9 +56,10 @@ no warnings qw(numeric redefine);
use Getopt::Long;
my %args;
my %deps;
+my @orig_argv = @ARGV;
GetOptions(
\%args, 'v|verbose',
- 'install', 'with-MYSQL',
+ 'install!', 'with-MYSQL',
'with-POSTGRESQL|with-pg|with-pgsql', 'with-SQLITE',
'with-ORACLE', 'with-FASTCGI',
'with-MODPERL1', 'with-MODPERL2',
@@ -293,7 +294,7 @@ Test::LongString
.
$deps{'FASTCGI'} = [ text_to_hash( << '.') ];
-FCGI
+FCGI 0.74
FCGI::ProcManager
.
@@ -344,7 +345,7 @@ URI 1.59
$deps{'GRAPHVIZ'} = [ text_to_hash( << '.') ];
GraphViz
-IPC::Run
+IPC::Run 0.90
.
$deps{'GD'} = [ text_to_hash( << '.') ];
@@ -359,6 +360,7 @@ Convert::Color
my %AVOID = (
'DBD::Oracle' => [qw(1.23)],
+ 'Email::Address' => [qw(1.893 1.894)],
);
if ($args{'download'}) {
@@ -403,7 +405,12 @@ foreach my $type (sort grep $args{$_}, keys %args) {
$Missing_By_Type{$type} = \%missing if keys %missing;
}
-conclude(%Missing_By_Type);
+if ( $args{'install'} && keys %Missing_By_Type ) {
+ exec($0, @orig_argv, '--no-install');
+}
+else {
+ conclude(%Missing_By_Type);
+}
sub test_deps {
my @deps = @_;
diff --git a/rt/sbin/standalone_httpd b/rt/sbin/standalone_httpd
index 3386cd1fe..cef0f3102 100755
--- a/rt/sbin/standalone_httpd
+++ b/rt/sbin/standalone_httpd
@@ -172,7 +172,7 @@ if (caller) {
require Plack::Runner;
my $is_fastcgi = $0 =~ m/fcgi$/;
-my $r = Plack::Runner->new( $0 =~ 'standalone' ? ( server => 'Standalone' ) :
+my $r = Plack::Runner->new( $0 =~ /standalone/ ? ( server => 'Standalone' ) :
$is_fastcgi ? ( server => 'FCGI' )
: (),
env => 'deployment' );
diff --git a/rt/sbin/standalone_httpd.in b/rt/sbin/standalone_httpd.in
index 45c377088..f84f6c103 100644
--- a/rt/sbin/standalone_httpd.in
+++ b/rt/sbin/standalone_httpd.in
@@ -172,7 +172,7 @@ if (caller) {
require Plack::Runner;
my $is_fastcgi = $0 =~ m/fcgi$/;
-my $r = Plack::Runner->new( $0 =~ 'standalone' ? ( server => 'Standalone' ) :
+my $r = Plack::Runner->new( $0 =~ /standalone/ ? ( server => 'Standalone' ) :
$is_fastcgi ? ( server => 'FCGI' )
: (),
env => 'deployment' );
diff --git a/rt/share/html/Admin/Queues/Modify.html b/rt/share/html/Admin/Queues/Modify.html
index 5682eee28..85cd62f16 100755
--- a/rt/share/html/Admin/Queues/Modify.html
+++ b/rt/share/html/Admin/Queues/Modify.html
@@ -51,7 +51,7 @@
-<form action="<%RT->Config->Get('WebPath')%>/Admin/Queues/Modify.html" name="ModifyQueue" method="post">
+<form action="<%RT->Config->Get('WebPath')%>/Admin/Queues/Modify.html" name="ModifyQueue" method="post" enctype="multipart/form-data">
<input type="hidden" class="hidden" name="SetEnabled" value="1" />
<input type="hidden" class="hidden" name="id" value="<% $Create? 'new': $QueueObj->Id %>" />
diff --git a/rt/share/html/Approvals/Elements/PendingMyApproval b/rt/share/html/Approvals/Elements/PendingMyApproval
index d2061da84..169c25cb6 100755
--- a/rt/share/html/Approvals/Elements/PendingMyApproval
+++ b/rt/share/html/Approvals/Elements/PendingMyApproval
@@ -74,7 +74,7 @@ $tickets->LimitOwner( VALUE => $session{'CurrentUser'}->Id );
# also consider AdminCcs as potential approvers.
my $group_tickets = RT::Tickets->new( $session{'CurrentUser'} );
-$group_tickets->LimitWatcher( VALUE => $session{'CurrentUser'}->UserObj->EmailAddress, TYPE => 'AdminCc' );
+$group_tickets->LimitWatcher( VALUE => $session{'CurrentUser'}->EmailAddress, TYPE => 'AdminCc' );
my $created_before = RT::Date->new( $session{'CurrentUser'} );
my $created_after = RT::Date->new( $session{'CurrentUser'} );
diff --git a/rt/share/html/Approvals/autohandler b/rt/share/html/Approvals/autohandler
index a05770654..3e0f2c6db 100644
--- a/rt/share/html/Approvals/autohandler
+++ b/rt/share/html/Approvals/autohandler
@@ -46,8 +46,13 @@
%#
%# END BPS TAGGED BLOCK }}}
<%init>
-$m->call_next(%ARGS) if $session{'CurrentUser'}->UserObj->HasRight(
+if ( $session{'CurrentUser'}->UserObj->HasRight(
Right => 'ShowApprovalsTab',
Object => $RT::System,
-);
+) ) {
+ $m->call_next(%ARGS);
+}
+else {
+ Abort("No permission to view approval");
+}
</%init>
diff --git a/rt/share/html/Dashboards/Subscription.html b/rt/share/html/Dashboards/Subscription.html
index 3669e4687..3a57102c7 100644
--- a/rt/share/html/Dashboards/Subscription.html
+++ b/rt/share/html/Dashboards/Subscription.html
@@ -171,7 +171,7 @@
<&|/l&>Recipient</&>:
</td><td class="value">
<input name="Recipient" id="Recipient" size="30" value="<%$fields{Recipient} ? $fields{Recipient} : ''%>" />
-<div class="hints"><% loc("Leave blank to send to your current email address ([_1])", $session{'CurrentUser'}->UserObj->EmailAddress) %></div>
+<div class="hints"><% loc("Leave blank to send to your current email address ([_1])", $session{'CurrentUser'}->EmailAddress) %></div>
</td></tr>
</table>
</&>
diff --git a/rt/share/html/Elements/AddCustomers b/rt/share/html/Elements/AddCustomers
index 9828d7d53..6517db42c 100644
--- a/rt/share/html/Elements/AddCustomers
+++ b/rt/share/html/Elements/AddCustomers
@@ -41,7 +41,7 @@ my @Customers = ();
if ( $CustomerString ) {
@Customers = &RT::URI::freeside::smart_search(
'search' => $CustomerString,
- 'no_fuzzy_on_exact' => 1, #pref?
+ 'no_fuzzy_on_exact' => ! $FS::CurrentUser::CurrentUser->option('enable_fuzzy_on_exact'),
);
}
diff --git a/rt/share/html/Elements/ColumnMap b/rt/share/html/Elements/ColumnMap
index b9c3b4bc8..f268a5d1c 100644
--- a/rt/share/html/Elements/ColumnMap
+++ b/rt/share/html/Elements/ColumnMap
@@ -118,7 +118,7 @@ my $COLUMN_MAP = {
CheckBox => {
title => sub {
my $name = $_[1] || 'SelectedTickets';
- my $checked = $m->request_args->{ $name .'All' }? 'checked="checked"': '';
+ my $checked = $DECODED_ARGS->{ $name .'All' }? 'checked="checked"': '';
return \qq{<input type="checkbox" name="}, $name, \qq{All" value="1" $checked
onclick="setCheckbox(this.form, },
@@ -130,9 +130,9 @@ my $COLUMN_MAP = {
my $name = $_[2] || 'SelectedTickets';
return \qq{<input type="checkbox" name="}, $name, \qq{" value="$id" checked="checked" />}
- if $m->request_args->{ $name . 'All'};
+ if $DECODED_ARGS->{ $name . 'All'};
- my $arg = $m->request_args->{ $name };
+ my $arg = $DECODED_ARGS->{ $name };
my $checked = '';
if ( $arg && ref $arg ) {
$checked = 'checked="checked"' if grep $_ == $id, @$arg;
@@ -149,7 +149,7 @@ my $COLUMN_MAP = {
my $id = $_[0]->id;
my $name = $_[2] || 'SelectedTicket';
- my $arg = $m->request_args->{ $name };
+ my $arg = $DECODED_ARGS->{ $name };
my $checked = '';
$checked = 'checked="checked"' if $arg && $arg == $id;
return \qq{<input type="radio" name="}, $name, \qq{" value="$id" $checked />};
diff --git a/rt/share/html/Elements/EditCustomField b/rt/share/html/Elements/EditCustomField
index b74c4844e..8b87fd425 100644
--- a/rt/share/html/Elements/EditCustomField
+++ b/rt/share/html/Elements/EditCustomField
@@ -71,7 +71,7 @@ if ( $Object && $Object->id ) {
# Always fill $Default with submited values if it's empty
if ( ( !defined $Default || !length $Default ) && $DefaultsFromTopArguments ) {
- my %TOP = $m->request_args;
+ my %TOP = %$DECODED_ARGS;
$Default = $TOP{ $NamePrefix .$CustomField->Id . '-Values' }
|| $TOP{ $NamePrefix .$CustomField->Id . '-Value' };
}
diff --git a/rt/share/html/Elements/Header b/rt/share/html/Elements/Header
index 1830c4bf2..65d06f879 100755
--- a/rt/share/html/Elements/Header
+++ b/rt/share/html/Elements/Header
@@ -130,7 +130,8 @@ if ($m->comp_exists($stylesheet_plugin) ) {
# $m->callback( %ARGS, CallbackName => 'Head' );
$head .= $m->scomp( '/Elements/Callback', _CallbackName => 'Head', %ARGS );
-my $etc = qq[ class="\L$style" ];
+my $sbs = RT->Config->Get("UseSideBySideLayout", $session{'CurrentUser'}) ? ' sidebyside' : '';
+my $etc = qq[ class="\L$style$sbs" ];
$etc .= qq[ id="comp-$id"] if $id;
</%INIT>
diff --git a/rt/share/html/Elements/HeaderJavascript b/rt/share/html/Elements/HeaderJavascript
index 28788db57..d5741f4e6 100644
--- a/rt/share/html/Elements/HeaderJavascript
+++ b/rt/share/html/Elements/HeaderJavascript
@@ -67,7 +67,7 @@ $onload => undef
% }
% if ( $RichText and RT->Config->Get('MessageBoxRichText', $session{'CurrentUser'})) {
- jQuery().ready(function () { ReplaceAllTextareas(<%$m->request_args->{'CKeditorEncoded'} || 0 |n,j%>) });
+ jQuery().ready(function () { ReplaceAllTextareas(<%$DECODED_ARGS->{'CKeditorEncoded'} || 0 |n,j%>) });
% }
--></script>
<%ARGS>
diff --git a/rt/share/html/Elements/ListActions b/rt/share/html/Elements/ListActions
index 999d3fe5b..8929ff731 100755
--- a/rt/share/html/Elements/ListActions
+++ b/rt/share/html/Elements/ListActions
@@ -65,7 +65,7 @@ if ( ref( $session{'Actions'}{''} ) eq 'ARRAY' ) {
unshift @actions, @{ delete $session{'Actions'}{''} };
}
-my $actions_pointer = $m->request_args->{'results'};
+my $actions_pointer = $DECODED_ARGS->{'results'};
if ($actions_pointer && ref( $session{'Actions'}->{$actions_pointer} ) eq 'ARRAY' ) {
unshift @actions, @{ delete $session{'Actions'}->{$actions_pointer} };
diff --git a/rt/share/html/Elements/MessageBox b/rt/share/html/Elements/MessageBox
index 61995e057..69227bfa9 100755
--- a/rt/share/html/Elements/MessageBox
+++ b/rt/share/html/Elements/MessageBox
@@ -46,7 +46,7 @@
%#
%# END BPS TAGGED BLOCK }}}
<textarea autocomplete="off" class="messagebox" <% $width_attr %>="<% $Width %>" rows="<% $Height %>" <% $wrap_type |n %> name="<% $Name %>" id="<% $Name %>">\
-% $m->comp('/Articles/Elements/IncludeArticle', %ARGS);
+% $m->comp('/Articles/Elements/IncludeArticle', %ARGS) if $IncludeArticle;
% $m->callback( %ARGS, SignatureRef => \$signature );
<% $Default || '' %><% $message %><% $signature %></textarea>
% $m->callback( %ARGS, CallbackName => 'AfterTextArea' );
@@ -89,4 +89,5 @@ $Width => RT->Config->Get('MessageBoxWidth', $session{'CurrentUser'}
$Height => RT->Config->Get('MessageBoxHeight', $session{'CurrentUser'} ) || 15
$Wrap => RT->Config->Get('MessageBoxWrap', $session{'CurrentUser'} ) || 'SOFT'
$IncludeSignature => RT->Config->Get('MessageBoxIncludeSignature');
+$IncludeArticle => 1;
</%ARGS>
diff --git a/rt/share/html/Elements/QueueSummaryByStatus b/rt/share/html/Elements/QueueSummaryByStatus
index 09f274f74..f649d2850 100644
--- a/rt/share/html/Elements/QueueSummaryByStatus
+++ b/rt/share/html/Elements/QueueSummaryByStatus
@@ -122,9 +122,13 @@ my $statuses = {};
use RT::Report::Tickets;
my $report = RT::Report::Tickets->new( RT->SystemUser );
-my $query = @queues
- ? join(' OR ', map "Queue = ".$_->{id}, @queues)
- : 'id < 0';
+my $query =
+ "(".
+ join(" OR ", map {s{(['\\])}{\\$1}g; "Status = '$_'"} @statuses) #'
+ .") AND (".
+ join(' OR ', map "Queue = ".$_->{id}, @queues)
+ .")";
+$query = 'id < 0' unless @queues;
$report->SetupGroupings( Query => $query, GroupBy => [qw(Status Queue)] );
while ( my $entry = $report->Next ) {
diff --git a/rt/share/html/Elements/RT__CustomField/ColumnMap b/rt/share/html/Elements/RT__CustomField/ColumnMap
index ecb219d9e..b04398434 100644
--- a/rt/share/html/Elements/RT__CustomField/ColumnMap
+++ b/rt/share/html/Elements/RT__CustomField/ColumnMap
@@ -118,7 +118,7 @@ my $COLUMN_MAP = {
RemoveCheckBox => {
title => sub {
my $name = 'RemoveCustomField';
- my $checked = $m->request_args->{ $name .'All' }? 'checked="checked"': '';
+ my $checked = $DECODED_ARGS->{ $name .'All' }? 'checked="checked"': '';
return \qq{<input type="checkbox" name="}, $name, \qq{All" value="1" $checked
onclick="setCheckbox(this.form, },
@@ -130,7 +130,7 @@ my $COLUMN_MAP = {
return '' if $_[0]->IsApplied;
my $name = 'RemoveCustomField';
- my $arg = $m->request_args->{ $name };
+ my $arg = $DECODED_ARGS->{ $name };
my $checked = '';
if ( $arg && ref $arg ) {
diff --git a/rt/share/html/Elements/SelectWatcherType b/rt/share/html/Elements/SelectWatcherType
index 44beee00d..4f1df60b2 100755
--- a/rt/share/html/Elements/SelectWatcherType
+++ b/rt/share/html/Elements/SelectWatcherType
@@ -56,7 +56,7 @@
<%INIT>
my @types;
-if ($Scope =~ 'queue') {
+if ($Scope =~ /queue/) {
@types = RT::Queue->ManageableRoleGroupTypes;
}
else {
diff --git a/rt/share/html/Elements/Tabs b/rt/share/html/Elements/Tabs
index 3193b488d..3aac9d803 100755
--- a/rt/share/html/Elements/Tabs
+++ b/rt/share/html/Elements/Tabs
@@ -845,7 +845,7 @@ my $build_selfservice_nav = sub {
} elsif ( $queue_id ) {
Menu->child( new => title => loc('New ticket'), path => '/SelfService/Create.html?Queue=' . $queue_id );
}
- my $tickets = Menu->child( tickets => title => loc('Tickets'));
+ my $tickets = Menu->child( tickets => title => loc('Tickets'), path => '/SelfService/' );
$tickets->child( open => title => loc('Open tickets'), path => '/SelfService/' );
$tickets->child( closed => title => loc('Closed tickets'), path => '/SelfService/Closed.html' );
diff --git a/rt/share/html/Helpers/Autocomplete/Users b/rt/share/html/Helpers/Autocomplete/Users
index dbc2d888f..c2b92c1bf 100644
--- a/rt/share/html/Helpers/Autocomplete/Users
+++ b/rt/share/html/Helpers/Autocomplete/Users
@@ -116,6 +116,9 @@ foreach (split /\s*,\s*/, $exclude) {
my @suggestions;
+$users->Limit( FIELD => $return, OPERATOR => '!=', VALUE => '' );
+$users->Limit( FIELD => $return, OPERATOR => 'IS NOT', VALUE => 'NULL', ENTRYAGGREGATOR => 'AND' );
+
while ( my $user = $users->Next ) {
next if $user->id == RT->SystemUser->id
or $user->id == RT->Nobody->id;
diff --git a/rt/share/html/NoAuth/css/aileron/boxes.css b/rt/share/html/NoAuth/css/aileron/boxes.css
index ed6623cba..f90ac9f77 100644
--- a/rt/share/html/NoAuth/css/aileron/boxes.css
+++ b/rt/share/html/NoAuth/css/aileron/boxes.css
@@ -91,10 +91,6 @@
.titlebox .titlebox-title {
position: relative;
- /* This is for [rt3 #19044]. Move it to an IE-specific file if it causes
- * problems. If we remove CSS3PIE, it can also probably go away, although it
- * probably won't hurt. */
- z-index: 1;
}
.titlebox .titlebox-title a {
diff --git a/rt/share/html/NoAuth/css/aileron/ticket.css b/rt/share/html/NoAuth/css/aileron/ticket.css
index 4d069d9f9..7b573f72c 100644
--- a/rt/share/html/NoAuth/css/aileron/ticket.css
+++ b/rt/share/html/NoAuth/css/aileron/ticket.css
@@ -87,8 +87,7 @@ div#ticket-history {
float: left;
margin: 0.25em 0.70em 0.25em 0.25em;
width: 1em;
- height: 1.25em;
- padding: 0.75em 0 0 0;
+ padding: 0;
border-right: 1px solid #999;
border-bottom: 1px solid #999;
-moz-border-radius-bottomright: 0.25em;
@@ -100,6 +99,16 @@ div#ticket-history {
div#ticket-history span.type a {
color: #fff;
+ padding-top: 0.75em;
+ display: block;
+}
+
+#ticket-history a#lasttrans {
+ display: inline;
+ height: 0;
+ width: 0;
+ padding: 0;
+ margin: 0;
}
diff --git a/rt/share/html/NoAuth/css/ballard/boxes.css b/rt/share/html/NoAuth/css/ballard/boxes.css
index 912ac55f4..9610cd5e7 100644
--- a/rt/share/html/NoAuth/css/ballard/boxes.css
+++ b/rt/share/html/NoAuth/css/ballard/boxes.css
@@ -54,6 +54,7 @@
margin-left: 1em;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
+ border-radius: 0.5em;
margin-bottom: 2em;
border-bottom: 2px solid #aaa;
border-right: 2px solid #aaa;
@@ -71,6 +72,7 @@
margin-top: 1em;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
+ border-radius: 0.5em;
margin-right: 0.25em;
}
@@ -114,6 +116,7 @@
padding-right: 0.75em;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
+ border-radius: 0.5em;
border-bottom: 2px solid #aaa;
border-right: 2px solid #aaa;
@@ -138,10 +141,12 @@
padding-top: 0.5em;
-moz-border-radius-bottomleft: 0.25em;
-webkit-border-bottom-left-radius: 0.25em;
+ border-bottom-left-radius: 0.25em;
-moz-border-radius-topright: 0.25em;
-webkit-border-top-right-radius: 0.25em;
+ border-top-right-radius: 0.25em;
}
diff --git a/rt/share/html/NoAuth/css/ballard/layout.css b/rt/share/html/NoAuth/css/ballard/layout.css
index 8dc0cc162..8b600b828 100644
--- a/rt/share/html/NoAuth/css/ballard/layout.css
+++ b/rt/share/html/NoAuth/css/ballard/layout.css
@@ -60,8 +60,10 @@ div#body {
padding: 1.8em 1em 1em 1em;
-moz-border-radius-topleft: 0.5em;
-webkit-border-top-left-radius: 0.5em;
+ border-top-left-radius: 0.5em;
-moz-border-radius-bottomleft: 0.5em;
-webkit-border-bottom-left-radius: 0.5em;
+ border-bottom-left-radius: 0.5em;
margin-left: 10em;
margin-top: 3em;
margin-right: 0;
@@ -89,8 +91,10 @@ div#footer {
border-left: 2px solid #aaa;
-moz-border-radius-topleft: 0.5em;
-webkit-border-top-left-radius: 0.5em;
+ border-top-left-radius: 0.5em;
-moz-border-radius-bottomleft: 0.5em;
-webkit-border-bottom-left-radius: 0.5em;
+ border-bottom-left-radius: 0.5em;
}
div#footer #time {
diff --git a/rt/share/html/NoAuth/css/ballard/nav.css b/rt/share/html/NoAuth/css/ballard/nav.css
index 196f0e6c0..dc29818fe 100644
--- a/rt/share/html/NoAuth/css/ballard/nav.css
+++ b/rt/share/html/NoAuth/css/ballard/nav.css
@@ -49,8 +49,10 @@
background-color: #fff;
-moz-border-radius-bottomright: 0.5em;
-webkit-border-bottom-right-radius: 0.5em;
+ border-bottom-right-radius: 0.5em;
-moz-border-radius-topright: 0.5em;
-webkit-border-top-right-radius: 0.5em;
+ border-top-right-radius: 0.5em;
width: 10em;
font-size: 0.85em;
position: absolute;
@@ -130,6 +132,7 @@
border: 1px solid #ccc;
-moz-border-radius-bottomleft: 0.5em;
-webkit-border-bottom-left-radius: 0.5em;
+ border-bottom-left-radius: 0.5em;
padding: 0;
padding-top: 0.5em;
padding-right: 0.5em;
diff --git a/rt/share/html/NoAuth/css/ballard/ticket-search.css b/rt/share/html/NoAuth/css/ballard/ticket-search.css
index 19ee847ff..fb252b5e3 100644
--- a/rt/share/html/NoAuth/css/ballard/ticket-search.css
+++ b/rt/share/html/NoAuth/css/ballard/ticket-search.css
@@ -163,6 +163,7 @@
border-bottom: 1px solid #999;
-moz-border-radius-bottomleft: 0.5em;
-webkit-border-bottom-left-radius: 0.5em;
+ border-bottom-left-radius: 0.5em;
}
diff --git a/rt/share/html/NoAuth/css/ballard/ticket.css b/rt/share/html/NoAuth/css/ballard/ticket.css
index 06b6678c9..4d416e175 100644
--- a/rt/share/html/NoAuth/css/ballard/ticket.css
+++ b/rt/share/html/NoAuth/css/ballard/ticket.css
@@ -77,6 +77,7 @@ div#ticket-history {
color: #ccc;
-moz-border-radius-bottomleft: 0.5em;
-webkit-border-bottom-left-radius: 0.5em;
+ border-bottom-left-radius: 0.5em;
white-space: nowrap;
}
@@ -91,6 +92,7 @@ div#ticket-history {
border-bottom: 1px solid #999;
-moz-border-radius: 0.25em;
-webkit-border-bottom-right-radius: 0.25em;
+ border-bottom-right-radius: 0.25em;
}
div#ticket-history span.type a {
@@ -150,6 +152,7 @@ border-bottom: 2px solid #aaa;
margin-top: 0.5em;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
+border-radius: 0.5em;
}
diff --git a/rt/share/html/NoAuth/css/base/forms.css b/rt/share/html/NoAuth/css/base/forms.css
index eab97b19b..19af1b2a3 100644
--- a/rt/share/html/NoAuth/css/base/forms.css
+++ b/rt/share/html/NoAuth/css/base/forms.css
@@ -87,6 +87,7 @@ input[type=reset], input[type=submit], input[class=button], button {
padding-right: 0.5em;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
+ border-radius: 0.5em;
}
input.button:hover, button:hover, input[type=reset]:hover, input[type=submit]:hover, input[class=button]:hover {
diff --git a/rt/share/html/NoAuth/css/base/jquery-ui-timepicker-addon.css b/rt/share/html/NoAuth/css/base/jquery-ui-timepicker-addon.css
new file mode 100644
index 000000000..7eb871568
--- /dev/null
+++ b/rt/share/html/NoAuth/css/base/jquery-ui-timepicker-addon.css
@@ -0,0 +1,7 @@
+.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; }
+.ui-timepicker-div dl { text-align: left; }
+.ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; }
+.ui-timepicker-div dl dd { margin: 0 10px 10px 65px; }
+.ui-timepicker-div td { font-size: 90%; }
+.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; }
+.ui-datepicker-buttonpane button.ui-datepicker-current { opacity: 1.0; }
diff --git a/rt/share/html/NoAuth/css/base/jquery-ui.css b/rt/share/html/NoAuth/css/base/jquery-ui.css
index 820996ea8..8fe4f1545 100644
--- a/rt/share/html/NoAuth/css/base/jquery-ui.css
+++ b/rt/share/html/NoAuth/css/base/jquery-ui.css
@@ -46,5 +46,3 @@
%#
%# END BPS TAGGED BLOCK }}}
@import "jquery-ui.custom.modified.css";
-@import "ui.timepickr.css";
-@import "ui.timepickr.custom.css";
diff --git a/rt/share/html/NoAuth/css/base/jquery-ui.custom.modified.css b/rt/share/html/NoAuth/css/base/jquery-ui.custom.modified.css
index 7a323229a..3b1e1a00e 100644
--- a/rt/share/html/NoAuth/css/base/jquery-ui.custom.modified.css
+++ b/rt/share/html/NoAuth/css/base/jquery-ui.custom.modified.css
@@ -452,3 +452,27 @@
width: 200px; /*must have*/
height: 200px; /*must have*/
}
+/*
+ * jQuery UI Slider 1.8.4
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider#theming
+ */
+.ui-slider { position: relative; text-align: left; }
+.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
+.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
+
+.ui-slider-horizontal { height: .8em; }
+.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
+.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
+.ui-slider-horizontal .ui-slider-range-min { left: 0; }
+.ui-slider-horizontal .ui-slider-range-max { right: 0; }
+
+.ui-slider-vertical { width: .8em; height: 100px; }
+.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
+.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
+.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
+.ui-slider-vertical .ui-slider-range-max { top: 0; }
diff --git a/rt/share/html/NoAuth/css/base/main.css b/rt/share/html/NoAuth/css/base/main.css
index 9f77c8aee..dac733d87 100644
--- a/rt/share/html/NoAuth/css/base/main.css
+++ b/rt/share/html/NoAuth/css/base/main.css
@@ -49,6 +49,7 @@
@import "yui-fonts.css";
@import "jquery-ui.css";
+@import "jquery-ui-timepicker-addon.css";
@import "superfish.css";
@import "superfish-navbar.css";
@import "superfish-vertical.css";
diff --git a/rt/share/html/NoAuth/css/base/superfish-navbar.css b/rt/share/html/NoAuth/css/base/superfish-navbar.css
index 9a3f24cd9..459156ec7 100644
--- a/rt/share/html/NoAuth/css/base/superfish-navbar.css
+++ b/rt/share/html/NoAuth/css/base/superfish-navbar.css
@@ -90,4 +90,6 @@ ul.sf-navbar .current ul ul {
-moz-border-radius-topright: 0;
-webkit-border-top-right-radius: 0;
-webkit-border-bottom-left-radius: 0;
+ border-top-right-radius: 0;
+ border-bottom-left-radius: 0;
}
diff --git a/rt/share/html/NoAuth/css/base/superfish.css b/rt/share/html/NoAuth/css/base/superfish.css
index 31198e423..7cb3b567c 100644
--- a/rt/share/html/NoAuth/css/base/superfish.css
+++ b/rt/share/html/NoAuth/css/base/superfish.css
@@ -130,6 +130,8 @@ li.sfHover > a > .sf-sub-indicator {
-moz-border-radius-topright: 17px;
-webkit-border-top-right-radius: 17px;
-webkit-border-bottom-left-radius: 17px;
+ border-top-right-radius: 17px;
+ border-bottom-left-radius: 17px;
}
.sf-shadow ul.sf-shadow-off {
background: transparent;
diff --git a/rt/share/html/NoAuth/css/base/ticket-form.css b/rt/share/html/NoAuth/css/base/ticket-form.css
index daab263b1..869eba774 100644
--- a/rt/share/html/NoAuth/css/base/ticket-form.css
+++ b/rt/share/html/NoAuth/css/base/ticket-form.css
@@ -82,21 +82,17 @@ iframe.richtext-editor {
.messagebox-container.action-response iframe
{
background-color: #fcc !important;
-}
-
-/*
-% if ( RT->Config->Get("UseSideBySideLayout", $session{'CurrentUser'}) ) {
-*/
+}
-#ticket-create-metadata,
-#ticket-update-metadata {
+.sidebyside #ticket-create-metadata,
+.sidebyside #ticket-update-metadata {
float: right;
width: 40%;
clear: right;
}
-#ticket-create-message,
-#ticket-update-message {
+.sidebyside #ticket-create-message,
+.sidebyside #ticket-update-message {
float: left;
width: 58%;
clear: left;
@@ -104,10 +100,10 @@ iframe.richtext-editor {
@media (max-width: 950px) {
/* Revert to a single column when we're less than 1000px wide */
- #ticket-create-metadata,
- #ticket-update-metadata,
- #ticket-create-message,
- #ticket-update-message
+ .sidebyside #ticket-create-metadata,
+ .sidebyside #ticket-update-metadata,
+ .sidebyside #ticket-create-message,
+ .sidebyside #ticket-update-message
{
float: none;
width: auto;
@@ -115,15 +111,12 @@ iframe.richtext-editor {
}
}
-#comp-Ticket-Update #body {
+.sidebyside #comp-Ticket-Update #body {
padding-top: 3em;
}
-#ticket-create-message .button[name="AddMoreAttach"],
-#ticket-update-message .button[name="AddMoreAttach"] {
+.sidebyside #ticket-create-message .button[name="AddMoreAttach"],
+.sidebyside #ticket-update-message .button[name="AddMoreAttach"] {
float: right;
}
-/*
-% }
-*/
diff --git a/rt/share/html/NoAuth/css/base/ui.timepickr.css b/rt/share/html/NoAuth/css/base/ui.timepickr.css
deleted file mode 100644
index e2dacf7a9..000000000
--- a/rt/share/html/NoAuth/css/base/ui.timepickr.css
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- jQuery ui.timepickr
- http://code.google.com/p/jquery-utils/
-
- copyright Maxime Haineault <haineault@gmail.com>
- http://haineault.com
-
- MIT License (http://www.opensource.org/licenses/mit-license.php
-*/
-.ui-timepickr {
- position:absolute;
- width:480px;
-}
-
-.ui-timepickr-row {
- margin:0;
- padding:0;
- margin-top:2px;
- display:none;
- position:relative;
-}
-
-.ui-timepickr-button {
- float:left;
- margin:0;
- padding:0;
- list-style:none;
- list-style-type:none;
-}
-
-.ui-timepickr-button span {
- font-size:.7em;
- padding:4px 6px 4px 6px;
- margin-left:2px;
- text-align:center;
- cursor:pointer;
- display:block;
- text-align:center;
-
-
- /* system theme (default) */
- border-width:1px;
- border-style:solid;
- /*border-color:ThreeDLightShadow ThreeDShadow ThreeDShadow ThreeDLightShadow;
- color:ButtonText;
- background:ButtonFace;*/
-}
-
-.ui-timepickr-button span.ui-state-hover {
- /*color:HighlightText;
- background:Highlight;*/
-}
-
-.ui-state-hover span {
- /*background:#c30;*/
-}
diff --git a/rt/share/html/NoAuth/css/base/ui.timepickr.custom.css b/rt/share/html/NoAuth/css/base/ui.timepickr.custom.css
deleted file mode 100644
index ad2aa66ce..000000000
--- a/rt/share/html/NoAuth/css/base/ui.timepickr.custom.css
+++ /dev/null
@@ -1,54 +0,0 @@
-%# BEGIN BPS TAGGED BLOCK {{{
-%#
-%# COPYRIGHT:
-%#
-%# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
-%# <sales@bestpractical.com>
-%#
-%# (Except where explicitly superseded by other copyright notices)
-%#
-%#
-%# LICENSE:
-%#
-%# This work is made available to you under the terms of Version 2 of
-%# the GNU General Public License. A copy of that license should have
-%# been provided with this software, but in any event can be snarfed
-%# from www.gnu.org.
-%#
-%# This work is distributed in the hope that it will be useful, but
-%# WITHOUT ANY WARRANTY; without even the implied warranty of
-%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-%# General Public License for more details.
-%#
-%# You should have received a copy of the GNU General Public License
-%# along with this program; if not, write to the Free Software
-%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-%# 02110-1301 or visit their web page on the internet at
-%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
-%#
-%#
-%# CONTRIBUTION SUBMISSION POLICY:
-%#
-%# (The following paragraph is not intended to limit the rights granted
-%# to you to modify and distribute this software under the terms of
-%# the GNU General Public License and is only of importance to you if
-%# you choose to contribute your changes and enhancements to the
-%# community by submitting them to Best Practical Solutions, LLC.)
-%#
-%# By intentionally submitting any modifications, corrections or
-%# derivatives to this work, or any other work intended for use with
-%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
-%# you are the copyright holder for those contributions and you grant
-%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
-%# royalty-free, perpetual, license to use, copy, create derivative
-%# works based on those contributions, and sublicense and distribute
-%# those contributions and any derivatives thereof.
-%#
-%# END BPS TAGGED BLOCK }}}
-.ui-timepickr {
- font-size: 1.1em;
-}
-
-.ui-timepickr-button span {
- background: white;
-}
diff --git a/rt/share/html/NoAuth/css/web2/nav.css b/rt/share/html/NoAuth/css/web2/nav.css
index be63c5984..e404b61c8 100644
--- a/rt/share/html/NoAuth/css/web2/nav.css
+++ b/rt/share/html/NoAuth/css/web2/nav.css
@@ -239,6 +239,7 @@
border: 1px solid #ccc;
-moz-border-radius-bottomleft: 0.5em;
-webkit-border-bottom-left-radius: 0.5em;
+ border-bottom-left-radius: 0.5em;
border-right: none;
border-top: none;
list-style-type: none;
diff --git a/rt/share/html/NoAuth/js/jquery-ui-1.8.4.custom.min.js b/rt/share/html/NoAuth/js/jquery-ui-1.8.4.custom.min.js
index e90b4fe4b..0466005dc 100644
--- a/rt/share/html/NoAuth/js/jquery-ui-1.8.4.custom.min.js
+++ b/rt/share/html/NoAuth/js/jquery-ui-1.8.4.custom.min.js
@@ -222,3 +222,53 @@ c=this._daylightSavingAdjust(new Date(c,e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(t
function(a){if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));
return this.each(function(){typeof a=="string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new L;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.4";window["DP_jQuery_"+y]=d})(jQuery);
;
+/*!
+ * jQuery UI Mouse 1.8.4
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Mouse
+ *
+ * Depends:
+ * jquery.ui.widget.js
+ */
+(function(c){c.widget("ui.mouse",{options:{cancel:":input,option",distance:1,delay:0},_mouseInit:function(){var a=this;this.element.bind("mousedown."+this.widgetName,function(b){return a._mouseDown(b)}).bind("click."+this.widgetName,function(b){if(a._preventClickEvent){a._preventClickEvent=false;b.stopImmediatePropagation();return false}});this.started=false},_mouseDestroy:function(){this.element.unbind("."+this.widgetName)},_mouseDown:function(a){a.originalEvent=a.originalEvent||{};if(!a.originalEvent.mouseHandled){this._mouseStarted&&
+this._mouseUp(a);this._mouseDownEvent=a;var b=this,e=a.which==1,f=typeof this.options.cancel=="string"?c(a.target).parents().add(a.target).filter(this.options.cancel).length:false;if(!e||f||!this._mouseCapture(a))return true;this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet)this._mouseDelayTimer=setTimeout(function(){b.mouseDelayMet=true},this.options.delay);if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a)){this._mouseStarted=this._mouseStart(a)!==false;if(!this._mouseStarted){a.preventDefault();
+return true}}this._mouseMoveDelegate=function(d){return b._mouseMove(d)};this._mouseUpDelegate=function(d){return b._mouseUp(d)};c(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);c.browser.safari||a.preventDefault();return a.originalEvent.mouseHandled=true}},_mouseMove:function(a){if(c.browser.msie&&!(document.documentMode>=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&&
+this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){c(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=false;this._preventClickEvent=a.target==this._mouseDownEvent.target;this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-
+a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery);
+/*
+ * jQuery UI Slider 1.8.4
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */
+(function(d){d.widget("ui.slider",d.ui.mouse,{widgetEventPrefix:"slide",options:{animate:false,distance:0,max:100,min:0,orientation:"horizontal",range:false,step:1,value:0,values:null},_create:function(){var a=this,b=this.options;this._mouseSliding=this._keySliding=false;this._animateOff=true;this._handleIndex=null;this._detectOrientation();this._mouseInit();this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget ui-widget-content ui-corner-all");b.disabled&&this.element.addClass("ui-slider-disabled ui-disabled");
+this.range=d([]);if(b.range){if(b.range===true){this.range=d("<div></div>");if(!b.values)b.values=[this._valueMin(),this._valueMin()];if(b.values.length&&b.values.length!==2)b.values=[b.values[0],b.values[0]]}else this.range=d("<div></div>");this.range.appendTo(this.element).addClass("ui-slider-range");if(b.range==="min"||b.range==="max")this.range.addClass("ui-slider-range-"+b.range);this.range.addClass("ui-widget-header")}d(".ui-slider-handle",this.element).length===0&&d("<a href='#'></a>").appendTo(this.element).addClass("ui-slider-handle");
+if(b.values&&b.values.length)for(;d(".ui-slider-handle",this.element).length<b.values.length;)d("<a href='#'></a>").appendTo(this.element).addClass("ui-slider-handle");this.handles=d(".ui-slider-handle",this.element).addClass("ui-state-default ui-corner-all");this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(c){c.preventDefault()}).hover(function(){b.disabled||d(this).addClass("ui-state-hover")},function(){d(this).removeClass("ui-state-hover")}).focus(function(){if(b.disabled)d(this).blur();
+else{d(".ui-slider .ui-state-focus").removeClass("ui-state-focus");d(this).addClass("ui-state-focus")}}).blur(function(){d(this).removeClass("ui-state-focus")});this.handles.each(function(c){d(this).data("index.ui-slider-handle",c)});this.handles.keydown(function(c){var e=true,f=d(this).data("index.ui-slider-handle"),h,g,i;if(!a.options.disabled){switch(c.keyCode){case d.ui.keyCode.HOME:case d.ui.keyCode.END:case d.ui.keyCode.PAGE_UP:case d.ui.keyCode.PAGE_DOWN:case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:e=
+false;if(!a._keySliding){a._keySliding=true;d(this).addClass("ui-state-active");h=a._start(c,f);if(h===false)return}break}i=a.options.step;h=a.options.values&&a.options.values.length?(g=a.values(f)):(g=a.value());switch(c.keyCode){case d.ui.keyCode.HOME:g=a._valueMin();break;case d.ui.keyCode.END:g=a._valueMax();break;case d.ui.keyCode.PAGE_UP:g=a._trimAlignValue(h+(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.PAGE_DOWN:g=a._trimAlignValue(h-(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:if(h===
+a._valueMax())return;g=a._trimAlignValue(h+i);break;case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:if(h===a._valueMin())return;g=a._trimAlignValue(h-i);break}a._slide(c,f,g);return e}}).keyup(function(c){var e=d(this).data("index.ui-slider-handle");if(a._keySliding){a._keySliding=false;a._stop(c,e);a._change(c,e);d(this).removeClass("ui-state-active")}});this._refreshValue();this._animateOff=false},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider");
+this._mouseDestroy();return this},_mouseCapture:function(a){var b=this.options,c,e,f,h,g;if(b.disabled)return false;this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();c=this._normValueFromMouse({x:a.pageX,y:a.pageY});e=this._valueMax()-this._valueMin()+1;h=this;this.handles.each(function(i){var j=Math.abs(c-h.values(i));if(e>j){e=j;f=d(this);g=i}});if(b.range===true&&this.values(1)===b.min){g+=1;f=d(this.handles[g])}if(this._start(a,
+g)===false)return false;this._mouseSliding=true;h._handleIndex=g;f.addClass("ui-state-active").focus();b=f.offset();this._clickOffset=!d(a.target).parents().andSelf().is(".ui-slider-handle")?{left:0,top:0}:{left:a.pageX-b.left-f.width()/2,top:a.pageY-b.top-f.height()/2-(parseInt(f.css("borderTopWidth"),10)||0)-(parseInt(f.css("borderBottomWidth"),10)||0)+(parseInt(f.css("marginTop"),10)||0)};this._slide(a,g,c);return this._animateOff=true},_mouseStart:function(){return true},_mouseDrag:function(a){var b=
+this._normValueFromMouse({x:a.pageX,y:a.pageY});this._slide(a,this._handleIndex,b);return false},_mouseStop:function(a){this.handles.removeClass("ui-state-active");this._mouseSliding=false;this._stop(a,this._handleIndex);this._change(a,this._handleIndex);this._clickOffset=this._handleIndex=null;return this._animateOff=false},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b;if(this.orientation==="horizontal"){b=
+this.elementSize.width;a=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{b=this.elementSize.height;a=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}b=a/b;if(b>1)b=1;if(b<0)b=0;if(this.orientation==="vertical")b=1-b;a=this._valueMax()-this._valueMin();return this._trimAlignValue(this._valueMin()+b*a)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b);
+c.values=this.values()}return this._trigger("start",a,c)},_slide:function(a,b,c){var e;if(this.options.values&&this.options.values.length){e=this.values(b?0:1);if(this.options.values.length===2&&this.options.range===true&&(b===0&&c>e||b===1&&c<e))c=e;if(c!==this.values(b)){e=this.values();e[b]=c;a=this._trigger("slide",a,{handle:this.handles[b],value:c,values:e});this.values(b?0:1);a!==false&&this.values(b,c,true)}}else if(c!==this.value()){a=this._trigger("slide",a,{handle:this.handles[b],value:c});
+a!==false&&this.value(c)}},_stop:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b);c.values=this.values()}this._trigger("stop",a,c)},_change:function(a,b){if(!this._keySliding&&!this._mouseSliding){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b);c.values=this.values()}this._trigger("change",a,c)}},value:function(a){if(arguments.length){this.options.value=
+this._trimAlignValue(a);this._refreshValue();this._change(null,0)}return this._value()},values:function(a,b){var c,e,f;if(arguments.length>1){this.options.values[a]=this._trimAlignValue(b);this._refreshValue();this._change(null,a)}if(arguments.length)if(d.isArray(arguments[0])){c=this.options.values;e=arguments[0];for(f=0;f<c.length;f+=1){c[f]=this._trimAlignValue(e[f]);this._change(null,f)}this._refreshValue()}else return this.options.values&&this.options.values.length?this._values(a):this.value();
+else return this._values()},_setOption:function(a,b){var c,e=0;if(d.isArray(this.options.values))e=this.options.values.length;d.Widget.prototype._setOption.apply(this,arguments);switch(a){case "disabled":if(b){this.handles.filter(".ui-state-focus").blur();this.handles.removeClass("ui-state-hover");this.handles.attr("disabled","disabled");this.element.addClass("ui-disabled")}else{this.handles.removeAttr("disabled");this.element.removeClass("ui-disabled")}break;case "orientation":this._detectOrientation();
+this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation);this._refreshValue();break;case "value":this._animateOff=true;this._refreshValue();this._change(null,0);this._animateOff=false;break;case "values":this._animateOff=true;this._refreshValue();for(c=0;c<e;c+=1)this._change(null,c);this._animateOff=false;break}},_value:function(){var a=this.options.value;return a=this._trimAlignValue(a)},_values:function(a){var b,c;if(arguments.length){b=this.options.values[a];
+return b=this._trimAlignValue(b)}else{b=this.options.values.slice();for(c=0;c<b.length;c+=1)b[c]=this._trimAlignValue(b[c]);return b}},_trimAlignValue:function(a){if(a<this._valueMin())return this._valueMin();if(a>this._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=a%b;a=a-c;if(Math.abs(c)*2>=b)a+=c>0?b:-b;return parseFloat(a.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var a=
+this.options.range,b=this.options,c=this,e=!this._animateOff?b.animate:false,f,h={},g,i,j,l;if(this.options.values&&this.options.values.length)this.handles.each(function(k){f=(c.values(k)-c._valueMin())/(c._valueMax()-c._valueMin())*100;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";d(this).stop(1,1)[e?"animate":"css"](h,b.animate);if(c.options.range===true)if(c.orientation==="horizontal"){if(k===0)c.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({width:f-
+g+"%"},{queue:false,duration:b.animate})}else{if(k===0)c.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({height:f-g+"%"},{queue:false,duration:b.animate})}g=f});else{i=this.value();j=this._valueMin();l=this._valueMax();f=l!==j?(i-j)/(l-j)*100:0;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";this.handle.stop(1,1)[e?"animate":"css"](h,b.animate);if(a==="min"&&this.orientation==="horizontal")this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"},
+b.animate);if(a==="max"&&this.orientation==="horizontal")this.range[e?"animate":"css"]({width:100-f+"%"},{queue:false,duration:b.animate});if(a==="min"&&this.orientation==="vertical")this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},b.animate);if(a==="max"&&this.orientation==="vertical")this.range[e?"animate":"css"]({height:100-f+"%"},{queue:false,duration:b.animate})}}});d.extend(d.ui.slider,{version:"1.8.4"})})(jQuery);
diff --git a/rt/share/html/NoAuth/js/jquery-ui-patch-datepicker.js b/rt/share/html/NoAuth/js/jquery-ui-patch-datepicker.js
index 40cc0db99..2ac101f93 100644
--- a/rt/share/html/NoAuth/js/jquery-ui-patch-datepicker.js
+++ b/rt/share/html/NoAuth/js/jquery-ui-patch-datepicker.js
@@ -58,4 +58,35 @@
return data;
};
+
+ $.datepicker._checkOffset_orig = $.datepicker._checkOffset;
+ $.datepicker._checkOffset = function(inst, offset, isFixed) {
+ // copied from the original
+ var dpHeight = inst.dpDiv.outerHeight();
+ var inputHeight = inst.input ? inst.input.outerHeight() : 0;
+ var viewHeight = document.documentElement.clientHeight + $(document).scrollTop();
+
+ // save the original offset rather than the new offset because the
+ // original function modifies the passed arg as a side-effect
+ var old_offset = { top: offset.top, left: offset.left };
+ offset = $.datepicker._checkOffset_orig(inst, offset, isFixed);
+
+ // Negate any up or down positioning by adding instead of subtracting
+ offset.top += Math.min(old_offset.top, (old_offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ?
+ Math.abs(dpHeight + inputHeight) : 0);
+
+ return offset;
+ };
+
+
+ $.timepicker._newInst_orig = $.timepicker._newInst;
+ $.timepicker._newInst = function($input, o) {
+ var tp_inst = $.timepicker._newInst_orig($input, o);
+ tp_inst._defaults.onClose = function(dateText, dp_inst) {
+ if ($.isFunction(o.onClose))
+ o.onClose.call($input[0], dateText, dp_inst, tp_inst);
+ };
+ return tp_inst;
+ };
+
})(jQuery);
diff --git a/rt/share/html/NoAuth/js/jquery-ui-timepicker-addon.js b/rt/share/html/NoAuth/js/jquery-ui-timepicker-addon.js
new file mode 100644
index 000000000..0a4ff026e
--- /dev/null
+++ b/rt/share/html/NoAuth/js/jquery-ui-timepicker-addon.js
@@ -0,0 +1,1326 @@
+/*
+* jQuery timepicker addon
+* By: Trent Richardson [http://trentrichardson.com]
+* Version 1.0.0
+* Last Modified: 02/05/2012
+*
+* Copyright 2012 Trent Richardson
+* Dual licensed under the MIT and GPL licenses.
+* http://trentrichardson.com/Impromptu/GPL-LICENSE.txt
+* http://trentrichardson.com/Impromptu/MIT-LICENSE.txt
+*
+* HERES THE CSS:
+* .ui-timepicker-div .ui-widget-header { margin-bottom: 8px; }
+* .ui-timepicker-div dl { text-align: left; }
+* .ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; }
+* .ui-timepicker-div dl dd { margin: 0 10px 10px 65px; }
+* .ui-timepicker-div td { font-size: 90%; }
+* .ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; }
+*/
+
+(function($) {
+
+// Prevent "Uncaught RangeError: Maximum call stack size exceeded"
+$.ui.timepicker = $.ui.timepicker || {};
+if ($.ui.timepicker.version) {
+ return;
+}
+
+$.extend($.ui, { timepicker: { version: "1.0.0" } });
+
+/* Time picker manager.
+ Use the singleton instance of this class, $.timepicker, to interact with the time picker.
+ Settings for (groups of) time pickers are maintained in an instance object,
+ allowing multiple different settings on the same page. */
+
+function Timepicker() {
+ this.regional = []; // Available regional settings, indexed by language code
+ this.regional[''] = { // Default regional settings
+ currentText: 'Now',
+ closeText: 'Done',
+ ampm: false,
+ amNames: ['AM', 'A'],
+ pmNames: ['PM', 'P'],
+ timeFormat: 'hh:mm tt',
+ timeSuffix: '',
+ timeOnlyTitle: 'Choose Time',
+ timeText: 'Time',
+ hourText: 'Hour',
+ minuteText: 'Minute',
+ secondText: 'Second',
+ millisecText: 'Millisecond',
+ timezoneText: 'Time Zone'
+ };
+ this._defaults = { // Global defaults for all the datetime picker instances
+ showButtonPanel: true,
+ timeOnly: false,
+ showHour: true,
+ showMinute: true,
+ showSecond: false,
+ showMillisec: false,
+ showTimezone: false,
+ showTime: true,
+ stepHour: 1,
+ stepMinute: 1,
+ stepSecond: 1,
+ stepMillisec: 1,
+ hour: 0,
+ minute: 0,
+ second: 0,
+ millisec: 0,
+ timezone: '+0000',
+ hourMin: 0,
+ minuteMin: 0,
+ secondMin: 0,
+ millisecMin: 0,
+ hourMax: 23,
+ minuteMax: 59,
+ secondMax: 59,
+ millisecMax: 999,
+ minDateTime: null,
+ maxDateTime: null,
+ onSelect: null,
+ hourGrid: 0,
+ minuteGrid: 0,
+ secondGrid: 0,
+ millisecGrid: 0,
+ alwaysSetTime: true,
+ separator: ' ',
+ altFieldTimeOnly: true,
+ showTimepicker: true,
+ timezoneIso8609: false,
+ timezoneList: null,
+ addSliderAccess: false,
+ sliderAccessArgs: null
+ };
+ $.extend(this._defaults, this.regional['']);
+};
+
+$.extend(Timepicker.prototype, {
+ $input: null,
+ $altInput: null,
+ $timeObj: null,
+ inst: null,
+ hour_slider: null,
+ minute_slider: null,
+ second_slider: null,
+ millisec_slider: null,
+ timezone_select: null,
+ hour: 0,
+ minute: 0,
+ second: 0,
+ millisec: 0,
+ timezone: '+0000',
+ hourMinOriginal: null,
+ minuteMinOriginal: null,
+ secondMinOriginal: null,
+ millisecMinOriginal: null,
+ hourMaxOriginal: null,
+ minuteMaxOriginal: null,
+ secondMaxOriginal: null,
+ millisecMaxOriginal: null,
+ ampm: '',
+ formattedDate: '',
+ formattedTime: '',
+ formattedDateTime: '',
+ timezoneList: null,
+
+ /* Override the default settings for all instances of the time picker.
+ @param settings object - the new settings to use as defaults (anonymous object)
+ @return the manager object */
+ setDefaults: function(settings) {
+ extendRemove(this._defaults, settings || {});
+ return this;
+ },
+
+ //########################################################################
+ // Create a new Timepicker instance
+ //########################################################################
+ _newInst: function($input, o) {
+ var tp_inst = new Timepicker(),
+ inlineSettings = {};
+
+ for (var attrName in this._defaults) {
+ var attrValue = $input.attr('time:' + attrName);
+ if (attrValue) {
+ try {
+ inlineSettings[attrName] = eval(attrValue);
+ } catch (err) {
+ inlineSettings[attrName] = attrValue;
+ }
+ }
+ }
+ tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, o, {
+ beforeShow: function(input, dp_inst) {
+ if ($.isFunction(o.beforeShow))
+ return o.beforeShow(input, dp_inst, tp_inst);
+ },
+ onChangeMonthYear: function(year, month, dp_inst) {
+ // Update the time as well : this prevents the time from disappearing from the $input field.
+ tp_inst._updateDateTime(dp_inst);
+ if ($.isFunction(o.onChangeMonthYear))
+ o.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst);
+ },
+ onClose: function(dateText, dp_inst) {
+ if (tp_inst.timeDefined === true && $input.val() != '')
+ tp_inst._updateDateTime(dp_inst);
+ if ($.isFunction(o.onClose))
+ o.onClose.call($input[0], dateText, dp_inst, tp_inst);
+ },
+ timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker');
+ });
+ tp_inst.amNames = $.map(tp_inst._defaults.amNames, function(val) { return val.toUpperCase(); });
+ tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function(val) { return val.toUpperCase(); });
+
+ if (tp_inst._defaults.timezoneList === null) {
+ var timezoneList = [];
+ for (var i = -11; i <= 12; i++)
+ timezoneList.push((i >= 0 ? '+' : '-') + ('0' + Math.abs(i).toString()).slice(-2) + '00');
+ if (tp_inst._defaults.timezoneIso8609)
+ timezoneList = $.map(timezoneList, function(val) {
+ return val == '+0000' ? 'Z' : (val.substring(0, 3) + ':' + val.substring(3));
+ });
+ tp_inst._defaults.timezoneList = timezoneList;
+ }
+
+ tp_inst.hour = tp_inst._defaults.hour;
+ tp_inst.minute = tp_inst._defaults.minute;
+ tp_inst.second = tp_inst._defaults.second;
+ tp_inst.millisec = tp_inst._defaults.millisec;
+ tp_inst.ampm = '';
+ tp_inst.$input = $input;
+
+ if (o.altField)
+ tp_inst.$altInput = $(o.altField)
+ .css({ cursor: 'pointer' })
+ .focus(function(){ $input.trigger("focus"); });
+
+ if(tp_inst._defaults.minDate==0 || tp_inst._defaults.minDateTime==0)
+ {
+ tp_inst._defaults.minDate=new Date();
+ }
+ if(tp_inst._defaults.maxDate==0 || tp_inst._defaults.maxDateTime==0)
+ {
+ tp_inst._defaults.maxDate=new Date();
+ }
+
+ // datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime..
+ if(tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date)
+ tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime());
+ if(tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date)
+ tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime());
+ if(tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date)
+ tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime());
+ if(tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date)
+ tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime());
+ return tp_inst;
+ },
+
+ //########################################################################
+ // add our sliders to the calendar
+ //########################################################################
+ _addTimePicker: function(dp_inst) {
+ var currDT = (this.$altInput && this._defaults.altFieldTimeOnly) ?
+ this.$input.val() + ' ' + this.$altInput.val() :
+ this.$input.val();
+
+ this.timeDefined = this._parseTime(currDT);
+ this._limitMinMaxDateTime(dp_inst, false);
+ this._injectTimePicker();
+ },
+
+ //########################################################################
+ // parse the time string from input value or _setTime
+ //########################################################################
+ _parseTime: function(timeString, withDate) {
+ var regstr = this._defaults.timeFormat.toString()
+ .replace(/h{1,2}/ig, '(\\d?\\d)')
+ .replace(/m{1,2}/ig, '(\\d?\\d)')
+ .replace(/s{1,2}/ig, '(\\d?\\d)')
+ .replace(/l{1}/ig, '(\\d?\\d?\\d)')
+ .replace(/t{1,2}/ig, this._getPatternAmpm())
+ .replace(/z{1}/ig, '(z|[-+]\\d\\d:?\\d\\d)?')
+ .replace(/\s/g, '\\s?') + this._defaults.timeSuffix + '$',
+ order = this._getFormatPositions(),
+ ampm = '',
+ treg;
+
+ if (!this.inst) this.inst = $.datepicker._getInst(this.$input[0]);
+
+ if (withDate || !this._defaults.timeOnly) {
+ // the time should come after x number of characters and a space.
+ // x = at least the length of text specified by the date format
+ var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat');
+ // escape special regex characters in the seperator
+ var specials = new RegExp("[.*+?|()\\[\\]{}\\\\]", "g");
+ regstr = '^.{' + dp_dateFormat.length + ',}?' + this._defaults.separator.replace(specials, "\\$&") + regstr;
+ }
+
+ treg = timeString.match(new RegExp(regstr, 'i'));
+
+ if (treg) {
+ if (order.t !== -1) {
+ if (treg[order.t] === undefined || treg[order.t].length === 0) {
+ ampm = '';
+ this.ampm = '';
+ } else {
+ ampm = $.inArray(treg[order.t].toUpperCase(), this.amNames) !== -1 ? 'AM' : 'PM';
+ this.ampm = this._defaults[ampm == 'AM' ? 'amNames' : 'pmNames'][0];
+ }
+ }
+
+ if (order.h !== -1) {
+ if (ampm == 'AM' && treg[order.h] == '12')
+ this.hour = 0; // 12am = 0 hour
+ else if (ampm == 'PM' && treg[order.h] != '12')
+ this.hour = (parseFloat(treg[order.h]) + 12).toFixed(0); // 12pm = 12 hour, any other pm = hour + 12
+ else this.hour = Number(treg[order.h]);
+ }
+
+ if (order.m !== -1) this.minute = Number(treg[order.m]);
+ if (order.s !== -1) this.second = Number(treg[order.s]);
+ if (order.l !== -1) this.millisec = Number(treg[order.l]);
+ if (order.z !== -1 && treg[order.z] !== undefined) {
+ var tz = treg[order.z].toUpperCase();
+ switch (tz.length) {
+ case 1: // Z
+ tz = this._defaults.timezoneIso8609 ? 'Z' : '+0000';
+ break;
+ case 5: // +hhmm
+ if (this._defaults.timezoneIso8609)
+ tz = tz.substring(1) == '0000'
+ ? 'Z'
+ : tz.substring(0, 3) + ':' + tz.substring(3);
+ break;
+ case 6: // +hh:mm
+ if (!this._defaults.timezoneIso8609)
+ tz = tz == 'Z' || tz.substring(1) == '00:00'
+ ? '+0000'
+ : tz.replace(/:/, '');
+ else if (tz.substring(1) == '00:00')
+ tz = 'Z';
+ break;
+ }
+ this.timezone = tz;
+ }
+
+ return true;
+
+ }
+ return false;
+ },
+
+ //########################################################################
+ // pattern for standard and localized AM/PM markers
+ //########################################################################
+ _getPatternAmpm: function() {
+ var markers = [],
+ o = this._defaults;
+ if (o.amNames)
+ $.merge(markers, o.amNames);
+ if (o.pmNames)
+ $.merge(markers, o.pmNames);
+ markers = $.map(markers, function(val) { return val.replace(/[.*+?|()\[\]{}\\]/g, '\\$&'); });
+ return '(' + markers.join('|') + ')?';
+ },
+
+ //########################################################################
+ // figure out position of time elements.. cause js cant do named captures
+ //########################################################################
+ _getFormatPositions: function() {
+ var finds = this._defaults.timeFormat.toLowerCase().match(/(h{1,2}|m{1,2}|s{1,2}|l{1}|t{1,2}|z)/g),
+ orders = { h: -1, m: -1, s: -1, l: -1, t: -1, z: -1 };
+
+ if (finds)
+ for (var i = 0; i < finds.length; i++)
+ if (orders[finds[i].toString().charAt(0)] == -1)
+ orders[finds[i].toString().charAt(0)] = i + 1;
+
+ return orders;
+ },
+
+ //########################################################################
+ // generate and inject html for timepicker into ui datepicker
+ //########################################################################
+ _injectTimePicker: function() {
+ var $dp = this.inst.dpDiv,
+ o = this._defaults,
+ tp_inst = this,
+ // Added by Peter Medeiros:
+ // - Figure out what the hour/minute/second max should be based on the step values.
+ // - Example: if stepMinute is 15, then minMax is 45.
+ hourMax = parseInt((o.hourMax - ((o.hourMax - o.hourMin) % o.stepHour)) ,10),
+ minMax = parseInt((o.minuteMax - ((o.minuteMax - o.minuteMin) % o.stepMinute)) ,10),
+ secMax = parseInt((o.secondMax - ((o.secondMax - o.secondMin) % o.stepSecond)) ,10),
+ millisecMax = parseInt((o.millisecMax - ((o.millisecMax - o.millisecMin) % o.stepMillisec)) ,10),
+ dp_id = this.inst.id.toString().replace(/([^A-Za-z0-9_])/g, '');
+
+ // Prevent displaying twice
+ //if ($dp.find("div#ui-timepicker-div-"+ dp_id).length === 0) {
+ if ($dp.find("div#ui-timepicker-div-"+ dp_id).length === 0 && o.showTimepicker) {
+ var noDisplay = ' style="display:none;"',
+ html = '<div class="ui-timepicker-div" id="ui-timepicker-div-' + dp_id + '"><dl>' +
+ '<dt class="ui_tpicker_time_label" id="ui_tpicker_time_label_' + dp_id + '"' +
+ ((o.showTime) ? '' : noDisplay) + '>' + o.timeText + '</dt>' +
+ '<dd class="ui_tpicker_time" id="ui_tpicker_time_' + dp_id + '"' +
+ ((o.showTime) ? '' : noDisplay) + '></dd>' +
+ '<dt class="ui_tpicker_hour_label" id="ui_tpicker_hour_label_' + dp_id + '"' +
+ ((o.showHour) ? '' : noDisplay) + '>' + o.hourText + '</dt>',
+ hourGridSize = 0,
+ minuteGridSize = 0,
+ secondGridSize = 0,
+ millisecGridSize = 0,
+ size = null;
+
+ // Hours
+ html += '<dd class="ui_tpicker_hour"><div id="ui_tpicker_hour_' + dp_id + '"' +
+ ((o.showHour) ? '' : noDisplay) + '></div>';
+ if (o.showHour && o.hourGrid > 0) {
+ html += '<div style="padding-left: 1px"><table class="ui-tpicker-grid-label"><tr>';
+
+ for (var h = o.hourMin; h <= hourMax; h += parseInt(o.hourGrid,10)) {
+ hourGridSize++;
+ var tmph = (o.ampm && h > 12) ? h-12 : h;
+ if (tmph < 10) tmph = '0' + tmph;
+ if (o.ampm) {
+ if (h == 0) tmph = 12 +'a';
+ else if (h < 12) tmph += 'a';
+ else tmph += 'p';
+ }
+ html += '<td>' + tmph + '</td>';
+ }
+
+ html += '</tr></table></div>';
+ }
+ html += '</dd>';
+
+ // Minutes
+ html += '<dt class="ui_tpicker_minute_label" id="ui_tpicker_minute_label_' + dp_id + '"' +
+ ((o.showMinute) ? '' : noDisplay) + '>' + o.minuteText + '</dt>'+
+ '<dd class="ui_tpicker_minute"><div id="ui_tpicker_minute_' + dp_id + '"' +
+ ((o.showMinute) ? '' : noDisplay) + '></div>';
+
+ if (o.showMinute && o.minuteGrid > 0) {
+ html += '<div style="padding-left: 1px"><table class="ui-tpicker-grid-label"><tr>';
+
+ for (var m = o.minuteMin; m <= minMax; m += parseInt(o.minuteGrid,10)) {
+ minuteGridSize++;
+ html += '<td>' + ((m < 10) ? '0' : '') + m + '</td>';
+ }
+
+ html += '</tr></table></div>';
+ }
+ html += '</dd>';
+
+ // Seconds
+ html += '<dt class="ui_tpicker_second_label" id="ui_tpicker_second_label_' + dp_id + '"' +
+ ((o.showSecond) ? '' : noDisplay) + '>' + o.secondText + '</dt>'+
+ '<dd class="ui_tpicker_second"><div id="ui_tpicker_second_' + dp_id + '"'+
+ ((o.showSecond) ? '' : noDisplay) + '></div>';
+
+ if (o.showSecond && o.secondGrid > 0) {
+ html += '<div style="padding-left: 1px"><table><tr>';
+
+ for (var s = o.secondMin; s <= secMax; s += parseInt(o.secondGrid,10)) {
+ secondGridSize++;
+ html += '<td>' + ((s < 10) ? '0' : '') + s + '</td>';
+ }
+
+ html += '</tr></table></div>';
+ }
+ html += '</dd>';
+
+ // Milliseconds
+ html += '<dt class="ui_tpicker_millisec_label" id="ui_tpicker_millisec_label_' + dp_id + '"' +
+ ((o.showMillisec) ? '' : noDisplay) + '>' + o.millisecText + '</dt>'+
+ '<dd class="ui_tpicker_millisec"><div id="ui_tpicker_millisec_' + dp_id + '"'+
+ ((o.showMillisec) ? '' : noDisplay) + '></div>';
+
+ if (o.showMillisec && o.millisecGrid > 0) {
+ html += '<div style="padding-left: 1px"><table><tr>';
+
+ for (var l = o.millisecMin; l <= millisecMax; l += parseInt(o.millisecGrid,10)) {
+ millisecGridSize++;
+ html += '<td>' + ((l < 10) ? '0' : '') + l + '</td>';
+ }
+
+ html += '</tr></table></div>';
+ }
+ html += '</dd>';
+
+ // Timezone
+ html += '<dt class="ui_tpicker_timezone_label" id="ui_tpicker_timezone_label_' + dp_id + '"' +
+ ((o.showTimezone) ? '' : noDisplay) + '>' + o.timezoneText + '</dt>';
+ html += '<dd class="ui_tpicker_timezone" id="ui_tpicker_timezone_' + dp_id + '"' +
+ ((o.showTimezone) ? '' : noDisplay) + '></dd>';
+
+ html += '</dl></div>';
+ $tp = $(html);
+
+ // if we only want time picker...
+ if (o.timeOnly === true) {
+ $tp.prepend(
+ '<div class="ui-widget-header ui-helper-clearfix ui-corner-all">' +
+ '<div class="ui-datepicker-title">' + o.timeOnlyTitle + '</div>' +
+ '</div>');
+ $dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide();
+ }
+
+ this.hour_slider = $tp.find('#ui_tpicker_hour_'+ dp_id).slider({
+ orientation: "horizontal",
+ value: this.hour,
+ min: o.hourMin,
+ max: hourMax,
+ step: o.stepHour,
+ slide: function(event, ui) {
+ tp_inst.hour_slider.slider( "option", "value", ui.value);
+ tp_inst._onTimeChange();
+ }
+ });
+
+
+ // Updated by Peter Medeiros:
+ // - Pass in Event and UI instance into slide function
+ this.minute_slider = $tp.find('#ui_tpicker_minute_'+ dp_id).slider({
+ orientation: "horizontal",
+ value: this.minute,
+ min: o.minuteMin,
+ max: minMax,
+ step: o.stepMinute,
+ slide: function(event, ui) {
+ tp_inst.minute_slider.slider( "option", "value", ui.value);
+ tp_inst._onTimeChange();
+ }
+ });
+
+ this.second_slider = $tp.find('#ui_tpicker_second_'+ dp_id).slider({
+ orientation: "horizontal",
+ value: this.second,
+ min: o.secondMin,
+ max: secMax,
+ step: o.stepSecond,
+ slide: function(event, ui) {
+ tp_inst.second_slider.slider( "option", "value", ui.value);
+ tp_inst._onTimeChange();
+ }
+ });
+
+ this.millisec_slider = $tp.find('#ui_tpicker_millisec_'+ dp_id).slider({
+ orientation: "horizontal",
+ value: this.millisec,
+ min: o.millisecMin,
+ max: millisecMax,
+ step: o.stepMillisec,
+ slide: function(event, ui) {
+ tp_inst.millisec_slider.slider( "option", "value", ui.value);
+ tp_inst._onTimeChange();
+ }
+ });
+
+ this.timezone_select = $tp.find('#ui_tpicker_timezone_'+ dp_id).append('<select></select>').find("select");
+ $.fn.append.apply(this.timezone_select,
+ $.map(o.timezoneList, function(val, idx) {
+ return $("<option />")
+ .val(typeof val == "object" ? val.value : val)
+ .text(typeof val == "object" ? val.label : val);
+ })
+ );
+ this.timezone_select.val((typeof this.timezone != "undefined" && this.timezone != null && this.timezone != "") ? this.timezone : o.timezone);
+ this.timezone_select.change(function() {
+ tp_inst._onTimeChange();
+ });
+
+ // Add grid functionality
+ if (o.showHour && o.hourGrid > 0) {
+ size = 100 * hourGridSize * o.hourGrid / (hourMax - o.hourMin);
+
+ $tp.find(".ui_tpicker_hour table").css({
+ width: size + "%",
+ marginLeft: (size / (-2 * hourGridSize)) + "%",
+ borderCollapse: 'collapse'
+ }).find("td").each( function(index) {
+ $(this).click(function() {
+ var h = $(this).html();
+ if(o.ampm) {
+ var ap = h.substring(2).toLowerCase(),
+ aph = parseInt(h.substring(0,2), 10);
+ if (ap == 'a') {
+ if (aph == 12) h = 0;
+ else h = aph;
+ } else if (aph == 12) h = 12;
+ else h = aph + 12;
+ }
+ tp_inst.hour_slider.slider("option", "value", h);
+ tp_inst._onTimeChange();
+ tp_inst._onSelectHandler();
+ }).css({
+ cursor: 'pointer',
+ width: (100 / hourGridSize) + '%',
+ textAlign: 'center',
+ overflow: 'hidden'
+ });
+ });
+ }
+
+ if (o.showMinute && o.minuteGrid > 0) {
+ size = 100 * minuteGridSize * o.minuteGrid / (minMax - o.minuteMin);
+ $tp.find(".ui_tpicker_minute table").css({
+ width: size + "%",
+ marginLeft: (size / (-2 * minuteGridSize)) + "%",
+ borderCollapse: 'collapse'
+ }).find("td").each(function(index) {
+ $(this).click(function() {
+ tp_inst.minute_slider.slider("option", "value", $(this).html());
+ tp_inst._onTimeChange();
+ tp_inst._onSelectHandler();
+ }).css({
+ cursor: 'pointer',
+ width: (100 / minuteGridSize) + '%',
+ textAlign: 'center',
+ overflow: 'hidden'
+ });
+ });
+ }
+
+ if (o.showSecond && o.secondGrid > 0) {
+ $tp.find(".ui_tpicker_second table").css({
+ width: size + "%",
+ marginLeft: (size / (-2 * secondGridSize)) + "%",
+ borderCollapse: 'collapse'
+ }).find("td").each(function(index) {
+ $(this).click(function() {
+ tp_inst.second_slider.slider("option", "value", $(this).html());
+ tp_inst._onTimeChange();
+ tp_inst._onSelectHandler();
+ }).css({
+ cursor: 'pointer',
+ width: (100 / secondGridSize) + '%',
+ textAlign: 'center',
+ overflow: 'hidden'
+ });
+ });
+ }
+
+ if (o.showMillisec && o.millisecGrid > 0) {
+ $tp.find(".ui_tpicker_millisec table").css({
+ width: size + "%",
+ marginLeft: (size / (-2 * millisecGridSize)) + "%",
+ borderCollapse: 'collapse'
+ }).find("td").each(function(index) {
+ $(this).click(function() {
+ tp_inst.millisec_slider.slider("option", "value", $(this).html());
+ tp_inst._onTimeChange();
+ tp_inst._onSelectHandler();
+ }).css({
+ cursor: 'pointer',
+ width: (100 / millisecGridSize) + '%',
+ textAlign: 'center',
+ overflow: 'hidden'
+ });
+ });
+ }
+
+ var $buttonPanel = $dp.find('.ui-datepicker-buttonpane');
+ if ($buttonPanel.length) $buttonPanel.before($tp);
+ else $dp.append($tp);
+
+ this.$timeObj = $tp.find('#ui_tpicker_time_'+ dp_id);
+
+ if (this.inst !== null) {
+ var timeDefined = this.timeDefined;
+ this._onTimeChange();
+ this.timeDefined = timeDefined;
+ }
+
+ //Emulate datepicker onSelect behavior. Call on slidestop.
+ var onSelectDelegate = function() {
+ tp_inst._onSelectHandler();
+ };
+ this.hour_slider.bind('slidestop',onSelectDelegate);
+ this.minute_slider.bind('slidestop',onSelectDelegate);
+ this.second_slider.bind('slidestop',onSelectDelegate);
+ this.millisec_slider.bind('slidestop',onSelectDelegate);
+
+ // slideAccess integration: http://trentrichardson.com/2011/11/11/jquery-ui-sliders-and-touch-accessibility/
+ if (this._defaults.addSliderAccess){
+ var sliderAccessArgs = this._defaults.sliderAccessArgs;
+ setTimeout(function(){ // fix for inline mode
+ if($tp.find('.ui-slider-access').length == 0){
+ $tp.find('.ui-slider:visible').sliderAccess(sliderAccessArgs);
+
+ // fix any grids since sliders are shorter
+ var sliderAccessWidth = $tp.find('.ui-slider-access:eq(0)').outerWidth(true);
+ if(sliderAccessWidth){
+ $tp.find('table:visible').each(function(){
+ var $g = $(this),
+ oldWidth = $g.outerWidth(),
+ oldMarginLeft = $g.css('marginLeft').toString().replace('%',''),
+ newWidth = oldWidth - sliderAccessWidth,
+ newMarginLeft = ((oldMarginLeft * newWidth)/oldWidth) + '%';
+
+ $g.css({ width: newWidth, marginLeft: newMarginLeft });
+ });
+ }
+ }
+ },0);
+ }
+ // end slideAccess integration
+
+ }
+ },
+
+ //########################################################################
+ // This function tries to limit the ability to go outside the
+ // min/max date range
+ //########################################################################
+ _limitMinMaxDateTime: function(dp_inst, adjustSliders){
+ var o = this._defaults,
+ dp_date = new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay);
+
+ if(!this._defaults.showTimepicker) return; // No time so nothing to check here
+
+ if($.datepicker._get(dp_inst, 'minDateTime') !== null && $.datepicker._get(dp_inst, 'minDateTime') !== undefined && dp_date){
+ var minDateTime = $.datepicker._get(dp_inst, 'minDateTime'),
+ minDateTimeDate = new Date(minDateTime.getFullYear(), minDateTime.getMonth(), minDateTime.getDate(), 0, 0, 0, 0);
+
+ if(this.hourMinOriginal === null || this.minuteMinOriginal === null || this.secondMinOriginal === null || this.millisecMinOriginal === null){
+ this.hourMinOriginal = o.hourMin;
+ this.minuteMinOriginal = o.minuteMin;
+ this.secondMinOriginal = o.secondMin;
+ this.millisecMinOriginal = o.millisecMin;
+ }
+
+ if(dp_inst.settings.timeOnly || minDateTimeDate.getTime() == dp_date.getTime()) {
+ this._defaults.hourMin = minDateTime.getHours();
+ if (this.hour <= this._defaults.hourMin) {
+ this.hour = this._defaults.hourMin;
+ this._defaults.minuteMin = minDateTime.getMinutes();
+ if (this.minute <= this._defaults.minuteMin) {
+ this.minute = this._defaults.minuteMin;
+ this._defaults.secondMin = minDateTime.getSeconds();
+ } else if (this.second <= this._defaults.secondMin){
+ this.second = this._defaults.secondMin;
+ this._defaults.millisecMin = minDateTime.getMilliseconds();
+ } else {
+ if(this.millisec < this._defaults.millisecMin)
+ this.millisec = this._defaults.millisecMin;
+ this._defaults.millisecMin = this.millisecMinOriginal;
+ }
+ } else {
+ this._defaults.minuteMin = this.minuteMinOriginal;
+ this._defaults.secondMin = this.secondMinOriginal;
+ this._defaults.millisecMin = this.millisecMinOriginal;
+ }
+ }else{
+ this._defaults.hourMin = this.hourMinOriginal;
+ this._defaults.minuteMin = this.minuteMinOriginal;
+ this._defaults.secondMin = this.secondMinOriginal;
+ this._defaults.millisecMin = this.millisecMinOriginal;
+ }
+ }
+
+ if($.datepicker._get(dp_inst, 'maxDateTime') !== null && $.datepicker._get(dp_inst, 'maxDateTime') !== undefined && dp_date){
+ var maxDateTime = $.datepicker._get(dp_inst, 'maxDateTime'),
+ maxDateTimeDate = new Date(maxDateTime.getFullYear(), maxDateTime.getMonth(), maxDateTime.getDate(), 0, 0, 0, 0);
+
+ if(this.hourMaxOriginal === null || this.minuteMaxOriginal === null || this.secondMaxOriginal === null){
+ this.hourMaxOriginal = o.hourMax;
+ this.minuteMaxOriginal = o.minuteMax;
+ this.secondMaxOriginal = o.secondMax;
+ this.millisecMaxOriginal = o.millisecMax;
+ }
+
+ if(dp_inst.settings.timeOnly || maxDateTimeDate.getTime() == dp_date.getTime()){
+ this._defaults.hourMax = maxDateTime.getHours();
+ if (this.hour >= this._defaults.hourMax) {
+ this.hour = this._defaults.hourMax;
+ this._defaults.minuteMax = maxDateTime.getMinutes();
+ if (this.minute >= this._defaults.minuteMax) {
+ this.minute = this._defaults.minuteMax;
+ this._defaults.secondMax = maxDateTime.getSeconds();
+ } else if (this.second >= this._defaults.secondMax) {
+ this.second = this._defaults.secondMax;
+ this._defaults.millisecMax = maxDateTime.getMilliseconds();
+ } else {
+ if(this.millisec > this._defaults.millisecMax) this.millisec = this._defaults.millisecMax;
+ this._defaults.millisecMax = this.millisecMaxOriginal;
+ }
+ } else {
+ this._defaults.minuteMax = this.minuteMaxOriginal;
+ this._defaults.secondMax = this.secondMaxOriginal;
+ this._defaults.millisecMax = this.millisecMaxOriginal;
+ }
+ }else{
+ this._defaults.hourMax = this.hourMaxOriginal;
+ this._defaults.minuteMax = this.minuteMaxOriginal;
+ this._defaults.secondMax = this.secondMaxOriginal;
+ this._defaults.millisecMax = this.millisecMaxOriginal;
+ }
+ }
+
+ if(adjustSliders !== undefined && adjustSliders === true){
+ var hourMax = parseInt((this._defaults.hourMax - ((this._defaults.hourMax - this._defaults.hourMin) % this._defaults.stepHour)) ,10),
+ minMax = parseInt((this._defaults.minuteMax - ((this._defaults.minuteMax - this._defaults.minuteMin) % this._defaults.stepMinute)) ,10),
+ secMax = parseInt((this._defaults.secondMax - ((this._defaults.secondMax - this._defaults.secondMin) % this._defaults.stepSecond)) ,10),
+ millisecMax = parseInt((this._defaults.millisecMax - ((this._defaults.millisecMax - this._defaults.millisecMin) % this._defaults.stepMillisec)) ,10);
+
+ if(this.hour_slider)
+ this.hour_slider.slider("option", { min: this._defaults.hourMin, max: hourMax }).slider('value', this.hour);
+ if(this.minute_slider)
+ this.minute_slider.slider("option", { min: this._defaults.minuteMin, max: minMax }).slider('value', this.minute);
+ if(this.second_slider)
+ this.second_slider.slider("option", { min: this._defaults.secondMin, max: secMax }).slider('value', this.second);
+ if(this.millisec_slider)
+ this.millisec_slider.slider("option", { min: this._defaults.millisecMin, max: millisecMax }).slider('value', this.millisec);
+ }
+
+ },
+
+
+ //########################################################################
+ // when a slider moves, set the internal time...
+ // on time change is also called when the time is updated in the text field
+ //########################################################################
+ _onTimeChange: function() {
+ var hour = (this.hour_slider) ? this.hour_slider.slider('value') : false,
+ minute = (this.minute_slider) ? this.minute_slider.slider('value') : false,
+ second = (this.second_slider) ? this.second_slider.slider('value') : false,
+ millisec = (this.millisec_slider) ? this.millisec_slider.slider('value') : false,
+ timezone = (this.timezone_select) ? this.timezone_select.val() : false,
+ o = this._defaults;
+
+ if (typeof(hour) == 'object') hour = false;
+ if (typeof(minute) == 'object') minute = false;
+ if (typeof(second) == 'object') second = false;
+ if (typeof(millisec) == 'object') millisec = false;
+ if (typeof(timezone) == 'object') timezone = false;
+
+ if (hour !== false) hour = parseInt(hour,10);
+ if (minute !== false) minute = parseInt(minute,10);
+ if (second !== false) second = parseInt(second,10);
+ if (millisec !== false) millisec = parseInt(millisec,10);
+
+ var ampm = o[hour < 12 ? 'amNames' : 'pmNames'][0];
+
+ // If the update was done in the input field, the input field should not be updated.
+ // If the update was done using the sliders, update the input field.
+ var hasChanged = (hour != this.hour || minute != this.minute
+ || second != this.second || millisec != this.millisec
+ || (this.ampm.length > 0
+ && (hour < 12) != ($.inArray(this.ampm.toUpperCase(), this.amNames) !== -1))
+ || timezone != this.timezone);
+
+ if (hasChanged) {
+
+ if (hour !== false)this.hour = hour;
+ if (minute !== false) this.minute = minute;
+ if (second !== false) this.second = second;
+ if (millisec !== false) this.millisec = millisec;
+ if (timezone !== false) this.timezone = timezone;
+
+ if (!this.inst) this.inst = $.datepicker._getInst(this.$input[0]);
+
+ this._limitMinMaxDateTime(this.inst, true);
+ }
+ if (o.ampm) this.ampm = ampm;
+
+ //this._formatTime();
+ this.formattedTime = $.datepicker.formatTime(this._defaults.timeFormat, this, this._defaults);
+ if (this.$timeObj) this.$timeObj.text(this.formattedTime + o.timeSuffix);
+ this.timeDefined = true;
+ if (hasChanged) this._updateDateTime();
+ },
+
+ //########################################################################
+ // call custom onSelect.
+ // bind to sliders slidestop, and grid click.
+ //########################################################################
+ _onSelectHandler: function() {
+ var onSelect = this._defaults.onSelect;
+ var inputEl = this.$input ? this.$input[0] : null;
+ if (onSelect && inputEl) {
+ onSelect.apply(inputEl, [this.formattedDateTime, this]);
+ }
+ },
+
+ //########################################################################
+ // left for any backwards compatibility
+ //########################################################################
+ _formatTime: function(time, format) {
+ time = time || { hour: this.hour, minute: this.minute, second: this.second, millisec: this.millisec, ampm: this.ampm, timezone: this.timezone };
+ var tmptime = (format || this._defaults.timeFormat).toString();
+
+ tmptime = $.datepicker.formatTime(tmptime, time, this._defaults);
+
+ if (arguments.length) return tmptime;
+ else this.formattedTime = tmptime;
+ },
+
+ //########################################################################
+ // update our input with the new date time..
+ //########################################################################
+ _updateDateTime: function(dp_inst) {
+ dp_inst = this.inst || dp_inst;
+ var dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)),
+ dateFmt = $.datepicker._get(dp_inst, 'dateFormat'),
+ formatCfg = $.datepicker._getFormatConfig(dp_inst),
+ timeAvailable = dt !== null && this.timeDefined;
+ this.formattedDate = $.datepicker.formatDate(dateFmt, (dt === null ? new Date() : dt), formatCfg);
+ var formattedDateTime = this.formattedDate;
+ if (dp_inst.lastVal !== undefined && (dp_inst.lastVal.length > 0 && this.$input.val().length === 0))
+ return;
+
+ if (this._defaults.timeOnly === true) {
+ formattedDateTime = this.formattedTime;
+ } else if (this._defaults.timeOnly !== true && (this._defaults.alwaysSetTime || timeAvailable)) {
+ formattedDateTime += this._defaults.separator + this.formattedTime + this._defaults.timeSuffix;
+ }
+
+ this.formattedDateTime = formattedDateTime;
+
+ if(!this._defaults.showTimepicker) {
+ this.$input.val(this.formattedDate);
+ } else if (this.$altInput && this._defaults.altFieldTimeOnly === true) {
+ this.$altInput.val(this.formattedTime);
+ this.$input.val(this.formattedDate);
+ } else if(this.$altInput) {
+ this.$altInput.val(formattedDateTime);
+ this.$input.val(formattedDateTime);
+ } else {
+ this.$input.val(formattedDateTime);
+ }
+
+ this.$input.trigger("change");
+ }
+
+});
+
+$.fn.extend({
+ //########################################################################
+ // shorthand just to use timepicker..
+ //########################################################################
+ timepicker: function(o) {
+ o = o || {};
+ var tmp_args = arguments;
+
+ if (typeof o == 'object') tmp_args[0] = $.extend(o, { timeOnly: true });
+
+ return $(this).each(function() {
+ $.fn.datetimepicker.apply($(this), tmp_args);
+ });
+ },
+
+ //########################################################################
+ // extend timepicker to datepicker
+ //########################################################################
+ datetimepicker: function(o) {
+ o = o || {};
+ tmp_args = arguments;
+
+ if (typeof(o) == 'string'){
+ if(o == 'getDate')
+ return $.fn.datepicker.apply($(this[0]), tmp_args);
+ else
+ return this.each(function() {
+ var $t = $(this);
+ $t.datepicker.apply($t, tmp_args);
+ });
+ }
+ else
+ return this.each(function() {
+ var $t = $(this);
+ $t.datepicker($.timepicker._newInst($t, o)._defaults);
+ });
+ }
+});
+
+//########################################################################
+// format the time all pretty...
+// format = string format of the time
+// time = a {}, not a Date() for timezones
+// options = essentially the regional[].. amNames, pmNames, ampm
+//########################################################################
+$.datepicker.formatTime = function(format, time, options) {
+ options = options || {};
+ options = $.extend($.timepicker._defaults, options);
+ time = $.extend({hour:0, minute:0, second:0, millisec:0, timezone:'+0000'}, time);
+
+ var tmptime = format;
+ var ampmName = options['amNames'][0];
+
+ var hour = parseInt(time.hour, 10);
+ if (options.ampm) {
+ if (hour > 11){
+ ampmName = options['pmNames'][0];
+ if(hour > 12)
+ hour = hour % 12;
+ }
+ if (hour === 0)
+ hour = 12;
+ }
+ tmptime = tmptime.replace(/(?:hh?|mm?|ss?|[tT]{1,2}|[lz])/g, function(match) {
+ switch (match.toLowerCase()) {
+ case 'hh': return ('0' + hour).slice(-2);
+ case 'h': return hour;
+ case 'mm': return ('0' + time.minute).slice(-2);
+ case 'm': return time.minute;
+ case 'ss': return ('0' + time.second).slice(-2);
+ case 's': return time.second;
+ case 'l': return ('00' + time.millisec).slice(-3);
+ case 'z': return time.timezone;
+ case 't': case 'tt':
+ if (options.ampm) {
+ if (match.length == 1)
+ ampmName = ampmName.charAt(0);
+ return match.charAt(0) == 'T' ? ampmName.toUpperCase() : ampmName.toLowerCase();
+ }
+ return '';
+ }
+ });
+
+ tmptime = $.trim(tmptime);
+ return tmptime;
+};
+
+//########################################################################
+// the bad hack :/ override datepicker so it doesnt close on select
+// inspired: http://stackoverflow.com/questions/1252512/jquery-datepicker-prevent-closing-picker-when-clicking-a-date/1762378#1762378
+//########################################################################
+$.datepicker._base_selectDate = $.datepicker._selectDate;
+$.datepicker._selectDate = function (id, dateStr) {
+ var inst = this._getInst($(id)[0]),
+ tp_inst = this._get(inst, 'timepicker');
+
+ if (tp_inst) {
+ tp_inst._limitMinMaxDateTime(inst, true);
+ inst.inline = inst.stay_open = true;
+ //This way the onSelect handler called from calendarpicker get the full dateTime
+ this._base_selectDate(id, dateStr);
+ inst.inline = inst.stay_open = false;
+ this._notifyChange(inst);
+ this._updateDatepicker(inst);
+ }
+ else this._base_selectDate(id, dateStr);
+};
+
+//#############################################################################################
+// second bad hack :/ override datepicker so it triggers an event when changing the input field
+// and does not redraw the datepicker on every selectDate event
+//#############################################################################################
+$.datepicker._base_updateDatepicker = $.datepicker._updateDatepicker;
+$.datepicker._updateDatepicker = function(inst) {
+
+ // don't popup the datepicker if there is another instance already opened
+ var input = inst.input[0];
+ if($.datepicker._curInst &&
+ $.datepicker._curInst != inst &&
+ $.datepicker._datepickerShowing &&
+ $.datepicker._lastInput != input) {
+ return;
+ }
+
+ if (typeof(inst.stay_open) !== 'boolean' || inst.stay_open === false) {
+
+ this._base_updateDatepicker(inst);
+
+ // Reload the time control when changing something in the input text field.
+ var tp_inst = this._get(inst, 'timepicker');
+ if(tp_inst) tp_inst._addTimePicker(inst);
+ }
+};
+
+//#######################################################################################
+// third bad hack :/ override datepicker so it allows spaces and colon in the input field
+//#######################################################################################
+$.datepicker._base_doKeyPress = $.datepicker._doKeyPress;
+$.datepicker._doKeyPress = function(event) {
+ var inst = $.datepicker._getInst(event.target),
+ tp_inst = $.datepicker._get(inst, 'timepicker');
+
+ if (tp_inst) {
+ if ($.datepicker._get(inst, 'constrainInput')) {
+ var ampm = tp_inst._defaults.ampm,
+ dateChars = $.datepicker._possibleChars($.datepicker._get(inst, 'dateFormat')),
+ datetimeChars = tp_inst._defaults.timeFormat.toString()
+ .replace(/[hms]/g, '')
+ .replace(/TT/g, ampm ? 'APM' : '')
+ .replace(/Tt/g, ampm ? 'AaPpMm' : '')
+ .replace(/tT/g, ampm ? 'AaPpMm' : '')
+ .replace(/T/g, ampm ? 'AP' : '')
+ .replace(/tt/g, ampm ? 'apm' : '')
+ .replace(/t/g, ampm ? 'ap' : '') +
+ " " +
+ tp_inst._defaults.separator +
+ tp_inst._defaults.timeSuffix +
+ (tp_inst._defaults.showTimezone ? tp_inst._defaults.timezoneList.join('') : '') +
+ (tp_inst._defaults.amNames.join('')) +
+ (tp_inst._defaults.pmNames.join('')) +
+ dateChars,
+ chr = String.fromCharCode(event.charCode === undefined ? event.keyCode : event.charCode);
+ return event.ctrlKey || (chr < ' ' || !dateChars || datetimeChars.indexOf(chr) > -1);
+ }
+ }
+
+ return $.datepicker._base_doKeyPress(event);
+};
+
+//#######################################################################################
+// Override key up event to sync manual input changes.
+//#######################################################################################
+$.datepicker._base_doKeyUp = $.datepicker._doKeyUp;
+$.datepicker._doKeyUp = function (event) {
+ var inst = $.datepicker._getInst(event.target),
+ tp_inst = $.datepicker._get(inst, 'timepicker');
+
+ if (tp_inst) {
+ if (tp_inst._defaults.timeOnly && (inst.input.val() != inst.lastVal)) {
+ try {
+ $.datepicker._updateDatepicker(inst);
+ }
+ catch (err) {
+ $.datepicker.log(err);
+ }
+ }
+ }
+
+ return $.datepicker._base_doKeyUp(event);
+};
+
+//#######################################################################################
+// override "Today" button to also grab the time.
+//#######################################################################################
+$.datepicker._base_gotoToday = $.datepicker._gotoToday;
+$.datepicker._gotoToday = function(id) {
+ var inst = this._getInst($(id)[0]),
+ $dp = inst.dpDiv;
+ this._base_gotoToday(id);
+ var now = new Date();
+ var tp_inst = this._get(inst, 'timepicker');
+ if (tp_inst && tp_inst._defaults.showTimezone && tp_inst.timezone_select) {
+ var tzoffset = now.getTimezoneOffset(); // If +0100, returns -60
+ var tzsign = tzoffset > 0 ? '-' : '+';
+ tzoffset = Math.abs(tzoffset);
+ var tzmin = tzoffset % 60;
+ tzoffset = tzsign + ('0' + (tzoffset - tzmin) / 60).slice(-2) + ('0' + tzmin).slice(-2);
+ if (tp_inst._defaults.timezoneIso8609)
+ tzoffset = tzoffset.substring(0, 3) + ':' + tzoffset.substring(3);
+ tp_inst.timezone_select.val(tzoffset);
+ }
+ this._setTime(inst, now);
+ $( '.ui-datepicker-today', $dp).click();
+};
+
+//#######################################################################################
+// Disable & enable the Time in the datetimepicker
+//#######################################################################################
+$.datepicker._disableTimepickerDatepicker = function(target, date, withDate) {
+ var inst = this._getInst(target),
+ tp_inst = this._get(inst, 'timepicker');
+ $(target).datepicker('getDate'); // Init selected[Year|Month|Day]
+ if (tp_inst) {
+ tp_inst._defaults.showTimepicker = false;
+ tp_inst._updateDateTime(inst);
+ }
+};
+
+$.datepicker._enableTimepickerDatepicker = function(target, date, withDate) {
+ var inst = this._getInst(target),
+ tp_inst = this._get(inst, 'timepicker');
+ $(target).datepicker('getDate'); // Init selected[Year|Month|Day]
+ if (tp_inst) {
+ tp_inst._defaults.showTimepicker = true;
+ tp_inst._addTimePicker(inst); // Could be disabled on page load
+ tp_inst._updateDateTime(inst);
+ }
+};
+
+//#######################################################################################
+// Create our own set time function
+//#######################################################################################
+$.datepicker._setTime = function(inst, date) {
+ var tp_inst = this._get(inst, 'timepicker');
+ if (tp_inst) {
+ var defaults = tp_inst._defaults,
+ // calling _setTime with no date sets time to defaults
+ hour = date ? date.getHours() : defaults.hour,
+ minute = date ? date.getMinutes() : defaults.minute,
+ second = date ? date.getSeconds() : defaults.second,
+ millisec = date ? date.getMilliseconds() : defaults.millisec;
+
+ //check if within min/max times..
+ if ((hour < defaults.hourMin || hour > defaults.hourMax) || (minute < defaults.minuteMin || minute > defaults.minuteMax) || (second < defaults.secondMin || second > defaults.secondMax) || (millisec < defaults.millisecMin || millisec > defaults.millisecMax)) {
+ hour = defaults.hourMin;
+ minute = defaults.minuteMin;
+ second = defaults.secondMin;
+ millisec = defaults.millisecMin;
+ }
+
+ tp_inst.hour = hour;
+ tp_inst.minute = minute;
+ tp_inst.second = second;
+ tp_inst.millisec = millisec;
+
+ if (tp_inst.hour_slider) tp_inst.hour_slider.slider('value', hour);
+ if (tp_inst.minute_slider) tp_inst.minute_slider.slider('value', minute);
+ if (tp_inst.second_slider) tp_inst.second_slider.slider('value', second);
+ if (tp_inst.millisec_slider) tp_inst.millisec_slider.slider('value', millisec);
+
+ tp_inst._onTimeChange();
+ tp_inst._updateDateTime(inst);
+ }
+};
+
+//#######################################################################################
+// Create new public method to set only time, callable as $().datepicker('setTime', date)
+//#######################################################################################
+$.datepicker._setTimeDatepicker = function(target, date, withDate) {
+ var inst = this._getInst(target),
+ tp_inst = this._get(inst, 'timepicker');
+
+ if (tp_inst) {
+ this._setDateFromField(inst);
+ var tp_date;
+ if (date) {
+ if (typeof date == "string") {
+ tp_inst._parseTime(date, withDate);
+ tp_date = new Date();
+ tp_date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec);
+ }
+ else tp_date = new Date(date.getTime());
+ if (tp_date.toString() == 'Invalid Date') tp_date = undefined;
+ this._setTime(inst, tp_date);
+ }
+ }
+
+};
+
+//#######################################################################################
+// override setDate() to allow setting time too within Date object
+//#######################################################################################
+$.datepicker._base_setDateDatepicker = $.datepicker._setDateDatepicker;
+$.datepicker._setDateDatepicker = function(target, date) {
+ var inst = this._getInst(target),
+ tp_date = (date instanceof Date) ? new Date(date.getTime()) : date;
+
+ this._updateDatepicker(inst);
+ this._base_setDateDatepicker.apply(this, arguments);
+ this._setTimeDatepicker(target, tp_date, true);
+};
+
+//#######################################################################################
+// override getDate() to allow getting time too within Date object
+//#######################################################################################
+$.datepicker._base_getDateDatepicker = $.datepicker._getDateDatepicker;
+$.datepicker._getDateDatepicker = function(target, noDefault) {
+ var inst = this._getInst(target),
+ tp_inst = this._get(inst, 'timepicker');
+
+ if (tp_inst) {
+ this._setDateFromField(inst, noDefault);
+ var date = this._getDate(inst);
+ if (date && tp_inst._parseTime($(target).val(), tp_inst.timeOnly)) date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec);
+ return date;
+ }
+ return this._base_getDateDatepicker(target, noDefault);
+};
+
+//#######################################################################################
+// override parseDate() because UI 1.8.14 throws an error about "Extra characters"
+// An option in datapicker to ignore extra format characters would be nicer.
+//#######################################################################################
+$.datepicker._base_parseDate = $.datepicker.parseDate;
+$.datepicker.parseDate = function(format, value, settings) {
+ var date;
+ try {
+ date = this._base_parseDate(format, value, settings);
+ } catch (err) {
+ if (err.indexOf(":") >= 0) {
+ // Hack! The error message ends with a colon, a space, and
+ // the "extra" characters. We rely on that instead of
+ // attempting to perfectly reproduce the parsing algorithm.
+ date = this._base_parseDate(format, value.substring(0,value.length-(err.length-err.indexOf(':')-2)), settings);
+ } else {
+ // The underlying error was not related to the time
+ throw err;
+ }
+ }
+ return date;
+};
+
+//#######################################################################################
+// override formatDate to set date with time to the input
+//#######################################################################################
+$.datepicker._base_formatDate = $.datepicker._formatDate;
+$.datepicker._formatDate = function(inst, day, month, year){
+ var tp_inst = this._get(inst, 'timepicker');
+ if(tp_inst) {
+ tp_inst._updateDateTime(inst);
+ return tp_inst.$input.val();
+ }
+ return this._base_formatDate(inst);
+};
+
+//#######################################################################################
+// override options setter to add time to maxDate(Time) and minDate(Time). MaxDate
+//#######################################################################################
+$.datepicker._base_optionDatepicker = $.datepicker._optionDatepicker;
+$.datepicker._optionDatepicker = function(target, name, value) {
+ var inst = this._getInst(target),
+ tp_inst = this._get(inst, 'timepicker');
+ if (tp_inst) {
+ var min = null, max = null, onselect = null;
+ if (typeof name == 'string') { // if min/max was set with the string
+ if (name === 'minDate' || name === 'minDateTime' )
+ min = value;
+ else if (name === 'maxDate' || name === 'maxDateTime')
+ max = value;
+ else if (name === 'onSelect')
+ onselect = value;
+ } else if (typeof name == 'object') { //if min/max was set with the JSON
+ if (name.minDate)
+ min = name.minDate;
+ else if (name.minDateTime)
+ min = name.minDateTime;
+ else if (name.maxDate)
+ max = name.maxDate;
+ else if (name.maxDateTime)
+ max = name.maxDateTime;
+ }
+ if(min) { //if min was set
+ if (min == 0)
+ min = new Date();
+ else
+ min = new Date(min);
+
+ tp_inst._defaults.minDate = min;
+ tp_inst._defaults.minDateTime = min;
+ } else if (max) { //if max was set
+ if(max==0)
+ max=new Date();
+ else
+ max= new Date(max);
+ tp_inst._defaults.maxDate = max;
+ tp_inst._defaults.maxDateTime = max;
+ } else if (onselect)
+ tp_inst._defaults.onSelect = onselect;
+ }
+ if (value === undefined)
+ return this._base_optionDatepicker(target, name);
+ return this._base_optionDatepicker(target, name, value);
+};
+
+//#######################################################################################
+// jQuery extend now ignores nulls!
+//#######################################################################################
+function extendRemove(target, props) {
+ $.extend(target, props);
+ for (var name in props)
+ if (props[name] === null || props[name] === undefined)
+ target[name] = props[name];
+ return target;
+};
+
+$.timepicker = new Timepicker(); // singleton instance
+$.timepicker.version = "1.0.0";
+
+})(jQuery);
diff --git a/rt/share/html/NoAuth/js/ui.timepickr.js b/rt/share/html/NoAuth/js/ui.timepickr.js
deleted file mode 100644
index 3b2040a21..000000000
--- a/rt/share/html/NoAuth/js/ui.timepickr.js
+++ /dev/null
@@ -1,941 +0,0 @@
-/*
- jQuery utils - @VERSION
- http://code.google.com/p/jquery-utils/
-
- (c) Maxime Haineault <haineault@gmail.com>
- http://haineault.com
-
- MIT License (http://www.opensource.org/licenses/mit-license.php
-
-*/
-
-(function($){
- $.extend($.expr[':'], {
- // case insensitive version of :contains
- icontains: function(a,i,m){return (a.textContent||a.innerText||jQuery(a).text()||"").toLowerCase().indexOf(m[3].toLowerCase())>=0;}
- });
-
- $.iterators = {
- getText: function() { return $(this).text(); },
- parseInt: function(v){ return parseInt(v, 10); }
- };
-
- $.extend({
-
- // Returns a range object
- // Author: Matthias Miller
- // Site: http://blog.outofhanwell.com/2006/03/29/javascript-range-function/
- range: function() {
- if (!arguments.length) { return []; }
- var min, max, step;
- if (arguments.length == 1) {
- min = 0;
- max = arguments[0]-1;
- step = 1;
- }
- else {
- // default step to 1 if it's zero or undefined
- min = arguments[0];
- max = arguments[1]-1;
- step = arguments[2] || 1;
- }
- // convert negative steps to positive and reverse min/max
- if (step < 0 && min >= max) {
- step *= -1;
- var tmp = min;
- min = max;
- max = tmp;
- min += ((max-min) % step);
- }
- var a = [];
- for (var i = min; i <= max; i += step) { a.push(i); }
- return a;
- },
-
- // Taken from ui.core.js.
- // Why are you keeping this gem for yourself guys ? :|
- keyCode: {
- BACKSPACE: 8, CAPS_LOCK: 20, COMMA: 188, CONTROL: 17, DELETE: 46, DOWN: 40,
- END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, INSERT: 45, LEFT: 37,
- NUMPAD_ADD: 107, NUMPAD_DECIMAL: 110, NUMPAD_DIVIDE: 111, NUMPAD_ENTER: 108,
- NUMPAD_MULTIPLY: 106, NUMPAD_SUBTRACT: 109, PAGE_DOWN: 34, PAGE_UP: 33,
- PERIOD: 190, RIGHT: 39, SHIFT: 16, SPACE: 32, TAB: 9, UP: 38
- },
-
- // Takes a keyboard event and return true if the keycode match the specified keycode
- keyIs: function(k, e) {
- return parseInt($.keyCode[k.toUpperCase()], 10) == parseInt((typeof(e) == 'number' )? e: e.keyCode, 10);
- },
-
- // Returns the key of an array
- keys: function(arr) {
- var o = [];
- for (k in arr) { o.push(k); }
- return o;
- },
-
- // Redirect to a specified url
- redirect: function(url) {
- window.location.href = url;
- return url;
- },
-
- // Stop event shorthand
- stop: function(e, preventDefault, stopPropagation) {
- if (preventDefault) { e.preventDefault(); }
- if (stopPropagation) { e.stopPropagation(); }
- return preventDefault && false || true;
- },
-
- // Returns the basename of a path
- basename: function(path) {
- var t = path.split('/');
- return t[t.length] === '' && s || t.slice(0, t.length).join('/');
- },
-
- // Returns the filename of a path
- filename: function(path) {
- return path.split('/').pop();
- },
-
- // Returns a formated file size
- filesizeformat: function(bytes, suffixes){
- var b = parseInt(bytes, 10);
- var s = suffixes || ['byte', 'bytes', 'KB', 'MB', 'GB'];
- if (isNaN(b) || b === 0) { return '0 ' + s[0]; }
- if (b == 1) { return '1 ' + s[0]; }
- if (b < 1024) { return b.toFixed(2) + ' ' + s[1]; }
- if (b < 1048576) { return (b / 1024).toFixed(2) + ' ' + s[2]; }
- if (b < 1073741824) { return (b / 1048576).toFixed(2) + ' '+ s[3]; }
- else { return (b / 1073741824).toFixed(2) + ' '+ s[4]; }
- },
-
- fileExtension: function(s) {
- var tokens = s.split('.');
- return tokens[tokens.length-1] || false;
- },
-
- // Returns true if an object is a String
- isString: function(o) {
- return typeof(o) == 'string' && true || false;
- },
-
- // Returns true if an object is a RegExp
- isRegExp: function(o) {
- return o && o.constructor.toString().indexOf('RegExp()') != -1 || false;
- },
-
- isObject: function(o) {
- return (typeof(o) == 'object');
- },
-
- // Convert input to currency (two decimal fixed number)
- toCurrency: function(i) {
- i = parseFloat(i, 10).toFixed(2);
- return (i=='NaN') ? '0.00' : i;
- },
-
- /*--------------------------------------------------------------------
- * javascript method: "pxToEm"
- * by:
- Scott Jehl (scott@filamentgroup.com)
- Maggie Wachs (maggie@filamentgroup.com)
- http://www.filamentgroup.com
- *
- * Copyright (c) 2008 Filament Group
- * Dual licensed under the MIT (filamentgroup.com/examples/mit-license.txt) and GPL (filamentgroup.com/examples/gpl-license.txt) licenses.
- *
- * Description: pxToEm converts a pixel value to ems depending on inherited font size.
- * Article: http://www.filamentgroup.com/lab/retaining_scalable_interfaces_with_pixel_to_em_conversion/
- * Demo: http://www.filamentgroup.com/examples/pxToEm/
- *
- * Options:
- scope: string or jQuery selector for font-size scoping
- reverse: Boolean, true reverses the conversion to em-px
- * Dependencies: jQuery library
- * Usage Example: myPixelValue.pxToEm(); or myPixelValue.pxToEm({'scope':'#navigation', reverse: true});
- *
- * Version: 2.1, 18.12.2008
- * Changelog:
- * 08.02.2007 initial Version 1.0
- * 08.01.2008 - fixed font-size calculation for IE
- * 18.12.2008 - removed native object prototyping to stay in jQuery's spirit, jsLinted (Maxime Haineault <haineault@gmail.com>)
- --------------------------------------------------------------------*/
-
- pxToEm: function(i, settings){
- //set defaults
- settings = jQuery.extend({
- scope: 'body',
- reverse: false
- }, settings);
-
- var pxVal = (i === '') ? 0 : parseFloat(i);
- var scopeVal;
- var getWindowWidth = function(){
- var de = document.documentElement;
- return self.innerWidth || (de && de.clientWidth) || document.body.clientWidth;
- };
-
- /* When a percentage-based font-size is set on the body, IE returns that percent of the window width as the font-size.
- For example, if the body font-size is 62.5% and the window width is 1000px, IE will return 625px as the font-size.
- When this happens, we calculate the correct body font-size (%) and multiply it by 16 (the standard browser font size)
- to get an accurate em value. */
-
- if (settings.scope == 'body' && $.browser.msie && (parseFloat($('body').css('font-size')) / getWindowWidth()).toFixed(1) > 0.0) {
- var calcFontSize = function(){
- return (parseFloat($('body').css('font-size'))/getWindowWidth()).toFixed(3) * 16;
- };
- scopeVal = calcFontSize();
- }
- else { scopeVal = parseFloat(jQuery(settings.scope).css("font-size")); }
-
- var result = (settings.reverse === true) ? (pxVal * scopeVal).toFixed(2) + 'px' : (pxVal / scopeVal).toFixed(2) + 'em';
- return result;
- }
- });
-
- $.extend($.fn, {
- type: function() {
- try { return $(this).get(0).nodeName.toLowerCase(); }
- catch(e) { return false; }
- },
- // Select a text range in a textarea
- selectRange: function(start, end){
- // use only the first one since only one input can be focused
- if ($(this).get(0).createTextRange) {
- var range = $(this).get(0).createTextRange();
- range.collapse(true);
- range.moveEnd('character', end);
- range.moveStart('character', start);
- range.select();
- }
- else if ($(this).get(0).setSelectionRange) {
- $(this).bind('focus', function(e){
- e.preventDefault();
- }).get(0).setSelectionRange(start, end);
- }
- return $(this);
- },
-
- /*--------------------------------------------------------------------
- * JQuery Plugin: "EqualHeights"
- * by: Scott Jehl, Todd Parker, Maggie Costello Wachs (http://www.filamentgroup.com)
- *
- * Copyright (c) 2008 Filament Group
- * Licensed under GPL (http://www.opensource.org/licenses/gpl-license.php)
- *
- * Description: Compares the heights or widths of the top-level children of a provided element
- and sets their min-height to the tallest height (or width to widest width). Sets in em units
- by default if pxToEm() method is available.
- * Dependencies: jQuery library, pxToEm method (article:
- http://www.filamentgroup.com/lab/retaining_scalable_interfaces_with_pixel_to_em_conversion/)
- * Usage Example: $(element).equalHeights();
- Optional: to set min-height in px, pass a true argument: $(element).equalHeights(true);
- * Version: 2.1, 18.12.2008
- *
- * Note: Changed pxToEm call to call $.pxToEm instead, jsLinted (Maxime Haineault <haineault@gmail.com>)
- --------------------------------------------------------------------*/
-
- equalHeights: function(px){
- $(this).each(function(){
- var currentTallest = 0;
- $(this).children().each(function(i){
- if ($(this).height() > currentTallest) { currentTallest = $(this).height(); }
- });
- if (!px || !$.pxToEm) { currentTallest = $.pxToEm(currentTallest); } //use ems unless px is specified
- // for ie6, set height since min-height isn't supported
- if ($.browser.msie && $.browser.version == 6.0) { $(this).children().css({'height': currentTallest}); }
- $(this).children().css({'min-height': currentTallest});
- });
- return this;
- },
-
- // Copyright (c) 2009 James Padolsey
- // http://james.padolsey.com/javascript/jquery-delay-plugin/
- delay: function(time, callback){
- jQuery.fx.step.delay = function(){};
- return this.animate({delay:1}, time, callback);
- }
- });
-})(jQuery);
-
-/*
- jQuery strings - 0.4
- http://code.google.com/p/jquery-utils/
-
- (c) Maxime Haineault <haineault@gmail.com>
- http://haineault.com
-
- MIT License (http://www.opensource.org/licenses/mit-license.php)
-
- Implementation of Python3K advanced string formatting
- http://www.python.org/dev/peps/pep-3101/
-
- Documentation: http://code.google.com/p/jquery-utils/wiki/StringFormat
-
-*/
-(function($){
- var strings = {
- strConversion: {
- // tries to translate any objects type into string gracefully
- __repr: function(i){
- switch(this.__getType(i)) {
- case 'array':case 'date':case 'number':
- return i.toString();
- case 'object': // Thanks to Richard Paul Lewis for the fix
- var o = [];
- var l = i.length;
- for(var x=0;x<l;x++) {
- o.push(x+': '+this.__repr(i[x]));
- }
- return o.join(', ');
- case 'string':
- return i;
- default:
- return i;
- }
- },
- // like typeof but less vague
- __getType: function(i) {
- if (!i || !i.constructor) { return typeof(i); }
- var match = i.constructor.toString().match(/Array|Number|String|Object|Date/);
- return match && match[0].toLowerCase() || typeof(i);
- },
- // Jonas Raoni Soares Silva (http://jsfromhell.com/string/pad)
- __pad: function(str, l, s, t){
- var p = s || ' ';
- var o = str;
- if (l - str.length > 0) {
- o = new Array(Math.ceil(l / p.length)).join(p).substr(0, t = !t ? l : t == 1 ? 0 : Math.ceil(l / 2)) + str + p.substr(0, l - t);
- }
- return o;
- },
- __getInput: function(arg, args) {
- var key = arg.getKey();
- switch(this.__getType(args)){
- case 'object': // Thanks to Jonathan Works for the patch
- var keys = key.split('.');
- var obj = args;
- for(var subkey = 0; subkey < keys.length; subkey++){
- obj = obj[keys[subkey]];
- }
- if (typeof(obj) != 'undefined') {
- if (strings.strConversion.__getType(obj) == 'array') {
- return arg.getFormat().match(/\.\*/) && obj[1] || obj;
- }
- return obj;
- }
- else {
- // TODO: try by numerical index
- }
- break;
- case 'array':
- key = parseInt(key, 10);
- if (arg.getFormat().match(/\.\*/) && typeof args[key+1] != 'undefined') { return args[key+1]; }
- else if (typeof args[key] != 'undefined') { return args[key]; }
- else { return key; }
- break;
- }
- return '{'+key+'}';
- },
- __formatToken: function(token, args) {
- var arg = new Argument(token, args);
- return strings.strConversion[arg.getFormat().slice(-1)](this.__getInput(arg, args), arg);
- },
-
- // Signed integer decimal.
- d: function(input, arg){
- var o = parseInt(input, 10); // enforce base 10
- var p = arg.getPaddingLength();
- if (p) { return this.__pad(o.toString(), p, arg.getPaddingString(), 0); }
- else { return o; }
- },
- // Signed integer decimal.
- i: function(input, args){
- return this.d(input, args);
- },
- // Unsigned octal
- o: function(input, arg){
- var o = input.toString(8);
- if (arg.isAlternate()) { o = this.__pad(o, o.length+1, '0', 0); }
- return this.__pad(o, arg.getPaddingLength(), arg.getPaddingString(), 0);
- },
- // Unsigned decimal
- u: function(input, args) {
- return Math.abs(this.d(input, args));
- },
- // Unsigned hexadecimal (lowercase)
- x: function(input, arg){
- var o = parseInt(input, 10).toString(16);
- o = this.__pad(o, arg.getPaddingLength(), arg.getPaddingString(),0);
- return arg.isAlternate() ? '0x'+o : o;
- },
- // Unsigned hexadecimal (uppercase)
- X: function(input, arg){
- return this.x(input, arg).toUpperCase();
- },
- // Floating point exponential format (lowercase)
- e: function(input, arg){
- return parseFloat(input, 10).toExponential(arg.getPrecision());
- },
- // Floating point exponential format (uppercase)
- E: function(input, arg){
- return this.e(input, arg).toUpperCase();
- },
- // Floating point decimal format
- f: function(input, arg){
- return this.__pad(parseFloat(input, 10).toFixed(arg.getPrecision()), arg.getPaddingLength(), arg.getPaddingString(),0);
- },
- // Floating point decimal format (alias)
- F: function(input, args){
- return this.f(input, args);
- },
- // Floating point format. Uses exponential format if exponent is greater than -4 or less than precision, decimal format otherwise
- g: function(input, arg){
- var o = parseFloat(input, 10);
- return (o.toString().length > 6) ? Math.round(o.toExponential(arg.getPrecision())): o;
- },
- // Floating point format. Uses exponential format if exponent is greater than -4 or less than precision, decimal format otherwise
- G: function(input, args){
- return this.g(input, args);
- },
- // Single character (accepts integer or single character string).
- c: function(input, args) {
- var match = input.match(/\w|\d/);
- return match && match[0] || '';
- },
- // String (converts any JavaScript object to anotated format)
- r: function(input, args) {
- return this.__repr(input);
- },
- // String (converts any JavaScript object using object.toString())
- s: function(input, args) {
- return input.toString && input.toString() || ''+input;
- }
- },
-
- format: function(str, args) {
- var end = 0;
- var start = 0;
- var match = false;
- var buffer = [];
- var token = '';
- var tmp = (str||'').split('');
- for(start=0; start < tmp.length; start++) {
- if (tmp[start] == '{' && tmp[start+1] !='{') {
- end = str.indexOf('}', start);
- token = tmp.slice(start+1, end).join('');
- if (tmp[start-1] != '{' && tmp[end+1] != '}') {
- var tokenArgs = (typeof arguments[1] != 'object')? arguments2Array(arguments, 2): args || [];
- buffer.push(strings.strConversion.__formatToken(token, tokenArgs));
- }
- else {
- buffer.push(token);
- }
- }
- else if (start > end || buffer.length < 1) { buffer.push(tmp[start]); }
- }
- return (buffer.length > 1)? buffer.join(''): buffer[0];
- },
-
- calc: function(str, args) {
- return eval(format(str, args));
- },
-
- repeat: function(s, n) {
- return new Array(n+1).join(s);
- },
-
- UTF8encode: function(s) {
- return unescape(encodeURIComponent(s));
- },
-
- UTF8decode: function(s) {
- return decodeURIComponent(escape(s));
- },
-
- tpl: function() {
- var out = '';
- var render = true;
- // Set
- // $.tpl('ui.test', ['<span>', helloWorld ,'</span>']);
- if (arguments.length == 2 && $.isArray(arguments[1])) {
- this[arguments[0]] = arguments[1].join('');
- return $(this[arguments[0]]);
- }
- // $.tpl('ui.test', '<span>hello world</span>');
- if (arguments.length == 2 && $.isString(arguments[1])) {
- this[arguments[0]] = arguments[1];
- return $(this[arguments[0]]);
- }
- // Call
- // $.tpl('ui.test');
- if (arguments.length == 1) {
- return $(this[arguments[0]]);
- }
- // $.tpl('ui.test', false);
- if (arguments.length == 2 && arguments[1] == false) {
- return this[arguments[0]];
- }
- // $.tpl('ui.test', {value:blah});
- if (arguments.length == 2 && $.isObject(arguments[1])) {
- return $($.format(this[arguments[0]], arguments[1]));
- }
- // $.tpl('ui.test', {value:blah}, false);
- if (arguments.length == 3 && $.isObject(arguments[1])) {
- return (arguments[2] == true)
- ? $.format(this[arguments[0]], arguments[1])
- : $($.format(this[arguments[0]], arguments[1]));
- }
- }
- };
-
- var Argument = function(arg, args) {
- this.__arg = arg;
- this.__args = args;
- this.__max_precision = parseFloat('1.'+ (new Array(32)).join('1'), 10).toString().length-3;
- this.__def_precision = 6;
- this.getString = function(){
- return this.__arg;
- };
- this.getKey = function(){
- return this.__arg.split(':')[0];
- };
- this.getFormat = function(){
- var match = this.getString().split(':');
- return (match && match[1])? match[1]: 's';
- };
- this.getPrecision = function(){
- var match = this.getFormat().match(/\.(\d+|\*)/g);
- if (!match) { return this.__def_precision; }
- else {
- match = match[0].slice(1);
- if (match != '*') { return parseInt(match, 10); }
- else if(strings.strConversion.__getType(this.__args) == 'array') {
- return this.__args[1] && this.__args[0] || this.__def_precision;
- }
- else if(strings.strConversion.__getType(this.__args) == 'object') {
- return this.__args[this.getKey()] && this.__args[this.getKey()][0] || this.__def_precision;
- }
- else { return this.__def_precision; }
- }
- };
- this.getPaddingLength = function(){
- var match = false;
- if (this.isAlternate()) {
- match = this.getString().match(/0?#0?(\d+)/);
- if (match && match[1]) { return parseInt(match[1], 10); }
- }
- match = this.getString().match(/(0|\.)(\d+|\*)/g);
- return match && parseInt(match[0].slice(1), 10) || 0;
- };
- this.getPaddingString = function(){
- var o = '';
- if (this.isAlternate()) { o = ' '; }
- // 0 take precedence on alternate format
- if (this.getFormat().match(/#0|0#|^0|\.\d+/)) { o = '0'; }
- return o;
- };
- this.getFlags = function(){
- var match = this.getString().matc(/^(0|\#|\-|\+|\s)+/);
- return match && match[0].split('') || [];
- };
- this.isAlternate = function() {
- return !!this.getFormat().match(/^0?#/);
- };
- };
-
- var arguments2Array = function(args, shift) {
- var o = [];
- for (l=args.length, x=(shift || 0)-1; x<l;x++) { o.push(args[x]); }
- return o;
- };
- $.extend(strings);
-})(jQuery);
-
-/*
- jQuery ui.timepickr - @VERSION
- http://code.google.com/p/jquery-utils/
-
- (c) Maxime Haineault <haineault@gmail.com>
- http://haineault.com
-
- MIT License (http://www.opensource.org/licenses/mit-license.php
-
- Note: if you want the original experimental plugin checkout the rev 224
-
- Dependencies
- ------------
- - jquery.utils.js
- - jquery.strings.js
- - jquery.ui.js
-
-*/
-
-(function($) {
-
-$.tpl('timepickr.menu', '<div class="ui-helper-reset ui-timepickr ui-widget" />');
-$.tpl('timepickr.row', '<ol class="ui-timepickr-row ui-helper-clearfix" />');
-$.tpl('timepickr.button', '<li class="{className:s}"><span class="ui-state-default">{label:s}</span></li>');
-
-$.widget('ui.timepickr', {
- plugins: {},
- _create: function() {
- this._dom = {
- menu: $.tpl('timepickr.menu'),
- row: $.tpl('timepickr.menu')
- };
- this._trigger('initialize');
- this._trigger('initialized');
- },
-
- _trigger: function(type, e, ui) {
- var ui = ui || this;
- $.ui.plugin.call(this, type, [e, ui]);
- return $.Widget.prototype._trigger.call(this, type, e, ui);
- },
-
- _createButton: function(i, format, className) {
- var o = format && $.format(format, i) || i;
- var cn = className && 'ui-timepickr-button '+ className || 'ui-timepickr-button';
- return $.tpl('timepickr.button', {className: cn, label: o}).data('id', i)
- .bind('mouseover', function(){
- $(this).siblings().find('span')
- .removeClass('ui-state-hover').end().end()
- .find('span').addClass('ui-state-hover');
- });
-
- },
-
- _addRow: function(range, format, className, insertAfter) {
- var ui = this;
- var btn = false;
- var row = $.tpl('timepickr.row').bind('mouseover', function(){
- $(this).next().show();
- });
- $.each(range, function(idx, val){
- ui._createButton(val, format || false).appendTo(row);
- });
- if (className) {
- $(row).addClass(className);
- }
- if (this.options.corners) {
- row.find('span').addClass('ui-corner-'+ this.options.corners);
- }
- if (insertAfter) {
- row.insertAfter(insertAfter);
- }
- else {
- ui._dom.menu.append(row);
- }
- return row;
- },
-
- _setVal: function(val) {
- val = val || this._getVal();
- if (!(val.h==='' && val.m==='')) {
- this.element.data('timepickr.initialValue', val);
- this.element.val(this._formatVal(val));
- }
- if(this._dom.menu.is(':hidden')) {
- this.element.trigger('change');
- }
- },
-
- _getVal: function() {
- var ols = this._dom.menu.find('ol');
- function get(unit) {
- var u = ols.filter('.'+unit).find('.ui-state-hover:first').text();
- return u || ols.filter('.'+unit+'li:first span').text();
- }
- return {
- h: get('hours'),
- m: get('minutes'),
- s: get('seconds'),
- a: get('prefix'),
- z: get('suffix'),
- f: this.options['format'+ this.c],
- c: this.c
- };
- },
-
- _formatVal: function(ival) {
- var val = ival || this._getVal();
- val.c = this.options.convention;
- val.f = val.c === 12 && this.options.format12 || this.options.format24;
- return (new Time(val)).getTime();
- },
-
- blur: function() {
- return this.element.blur();
- },
-
- focus: function() {
- return this.element.focus();
- },
- show: function() {
- this._trigger('show');
- this.element.trigger(this.options.trigger);
- },
- hide: function() {
- this._trigger('hide');
- this._dom.menu.hide();
- }
-
-});
-
-// These properties are shared accross every instances of timepickr
-$.extend($.ui.timepickr.prototype, {
- version: '@VERSION',
- //eventPrefix: '',
- //getter: '',
- options: {
- convention: 24, // 24, 12
- trigger: 'mouseover',
- format12: '{h:02.d}:{m:02.d} {z:s}',
- format24: '{h:02.d}:{m:02.d}',
- hours: true,
- prefix: ['am', 'pm'],
- suffix: ['am', 'pm'],
- prefixVal: false,
- suffixVal: true,
- rangeHour12: $.range(1, 13),
- rangeHour24: [$.range(0, 12), $.range(12, 24)],
- rangeMin: $.range(0, 60, 15),
- rangeSec: $.range(0, 60, 15),
- corners: 'all',
- // plugins
- core: true,
- minutes: true,
- seconds: false,
- val: false,
- updateLive: true,
- resetOnBlur: true,
- keyboardnav: true,
- handle: false,
- handleEvent: 'click'
- }
-});
-
-$.ui.plugin.add('timepickr', 'core', {
- initialized: function(e, ui) {
- var menu = ui._dom.menu;
- var pos = ui.element.position();
-
- menu.insertAfter(ui.element).css('left', pos.left);
-
- if (!$.boxModel) { // IE alignement fix
- menu.css('margin-top', ui.element.height() + 8);
- }
-
- ui.element
- .bind(ui.options.trigger, function() {
- ui._dom.menu.show();
- ui._dom.menu.find('ol:first').show();
- ui._trigger('focus');
- if (ui.options.trigger != 'focus') {
- ui.element.focus();
- }
- ui._trigger('focus');
- })
- .bind('blur', function() {
- ui.hide();
- ui._trigger('blur');
- });
-
- menu.find('li').bind('mouseover.timepickr', function() {
- ui._trigger('refresh');
- });
- },
- refresh: function(e, ui) {
- // Realign each menu layers
- ui._dom.menu.find('ol').each(function(){
- var p = $(this).prev('ol');
- try { // .. to not fuckup IE
- $(this).css('left', p.position().left + p.find('.ui-state-hover').position().left);
- } catch(e) {};
- });
- }
-});
-
-$.ui.plugin.add('timepickr', 'hours', {
- initialize: function(e, ui) {
- if (ui.options.convention === 24) {
- // prefix is required in 24h mode
- ui._dom.prefix = ui._addRow(ui.options.prefix, false, 'prefix');
-
- // split-range
- if ($.isArray(ui.options.rangeHour24[0])) {
- var range = [];
- $.merge(range, ui.options.rangeHour24[0]);
- $.merge(range, ui.options.rangeHour24[1]);
- ui._dom.hours = ui._addRow(range, '{0:0.2d}', 'hours');
- ui._dom.hours.find('li').slice(ui.options.rangeHour24[0].length, -1).hide();
- var lis = ui._dom.hours.find('li');
-
- var show = [
- function() {
- lis.slice(ui.options.rangeHour24[0].length).hide().end()
- .slice(0, ui.options.rangeHour24[0].length).show()
- .filter(':visible:first').trigger('mouseover');
-
- },
- function() {
- lis.slice(0, ui.options.rangeHour24[0].length).hide().end()
- .slice(ui.options.rangeHour24[0].length).show()
- .filter(':visible:first').trigger('mouseover');
- }
- ];
-
- ui._dom.prefix.find('li').bind('mouseover.timepickr', function(){
- var index = ui._dom.menu.find('.prefix li').index(this);
- show[index].call();
- });
- }
- else {
- ui._dom.hours = ui._addRow(ui.options.rangeHour24, '{0:0.2d}', 'hours');
- ui._dom.hours.find('li').slice(12, -1).hide();
- }
- }
- else {
- ui._dom.hours = ui._addRow(ui.options.rangeHour12, '{0:0.2d}', 'hours');
- // suffix is required in 12h mode
- ui._dom.suffix = ui._addRow(ui.options.suffix, false, 'suffix');
- }
- }});
-
-$.ui.plugin.add('timepickr', 'minutes', {
- initialize: function(e, ui) {
- var p = ui._dom.hours && ui._dom.hours || false;
- ui._dom.minutes = ui._addRow(ui.options.rangeMin, '{0:0.2d}', 'minutes', p);
- }
-});
-
-$.ui.plugin.add('timepickr', 'seconds', {
- initialize: function(e, ui) {
- var p = ui._dom.minutes && ui._dom.minutes || false;
- ui._dom.seconds = ui._addRow(ui.options.rangeSec, '{0:0.2d}', 'seconds', p);
- }
-});
-
-$.ui.plugin.add('timepickr', 'val', {
- initialized: function(e, ui) {
- ui._setVal(ui.options.val);
- }
-});
-
-$.ui.plugin.add('timepickr', 'updateLive', {
- refresh: function(e, ui) {
- ui._setVal();
- }
-});
-
-$.ui.plugin.add('timepickr', 'resetOnBlur', {
- initialized: function(e, ui) {
- ui.element.data('timepickr.initialValue', ui._getVal());
- ui._dom.menu.find('li > span').bind('mousedown.timepickr', function(){
- ui.element.data('timepickr.initialValue', ui._getVal());
- });
- },
- blur: function(e, ui) {
- ui._setVal(ui.element.data('timepickr.initialValue'));
- }
-});
-
-$.ui.plugin.add('timepickr', 'handle', {
- initialized: function(e, ui) {
- $(ui.options.handle).bind(ui.options.handleEvent + '.timepickr', function(){
- ui.show();
- });
- }
-});
-
-$.ui.plugin.add('timepickr', 'keyboardnav', {
- initialized: function(e, ui) {
- ui.element
- .bind('keydown', function(e) {
- if ($.keyIs('enter', e)) {
- ui._setVal();
- ui.blur();
- }
- else if ($.keyIs('escape', e)) {
- ui.blur();
- }
- });
- }
-});
-
-var Time = function() { // arguments: h, m, s, c, z, f || time string
- if (!(this instanceof arguments.callee)) {
- throw Error("Constructor called as a function");
- }
- // arguments as literal object
- if (arguments.length == 1 && $.isObject(arguments[0])) {
- this.h = arguments[0].h || 0;
- this.m = arguments[0].m || 0;
- this.s = arguments[0].s || 0;
- this.c = arguments[0].c && ($.inArray(arguments[0].c, [12, 24]) >= 0) && arguments[0].c || 24;
- this.f = arguments[0].f || ((this.c == 12) && '{h:02.d}:{m:02.d} {z:02.d}' || '{h:02.d}:{m:02.d}');
- this.z = arguments[0].z || 'am';
- }
- // arguments as string
- else if (arguments.length < 4 && $.isString(arguments[1])) {
- this.c = arguments[2] && ($.inArray(arguments[0], [12, 24]) >= 0) && arguments[0] || 24;
- this.f = arguments[3] || ((this.c == 12) && '{h:02.d}:{m:02.d} {z:02.d}' || '{h:02.d}:{m:02.d}');
- this.z = arguments[4] || 'am';
-
- this.h = arguments[1] || 0; // parse
- this.m = arguments[1] || 0; // parse
- this.s = arguments[1] || 0; // parse
- }
- // no arguments (now)
- else if (arguments.length === 0) {
- // now
- }
- // standards arguments
- else {
- this.h = arguments[0] || 0;
- this.m = arguments[1] || 0;
- this.s = arguments[2] || 0;
- this.c = arguments[3] && ($.inArray(arguments[3], [12, 24]) >= 0) && arguments[3] || 24;
- this.f = this.f || ((this.c == 12) && '{h:02.d}:{m:02.d} {z:02.d}' || '{h:02.d}:{m:02.d}');
- this.z = 'am';
- }
- return this;
-};
-
-Time.prototype.get = function(p, f, u) { return u && this.h || $.format(f, this.h); };
-Time.prototype.getHours = function(unformated) { return this.get('h', '{0:02.d}', unformated); };
-Time.prototype.getMinutes = function(unformated) { return this.get('m', '{0:02.d}', unformated); };
-Time.prototype.getSeconds = function(unformated) { return this.get('s', '{0:02.d}', unformated); };
-Time.prototype.setFormat = function(format) { return this.f = format; };
-Time.prototype.getObject = function() { return { h: this.h, m: this.m, s: this.s, c: this.c, f: this.f, z: this.z }; };
-Time.prototype.getTime = function() { return $.format(this.f, {h: this.h, m: this.m, suffix: this.z}); }; // Thanks to Jackson for the fix.
-Time.prototype.parse = function(str) {
- // 12h formats
- if (this.c === 12) {
- // Supported formats: (can't find any *official* standards for 12h..)
- // - [hh]:[mm]:[ss] [zz] | [hh]:[mm] [zz] | [hh] [zz]
- // - [hh]:[mm]:[ss] [z.z.] | [hh]:[mm] [z.z.] | [hh] [z.z.]
- this.tokens = str.split(/\s|:/);
- this.h = this.tokens[0] || 0;
- this.m = this.tokens[1] || 0;
- this.s = this.tokens[2] || 0;
- this.z = this.tokens[3] || '';
- return this.getObject();
- }
- // 24h formats
- else {
- // Supported formats:
- // - ISO 8601: [hh][mm][ss] | [hh][mm] | [hh]
- // - ISO 8601 extended: [hh]:[mm]:[ss] | [hh]:[mm] | [hh]
- this.tokens = /:/.test(str) && str.split(/:/) || str.match(/[0-9]{2}/g);
- this.h = this.tokens[0] || 0;
- this.m = this.tokens[1] || 0;
- this.s = this.tokens[2] || 0;
- this.z = this.tokens[3] || '';
- return this.getObject();
- }
-};
-
-})(jQuery);
diff --git a/rt/share/html/NoAuth/js/util.js b/rt/share/html/NoAuth/js/util.js
index 5bfce411d..fe5c0a3ff 100644
--- a/rt/share/html/NoAuth/js/util.js
+++ b/rt/share/html/NoAuth/js/util.js
@@ -222,35 +222,47 @@ function doOnLoad( js ) {
}
jQuery(function() {
- jQuery(".ui-datepicker:not(.withtime)").datepicker( {
- dateFormat: 'yy-mm-dd',
- constrainInput: false
- } );
-
- jQuery(".ui-datepicker.withtime").datepicker( {
+ var opts = {
dateFormat: 'yy-mm-dd',
constrainInput: false,
- onSelect: function( dateText, inst ) {
- // trigger timepicker to get time
- var button = document.createElement('input');
- button.setAttribute('type', 'button');
- jQuery(button).width('5em');
- jQuery(button).insertAfter(this);
- jQuery(button).timepickr({val: '00:00'});
- var date_input = this;
-
- jQuery(button).blur( function() {
- var time = jQuery(button).val();
- if ( ! time.match(/\d\d:\d\d/) ) {
- time = '00:00';
- }
- jQuery(date_input).val( dateText + ' ' + time + ':00' );
- jQuery(button).remove();
- } );
-
- jQuery(button).focus();
- }
- } );
+ showButtonPanel: true,
+ changeMonth: true,
+ changeYear: true,
+ showOtherMonths: true,
+ selectOtherMonths: true
+ };
+ jQuery(".ui-datepicker:not(.withtime)").datepicker(opts);
+ jQuery(".ui-datepicker.withtime").datetimepicker( jQuery.extend({}, opts, {
+ stepHour: 1,
+ // We fake this by snapping below for the minute slider
+ //stepMinute: 5,
+ hourGrid: 6,
+ minuteGrid: 15,
+ showSecond: false,
+ timeFormat: 'hh:mm:ss'
+ }) ).each(function(index, el) {
+ var tp = jQuery.datepicker._get( jQuery.datepicker._getInst(el), 'timepicker');
+ if (!tp) return;
+
+ // Hook after _injectTimePicker so we can modify the minute_slider
+ // right after it's first created
+ tp._base_injectTimePicker = tp._injectTimePicker;
+ tp._injectTimePicker = function() {
+ this._base_injectTimePicker.apply(this, arguments);
+
+ // Now that we have minute_slider, modify it to be stepped for mouse movements
+ var slider = jQuery.data(this.minute_slider[0], "slider");
+ slider._base_normValueFromMouse = slider._normValueFromMouse;
+ slider._normValueFromMouse = function() {
+ var value = this._base_normValueFromMouse.apply(this, arguments);
+ var old_step = this.options.step;
+ this.options.step = 5;
+ var aligned = this._trimAlignValue( value );
+ this.options.step = old_step;
+ return aligned;
+ };
+ };
+ });
});
function textToHTML(value) {
diff --git a/rt/share/html/Prefs/Other.html b/rt/share/html/Prefs/Other.html
index 9f7e04a9c..b5d3edd95 100644
--- a/rt/share/html/Prefs/Other.html
+++ b/rt/share/html/Prefs/Other.html
@@ -53,6 +53,7 @@
% foreach my $section( RT->Config->Sections ) {
<&|/Widgets/TitleBox, title => loc( $section ) &>
% foreach my $option( RT->Config->Options( Section => $section ) ) {
+% next if $option eq 'EmailFrequency' && !RT->Config->Get('RecordOutgoingEmail');
% my $meta = RT->Config->Meta( $option );
<& $meta->{'Widget'},
Default => 1,
diff --git a/rt/share/html/REST/1.0/Forms/ticket/default b/rt/share/html/REST/1.0/Forms/ticket/default
index 9a2212b55..016a50c73 100755
--- a/rt/share/html/REST/1.0/Forms/ticket/default
+++ b/rt/share/html/REST/1.0/Forms/ticket/default
@@ -167,6 +167,17 @@ else {
elsif (lc $k eq 'text') {
$text = delete $data{$k};
}
+ elsif ( lc $k ne 'id' ) {
+ $e = 1;
+ push @$o, $k;
+ push(@comments, "# $k: Unknown field");
+ }
+ }
+
+ if ( $e ) {
+ unshift @comments, "# Could not create ticket.";
+ $k = \%data;
+ goto DONE;
}
# people fields allow multiple values
@@ -292,8 +303,10 @@ else {
elsif (exists $simple{$key}) {
$key = $simple{$key};
$set = "Set$key";
+ my $current = $ticket->$key;
+ $current = '' unless defined $current;
- next if (($val eq ($ticket->$key||''))|| ($ticket->$key =~ /^\d+$/ && $val =~ /^\d+$/ && $val == $ticket->$key));
+ next if ($val eq $current) or ($current =~ /^\d+$/ && $val =~ /^\d+$/ && $val == $current);
($n, $s) = $ticket->$set("$val");
}
elsif (exists $dates{$key}) {
@@ -331,13 +344,6 @@ else {
}
}
foreach $p (keys %new) {
- # XXX: This is a stupid test.
- unless ($p =~ /^[\w.+-]+\@([\w.-]+\.)*\w+.?$/) {
- $s = 0;
- $n = "$p is not a valid email address.";
- push @msgs, [ $s, $n ];
- next;
- }
unless ($ticket->IsWatcher(Type => $type, Email => $p)) {
($s, $n) = $ticket->AddWatcher(Type => $type,
Email => $p);
diff --git a/rt/share/html/Search/Chart.html b/rt/share/html/Search/Chart.html
index 070ce7cf7..571c3d3a0 100644
--- a/rt/share/html/Search/Chart.html
+++ b/rt/share/html/Search/Chart.html
@@ -98,14 +98,14 @@ my %query;
for(@session_fields) {
$query{$_} = $current->{$_} unless defined $query{$_};
- $query{$_} = $m->request_args->{$_} unless defined $query{$_};
+ $query{$_} = $DECODED_ARGS->{$_} unless defined $query{$_};
}
- if ($m->request_args->{'SavedSearchLoadSubmit'}) {
- $query{'SavedChartSearchId'} = $m->request_args->{'SavedSearchLoad'};
+ if ($DECODED_ARGS->{'SavedSearchLoadSubmit'}) {
+ $query{'SavedChartSearchId'} = $DECODED_ARGS->{'SavedSearchLoad'};
}
- if ($m->request_args->{'SavedSearchSave'}) {
+ if ($DECODED_ARGS->{'SavedSearchSave'}) {
$query{'SavedChartSearchId'} = $saved_search->{'SearchId'};
}
diff --git a/rt/share/html/Search/Elements/SelectPersonType b/rt/share/html/Search/Elements/SelectPersonType
index d07e49c22..bc2911196 100644
--- a/rt/share/html/Search/Elements/SelectPersonType
+++ b/rt/share/html/Search/Elements/SelectPersonType
@@ -62,7 +62,7 @@
<%INIT>
my @types;
-if ($Scope =~ 'queue') {
+if ($Scope =~ /queue/) {
@types = qw(Cc AdminCc);
}
elsif ($Suffix eq 'Group') {
diff --git a/rt/share/html/Search/Results.html b/rt/share/html/Search/Results.html
index 171b38d92..4fee86506 100755
--- a/rt/share/html/Search/Results.html
+++ b/rt/share/html/Search/Results.html
@@ -151,6 +151,7 @@ if ($ARGS{'TicketsRefreshInterval'}) {
my $refresh = $session{'tickets_refresh_interval'}
|| RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'} );
+# Check $m->request_args, not $DECODED_ARGS, to avoid creating a new CSRF token on each refresh
if (RT->Config->Get('RestrictReferrer') and $refresh and not $m->request_args->{CSRF_Token}) {
my $token = RT::Interface::Web::StoreRequestToken( $session{'CurrentSearchHash'} );
$m->notes->{RefreshURL} = RT->Config->Get('WebURL')
diff --git a/rt/share/html/Search/Results.xls b/rt/share/html/Search/Results.xls
index 52a05daed..8b94e22ba 100644
--- a/rt/share/html/Search/Results.xls
+++ b/rt/share/html/Search/Results.xls
@@ -54,6 +54,7 @@ $Format => undef
<%INIT>
use Spreadsheet::WriteExcel;
+use OLE::Storage_Lite;
use List::Util qw( max );
use Date::Format qw( time2str );
diff --git a/rt/share/html/Ticket/Attachment/dhandler b/rt/share/html/Ticket/Attachment/dhandler
index 75efeffbc..eb291e4f5 100755
--- a/rt/share/html/Ticket/Attachment/dhandler
+++ b/rt/share/html/Ticket/Attachment/dhandler
@@ -48,7 +48,7 @@
<%perl>
my ($ticket, $trans,$attach, $filename);
my $arg = $m->dhandler_arg; # get rest of path
- if ($arg =~ '^(\d+)/(\d+)') {
+ if ($arg =~ m{^(\d+)/(\d+)}) {
$trans = $1;
$attach = $2;
}
@@ -79,7 +79,12 @@
my $enc = $AttachmentObj->OriginalEncoding || 'utf-8';
my $iana = Encode::find_encoding( $enc );
$iana = $iana? $iana->mime_name : $enc;
- $content_type .= ";charset=$iana";
+
+ require MIME::Types;
+ my $mimetype = MIME::Types->new->type($content_type);
+ unless ( $mimetype && $mimetype->isBinary ) {
+ $content_type .= ";charset=$iana";
+ }
$r->subprocess_env('no-gzip' => 1); # disable mod_deflate
$r->content_type( $content_type );
diff --git a/rt/share/html/Ticket/Elements/AddCustomers b/rt/share/html/Ticket/Elements/AddCustomers
index 3c2c82add..13fb2f010 100644
--- a/rt/share/html/Ticket/Elements/AddCustomers
+++ b/rt/share/html/Ticket/Elements/AddCustomers
@@ -58,7 +58,7 @@ my @Customers = ();
if ( $CustomerString ) {
@Customers = &RT::URI::freeside::smart_search(
'search' => $CustomerString,
- 'no_fuzzy_on_exact' => 1, #pref?
+ 'no_fuzzy_on_exact' => ! $FS::CurrentUser::CurrentUser->option('enable_fuzzy_on_exact'),
);
}
diff --git a/rt/share/html/Ticket/Elements/ShowMembers b/rt/share/html/Ticket/Elements/ShowMembers
index c17c6e7b8..1ffbda2a1 100755
--- a/rt/share/html/Ticket/Elements/ShowMembers
+++ b/rt/share/html/Ticket/Elements/ShowMembers
@@ -48,8 +48,9 @@
<ul>
% while (my $link = $members->Next) {
<li><& /Elements/ShowLink, URI => $link->BaseURI &><br />
+% next if $link->BaseObj and $checked->{$link->BaseObj->id};
% if ($depth < 8) {
-<& /Ticket/Elements/ShowMembers, Ticket => $link->BaseObj, depth => ($depth+1) &>
+<& /Ticket/Elements/ShowMembers, Ticket => $link->BaseObj, depth => ($depth+1), checked => $checked &>
% }
</li>
% }
@@ -61,9 +62,13 @@ return unless $Ticket;
my $members = $Ticket->Members;
return unless $members->Count;
+return if $checked->{$Ticket->id};
+
+$checked->{$Ticket->id} = 1;
</%INIT>
<%ARGS>
$Ticket => undef
$depth => 1
+$checked => {}
</%ARGS>
diff --git a/rt/share/html/Ticket/Elements/ShowTransactionAttachments b/rt/share/html/Ticket/Elements/ShowTransactionAttachments
index 877201f55..95a23411b 100644
--- a/rt/share/html/Ticket/Elements/ShowTransactionAttachments
+++ b/rt/share/html/Ticket/Elements/ShowTransactionAttachments
@@ -144,6 +144,8 @@ my $render_attachment = sub {
my $message = shift;
my $name = defined $message->Filename && length $message->Filename ? $message->Filename : '';
+ my $content_type = lc $message->ContentType;
+
# if it has a content-disposition: attachment, don't show inline
my $disposition = $message->GetHeader('Content-Disposition');
@@ -154,7 +156,7 @@ my $render_attachment = sub {
}
# If it's text
- if ( $message->ContentType =~ m{^(text|message)}i ) {
+ if ( $content_type =~ m{^(text|message)/} ) {
my $max_size = RT->Config->Get( 'MaxInlineBody', $session{'CurrentUser'} );
if ( $disposition ne 'inline' ) {
$m->out('<p>'. loc( 'Message body is not shown because sender requested not to inline it.' ) .'</p>');
@@ -175,16 +177,16 @@ my $render_attachment = sub {
!$ParentObj
# or its parent isn't a multipart alternative
- || ( $ParentObj->ContentType !~ m{^multipart/alternative$}i )
+ || ( $ParentObj->ContentType !~ m{^multipart/(?:alternative|related)$}i )
# or it's of our prefered alterative type
|| (
(
RT->Config->Get('PreferRichText')
- && ( $message->ContentType =~ m{^text/(?:html|enriched)$} )
+ && ( $content_type =~ m{^text/(?:html|enriched)$} )
)
|| ( !RT->Config->Get('PreferRichText')
- && ( $message->ContentType !~ m{^text/(?:html|enriched)$} )
+ && ( $content_type !~ m{^text/(?:html|enriched)$} )
)
)
) {
@@ -198,7 +200,6 @@ my $render_attachment = sub {
$content = $message->Content;
}
- my $content_type = lc $message->ContentType;
$RT::Logger->debug(
"Rendering attachment #". $message->id
." of '$content_type' type"
@@ -231,9 +232,8 @@ my $render_attachment = sub {
$m->out( $content );
}
- # if it's a text/plain show the body
- elsif ( $message->ContentType =~ m{^(text|message)}i ) {
-
+ # It's a text type we don't have special handling for
+ else {
unless ( length $name ) {
eval { require Text::Quoted; $content = Text::Quoted::extract($content); };
if ($@) { $RT::Logger->warning( "Text::Quoted failed: $@" ) }
@@ -250,7 +250,7 @@ my $render_attachment = sub {
}
# if it's an image, show it as an image
- elsif ( RT->Config->Get('ShowTransactionImages') and $message->ContentType =~ /^image\//i ) {
+ elsif ( RT->Config->Get('ShowTransactionImages') and $content_type =~ m{^image/} ) {
if ( $disposition ne 'inline' ) {
$m->out('<p>'. loc( 'Message body is not shown because sender requested not to inline it.' ) .'</p>');
return;
diff --git a/rt/share/html/m/_elements/raw_style b/rt/share/html/m/_elements/raw_style
index 8c1997743..a34982958 100644
--- a/rt/share/html/m/_elements/raw_style
+++ b/rt/share/html/m/_elements/raw_style
@@ -33,6 +33,7 @@ div.buttons {
background-color: #ccc;
-moz-border-radius: 0.25em;
-webkit-border-radius: 0.25em;
+ border-radius: 0.25em;
-webkit-box-shadow: #333 0px 0px 5px;
-moz-box-shadow: #333 0px 0px 5px;
box-shadow: #333 0px 0px 5px;
@@ -85,6 +86,7 @@ ul.menu li#active a
div.titlebox, #bpscredits, .ticket_menu{
-moz-border-radius: 1em;
-webkit-border-radius: 1em;
+ border-radius: 1em;
margin: 0.5em;
background-color: #fff;
padding-top: 1em;
@@ -336,6 +338,7 @@ input[type=submit], input[type=button], button, #paging a {
padding-right: 0.6em;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
+ border-radius: 0.5em;
background-color: #006699;
color: #fff;
}
@@ -353,6 +356,7 @@ form {
border-bottom: 1px solid black;
-moz-border-radius-bottomleft: 1em;
-webkit-border-bottom-left-radius: 1em;
+ border-bottom-left-radius: 1em;
padding: 0.5em;
background-color: #fff;
}
diff --git a/rt/share/html/m/_elements/wrapper b/rt/share/html/m/_elements/wrapper
index 794385db4..1891079bd 100644
--- a/rt/share/html/m/_elements/wrapper
+++ b/rt/share/html/m/_elements/wrapper
@@ -3,7 +3,7 @@ $title => ''
$show_home_button => 1
</%args>
<%init>
-if ($m->request_args->{'NotMobile'}) {
+if ($DECODED_ARGS->{'NotMobile'}) {
$session{'NotMobile'} = 1;
RT::Interface::Web::Redirect(RT->Config->Get('WebURL'));
$m->abort();
diff --git a/rt/t/api/config.t b/rt/t/api/config.t
index a986c3c4f..62b77dffa 100644
--- a/rt/t/api/config.t
+++ b/rt/t/api/config.t
@@ -1,7 +1,8 @@
use strict;
use warnings;
use RT;
-use RT::Test nodb => 1, tests => 9;
+use RT::Test nodb => 1, tests => 11;
+use Test::Warn;
ok(
RT::Config->AddOption(
@@ -31,3 +32,12 @@ is( $meta->{Widget}, '/Widgets/Form/Boolean', 'widget is updated to boolean' );
ok( RT::Config->DeleteOption( Name => 'foo' ), 'removed option foo' );
is( RT::Config->Meta('foo'), undef, 'foo is indeed deleted' );
+# Test EmailInputEncodings PostLoadCheck code
+RT::Config->Set('EmailInputEncodings', qw(utf-8 iso-8859-1 us-ascii foo));
+my @encodings = qw(utf-8-strict iso-8859-1 ascii);
+
+warning_is {RT::Config->PostLoadCheck} "Unknown encoding 'foo' in \@EmailInputEncodings option",
+ 'Correct warning for encoding foo';
+
+my @canonical_encodings = RT::Config->Get('EmailInputEncodings');
+is_deeply(\@encodings, \@canonical_encodings, 'Got correct encoding list');
diff --git a/rt/t/api/template-insert.t b/rt/t/api/template-insert.t
deleted file mode 100644
index 1bf5fc390..000000000
--- a/rt/t/api/template-insert.t
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/usr/bin/perl
-
-use warnings;
-use strict;
-
-
-use RT;
-use RT::Test tests => 7;
-
-
-
-# This tiny little test script triggers an interaction bug between DBD::Oracle 1.16, SB 1.15 and RT 3.4
-
-use_ok('RT::Template');
-my $template = RT::Template->new(RT->SystemUser);
-
-isa_ok($template, 'RT::Template');
-my ($val,$msg) = $template->Create(Queue => 1,
- Name => 'InsertTest',
- Content => 'This is template content');
-ok($val,$msg);
-is($template->Name, 'InsertTest');
-is($template->Content, 'This is template content', "We created the object right");
-($val, $msg) = $template->SetContent( 'This is new template content');
-ok($val,$msg);
-is($template->Content, 'This is new template content', "We managed to _Set_ the content");
diff --git a/rt/t/api/template-simple.t b/rt/t/api/template-simple.t
deleted file mode 100644
index bbdebb31f..000000000
--- a/rt/t/api/template-simple.t
+++ /dev/null
@@ -1,275 +0,0 @@
-use strict;
-use warnings;
-use RT;
-use RT::Test tests => 231;
-use Test::Warn;
-
-my $queue = RT::Queue->new(RT->SystemUser);
-$queue->Load("General");
-
-my $ticket_cf = RT::CustomField->new(RT->SystemUser);
-$ticket_cf->Create(
- Name => 'Department',
- Queue => '0',
- Type => 'FreeformSingle',
-);
-
-my $txn_cf = RT::CustomField->new(RT->SystemUser);
-$txn_cf->Create(
- Name => 'Category',
- LookupType => RT::Transaction->CustomFieldLookupType,
- Type => 'FreeformSingle',
-);
-$txn_cf->AddToObject($queue);
-
-my $ticket = RT::Ticket->new(RT->SystemUser);
-my ($id, $msg) = $ticket->Create(
- Subject => "template testing",
- Queue => "General",
- Owner => 'root@localhost',
- Requestor => ["dom\@example.com"],
- "CustomField-" . $txn_cf->id => "Special",
-);
-ok($id, "Created ticket: $msg");
-my $txn = $ticket->Transactions->First;
-
-$ticket->AddCustomFieldValue(
- Field => 'Department',
- Value => 'Coolio',
-);
-
-TemplateTest(
- Content => "\ntest",
- PerlOutput => "test",
- SimpleOutput => "test",
-);
-
-TemplateTest(
- Content => "\ntest { 5 * 5 }",
- PerlOutput => "test 25",
- SimpleOutput => "test { 5 * 5 }",
-);
-
-TemplateTest(
- Content => "\ntest { \$Requestor }",
- PerlOutput => "test dom\@example.com",
- SimpleOutput => "test dom\@example.com",
-);
-
-TemplateTest(
- Content => "\ntest { \$TicketSubject }",
- PerlOutput => "test ",
- SimpleOutput => "test template testing",
-);
-
-SimpleTemplateTest(
- Content => "\ntest { \$TicketQueueId }",
- Output => "test 1",
-);
-
-SimpleTemplateTest(
- Content => "\ntest { \$TicketQueueName }",
- Output => "test General",
-);
-
-SimpleTemplateTest(
- Content => "\ntest { \$TicketOwnerId }",
- Output => "test 12",
-);
-
-SimpleTemplateTest(
- Content => "\ntest { \$TicketOwnerName }",
- Output => "test root",
-);
-
-SimpleTemplateTest(
- Content => "\ntest { \$TicketOwnerEmailAddress }",
- Output => "test root\@localhost",
-);
-
-SimpleTemplateTest(
- Content => "\ntest { \$TicketStatus }",
- Output => "test new",
-);
-
-SimpleTemplateTest(
- Content => "\ntest #{ \$TicketId }",
- Output => "test #" . $ticket->id,
-);
-
-SimpleTemplateTest(
- Content => "\ntest { \$TicketCFDepartment }",
- Output => "test Coolio",
-);
-
-SimpleTemplateTest(
- Content => "\ntest #{ \$TransactionId }",
- Output => "test #" . $txn->id,
-);
-
-SimpleTemplateTest(
- Content => "\ntest { \$TransactionType }",
- Output => "test Create",
-);
-
-SimpleTemplateTest(
- Content => "\ntest { \$TransactionCFCategory }",
- Output => "test Special",
-);
-
-SimpleTemplateTest(
- Content => "\ntest { \$TicketDelete }",
- Output => "test { \$TicketDelete }",
-);
-
-SimpleTemplateTest(
- Content => "\ntest { \$Nonexistent }",
- Output => "test { \$Nonexistent }",
-);
-
-warning_like {
- TemplateTest(
- Content => "\ntest { \$Ticket->Nonexistent }",
- PerlOutput => undef,
- SimpleOutput => "test { \$Ticket->Nonexistent }",
- );
-} qr/RT::Ticket::Nonexistent Unimplemented/;
-
-warning_like {
- TemplateTest(
- Content => "\ntest { \$Nonexistent->Nonexistent }",
- PerlOutput => undef,
- SimpleOutput => "test { \$Nonexistent->Nonexistent }",
- );
-} qr/Can't call method "Nonexistent" on an undefined value/;
-
-TemplateTest(
- Content => "\ntest { \$Ticket->OwnerObj->Name }",
- PerlOutput => "test root",
- SimpleOutput => "test { \$Ticket->OwnerObj->Name }",
-);
-
-warning_like {
- TemplateTest(
- Content => "\ntest { *!( }",
- SyntaxError => 1,
- PerlOutput => undef,
- SimpleOutput => "test { *!( }",
- );
-} qr/Template parsing error: syntax error/;
-
-TemplateTest(
- Content => "\ntest { \$rtname ",
- SyntaxError => 1,
- PerlOutput => undef,
- SimpleOutput => undef,
-);
-
-is($ticket->Status, 'new', "test setup");
-SimpleTemplateTest(
- Content => "\ntest { \$Ticket->SetStatus('resolved') }",
- Output => "test { \$Ticket->SetStatus('resolved') }",
-);
-is($ticket->Status, 'new', "simple templates can't call ->SetStatus");
-
-# Make sure changing the template's type works
-my $template = RT::Template->new(RT->SystemUser);
-$template->Create(
- Name => "type chameleon",
- Type => "Perl",
- Content => "\ntest { 10 * 7 }",
-);
-ok($id = $template->id, "Created template");
-$template->Parse;
-is($template->MIMEObj->stringify_body, "test 70", "Perl output");
-
-$template = RT::Template->new(RT->SystemUser);
-$template->Load($id);
-is($template->Name, "type chameleon");
-
-$template->SetType('Simple');
-$template->Parse;
-is($template->MIMEObj->stringify_body, "test { 10 * 7 }", "Simple output");
-
-$template = RT::Template->new(RT->SystemUser);
-$template->Load($id);
-is($template->Name, "type chameleon");
-
-$template->SetType('Perl');
-$template->Parse;
-is($template->MIMEObj->stringify_body, "test 70", "Perl output");
-
-undef $ticket;
-
-my $counter = 0;
-sub IndividualTemplateTest {
- local $Test::Builder::Level = $Test::Builder::Level + 1;
-
- my %args = (
- Name => "Test-" . ++$counter,
- Type => "Perl",
- @_,
- );
-
- my $t = RT::Template->new(RT->SystemUser);
- $t->Create(
- Name => $args{Name},
- Type => $args{Type},
- Content => $args{Content},
- );
-
- ok($t->id, "Created $args{Type} template");
- is($t->Name, $args{Name}, "$args{Type} template name");
- is($t->Content, $args{Content}, "$args{Type} content");
- is($t->Type, $args{Type}, "template type");
-
- # this should never blow up!
- my ($ok, $msg) = $t->CompileCheck;
-
- # we don't need to syntax check simple templates since if you mess them up
- # it's safe to just use the input directly as the template's output
- if ($args{SyntaxError} && $args{Type} eq 'Perl') {
- ok(!$ok, "got a syntax error");
- }
- else {
- ok($ok, $msg);
- }
-
- ($ok, $msg) = $t->Parse(
- TicketObj => $ticket,
- TransactionObj => $txn,
- );
- if (defined $args{Output}) {
- ok($ok, $msg);
- is($t->MIMEObj->stringify_body, $args{Output}, "$args{Type} template's output");
- }
- else {
- ok(!$ok, "expected a failure");
- }
-}
-
-sub TemplateTest {
- local $Test::Builder::Level = $Test::Builder::Level + 1;
- my %args = @_;
-
- for my $type ('Perl', 'Simple') {
- next if $args{"Skip$type"};
-
- IndividualTemplateTest(
- %args,
- Type => $type,
- Output => $args{$type . 'Output'},
- );
- }
-}
-
-sub SimpleTemplateTest {
- local $Test::Builder::Level = $Test::Builder::Level + 1;
- my %args = @_;
-
- IndividualTemplateTest(
- %args,
- Type => 'Simple',
- );
-}
-
diff --git a/rt/t/api/template.t b/rt/t/api/template.t
index 2fadede38..331d9f996 100644
--- a/rt/t/api/template.t
+++ b/rt/t/api/template.t
@@ -1,25 +1,34 @@
-use strict;
use warnings;
-use RT;
-use RT::Test tests => 2;
-
-
-{
-
-ok(require RT::Template);
+use strict;
+use RT;
+use RT::Test tests => 10;
-}
+my $queue = RT::Test->load_or_create_queue( Name => 'Templates' );
+ok $queue && $queue->id, "loaded or created a queue";
{
-
-my $t = RT::Template->new(RT->SystemUser);
-$t->Create(Name => "Foo", Queue => 1);
-my $t2 = RT::Template->new(RT->Nobody);
-$t2->Load($t->Id);
-ok($t2->QueueObj->id, "Got the template's queue objet");
-
-
+ my $template = RT::Template->new( RT->SystemUser );
+ isa_ok($template, 'RT::Template');
+ my ($val,$msg) = $template->Create(
+ Queue => $queue->id,
+ Name => 'InsertTest',
+ Content => 'This is template content'
+ );
+ ok $val, "created a template" or diag "error: $msg";
+ ok my $id = $template->id, "id is defined";
+ is $template->Name, 'InsertTest';
+ is $template->Content, 'This is template content', "We created the object right";
+
+ ($val, $msg) = $template->SetContent( 'This is new template content');
+ ok $val, "changed content" or diag "error: $msg";
+
+ is $template->Content, 'This is new template content', "We managed to _Set_ the content";
+
+ ($val, $msg) = $template->Delete;
+ ok $val, "deleted template";
+
+ $template->Load($id);
+ ok !$template->id, "can not load template after deletion";
}
-
diff --git a/rt/t/articles/search-interface.t b/rt/t/articles/search-interface.t
index eb3a4f763..bcba3116b 100644
--- a/rt/t/articles/search-interface.t
+++ b/rt/t/articles/search-interface.t
@@ -3,7 +3,7 @@
use strict;
use warnings;
-use RT::Test tests => 23;
+use RT::Test tests => 44;
use RT::CustomField;
use RT::Queue;
@@ -67,7 +67,12 @@ my %cvals = ('article1q' => 'Some question about swallows',
'article3q' => 'Why should I eat my supper?',
'article3a' => 'There are starving children in Africa',
'article4q' => 'What did Brian originally write?',
- 'article4a' => 'Romanes eunt domus');
+ 'article4a' => 'This is an answer that is longer than 255 '
+ . 'characters so these tests will be sure to use the LargeContent '
+ . 'SQL as well as the normal SQL that would be generated if this '
+ . 'was an answer that was shorter than 255 characters. This second '
+ . 'sentence has a few extra characters to get this string to go '
+ . 'over the 255 character boundary. Lorem ipsum.');
# Create an article or two with our custom field values.
@@ -108,6 +113,52 @@ isa_ok($m, 'Test::WWW::Mechanize');
ok($m->login, 'logged in');
$m->follow_link_ok( { text => 'Articles', url_regex => qr!^/Articles/! },
'UI -> Articles' );
-$m->follow_link_ok( {text => 'Search'}, 'Articles -> Search');
-$m->follow_link_ok( {text => 'in class '.$class->Name}, 'Articles in class '.$class->Name);
-$m->content_contains($article1->Name);
+
+# In all of the search results below, the results page should
+# have the summary text of the article it occurs in.
+
+# Case sensitive search on small field.
+DoArticleSearch($m, $class->Name, 'Africa');
+$m->text_contains('Search results'); # Did we do a search?
+$m->text_contains('blah blah 1');
+
+# Case insensitive search on small field.
+DoArticleSearch($m, $class->Name, 'africa');
+$m->text_contains('Search results'); # Did we do a search?
+$m->text_contains('blah blah 1');
+
+# Case sensitive search on large field.
+DoArticleSearch($m, $class->Name, 'ipsum');
+$m->text_contains('Search results'); # Did we do a search?
+$m->text_contains('hoi polloi 4');
+
+# Case insensitive search on large field.
+DoArticleSearch($m, $class->Name, 'lorem');
+$m->text_contains('Search results'); # Did we do a search?
+TODO:{
+ local $TODO = 'Case insensitive search on LONGBLOB not available in MySQL'
+ if RT->Config->Get('DatabaseType') eq 'mysql';
+ $m->text_contains('hoi polloi 4');
+}
+
+# When you send $m to this sub, it must be on a page with
+# a Search link.
+sub DoArticleSearch{
+ my $m = shift;
+ my $class_name = shift;
+ my $search_text = shift;
+
+ $m->follow_link_ok( {text => 'Search'}, 'Articles -> Search');
+ $m->follow_link_ok( {text => 'in class '. $class_name}, 'Articles in class '. $class_name);
+ $m->text_contains('First article');
+
+ $m->submit_form_ok( {
+ form_number => 3,
+ fields => {
+ 'Article~' => $search_text
+ },
+ }, "Search for $search_text"
+ );
+ return;
+}
+
diff --git a/rt/t/articles/uri-a.t b/rt/t/articles/uri-a.t
index 82d0f1b01..5c1fdaf36 100644
--- a/rt/t/articles/uri-a.t
+++ b/rt/t/articles/uri-a.t
@@ -3,7 +3,7 @@
use strict;
use warnings;
-use RT::Test tests => 7;
+use RT::Test tests => 15;
use_ok("RT::URI::a");
my $uri = RT::URI::a->new($RT::SystemUser);
@@ -26,3 +26,39 @@ is(ref($uri->Object), "RT::Article", "Object loaded is an article");
is($uri->Object->Id, $article->Id, "Object loaded has correct ID");
is($article->URI, 'fsck.com-article://example.com/article/'.$article->Id,
"URI object has correct URI string");
+
+{
+ my $aid = $article->id;
+ my $ticket = RT::Ticket->new( RT->SystemUser );
+ my ($id, $msg) = $ticket->Create(
+ Queue => 1,
+ Subject => 'test ticket',
+ );
+ ok $id, "Created a test ticket";
+
+ # Try searching
+ my $tickets = RT::Tickets->new( RT->SystemUser );
+ $tickets->FromSQL(" RefersTo = 'a:$aid' ");
+ is $tickets->Count, 0, "No results yet";
+
+ # try with the full uri
+ $tickets->FromSQL(" RefersTo = '@{[ $article->URI ]}' ");
+ is $tickets->Count, 0, "Still no results";
+
+ # add the link
+ $ticket->AddLink( Type => 'RefersTo', Target => "a:$aid" );
+
+ # verify the ticket has it
+ my @links = @{$ticket->RefersTo->ItemsArrayRef};
+ is scalar @links, 1, "Has one RefersTo link";
+ is ref $links[0]->TargetObj, "RT::Article", "Link points to an article";
+ is $links[0]->TargetObj->id, $aid, "Link points to the article we specified";
+
+ # search again
+ $tickets->FromSQL(" RefersTo = 'a:$aid' ");
+ is $tickets->Count, 1, "Found one ticket with short URI";
+
+ # search with the full uri
+ $tickets->FromSQL(" RefersTo = '@{[ $article->URI ]}' ");
+ is $tickets->Count, 1, "Found one ticket with full URI";
+}
diff --git a/rt/t/data/configs/apache2.2+fastcgi.conf.in b/rt/t/data/configs/apache2.2+fastcgi.conf.in
index 3ec36dd0f..03eaa9a70 100644
--- a/rt/t/data/configs/apache2.2+fastcgi.conf.in
+++ b/rt/t/data/configs/apache2.2+fastcgi.conf.in
@@ -12,6 +12,7 @@ Group @WEB_GROUP@
</IfModule>
</IfModule>
+ServerName localhost
Listen %%LISTEN%%
ErrorLog "%%LOG_FILE%%"
diff --git a/rt/t/data/configs/apache2.2+mod_perl.conf.in b/rt/t/data/configs/apache2.2+mod_perl.conf.in
index 3b1f3f618..20d2f44e5 100644
--- a/rt/t/data/configs/apache2.2+mod_perl.conf.in
+++ b/rt/t/data/configs/apache2.2+mod_perl.conf.in
@@ -30,6 +30,7 @@ Group @WEB_GROUP@
</IfModule>
</IfModule>
+ServerName localhost
Listen %%LISTEN%%
ErrorLog "%%LOG_FILE%%"
diff --git a/rt/t/mail/dashboard-chart-with-utf8.t b/rt/t/mail/dashboard-chart-with-utf8.t
index 6d07b963b..79f5f0e11 100644
--- a/rt/t/mail/dashboard-chart-with-utf8.t
+++ b/rt/t/mail/dashboard-chart-with-utf8.t
@@ -1,7 +1,17 @@
use strict;
use warnings;
-use RT::Test tests => 15;
+BEGIN {
+ require RT::Test;
+
+ if (eval { require GD }) {
+ RT::Test->import(tests => 15);
+ }
+ else {
+ RT::Test->import(skip_all => 'GD required.');
+ }
+}
+
use utf8;
my $root = RT::Test->load_or_create_user( Name => 'root' );
diff --git a/rt/t/mail/dashboards.t b/rt/t/mail/dashboards.t
index 7a7a54ce6..00cfc6acd 100644
--- a/rt/t/mail/dashboards.t
+++ b/rt/t/mail/dashboards.t
@@ -2,7 +2,7 @@
use strict;
use warnings;
-use RT::Test tests => 187;
+use RT::Test tests => 181;
use Test::Warn;
use RT::Dashboard::Mailer;
@@ -138,17 +138,6 @@ sub delete_dashboard { # {{{
ok($ok, $msg);
} # }}}
-sub delete_subscriptions { # {{{
- my $subscription_id = shift;
- # delete the dashboard and make sure we get exactly one subscription failure
- # notice
- my $user = RT::User->new(RT->SystemUser);
- $user->Load('root');
- for my $subscription ($user->Attributes->Named('Subscription')) {
- $subscription->Delete;
- }
-} # }}}
-
my $good_time = 1290423660; # 6:01 EST on a monday
my $bad_time = 1290427260; # 7:01 EST on a monday
@@ -223,21 +212,9 @@ SKIP: {
delete_dashboard($dashboard_id);
-warning_like {
- RT::Dashboard::Mailer->MailDashboards(All => 1);
-} qr/Unable to load dashboard $dashboard_id of subscription $subscription_id for user root/;
-
-@mails = RT::Test->fetch_caught_mails;
-is(@mails, 1, "one mail for subscription failure");
-$mail = parse_mail($mails[0]);
-is($mail->head->get('Subject'), "[example.com] Missing dashboard!\n");
-is($mail->head->get('From'), "dashboard\@example.com\n");
-is($mail->head->get('X-RT-Dashboard-Id'), "$dashboard_id\n");
-is($mail->head->get('X-RT-Dashboard-Subscription-Id'), "$subscription_id\n");
-
RT::Dashboard::Mailer->MailDashboards(All => 1);
@mails = RT::Test->fetch_caught_mails;
-is(@mails, 0, "no mail because the subscription notice happens only once");
+is(@mails, 0, "no mail because the subscription is deleted");
RT::Test->stop_server;
RT::Test->clean_caught_mails;
@@ -277,7 +254,6 @@ RT->Config->Set('EmailDashboardRemove' => (qr/My dashboards/, "Testing!"));
($baseurl, $m) = RT::Test->started_ok;
delete_dashboard($dashboard_id);
-delete_subscriptions();
RT::Test->clean_caught_mails;
@@ -330,7 +306,6 @@ RT->Config->Set('EmailDashboardRemove' => (qr/My dashboards/, "Testing!"));
($baseurl, $m) = RT::Test->started_ok;
delete_dashboard($dashboard_id);
-delete_subscriptions();
RT::Test->clean_caught_mails;
@@ -373,7 +348,6 @@ RT->Config->Set('EmailDashboardRemove' => (qr/My dashboards/, "Testing!"));
($baseurl, $m) = RT::Test->started_ok;
delete_dashboard($dashboard_id);
-delete_subscriptions();
RT::Test->clean_caught_mails;
diff --git a/rt/t/mail/gateway.t b/rt/t/mail/gateway.t
index 9f0e669a3..98eabd56e 100644
--- a/rt/t/mail/gateway.t
+++ b/rt/t/mail/gateway.t
@@ -57,7 +57,7 @@ use strict;
use warnings;
-use RT::Test config => 'Set( $UnsafeEmailCommands, 1);', tests => 221, actual_server => 1;
+use RT::Test config => 'Set( $UnsafeEmailCommands, 1);', tests => 228, actual_server => 1;
my ($baseurl, $m) = RT::Test->started_ok;
use RT::Tickets;
@@ -608,6 +608,35 @@ EOF
$m->no_warnings_ok;
}
+diag "make sure we check that UTF-8 is really UTF-8";
+{
+ my $text = <<EOF;
+From: root\@localhost
+To: rtemail\@@{[RT->Config->Get('rtname')]}
+Subject: This is test wrong utf-8 chars
+Content-Type: text/plain; charset="utf-8"
+
+utf-8: informaci\303\263n confidencial
+latin1: informaci\363n confidencial
+
+bye
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate_and_http($text);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "created ticket");
+
+ my $tick = RT::Test->last_ticket;
+ is ($tick->Id, $id, "correct ticket");
+
+ my $content = $tick->Transactions->First->Content;
+ Encode::_utf8_off($content);
+
+ like $content, qr{informaci\303\263n confidencial};
+ like $content, qr{informaci\357\277\275n confidencial};
+
+ $m->no_warnings_ok;
+}
+
diag "check that mailgate doesn't suffer from empty Reply-To:";
{
my $text = <<EOF;
diff --git a/rt/t/shredder/01ticket.t b/rt/t/shredder/01ticket.t
index 7dff16df3..a7abeef6e 100644
--- a/rt/t/shredder/01ticket.t
+++ b/rt/t/shredder/01ticket.t
@@ -78,7 +78,11 @@ cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"
my $shredder = shredder_new();
$shredder->PutObjects( Objects => $child );
$shredder->WipeoutAll;
- cmp_deeply( dump_current_and_savepoint('parent_ticket'), "current DB equal to savepoint");
+
+ TODO: {
+ local $TODO = "Shredder doesn't delete all links and transactions";
+ cmp_deeply( dump_current_and_savepoint('parent_ticket'), "current DB equal to savepoint");
+ }
$shredder->PutObjects( Objects => $parent );
$shredder->WipeoutAll;
diff --git a/rt/t/shredder/03plugin_tickets.t b/rt/t/shredder/03plugin_tickets.t
index 092b57052..e63eef8fd 100644
--- a/rt/t/shredder/03plugin_tickets.t
+++ b/rt/t/shredder/03plugin_tickets.t
@@ -34,6 +34,7 @@ use_ok('RT::Tickets');
my $child = RT::Ticket->new( RT->SystemUser );
my ($cid) = $child->Create( Subject => 'child', Queue => 1, MemberOf => $pid );
ok( $cid, "created new ticket" );
+ $_->ApplyTransactionBatch for $parent, $child;
my $plugin = RT::Shredder::Plugin::Tickets->new;
isa_ok($plugin, 'RT::Shredder::Plugin::Tickets');
@@ -77,6 +78,8 @@ cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"
my ($status, $msg) = $child->AddLink( Target => $pid, Type => 'DependsOn' );
ok($status, "added reqursive link") or diag "error: $msg";
+ $_->ApplyTransactionBatch for $parent, $child;
+
my $plugin = RT::Shredder::Plugin::Tickets->new;
isa_ok($plugin, 'RT::Shredder::Plugin::Tickets');
@@ -121,6 +124,8 @@ cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"
ok( $cid2, "created new ticket" );
$child2->SetStatus('resolved');
+ $_->ApplyTransactionBatch for $parent, $child1, $child2;
+
my $plugin = RT::Shredder::Plugin::Tickets->new;
isa_ok($plugin, 'RT::Shredder::Plugin::Tickets');
diff --git a/rt/t/shredder/03plugin_users.t b/rt/t/shredder/03plugin_users.t
index 4f4ecc89c..1f4cb4934 100644
--- a/rt/t/shredder/03plugin_users.t
+++ b/rt/t/shredder/03plugin_users.t
@@ -5,8 +5,8 @@ use warnings;
use Test::Deep;
use File::Spec;
-use Test::More tests => 9;
-use RT::Test nodb => 1;
+use Test::More tests => 21;
+use RT::Test ();
BEGIN {
my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
File::Spec->curdir());
@@ -38,3 +38,61 @@ use_ok('RT::Shredder::Plugin::Users');
ok(!$status, "bad 'status' arg value");
}
+init_db();
+
+RT::Test->set_rights(
+ { Principal => 'Everyone', Right => [qw(CreateTicket)] },
+);
+
+create_savepoint('clean');
+
+{ # Create two users and a ticket. Shred second user and replace relations with first user
+ my ($uidA, $uidB, $msg);
+ my $userA = RT::User->new( RT->SystemUser );
+ ($uidA, $msg) = $userA->Create( Name => 'userA', Privileged => 1, Disabled => 0 );
+ ok( $uidA, "created user A" ) or diag "error: $msg";
+
+ my $userB = RT::User->new( RT->SystemUser );
+ ($uidB, $msg) = $userB->Create( Name => 'userB', Privileged => 1, Disabled => 0 );
+ ok( $uidB, "created user B" ) or diag "error: $msg";
+
+ my ($tid, $trid);
+ my $ticket = RT::Ticket->new( RT::CurrentUser->new($userB) );
+ ($tid, $trid, $msg) = $ticket->Create( Subject => 'UserB Ticket', Queue => 1 );
+ ok( $tid, "created new ticket") or diag "error: $msg";
+
+ my $transaction = RT::Transaction->new( RT->SystemUser );
+ $transaction->Load($trid);
+ is ( $transaction->Creator, $uidB, "ticket creator is user B" );
+
+ my $plugin = RT::Shredder::Plugin::Users->new;
+ isa_ok($plugin, 'RT::Shredder::Plugin::Users');
+
+ my $status;
+ ($status, $msg) = $plugin->TestArgs( status => 'any', name => 'userB', replace_relations => $uidA );
+ ok($status, "plugin arguments are ok") or diag "error: $msg";
+
+ my @objs;
+ ($status, @objs) = $plugin->Run;
+ ok($status, "executed plugin successfully") or diag "error: @objs";
+ @objs = RT::Shredder->CastObjectsToRecords( Objects => \@objs );
+ is(scalar @objs, 1, "one object in the result set");
+
+ my $shredder = shredder_new();
+
+ ($status, $msg) = $plugin->SetResolvers( Shredder => $shredder );
+ ok($status, "set conflicts resolver") or diag "error: $msg";
+
+ $shredder->PutObjects( Objects => \@objs );
+ $shredder->WipeoutAll;
+
+ $ticket->Load( $tid );
+ is($ticket->id, $tid, 'loaded ticket');
+
+ $transaction->Load($trid);
+ is ( $transaction->Creator, $uidA, "ticket creator is now user A" );
+
+ $shredder->Wipeout( Object => $ticket );
+ $shredder->Wipeout( Object => $userA );
+}
+cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
diff --git a/rt/t/shredder/utils.pl b/rt/t/shredder/utils.pl
index 5f5c1822f..9b848c662 100644
--- a/rt/t/shredder/utils.pl
+++ b/rt/t/shredder/utils.pl
@@ -283,7 +283,7 @@ sub dump_sqlite
my $old_fhkn = $dbh->{'FetchHashKeyName'};
$dbh->{'FetchHashKeyName'} = 'NAME_lc';
- my $sth = $dbh->table_info( '', '', '%', 'TABLE' ) || die $DBI::err;
+ my $sth = $dbh->table_info( '', '%', '%', 'TABLE' ) || die $DBI::err;
my @tables = keys %{$sth->fetchall_hashref( 'table_name' )};
my $res = {};
diff --git a/rt/t/ticket/search_by_watcher.t b/rt/t/ticket/search_by_watcher.t
index 809450b56..cfc7b1c22 100644
--- a/rt/t/ticket/search_by_watcher.t
+++ b/rt/t/ticket/search_by_watcher.t
@@ -142,8 +142,8 @@ sub run_auto_tests {
@conditions = (
'Cc = "not@exist"' => sub { 0 },
'Cc != "not@exist"' => sub { 1 },
- 'Cc IS NULL' => sub { $_[0] =~ 'c:-;' },
- 'Cc IS NOT NULL' => sub { $_[0] !~ 'c:-;' },
+ 'Cc IS NULL' => sub { $_[0] =~ /c:-;/ },
+ 'Cc IS NOT NULL' => sub { $_[0] !~ /c:-;/ },
'Cc = "x@foo.com"' => sub { $_[0] =~ /c:[^;]*x/ },
'Cc != "x@foo.com"' => sub { $_[0] !~ /c:[^;]*x/ },
'Cc LIKE "@bar.com"' => sub { $_[0] =~ /c:[^;]*(?:y|z)/ },
@@ -152,8 +152,8 @@ sub run_auto_tests {
'Requestor = "not@exist"' => sub { 0 },
'Requestor != "not@exist"' => sub { 1 },
- 'Requestor IS NULL' => sub { $_[0] =~ 'r:-;' },
- 'Requestor IS NOT NULL' => sub { $_[0] !~ 'r:-;' },
+ 'Requestor IS NULL' => sub { $_[0] =~ /r:-;/ },
+ 'Requestor IS NOT NULL' => sub { $_[0] !~ /r:-;/ },
'Requestor = "x@foo.com"' => sub { $_[0] =~ /r:[^;]*x/ },
'Requestor != "x@foo.com"' => sub { $_[0] !~ /r:[^;]*x/ },
'Requestor LIKE "@bar.com"' => sub { $_[0] =~ /r:[^;]*(?:y|z)/ },
@@ -174,7 +174,7 @@ sub run_auto_tests {
'Subject LIKE "ne"' => sub { 0 },
'Subject NOT LIKE "ne"' => sub { 1 },
'Subject = "r:x;c:y;"' => sub { $_[0] eq 'r:x;c:y;' },
- 'Subject LIKE "x"' => sub { $_[0] =~ 'x' },
+ 'Subject LIKE "x"' => sub { $_[0] =~ /x/ },
);
@tickets = generate_tix();
diff --git a/rt/t/web/attachments.t b/rt/t/web/attachments.t
index 8c75f6caf..784cbbe88 100644
--- a/rt/t/web/attachments.t
+++ b/rt/t/web/attachments.t
@@ -1,10 +1,11 @@
#!/usr/bin/perl -w
use strict;
-use RT::Test tests => 25;
+use RT::Test tests => 33;
use constant LogoFile => $RT::MasonComponentRoot .'/NoAuth/images/bpslogo.png';
use constant FaviconFile => $RT::MasonComponentRoot .'/NoAuth/images/favicon.png';
+use constant TextFile => $RT::MasonComponentRoot .'/NoAuth/css/print.css';
my ($baseurl, $m) = RT::Test->started_ok;
ok $m->login, 'logged in';
@@ -30,9 +31,18 @@ $m->content_contains('Attachments test', 'we have subject on the page');
$m->content_contains('Some content', 'and content');
$m->content_contains('Download bpslogo.png', 'page has file name');
+open LOGO, "<", LogoFile or die "Can't open logo file: $!";
+binmode LOGO;
+my $logo_contents = do {local $/; <LOGO>};
+close LOGO;
+$m->follow_link_ok({text => "Download bpslogo.png"});
+is($m->content_type, "image/png");
+is($m->content, $logo_contents, "Binary content matches");
+
+$m->back;
$m->follow_link_ok({text => 'Reply'}, "reply to the ticket");
$m->form_name('TicketUpdate');
-$m->field('Attach', LogoFile);
+$m->field('Attach', TextFile);
$m->click('AddMoreAttach');
is($m->status, 200, "request successful");
@@ -44,7 +54,16 @@ is($m->status, 200, "request successful");
$m->content_contains('Download bpslogo.png', 'page has file name');
$m->content_contains('Download favicon.png', 'page has file name');
+$m->content_contains('Download print.css', 'page has file name');
+
+$m->follow_link_ok( { text => 'Download bpslogo.png' } );
+is( $m->response->header('Content-Type'), 'image/png', 'Content-Type of png lacks charset' );
+
+$m->back;
+$m->follow_link_ok( { text => 'Download print.css' } );
+is( $m->response->header('Content-Type'),
+ 'text/css;charset=UTF-8', 'Content-Type of text has charset' );
diag "test mobile ui";
$m->get_ok( $baseurl . '/m/ticket/create?Queue=' . $qid );
diff --git a/rt/t/web/command_line.t b/rt/t/web/command_line.t
index 1fed8e69e..394daaba9 100644
--- a/rt/t/web/command_line.t
+++ b/rt/t/web/command_line.t
@@ -3,7 +3,7 @@
use strict;
use File::Spec ();
use Test::Expect;
-use RT::Test tests => 303, actual_server => 1;
+use RT::Test tests => 315, actual_server => 1;
my ($baseurl, $m) = RT::Test->started_ok;
use RT::User;
@@ -480,6 +480,8 @@ expect_like(qr/Merged into ticket #$merge_ticket_A by root/, 'Merge recorded in
expect_like(qr/Created link $link1_id $reln $link2_id/, 'Linked');
expect_send("show -s ticket/$link1_id/links", "Checking creation of $reln...");
expect_like(qr/$display_relns{$reln}: [\w\d\.\-]+:\/\/[\w\d\.]+\/ticket\/$link2_id/, "Created link $reln");
+ expect_send("show ticket/$link1_id/links", "Checking show links without format");
+ expect_like(qr/$display_relns{$reln}: [\w\d\.\-]+:\/\/[\w\d\.]+\/ticket\/$link2_id/, "Found link $reln");
# delete link
expect_send("link -d $link1_id $reln $link2_id", "Delete $reln...");
diff --git a/rt/t/web/command_line_with_unknown_field.t b/rt/t/web/command_line_with_unknown_field.t
index 736be4d1c..d63956be3 100644
--- a/rt/t/web/command_line_with_unknown_field.t
+++ b/rt/t/web/command_line_with_unknown_field.t
@@ -3,7 +3,7 @@
use strict;
use File::Spec ();
use Test::Expect;
-use RT::Test tests => 14, actual_server => 1;
+use RT::Test tests => 17, actual_server => 1;
my ($baseurl, $m) = RT::Test->started_ok;
my $rt_tool_path = "$RT::BinPath/rt";
@@ -19,6 +19,11 @@ expect_run(
prompt => 'rt> ',
quit => 'quit',
);
+
+expect_send( q{create -t ticket set foo=bar}, "create ticket with unknown field" );
+expect_like(qr/foo: Unknown field/, 'foo is unknown field');
+expect_like(qr/Could not create ticket/, 'ticket is not created');
+
expect_send(q{create -t ticket set subject='new ticket' add cc=foo@example.com}, "Creating a ticket...");
expect_like(qr/Ticket \d+ created/, "Created the ticket");
diff --git a/rt/t/web/crypt-gnupg.t b/rt/t/web/crypt-gnupg.t
index 6bdefdac7..8c0eb570d 100644
--- a/rt/t/web/crypt-gnupg.t
+++ b/rt/t/web/crypt-gnupg.t
@@ -53,6 +53,7 @@ RT::Test->clean_caught_mails;
$m->goto_create_ticket( $queue );
$m->form_name('TicketCreate');
+$m->field('Requestors', 'recipient@example.com');
$m->field('Subject', 'Encryption test');
$m->field('Content', 'Some content');
ok($m->value('Encrypt', 2), "encrypt tick box is checked");
@@ -122,6 +123,7 @@ RT::Test->clean_caught_mails;
$m->goto_create_ticket( $queue );
$m->form_name('TicketCreate');
+$m->field('Requestors', 'recipient@example.com');
$m->field('Subject', 'Signing test');
$m->field('Content', 'Some other content');
ok(!$m->value('Encrypt', 2), "encrypt tick box is unchecked");
@@ -195,6 +197,7 @@ RT::Test->clean_caught_mails;
$m->goto_create_ticket( $queue );
$m->form_name('TicketCreate');
+$m->field('Requestors', 'recipient@example.com');
$m->field('Subject', 'Crypt+Sign test');
$m->field('Content', 'Some final? content');
ok($m->value('Encrypt', 2), "encrypt tick box is checked");
@@ -260,6 +263,7 @@ RT::Test->clean_caught_mails;
$m->goto_create_ticket( $queue );
$m->form_name('TicketCreate');
+$m->field('Requestors', 'recipient@example.com');
$m->field('Subject', 'Test crypt-off on encrypted queue');
$m->field('Content', 'Thought you had me figured out didya');
$m->field(Encrypt => undef, 2); # turn off encryption
diff --git a/rt/t/web/googleish_search.t b/rt/t/web/googleish_search.t
index e2a4e9116..f4c8fa4b6 100644
--- a/rt/t/web/googleish_search.t
+++ b/rt/t/web/googleish_search.t
@@ -2,7 +2,8 @@
use strict;
use warnings;
-use RT::Test tests => 96, config => 'Set( %FullTextSearch, Enable => 1, Indexed => 0 );';
+use RT::Test tests => undef,
+ config => 'Set( %FullTextSearch, Enable => 1, Indexed => 0 );';
my ($baseurl, $m) = RT::Test->started_ok;
my $url = $m->rt_base_url;
@@ -57,6 +58,7 @@ ok $two_words_queue && $two_words_queue->id, 'loaded or created a queue';
is $parser->QueryToSQL("'me'"), "$active AND ( Subject LIKE 'me' )", "correct parsing";
is $parser->QueryToSQL("owner:me"), "( Owner.id = '__CurrentUser__' ) AND $active", "correct parsing";
is $parser->QueryToSQL("owner:'me'"), "( Owner = 'me' ) AND $active", "correct parsing";
+ is $parser->QueryToSQL('owner:root@localhost'), "( Owner.EmailAddress = 'root\@localhost' ) AND $active", "Email address as owner";
is $parser->QueryToSQL("resolved me"), "( Owner.id = '__CurrentUser__' ) AND ( Status = 'resolved' )", "correct parsing";
is $parser->QueryToSQL("resolved active me"), "( Owner.id = '__CurrentUser__' ) AND ( Status = 'resolved' OR Status = 'new' OR Status = 'open' OR Status = 'stalled' )", "correct parsing";
@@ -217,3 +219,5 @@ for my $quote ( q{'}, q{"} ) {
}
}
+undef $m;
+done_testing;
diff --git a/rt/t/web/query_builder_queue_limits.t b/rt/t/web/query_builder_queue_limits.t
index a3b976524..f583d64cc 100644
--- a/rt/t/web/query_builder_queue_limits.t
+++ b/rt/t/web/query_builder_queue_limits.t
@@ -11,6 +11,9 @@ $lifecycles->{foo} = {
};
+# explicitly Set so RT::Test can catch our change
+RT->Config->Set( Lifecycles => %$lifecycles );
+
RT::Lifecycle->FillCache();
my $general = RT::Test->load_or_create_queue( Name => 'General' );
diff --git a/rt/t/web/search_simple.t b/rt/t/web/search_simple.t
index 1efc9a566..a1a3ce806 100644
--- a/rt/t/web/search_simple.t
+++ b/rt/t/web/search_simple.t
@@ -1,7 +1,7 @@
use strict;
use warnings;
-use RT::Test tests => 16;
+use RT::Test tests => 30;
my ( $baseurl, $m ) = RT::Test->started_ok;
RT::Test->create_tickets(
@@ -19,4 +19,58 @@ $m->content_contains( 'Show Results', "has page menu" );
$m->title_is( 'Found 1 ticket', 'title' );
$m->content_contains( 'ticket foo', 'has ticket foo' );
+# Test searches on custom fields
+my $cf1 = RT::Test->load_or_create_custom_field(
+ Name => 'Location',
+ Queue => 'General',
+ Type => 'FreeformSingle', );
+isa_ok( $cf1, 'RT::CustomField' );
+
+my $cf2 = RT::Test->load_or_create_custom_field(
+ Name => 'Server-name',
+ Queue => 'General',
+ Type => 'FreeformSingle', );
+isa_ok( $cf2, 'RT::CustomField' );
+
+my $t = RT::Ticket->new(RT->SystemUser);
+
+{
+ my ($id,undef,$msg) = $t->Create(
+ Queue => 'General',
+ Subject => 'Test searching CFs');
+ ok( $id, "Created ticket - $msg" );
+}
+
+{
+ my ($status, $msg) = $t->AddCustomFieldValue(
+ Field => $cf1->id,
+ Value => 'Downtown');
+ ok( $status, "Added CF value - $msg" );
+}
+
+{
+ my ($status, $msg) = $t->AddCustomFieldValue(
+ Field => $cf2->id,
+ Value => 'Proxy');
+ ok( $status, "Added CF value - $msg" );
+}
+
+# Regular search
+my $search = 'cf.Location:Downtown';
+$m->get_ok("/Search/Simple.html?q=$search");
+$m->title_is( 'Found 1 ticket', 'Found 1 ticket' );
+$m->text_contains( 'Test searching CFs', "Found test CF ticket with $search" );
+
+# Case insensitive
+$search = "cf.Location:downtown";
+$m->get_ok("/Search/Simple.html?q=$search");
+$m->title_is( 'Found 1 ticket', 'Found 1 ticket' );
+$m->text_contains( 'Test searching CFs', "Found test CF ticket with $search" );
+
+# With dash in CF name
+$search = "cf.Server-name:Proxy";
+$m->get_ok("/Search/Simple.html?q=$search");
+$m->title_is( 'Found 1 ticket', 'Found 1 ticket' );
+$m->text_contains( 'Test searching CFs', "Found test CF ticket with $search" );
+
# TODO more simple search tests
diff --git a/rt/t/web/ticket_modify_all.t b/rt/t/web/ticket_modify_all.t
index c9dd7e7cd..2f0c4d1b3 100644
--- a/rt/t/web/ticket_modify_all.t
+++ b/rt/t/web/ticket_modify_all.t
@@ -1,7 +1,7 @@
use strict;
use warnings;
-use RT::Test tests => 15;
+use RT::Test tests => 22;
my $ticket = RT::Test->create_ticket(
Subject => 'test bulk update',
@@ -40,5 +40,44 @@ $m->click('SubmitTicket');
$m->form_name('TicketModifyAll');
is($m->value('Owner'), 'root', 'owner was successfully changed to root');
-# XXX TODO test other parts, i.e. basic, dates, people and links
+$m->get_ok($url . "/Ticket/ModifyAll.html?id=" . $ticket->id);
+$m->form_name('TicketModifyAll');
+$m->field('Starts_Date' => "2013-01-01 00:00:00");
+$m->click('SubmitTicket');
+$m->text_contains("Starts: (Tue Jan 01 00:00:00 2013)", 'start date successfully updated');
+
+$m->form_name('TicketModifyAll');
+$m->field('Started_Date' => "2014-01-01 00:00:00");
+$m->click('SubmitTicket');
+$m->text_contains("Started: (Wed Jan 01 00:00:00 2014)", 'started date successfully updated');
+
+$m->form_name('TicketModifyAll');
+$m->field('Told_Date' => "2015-01-01 00:00:00");
+$m->click('SubmitTicket');
+$m->text_contains("Last Contact: (Thu Jan 01 00:00:00 2015)", 'told date successfully updated');
+
+$m->form_name('TicketModifyAll');
+$m->field('Due_Date' => "2016-01-01 00:00:00");
+$m->click('SubmitTicket');
+$m->text_contains("Due: (Fri Jan 01 00:00:00 2016)", 'due date successfully updated');
+
+$m->get( $url . '/Ticket/ModifyAll.html?id=' . $ticket->id );
+$m->form_name('TicketModifyAll');
+$m->field(WatcherTypeEmail => 'Requestor');
+$m->field(WatcherAddressEmail => 'root@localhost');
+$m->click('SubmitTicket');
+$m->text_contains(
+ "Added principal as a Requestor for this ticket",
+ 'watcher is added',
+);
+$m->form_name('TicketModifyAll');
+$m->field(WatcherTypeEmail => 'Requestor');
+$m->field(WatcherAddressEmail => 'root@localhost');
+$m->click('SubmitTicket');
+$m->text_contains(
+ "That principal is already a Requestor for this ticket",
+ 'no duplicate watchers',
+);
+
+# XXX TODO test other parts, i.e. links
diff --git a/rt/t/web/transaction_batch.t b/rt/t/web/transaction_batch.t
index ae04e1fca..12d01fba4 100644
--- a/rt/t/web/transaction_batch.t
+++ b/rt/t/web/transaction_batch.t
@@ -12,7 +12,14 @@ my ($val, $msg) =$s1->Create( Queue => $q->Id,
ScripAction => 'User Defined',
CustomIsApplicableCode => 'return ($self->TransactionObj->Field||"") eq "TimeEstimated"',
CustomPrepareCode => 'return 1',
- CustomCommitCode => '$self->TicketObj->SetPriority($self->TicketObj->Priority + 2); return 1;',
+ CustomCommitCode => '
+if ( $self->TicketObj->CurrentUser->Name ne "RT_System" ) {
+ warn "Ticket obj has incorrect CurrentUser (should be RT_System) ".$self->TicketObj->CurrentUser->Name
+}
+if ( $self->TicketObj->QueueObj->CurrentUser->Name ne "RT_System" ) {
+ warn "Queue obj has incorrect CurrentUser (should be RT_System) ".$self->TicketObj->QueueObj->CurrentUser->Name
+}
+$self->TicketObj->SetPriority($self->TicketObj->Priority + 2); return 1;',
Template => 'Blank',
Stage => 'TransactionBatch',
);