Merge branch 'master' of https://github.com/jgoodman/Freeside
authorIvan Kohler <ivan@freeside.biz>
Thu, 6 Mar 2014 23:19:53 +0000 (15:19 -0800)
committerIvan Kohler <ivan@freeside.biz>
Thu, 6 Mar 2014 23:19:53 +0000 (15:19 -0800)
(github pull request #24 -- change refunds to proper refunds)

963 files changed:
FS/FS/API.pm [new file with mode: 0644]
FS/FS/ClientAPI/MyAccount.pm
FS/FS/ClientAPI/Signup.pm
FS/FS/ClientAPI_XMLRPC.pm
FS/FS/Conf.pm
FS/FS/Daemon/Preforking.pm [new file with mode: 0644]
FS/FS/Mason.pm
FS/FS/Misc.pm
FS/FS/Report/Table.pm
FS/FS/Schema.pm
FS/FS/TemplateItem_Mixin.pm
FS/FS/Template_Mixin.pm
FS/FS/XMLRPC.pm [deleted file]
FS/FS/XMLRPC_Lite.pm [new file with mode: 0644]
FS/FS/cust_bill.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_bill_pkg_fee.pm
FS/FS/cust_credit.pm
FS/FS/cust_credit_bill_pkg.pm
FS/FS/cust_event_fee.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_msg.pm
FS/FS/cust_pay.pm
FS/FS/cust_pay_pending.pm
FS/FS/cust_pay_void.pm
FS/FS/cust_pkg.pm
FS/FS/cust_pkg/Search.pm
FS/FS/cust_svc.pm
FS/FS/option_Common.pm
FS/FS/part_event/Action/Mixin/fee.pm
FS/FS/part_event/Action/cust_bill_fee.pm
FS/FS/part_event/Action/cust_fee.pm
FS/FS/part_event/Action/fee.pm
FS/FS/part_event/Action/pkg_fee.pm [new file with mode: 0644]
FS/FS/part_export.pm
FS/FS/part_export/send_email.pm
FS/FS/part_fee.pm
FS/FS/part_fee_usage.pm [new file with mode: 0644]
FS/FS/pay_batch/nacha.pm
FS/FS/quotation_pkg.pm
FS/FS/tax_rate.pm
FS/MANIFEST
FS/bin/freeside-selfservice-xmlrpcd
FS/bin/freeside-xmlrpcd [new file with mode: 0644]
FS/t/part_fee_usage.t [new file with mode: 0644]
Makefile
bin/freeside-init [deleted file]
bin/test_scrub_sql
bin/xmlrpc-customer_info [new file with mode: 0755]
bin/xmlrpc-insert_credit_phonenum [new file with mode: 0755]
bin/xmlrpc-insert_payment [new file with mode: 0755]
bin/xmlrpc-insert_payment_phonenum [new file with mode: 0755]
bin/xmlrpc-insert_refund_phonenum [new file with mode: 0755]
bin/xmlrpc-location_info [new file with mode: 0755]
bin/xmlrpc-new_customer [new file with mode: 0755]
bin/xmlrpcd-phonenum_balance.pl
eg/xmlrpc-example.pl [deleted file]
httemplate/browse/part_fee.html
httemplate/config/config-view.cgi
httemplate/edit/credit-cust_bill_pkg.html
httemplate/edit/part_fee.html
httemplate/edit/process/part_fee.html
httemplate/elements/email-link.html
httemplate/elements/tr-part_fee_usage.html [new file with mode: 0644]
httemplate/elements/tr-select-from_to.html
httemplate/misc/cust-part_pkg.cgi
httemplate/misc/email-customers.html
httemplate/misc/xmlhttp-calculate_taxes.html
httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html
httemplate/misc/xmlrpc.cgi [deleted file]
httemplate/search/477.html
httemplate/search/477partIA.html
httemplate/search/477partIIA.html
httemplate/search/477partIIB.html
httemplate/search/477partV.html
httemplate/search/477partVI_census.html
httemplate/search/cust_bill_pkg.cgi
httemplate/search/cust_bill_pkg_referral.html
httemplate/search/cust_msg.html
httemplate/search/elements/search-html.html
httemplate/search/report_477.html
httemplate/search/report_tax.cgi
httemplate/search/sales_commission.html
httemplate/search/sqlradius.cgi
httemplate/view/cust_main.cgi
init.d/freeside-init
rt/Makefile.in
rt/README
rt/bin/rt
rt/bin/rt-crontool.in
rt/bin/rt-mailgate.in
rt/bin/rt.in
rt/configure
rt/configure.ac
rt/devel/tools/change-loc-msgstr
rt/devel/tools/extract-message-catalog
rt/devel/tools/factory
rt/devel/tools/license_tag
rt/devel/tools/merge-rosetta.pl
rt/devel/tools/rt-attributes-editor
rt/devel/tools/tweak-template-locstring
rt/docs/UPGRADING-3.8
rt/docs/UPGRADING-4.0
rt/etc/RT_Config.pm.in
rt/etc/schema.mysql
rt/etc/upgrade/3.7.19/content
rt/etc/upgrade/3.8-ical-extension.in
rt/etc/upgrade/3.9.8/content
rt/etc/upgrade/generate-rtaddressregexp.in
rt/etc/upgrade/sanity-check-stylesheets.pl
rt/etc/upgrade/shrink_cgm_table.pl
rt/etc/upgrade/shrink_transactions_table.pl
rt/etc/upgrade/split-out-cf-categories.in
rt/etc/upgrade/upgrade-articles
rt/etc/upgrade/upgrade-articles.in
rt/etc/upgrade/upgrade-mysql-schema.pl
rt/etc/upgrade/vulnerable-passwords.in
rt/lib/RT.pm
rt/lib/RT/ACE.pm
rt/lib/RT/ACL.pm
rt/lib/RT/Action.pm
rt/lib/RT/Action/AutoOpen.pm
rt/lib/RT/Action/Autoreply.pm
rt/lib/RT/Action/CreateTickets.pm
rt/lib/RT/Action/EscalatePriority.pm
rt/lib/RT/Action/ExtractSubjectTag.pm
rt/lib/RT/Action/LinearEscalate.pm
rt/lib/RT/Action/Notify.pm
rt/lib/RT/Action/NotifyAsComment.pm
rt/lib/RT/Action/NotifyGroup.pm
rt/lib/RT/Action/NotifyGroupAsComment.pm
rt/lib/RT/Action/RecordComment.pm
rt/lib/RT/Action/RecordCorrespondence.pm
rt/lib/RT/Action/SendEmail.pm
rt/lib/RT/Action/SetPriority.pm
rt/lib/RT/Action/SetStatus.pm
rt/lib/RT/Action/UserDefined.pm
rt/lib/RT/Approval.pm
rt/lib/RT/Approval/Rule.pm
rt/lib/RT/Approval/Rule/Created.pm
rt/lib/RT/Approval/Rule/NewPending.pm
rt/lib/RT/Approval/Rule/Passed.pm
rt/lib/RT/Approval/Rule/Rejected.pm
rt/lib/RT/Article.pm
rt/lib/RT/Articles.pm
rt/lib/RT/Attachment.pm
rt/lib/RT/Attachments.pm
rt/lib/RT/Attribute.pm
rt/lib/RT/Attributes.pm
rt/lib/RT/Base.pm
rt/lib/RT/CachedGroupMember.pm
rt/lib/RT/CachedGroupMembers.pm
rt/lib/RT/Class.pm
rt/lib/RT/Classes.pm
rt/lib/RT/Condition.pm
rt/lib/RT/Condition/AnyTransaction.pm
rt/lib/RT/Condition/BeforeDue.pm
rt/lib/RT/Condition/CloseTicket.pm
rt/lib/RT/Condition/Overdue.pm
rt/lib/RT/Condition/OwnerChange.pm
rt/lib/RT/Condition/PriorityChange.pm
rt/lib/RT/Condition/PriorityExceeds.pm
rt/lib/RT/Condition/QueueChange.pm
rt/lib/RT/Condition/ReopenTicket.pm
rt/lib/RT/Condition/StatusChange.pm
rt/lib/RT/Condition/UserDefined.pm
rt/lib/RT/Config.pm
rt/lib/RT/Crypt/GnuPG.pm
rt/lib/RT/CurrentUser.pm
rt/lib/RT/CustomField.pm
rt/lib/RT/CustomFieldValue.pm
rt/lib/RT/CustomFieldValues.pm
rt/lib/RT/CustomFieldValues/External.pm
rt/lib/RT/CustomFieldValues/Groups.pm
rt/lib/RT/CustomFields.pm
rt/lib/RT/Dashboard.pm
rt/lib/RT/Dashboard/Mailer.pm
rt/lib/RT/Dashboards.pm
rt/lib/RT/Date.pm
rt/lib/RT/EmailParser.pm
rt/lib/RT/Generated.pm
rt/lib/RT/Generated.pm.in
rt/lib/RT/Graph/Tickets.pm
rt/lib/RT/Group.pm
rt/lib/RT/GroupMember.pm
rt/lib/RT/GroupMembers.pm
rt/lib/RT/Groups.pm
rt/lib/RT/Handle.pm
rt/lib/RT/I18N.pm
rt/lib/RT/I18N/cs.pm
rt/lib/RT/I18N/i_default.pm
rt/lib/RT/I18N/ru.pm
rt/lib/RT/Installer.pm
rt/lib/RT/Interface/CLI.pm
rt/lib/RT/Interface/Email.pm
rt/lib/RT/Interface/Email/Auth/GnuPG.pm
rt/lib/RT/Interface/Email/Auth/MailFrom.pm
rt/lib/RT/Interface/REST.pm
rt/lib/RT/Interface/Web.pm
rt/lib/RT/Interface/Web/Handler.pm
rt/lib/RT/Interface/Web/Menu.pm
rt/lib/RT/Interface/Web/QueryBuilder.pm
rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm
rt/lib/RT/Interface/Web/Request.pm
rt/lib/RT/Interface/Web/Session.pm
rt/lib/RT/Lifecycle.pm
rt/lib/RT/Link.pm
rt/lib/RT/Links.pm
rt/lib/RT/ObjectClass.pm
rt/lib/RT/ObjectClasses.pm
rt/lib/RT/ObjectCustomField.pm
rt/lib/RT/ObjectCustomFieldValue.pm
rt/lib/RT/ObjectCustomFieldValues.pm
rt/lib/RT/ObjectCustomFields.pm
rt/lib/RT/ObjectTopic.pm
rt/lib/RT/ObjectTopics.pm
rt/lib/RT/Plugin.pm
rt/lib/RT/Pod/HTML.pm
rt/lib/RT/Pod/HTMLBatch.pm
rt/lib/RT/Pod/Search.pm
rt/lib/RT/Principal.pm
rt/lib/RT/Principals.pm
rt/lib/RT/Queue.pm
rt/lib/RT/Queues.pm
rt/lib/RT/Record.pm
rt/lib/RT/Reminders.pm
rt/lib/RT/Report/Tickets.pm
rt/lib/RT/Report/Tickets/Entry.pm
rt/lib/RT/Rule.pm
rt/lib/RT/Ruleset.pm
rt/lib/RT/SQL.pm
rt/lib/RT/SavedSearch.pm
rt/lib/RT/SavedSearches.pm
rt/lib/RT/Scrip.pm
rt/lib/RT/ScripAction.pm
rt/lib/RT/ScripActions.pm
rt/lib/RT/ScripCondition.pm
rt/lib/RT/ScripConditions.pm
rt/lib/RT/Scrips.pm
rt/lib/RT/Search.pm
rt/lib/RT/Search/ActiveTicketsInQueue.pm
rt/lib/RT/Search/FromSQL.pm
rt/lib/RT/Search/Googleish.pm
rt/lib/RT/SearchBuilder.pm
rt/lib/RT/SharedSetting.pm
rt/lib/RT/SharedSettings.pm
rt/lib/RT/Shredder.pm
rt/lib/RT/Shredder/ACE.pm
rt/lib/RT/Shredder/Attachment.pm
rt/lib/RT/Shredder/CachedGroupMember.pm
rt/lib/RT/Shredder/Constants.pm
rt/lib/RT/Shredder/CustomField.pm
rt/lib/RT/Shredder/CustomFieldValue.pm
rt/lib/RT/Shredder/Dependencies.pm
rt/lib/RT/Shredder/Dependency.pm
rt/lib/RT/Shredder/Exceptions.pm
rt/lib/RT/Shredder/Group.pm
rt/lib/RT/Shredder/GroupMember.pm
rt/lib/RT/Shredder/Link.pm
rt/lib/RT/Shredder/ObjectCustomFieldValue.pm
rt/lib/RT/Shredder/POD.pm
rt/lib/RT/Shredder/Plugin.pm
rt/lib/RT/Shredder/Plugin/Attachments.pm
rt/lib/RT/Shredder/Plugin/Base.pm
rt/lib/RT/Shredder/Plugin/Base/Dump.pm
rt/lib/RT/Shredder/Plugin/Base/Search.pm
rt/lib/RT/Shredder/Plugin/Objects.pm
rt/lib/RT/Shredder/Plugin/SQLDump.pm
rt/lib/RT/Shredder/Plugin/Summary.pm
rt/lib/RT/Shredder/Plugin/Tickets.pm
rt/lib/RT/Shredder/Plugin/Users.pm
rt/lib/RT/Shredder/Principal.pm
rt/lib/RT/Shredder/Queue.pm
rt/lib/RT/Shredder/Record.pm
rt/lib/RT/Shredder/Scrip.pm
rt/lib/RT/Shredder/ScripAction.pm
rt/lib/RT/Shredder/ScripCondition.pm
rt/lib/RT/Shredder/Template.pm
rt/lib/RT/Shredder/Ticket.pm
rt/lib/RT/Shredder/Transaction.pm
rt/lib/RT/Shredder/User.pm
rt/lib/RT/Squish.pm
rt/lib/RT/Squish/CSS.pm
rt/lib/RT/Squish/JS.pm
rt/lib/RT/System.pm
rt/lib/RT/Template.pm
rt/lib/RT/Templates.pm
rt/lib/RT/Test.pm
rt/lib/RT/Test/Apache.pm
rt/lib/RT/Test/Email.pm
rt/lib/RT/Test/GnuPG.pm
rt/lib/RT/Test/Web.pm
rt/lib/RT/Ticket.pm
rt/lib/RT/Tickets.pm
rt/lib/RT/Tickets_SQL.pm
rt/lib/RT/Topic.pm
rt/lib/RT/Topics.pm
rt/lib/RT/Transaction.pm
rt/lib/RT/Transactions.pm
rt/lib/RT/URI.pm
rt/lib/RT/URI/a.pm
rt/lib/RT/URI/base.pm
rt/lib/RT/URI/fsck_com_article.pm
rt/lib/RT/URI/fsck_com_rt.pm
rt/lib/RT/URI/t.pm
rt/lib/RT/User.pm
rt/lib/RT/Users.pm
rt/lib/RT/Util.pm
rt/sbin/rt-attributes-viewer.in
rt/sbin/rt-clean-sessions.in
rt/sbin/rt-dump-metadata.in
rt/sbin/rt-email-dashboards.in
rt/sbin/rt-email-digest.in
rt/sbin/rt-email-group-admin.in
rt/sbin/rt-fulltext-indexer [deleted file]
rt/sbin/rt-fulltext-indexer.in
rt/sbin/rt-message-catalog
rt/sbin/rt-preferences-viewer.in
rt/sbin/rt-server.fcgi.in
rt/sbin/rt-server.in
rt/sbin/rt-session-viewer [deleted file]
rt/sbin/rt-session-viewer.in
rt/sbin/rt-setup-database.in
rt/sbin/rt-setup-fulltext-index [deleted file]
rt/sbin/rt-setup-fulltext-index.in
rt/sbin/rt-shredder.in
rt/sbin/rt-test-dependencies.in
rt/sbin/rt-validate-aliases.in
rt/sbin/rt-validator.in
rt/sbin/standalone_httpd [deleted file]
rt/sbin/standalone_httpd.in
rt/share/html/Admin/Articles/Classes/CustomFields.html
rt/share/html/Admin/Articles/Classes/GroupRights.html
rt/share/html/Admin/Articles/Classes/Modify.html
rt/share/html/Admin/Articles/Classes/Objects.html
rt/share/html/Admin/Articles/Classes/Topics.html
rt/share/html/Admin/Articles/Classes/UserRights.html
rt/share/html/Admin/Articles/Classes/index.html
rt/share/html/Admin/Articles/Elements/Topics
rt/share/html/Admin/Articles/index.html
rt/share/html/Admin/CustomFields/GroupRights.html
rt/share/html/Admin/CustomFields/Modify.html
rt/share/html/Admin/CustomFields/Objects.html
rt/share/html/Admin/CustomFields/UserRights.html
rt/share/html/Admin/CustomFields/index.html
rt/share/html/Admin/Elements/AddCustomFieldValue
rt/share/html/Admin/Elements/ConfigureMyRT
rt/share/html/Admin/Elements/CreateUserCalled
rt/share/html/Admin/Elements/EditCustomField
rt/share/html/Admin/Elements/EditCustomFieldValues
rt/share/html/Admin/Elements/EditCustomFieldValuesSource
rt/share/html/Admin/Elements/EditCustomFields
rt/share/html/Admin/Elements/EditQueueWatcherGroup
rt/share/html/Admin/Elements/EditQueueWatchers
rt/share/html/Admin/Elements/EditRights
rt/share/html/Admin/Elements/EditRightsCategoryTabs
rt/share/html/Admin/Elements/EditScrip
rt/share/html/Admin/Elements/EditScrips
rt/share/html/Admin/Elements/EditTemplates
rt/share/html/Admin/Elements/EditUserComments
rt/share/html/Admin/Elements/Header
rt/share/html/Admin/Elements/ListGlobalCustomFields
rt/share/html/Admin/Elements/ListGlobalScrips
rt/share/html/Admin/Elements/ModifyTemplate
rt/share/html/Admin/Elements/PickCustomFields
rt/share/html/Admin/Elements/PickObjects
rt/share/html/Admin/Elements/Portal
rt/share/html/Admin/Elements/QueueRightsForUser
rt/share/html/Admin/Elements/SelectCustomField
rt/share/html/Admin/Elements/SelectCustomFieldLookupType
rt/share/html/Admin/Elements/SelectCustomFieldRenderType
rt/share/html/Admin/Elements/SelectCustomFieldType
rt/share/html/Admin/Elements/SelectGroups
rt/share/html/Admin/Elements/SelectModifyGroup
rt/share/html/Admin/Elements/SelectModifyQueue
rt/share/html/Admin/Elements/SelectModifyUser
rt/share/html/Admin/Elements/SelectNewGroupMembers
rt/share/html/Admin/Elements/SelectRights
rt/share/html/Admin/Elements/SelectScrip
rt/share/html/Admin/Elements/SelectScripAction
rt/share/html/Admin/Elements/SelectScripCondition
rt/share/html/Admin/Elements/SelectSingleOrMultiple
rt/share/html/Admin/Elements/SelectStage
rt/share/html/Admin/Elements/SelectTemplate
rt/share/html/Admin/Elements/SelectUsers
rt/share/html/Admin/Elements/ShowKeyInfo
rt/share/html/Admin/Global/CustomFields/Class-Article.html
rt/share/html/Admin/Global/CustomFields/Groups.html
rt/share/html/Admin/Global/CustomFields/Queue-Tickets.html
rt/share/html/Admin/Global/CustomFields/Queue-Transactions.html
rt/share/html/Admin/Global/CustomFields/Queues.html
rt/share/html/Admin/Global/CustomFields/Users.html
rt/share/html/Admin/Global/CustomFields/index.html
rt/share/html/Admin/Global/GroupRights.html
rt/share/html/Admin/Global/MyRT.html
rt/share/html/Admin/Global/Scrip.html
rt/share/html/Admin/Global/Scrips.html
rt/share/html/Admin/Global/Template.html
rt/share/html/Admin/Global/Templates.html
rt/share/html/Admin/Global/Topics.html
rt/share/html/Admin/Global/UserRights.html
rt/share/html/Admin/Global/index.html
rt/share/html/Admin/Groups/GroupRights.html
rt/share/html/Admin/Groups/History.html
rt/share/html/Admin/Groups/Members.html
rt/share/html/Admin/Groups/Modify.html
rt/share/html/Admin/Groups/UserRights.html
rt/share/html/Admin/Groups/index.html
rt/share/html/Admin/Queues/CustomField.html
rt/share/html/Admin/Queues/CustomFields.html
rt/share/html/Admin/Queues/GroupRights.html
rt/share/html/Admin/Queues/History.html
rt/share/html/Admin/Queues/Modify.html
rt/share/html/Admin/Queues/People.html
rt/share/html/Admin/Queues/Scrip.html
rt/share/html/Admin/Queues/Scrips.html
rt/share/html/Admin/Queues/Template.html
rt/share/html/Admin/Queues/Templates.html
rt/share/html/Admin/Queues/UserRights.html
rt/share/html/Admin/Queues/index.html
rt/share/html/Admin/Tools/Configuration.html
rt/share/html/Admin/Tools/Queries.html
rt/share/html/Admin/Tools/Shredder/Dumps/dhandler
rt/share/html/Admin/Tools/Shredder/Elements/DumpFileLink
rt/share/html/Admin/Tools/Shredder/Elements/Error/NoRights
rt/share/html/Admin/Tools/Shredder/Elements/Error/NoStorage
rt/share/html/Admin/Tools/Shredder/Elements/Object/RT--Attachment
rt/share/html/Admin/Tools/Shredder/Elements/Object/RT--Ticket
rt/share/html/Admin/Tools/Shredder/Elements/Object/RT--User
rt/share/html/Admin/Tools/Shredder/Elements/ObjectCheckBox
rt/share/html/Admin/Tools/Shredder/Elements/PluginArguments
rt/share/html/Admin/Tools/Shredder/Elements/PluginHelp
rt/share/html/Admin/Tools/Shredder/Elements/SelectObjects
rt/share/html/Admin/Tools/Shredder/Elements/SelectPlugin
rt/share/html/Admin/Tools/Shredder/autohandler
rt/share/html/Admin/Tools/Shredder/index.html
rt/share/html/Admin/Tools/Theme.html
rt/share/html/Admin/Tools/index.html
rt/share/html/Admin/Users/CustomFields.html
rt/share/html/Admin/Users/GnuPG.html
rt/share/html/Admin/Users/History.html
rt/share/html/Admin/Users/Memberships.html
rt/share/html/Admin/Users/Modify.html
rt/share/html/Admin/Users/MyRT.html
rt/share/html/Admin/Users/index.html
rt/share/html/Admin/autohandler
rt/share/html/Admin/index.html
rt/share/html/Approvals/Display.html
rt/share/html/Approvals/Elements/Approve
rt/share/html/Approvals/Elements/PendingMyApproval
rt/share/html/Approvals/Elements/ShowDependency
rt/share/html/Approvals/autohandler
rt/share/html/Approvals/index.html
rt/share/html/Articles/Article/Delete.html
rt/share/html/Articles/Article/Display.html
rt/share/html/Articles/Article/Edit.html
rt/share/html/Articles/Article/Elements/EditBasics
rt/share/html/Articles/Article/Elements/EditCustomFields
rt/share/html/Articles/Article/Elements/EditLinks
rt/share/html/Articles/Article/Elements/EditTopics
rt/share/html/Articles/Article/Elements/LinkEntryInstructions
rt/share/html/Articles/Article/Elements/Preformatted
rt/share/html/Articles/Article/Elements/SearchByCustomField
rt/share/html/Articles/Article/Elements/SelectSavedSearches
rt/share/html/Articles/Article/Elements/SelectSearchPrivacy
rt/share/html/Articles/Article/Elements/ShowHistory
rt/share/html/Articles/Article/Elements/ShowLinks
rt/share/html/Articles/Article/Elements/ShowSavedSearches
rt/share/html/Articles/Article/Elements/ShowSearchCriteria
rt/share/html/Articles/Article/Elements/ShowTopics
rt/share/html/Articles/Article/ExtractFromTicket.html
rt/share/html/Articles/Article/ExtractIntoClass.html
rt/share/html/Articles/Article/ExtractIntoTopic.html
rt/share/html/Articles/Article/History.html
rt/share/html/Articles/Article/PreCreate.html
rt/share/html/Articles/Article/Search.html
rt/share/html/Articles/Elements/BeforeMessageBox
rt/share/html/Articles/Elements/CheckSkipCreate
rt/share/html/Articles/Elements/CreateArticle
rt/share/html/Articles/Elements/GotoArticle
rt/share/html/Articles/Elements/IncludeArticle
rt/share/html/Articles/Elements/NewestArticles
rt/share/html/Articles/Elements/QuickSearch
rt/share/html/Articles/Elements/SelectClass
rt/share/html/Articles/Elements/ShowTopic
rt/share/html/Articles/Elements/ShowTopicLink
rt/share/html/Articles/Elements/UpdatedArticles
rt/share/html/Articles/Topics.html
rt/share/html/Articles/index.html
rt/share/html/Dashboards/Elements/DashboardsForObject
rt/share/html/Dashboards/Elements/Deleted
rt/share/html/Dashboards/Elements/HiddenSearches
rt/share/html/Dashboards/Elements/ListOfDashboards
rt/share/html/Dashboards/Elements/SelectPrivacy
rt/share/html/Dashboards/Elements/ShowDashboards
rt/share/html/Dashboards/Elements/ShowPortlet/component
rt/share/html/Dashboards/Elements/ShowPortlet/dashboard
rt/share/html/Dashboards/Elements/ShowPortlet/search
rt/share/html/Dashboards/Modify.html
rt/share/html/Dashboards/Queries.html
rt/share/html/Dashboards/Render.html
rt/share/html/Dashboards/Subscription.html
rt/share/html/Dashboards/dhandler
rt/share/html/Dashboards/index.html
rt/share/html/Download/CustomFieldValue/dhandler
rt/share/html/Elements/BevelBoxRaisedEnd
rt/share/html/Elements/BevelBoxRaisedStart
rt/share/html/Elements/CSRF
rt/share/html/Elements/Callback
rt/share/html/Elements/Checkbox
rt/share/html/Elements/CollectionAsTable/Header
rt/share/html/Elements/CollectionAsTable/ParseFormat
rt/share/html/Elements/CollectionAsTable/Row
rt/share/html/Elements/CollectionList
rt/share/html/Elements/CollectionListPaging
rt/share/html/Elements/ColumnMap
rt/share/html/Elements/CreateTicket
rt/share/html/Elements/Dashboards
rt/share/html/Elements/EditCustomField
rt/share/html/Elements/EditCustomFieldAutocomplete
rt/share/html/Elements/EditCustomFieldBinary
rt/share/html/Elements/EditCustomFieldCombobox
rt/share/html/Elements/EditCustomFieldDate
rt/share/html/Elements/EditCustomFieldDateTime
rt/share/html/Elements/EditCustomFieldFreeform
rt/share/html/Elements/EditCustomFieldIPAddress
rt/share/html/Elements/EditCustomFieldIPAddressRange
rt/share/html/Elements/EditCustomFieldImage
rt/share/html/Elements/EditCustomFieldSelect
rt/share/html/Elements/EditCustomFieldText
rt/share/html/Elements/EditCustomFieldWikitext
rt/share/html/Elements/EditLinks
rt/share/html/Elements/EditPassword
rt/share/html/Elements/EditTimeValue
rt/share/html/Elements/EmailInput
rt/share/html/Elements/Error
rt/share/html/Elements/Footer
rt/share/html/Elements/Framekiller
rt/share/html/Elements/GnuPG/KeyIssues
rt/share/html/Elements/GnuPG/SelectKeyForEncryption
rt/share/html/Elements/GnuPG/SelectKeyForSigning
rt/share/html/Elements/GnuPG/SignEncryptWidget
rt/share/html/Elements/GotoTicket
rt/share/html/Elements/Header
rt/share/html/Elements/HeaderJavascript
rt/share/html/Elements/ListActions
rt/share/html/Elements/ListMenu
rt/share/html/Elements/Login
rt/share/html/Elements/LoginRedirectWarning
rt/share/html/Elements/Logo
rt/share/html/Elements/MakeClicky
rt/share/html/Elements/Menu
rt/share/html/Elements/MessageBox
rt/share/html/Elements/MyAdminQueues
rt/share/html/Elements/MyRT
rt/share/html/Elements/MyReminders
rt/share/html/Elements/MyRequests
rt/share/html/Elements/MySupportQueues
rt/share/html/Elements/MyTickets
rt/share/html/Elements/PageLayout
rt/share/html/Elements/PersonalQuickbar
rt/share/html/Elements/QueriesAsComment
rt/share/html/Elements/QueryString
rt/share/html/Elements/QueueSummaryByLifecycle
rt/share/html/Elements/QueueSummaryByStatus
rt/share/html/Elements/QuickCreate
rt/share/html/Elements/Quicksearch
rt/share/html/Elements/RT__Article/ColumnMap
rt/share/html/Elements/RT__Class/ColumnMap
rt/share/html/Elements/RT__CustomField/ColumnMap
rt/share/html/Elements/RT__Dashboard/ColumnMap
rt/share/html/Elements/RT__Group/ColumnMap
rt/share/html/Elements/RT__Queue/ColumnMap
rt/share/html/Elements/RT__SavedSearch/ColumnMap
rt/share/html/Elements/RT__Scrip/ColumnMap
rt/share/html/Elements/RT__Template/ColumnMap
rt/share/html/Elements/RT__Ticket/ColumnMap
rt/share/html/Elements/RT__User/ColumnMap
rt/share/html/Elements/Refresh
rt/share/html/Elements/RefreshHomepage
rt/share/html/Elements/SavedSearches
rt/share/html/Elements/ScrubHTML
rt/share/html/Elements/Section
rt/share/html/Elements/SelectAttachmentField
rt/share/html/Elements/SelectBoolean
rt/share/html/Elements/SelectCustomFieldOperator
rt/share/html/Elements/SelectCustomFieldValue
rt/share/html/Elements/SelectDate
rt/share/html/Elements/SelectDateRelation
rt/share/html/Elements/SelectDateType
rt/share/html/Elements/SelectEqualityOperator
rt/share/html/Elements/SelectGroups
rt/share/html/Elements/SelectIPRelation
rt/share/html/Elements/SelectLang
rt/share/html/Elements/SelectLinkType
rt/share/html/Elements/SelectMatch
rt/share/html/Elements/SelectNewTicketQueue
rt/share/html/Elements/SelectOwner
rt/share/html/Elements/SelectOwnerAutocomplete
rt/share/html/Elements/SelectOwnerDropdown
rt/share/html/Elements/SelectPriority
rt/share/html/Elements/SelectQueue
rt/share/html/Elements/SelectResultsPerPage
rt/share/html/Elements/SelectSortOrder
rt/share/html/Elements/SelectStatus
rt/share/html/Elements/SelectTicketSortBy
rt/share/html/Elements/SelectTicketTypes
rt/share/html/Elements/SelectTimeUnits
rt/share/html/Elements/SelectTimezone
rt/share/html/Elements/SelectUsers
rt/share/html/Elements/SelectWatcherType
rt/share/html/Elements/SetupSessionCookie
rt/share/html/Elements/ShowCustomFieldBinary
rt/share/html/Elements/ShowCustomFieldDate
rt/share/html/Elements/ShowCustomFieldDateTime
rt/share/html/Elements/ShowCustomFieldImage
rt/share/html/Elements/ShowCustomFieldText
rt/share/html/Elements/ShowCustomFieldWikitext
rt/share/html/Elements/ShowCustomFields
rt/share/html/Elements/ShowLink
rt/share/html/Elements/ShowLinks
rt/share/html/Elements/ShowMemberships
rt/share/html/Elements/ShowRelationLabel
rt/share/html/Elements/ShowReminders
rt/share/html/Elements/ShowSearch
rt/share/html/Elements/ShowUser
rt/share/html/Elements/ShowUserConcise
rt/share/html/Elements/ShowUserEmailFrequency
rt/share/html/Elements/ShowUserVerbose
rt/share/html/Elements/SimpleSearch
rt/share/html/Elements/Submit
rt/share/html/Elements/Tabs
rt/share/html/Elements/TicketList
rt/share/html/Elements/TitleBox
rt/share/html/Elements/TitleBoxEnd
rt/share/html/Elements/TitleBoxStart
rt/share/html/Elements/ValidateCustomFields
rt/share/html/Elements/WidgetBar
rt/share/html/Helpers/Autocomplete/CustomFieldValues
rt/share/html/Helpers/Autocomplete/Groups
rt/share/html/Helpers/Autocomplete/Owners
rt/share/html/Helpers/Autocomplete/Users
rt/share/html/Helpers/Autocomplete/autohandler
rt/share/html/Helpers/TicketHistory
rt/share/html/Helpers/Toggle/ShowRequestor
rt/share/html/Helpers/Toggle/TicketBookmark
rt/share/html/Helpers/autohandler
rt/share/html/Install/Basics.html
rt/share/html/Install/DatabaseDetails.html
rt/share/html/Install/DatabaseType.html
rt/share/html/Install/Elements/Errors
rt/share/html/Install/Elements/Wrapper
rt/share/html/Install/Finish.html
rt/share/html/Install/Global.html
rt/share/html/Install/Initialize.html
rt/share/html/Install/Sendmail.html
rt/share/html/Install/autohandler
rt/share/html/Install/index.html
rt/share/html/NoAuth/Helpers/CustomLogo/dhandler
rt/share/html/NoAuth/Login.html
rt/share/html/NoAuth/Logout.html
rt/share/html/NoAuth/Reminder.html
rt/share/html/NoAuth/RichText/autohandler
rt/share/html/NoAuth/RichText/dhandler
rt/share/html/NoAuth/css/aileron/InHeader
rt/share/html/NoAuth/css/aileron/base.css
rt/share/html/NoAuth/css/aileron/boxes.css
rt/share/html/NoAuth/css/aileron/forms.css
rt/share/html/NoAuth/css/aileron/images/dhandler
rt/share/html/NoAuth/css/aileron/layout.css
rt/share/html/NoAuth/css/aileron/login.css
rt/share/html/NoAuth/css/aileron/main.css
rt/share/html/NoAuth/css/aileron/misc.css
rt/share/html/NoAuth/css/aileron/msie.css
rt/share/html/NoAuth/css/aileron/msie6.css
rt/share/html/NoAuth/css/aileron/nav.css
rt/share/html/NoAuth/css/aileron/ticket-lists.css
rt/share/html/NoAuth/css/aileron/ticket-search.css
rt/share/html/NoAuth/css/aileron/ticket.css
rt/share/html/NoAuth/css/autohandler
rt/share/html/NoAuth/css/ballard/InHeader
rt/share/html/NoAuth/css/ballard/base.css
rt/share/html/NoAuth/css/ballard/boxes.css
rt/share/html/NoAuth/css/ballard/images/dhandler
rt/share/html/NoAuth/css/ballard/layout.css
rt/share/html/NoAuth/css/ballard/main.css
rt/share/html/NoAuth/css/ballard/misc.css
rt/share/html/NoAuth/css/ballard/msie.css
rt/share/html/NoAuth/css/ballard/msie6.css
rt/share/html/NoAuth/css/ballard/nav.css
rt/share/html/NoAuth/css/ballard/ticket-lists.css
rt/share/html/NoAuth/css/ballard/ticket-search.css
rt/share/html/NoAuth/css/ballard/ticket.css
rt/share/html/NoAuth/css/base/admin.css
rt/share/html/NoAuth/css/base/articles.css
rt/share/html/NoAuth/css/base/collection.css
rt/share/html/NoAuth/css/base/farbtastic.css
rt/share/html/NoAuth/css/base/forms.css
rt/share/html/NoAuth/css/base/history-folding.css
rt/share/html/NoAuth/css/base/jquery-ui.css
rt/share/html/NoAuth/css/base/login.css
rt/share/html/NoAuth/css/base/main.css
rt/share/html/NoAuth/css/base/misc.css
rt/share/html/NoAuth/css/base/nav.css
rt/share/html/NoAuth/css/base/portlets.css
rt/share/html/NoAuth/css/base/rights-editor.css
rt/share/html/NoAuth/css/base/theme-editor.css
rt/share/html/NoAuth/css/base/ticket-form.css
rt/share/html/NoAuth/css/base/ticket.css
rt/share/html/NoAuth/css/base/tools.css
rt/share/html/NoAuth/css/dhandler
rt/share/html/NoAuth/css/print.css
rt/share/html/NoAuth/css/web2/InHeader
rt/share/html/NoAuth/css/web2/base.css
rt/share/html/NoAuth/css/web2/boxes.css
rt/share/html/NoAuth/css/web2/images/dhandler
rt/share/html/NoAuth/css/web2/layout.css
rt/share/html/NoAuth/css/web2/main.css
rt/share/html/NoAuth/css/web2/misc.css
rt/share/html/NoAuth/css/web2/msie.css
rt/share/html/NoAuth/css/web2/msie6.css
rt/share/html/NoAuth/css/web2/nav.css
rt/share/html/NoAuth/css/web2/ticket-lists.css
rt/share/html/NoAuth/css/web2/ticket-search.css
rt/share/html/NoAuth/css/web2/ticket.css
rt/share/html/NoAuth/iCal/dhandler
rt/share/html/NoAuth/images/autohandler
rt/share/html/NoAuth/js/autohandler
rt/share/html/NoAuth/js/cascaded.js
rt/share/html/NoAuth/js/combobox.js
rt/share/html/NoAuth/js/dhandler
rt/share/html/NoAuth/js/history-folding.js
rt/share/html/NoAuth/js/jquery-ui-patch-datepicker.js
rt/share/html/NoAuth/js/jquery_noconflict.js
rt/share/html/NoAuth/js/late.js
rt/share/html/NoAuth/js/titlebox-state.js
rt/share/html/NoAuth/js/userautocomplete.js
rt/share/html/NoAuth/js/util.js
rt/share/html/NoAuth/rss/dhandler
rt/share/html/Prefs/MyRT.html
rt/share/html/Prefs/Other.html
rt/share/html/Prefs/Quicksearch.html
rt/share/html/Prefs/Search.html
rt/share/html/Prefs/SearchOptions.html
rt/share/html/REST/1.0/Forms/attachment/default
rt/share/html/REST/1.0/Forms/group/customfields
rt/share/html/REST/1.0/Forms/group/default
rt/share/html/REST/1.0/Forms/group/ns
rt/share/html/REST/1.0/Forms/queue/customfields
rt/share/html/REST/1.0/Forms/queue/default
rt/share/html/REST/1.0/Forms/queue/ns
rt/share/html/REST/1.0/Forms/queue/ticketcustomfields
rt/share/html/REST/1.0/Forms/ticket/attachments
rt/share/html/REST/1.0/Forms/ticket/comment
rt/share/html/REST/1.0/Forms/ticket/default
rt/share/html/REST/1.0/Forms/ticket/history
rt/share/html/REST/1.0/Forms/ticket/links
rt/share/html/REST/1.0/Forms/ticket/merge
rt/share/html/REST/1.0/Forms/ticket/take
rt/share/html/REST/1.0/Forms/transaction/default
rt/share/html/REST/1.0/Forms/user/default
rt/share/html/REST/1.0/Forms/user/ns
rt/share/html/REST/1.0/NoAuth/mail-gateway
rt/share/html/REST/1.0/autohandler
rt/share/html/REST/1.0/dhandler
rt/share/html/REST/1.0/logout
rt/share/html/REST/1.0/search/dhandler
rt/share/html/REST/1.0/search/ticket
rt/share/html/REST/1.0/ticket/comment
rt/share/html/REST/1.0/ticket/link
rt/share/html/REST/1.0/ticket/merge
rt/share/html/Search/Article.html
rt/share/html/Search/Build.html
rt/share/html/Search/Bulk.html
rt/share/html/Search/Chart
rt/share/html/Search/Chart.html
rt/share/html/Search/Edit.html
rt/share/html/Search/Elements/Article
rt/share/html/Search/Elements/BuildFormatString
rt/share/html/Search/Elements/Chart
rt/share/html/Search/Elements/ConditionRow
rt/share/html/Search/Elements/DisplayOptions
rt/share/html/Search/Elements/EditFormat
rt/share/html/Search/Elements/EditQuery
rt/share/html/Search/Elements/EditSearches
rt/share/html/Search/Elements/EditSort
rt/share/html/Search/Elements/Graph
rt/share/html/Search/Elements/NewListActions
rt/share/html/Search/Elements/PickBasics
rt/share/html/Search/Elements/PickCFs
rt/share/html/Search/Elements/PickCriteria
rt/share/html/Search/Elements/ResultsRSSView
rt/share/html/Search/Elements/SearchPrivacy
rt/share/html/Search/Elements/SearchesForObject
rt/share/html/Search/Elements/SelectAndOr
rt/share/html/Search/Elements/SelectChartType
rt/share/html/Search/Elements/SelectGroup
rt/share/html/Search/Elements/SelectGroupBy
rt/share/html/Search/Elements/SelectLinks
rt/share/html/Search/Elements/SelectPersonType
rt/share/html/Search/Elements/SelectSearchObject
rt/share/html/Search/Elements/SelectSearchesForObjects
rt/share/html/Search/Graph.html
rt/share/html/Search/Results.html
rt/share/html/Search/Results.rdf
rt/share/html/Search/Results.tsv
rt/share/html/Search/Simple.html
rt/share/html/SelfService/Article/Display.html
rt/share/html/SelfService/Article/Search.html
rt/share/html/SelfService/Article/autohandler
rt/share/html/SelfService/Attachment/dhandler
rt/share/html/SelfService/Closed.html
rt/share/html/SelfService/Create.html
rt/share/html/SelfService/CreateTicketInQueue.html
rt/share/html/SelfService/Display.html
rt/share/html/SelfService/Elements/GotoTicket
rt/share/html/SelfService/Elements/Header
rt/share/html/SelfService/Elements/MyRequests
rt/share/html/SelfService/Elements/SearchArticle
rt/share/html/SelfService/Error.html
rt/share/html/SelfService/Prefs.html
rt/share/html/SelfService/Update.html
rt/share/html/SelfService/index.html
rt/share/html/Ticket/Attachment/WithHeaders/dhandler
rt/share/html/Ticket/Attachment/dhandler
rt/share/html/Ticket/Create.html
rt/share/html/Ticket/Display.html
rt/share/html/Ticket/Elements/AddAttachments
rt/share/html/Ticket/Elements/AddWatchers
rt/share/html/Ticket/Elements/Bookmark
rt/share/html/Ticket/Elements/BulkLinks
rt/share/html/Ticket/Elements/ClickToShowHistory
rt/share/html/Ticket/Elements/EditBasics
rt/share/html/Ticket/Elements/EditCustomFields
rt/share/html/Ticket/Elements/EditDates
rt/share/html/Ticket/Elements/EditPeople
rt/share/html/Ticket/Elements/EditTransactionCustomFields
rt/share/html/Ticket/Elements/EditWatchers
rt/share/html/Ticket/Elements/FindAttachments
rt/share/html/Ticket/Elements/FindTransactions
rt/share/html/Ticket/Elements/FoldStanzaJS
rt/share/html/Ticket/Elements/LoadTextAttachments
rt/share/html/Ticket/Elements/PreviewScrips
rt/share/html/Ticket/Elements/Reminders
rt/share/html/Ticket/Elements/ShowAttachments
rt/share/html/Ticket/Elements/ShowBasics
rt/share/html/Ticket/Elements/ShowCustomFields
rt/share/html/Ticket/Elements/ShowDates
rt/share/html/Ticket/Elements/ShowDependencies
rt/share/html/Ticket/Elements/ShowGnuPGStatus
rt/share/html/Ticket/Elements/ShowGroupMembers
rt/share/html/Ticket/Elements/ShowHistory
rt/share/html/Ticket/Elements/ShowMembers
rt/share/html/Ticket/Elements/ShowMessageHeaders
rt/share/html/Ticket/Elements/ShowMessageStanza
rt/share/html/Ticket/Elements/ShowParents
rt/share/html/Ticket/Elements/ShowPeople
rt/share/html/Ticket/Elements/ShowPriority
rt/share/html/Ticket/Elements/ShowQueue
rt/share/html/Ticket/Elements/ShowRequestor
rt/share/html/Ticket/Elements/ShowRequestorExtraInfo
rt/share/html/Ticket/Elements/ShowRequestorTickets
rt/share/html/Ticket/Elements/ShowRequestorTicketsActive
rt/share/html/Ticket/Elements/ShowRequestorTicketsAll
rt/share/html/Ticket/Elements/ShowRequestorTicketsInactive
rt/share/html/Ticket/Elements/ShowSimplifiedRecipients
rt/share/html/Ticket/Elements/ShowSummary
rt/share/html/Ticket/Elements/ShowTime
rt/share/html/Ticket/Elements/ShowTransaction
rt/share/html/Ticket/Elements/ShowTransactionAttachments
rt/share/html/Ticket/Elements/ShowUpdateStatus
rt/share/html/Ticket/Elements/ShowUserEntry
rt/share/html/Ticket/Elements/UpdateCc
rt/share/html/Ticket/Forward.html
rt/share/html/Ticket/GnuPG.html
rt/share/html/Ticket/Graphs/Elements/EditGraphProperties
rt/share/html/Ticket/Graphs/Elements/ShowGraph
rt/share/html/Ticket/Graphs/Elements/ShowLegends
rt/share/html/Ticket/Graphs/dhandler
rt/share/html/Ticket/Graphs/index.html
rt/share/html/Ticket/History.html
rt/share/html/Ticket/Modify.html
rt/share/html/Ticket/ModifyAll.html
rt/share/html/Ticket/ModifyDates.html
rt/share/html/Ticket/ModifyLinks.html
rt/share/html/Ticket/ModifyPeople.html
rt/share/html/Ticket/Reminders.html
rt/share/html/Ticket/ShowEmailRecord.html
rt/share/html/Ticket/Update.html
rt/share/html/Ticket/autohandler
rt/share/html/Tools/MyDay.html
rt/share/html/Tools/MyReminders.html
rt/share/html/Tools/Offline.html
rt/share/html/Tools/index.html
rt/share/html/User/Prefs.html
rt/share/html/Widgets/BulkEdit
rt/share/html/Widgets/BulkProcess
rt/share/html/Widgets/ComboBox
rt/share/html/Widgets/FinalizeWidgetArguments
rt/share/html/Widgets/Form/Boolean
rt/share/html/Widgets/Form/Integer
rt/share/html/Widgets/Form/Select
rt/share/html/Widgets/Form/String
rt/share/html/Widgets/SavedSearch
rt/share/html/Widgets/SelectionBox
rt/share/html/Widgets/TitleBox
rt/share/html/Widgets/TitleBoxEnd
rt/share/html/Widgets/TitleBoxStart
rt/share/html/autohandler
rt/share/html/dhandler
rt/share/html/index.html
rt/share/html/l
rt/share/html/l_unsafe
rt/share/html/m/_elements/footer
rt/share/html/m/_elements/full_site_link
rt/share/html/m/_elements/header
rt/share/html/m/_elements/login
rt/share/html/m/_elements/menu
rt/share/html/m/_elements/raw_style
rt/share/html/m/_elements/ticket_list
rt/share/html/m/_elements/ticket_menu
rt/share/html/m/_elements/wrapper
rt/share/html/m/dhandler
rt/share/html/m/index.html
rt/share/html/m/logout
rt/share/html/m/style.css
rt/share/html/m/ticket/autohandler
rt/share/html/m/ticket/create
rt/share/html/m/ticket/history
rt/share/html/m/ticket/reply
rt/share/html/m/ticket/select_create_queue
rt/share/html/m/ticket/show
rt/share/html/m/tickets/search
rt/t/api/action-createtickets.t
rt/t/api/date.t
rt/t/api/group.t
rt/t/api/rights.t
rt/t/api/rights_show_ticket.t
rt/t/api/searchbuilder.t
rt/t/fts/indexed_mysql.t
rt/t/fts/indexed_oracle.t
rt/t/mail/dashboards.t
rt/t/mail/digest-attributes.t
rt/t/mail/disposition-outgoing.t
rt/t/mail/gnupg-reverification.t
rt/t/mail/mime_decoding.t
rt/t/shredder/utils.pl
rt/t/ticket/search_by_links.t
rt/t/ticket/search_by_watcher.t
rt/t/web/articles-links.t
rt/t/web/case-sensitivity.t
rt/t/web/cf_date.t
rt/t/web/cf_datetime.t
rt/t/web/charting.t
rt/t/web/command_line.t
rt/t/web/crypt-gnupg.t
rt/t/web/gnupg-select-keys-on-create.t
rt/t/web/gnupg-select-keys-on-update.t
rt/t/web/installer.t
rt/t/web/reminders.t
rt/t/web/rest_cfs_with_same_name.t
rt/t/web/self_service.t
rt/t/web/user_update.t

diff --git a/FS/FS/API.pm b/FS/FS/API.pm
new file mode 100644 (file)
index 0000000..36587da
--- /dev/null
@@ -0,0 +1,374 @@
+package FS::API;
+
+use FS::Conf;
+use FS::Record qw( qsearch qsearchs );
+use FS::cust_main;
+use FS::cust_location;
+use FS::cust_pay;
+use FS::cust_credit;
+use FS::cust_refund;
+
+=head1 NAME
+
+FS::API - Freeside backend API
+
+=head1 SYNOPSIS
+
+  use FS::API;
+
+=head1 DESCRIPTION
+
+This module implements a backend API for advanced back-office integration.
+
+In contrast to the self-service API, which authenticates an end-user and offers
+functionality to that end user, the backend API performs a simple shared-secret
+authentication and offers full, administrator functionality, enabling
+integration with other back-office systems.
+
+If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
+the port by default, only allow access from back-office servers with the same
+security precations as the Freeside server, and encrypt the communication
+channel (for exampple, with an SSH tunnel or VPN) rather than accessing it
+in plaintext.
+
+=head1 METHODS
+
+=over 4
+
+=item insert_payment
+
+Example:
+
+  my $result = FS::API->insert_payment(
+    'secret'  => 'sharingiscaring',
+    'custnum' => 181318,
+    'payby'   => 'CASH',
+    'paid'    => '54.32',
+
+    #optional
+    '_date'   => 1397977200, #UNIX timestamp
+  );
+
+  if ( $result->{'error'} ) {
+    die $result->{'error'};
+  } else {
+    #payment was inserted
+    print "paynum ". $result->{'paynum'};
+  }
+
+=cut
+
+#enter cash payment
+sub insert_payment {
+  my($class, %opt) = @_;
+  my $conf = new FS::Conf;
+  return { 'error' => 'Incorrect shared secret' }
+    unless $opt{secret} eq $conf->config('api_shared_secret');
+
+  #less "raw" than this?  we are the backoffice API, and aren't worried
+  # about version migration ala cust_main/cust_location here
+  my $cust_pay = new FS::cust_pay { %opt };
+  my $error = $cust_pay->insert( 'manual'=>1 );
+  return { 'error'  => $error,
+           'paynum' => $cust_pay->paynum,
+         };
+}
+
+# pass the phone number ( from svc_phone ) 
+sub insert_payment_phonenum {
+  my($class, %opt) = @_;
+  my $conf = new FS::Conf;
+  return { 'error' => 'Incorrect shared secret' }
+    unless $opt{secret} eq $conf->config('api_shared_secret');
+
+  $class->_by_phonenum('insert_payment', %opt);
+
+}
+
+sub _by_phonenum {
+  my($class, $method, %opt) = @_;
+  my $conf = new FS::Conf;
+  return { 'error' => 'Incorrect shared secret' }
+    unless $opt{secret} eq $conf->config('api_shared_secret');
+
+  my $phonenum = delete $opt{'phonenum'};
+
+  my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
+    or return { 'error' => 'Unknown phonenum' };
+
+  my $cust_pkg = $svc_phone->cust_svc->cust_pkg
+    or return { 'error' => 'Unlinked phonenum' };
+
+  $opt{'custnum'} = $cust_pkg->custnum;
+
+  $class->$method(%opt);
+
+}
+
+=item insert_credit
+
+Example:
+
+  my $result = FS::API->insert_credit(
+    'secret'  => 'sharingiscaring',
+    'custnum' => 181318,
+    'amount'  => '54.32',
+
+    #optional
+    '_date'   => 1397977200, #UNIX timestamp
+  );
+
+  if ( $result->{'error'} ) {
+    die $result->{'error'};
+  } else {
+    #credit was inserted
+    print "crednum ". $result->{'crednum'};
+  }
+
+=cut
+
+#Enter credit
+sub insert_credit {
+  my($class, %opt) = @_;
+  my $conf = new FS::Conf;
+  return { 'error' => 'Incorrect shared secret' }
+    unless $opt{secret} eq $conf->config('api_shared_secret');
+
+  $opt{'reasonnum'} ||= $conf->config('api_credit_reason');
+
+  #less "raw" than this?  we are the backoffice API, and aren't worried
+  # about version migration ala cust_main/cust_location here
+  my $cust_credit = new FS::cust_credit { %opt };
+  my $error = $cust_credit->insert;
+  return { 'error'  => $error,
+           'crednum' => $cust_credit->crednum,
+         };
+}
+
+# pass the phone number ( from svc_phone ) 
+sub insert_credit_phonenum {
+  my($class, %opt) = @_;
+  my $conf = new FS::Conf;
+  return { 'error' => 'Incorrect shared secret' }
+    unless $opt{secret} eq $conf->config('api_shared_secret');
+
+  $class->_by_phonenum('insert_credit', %opt);
+
+}
+
+=item insert_refund
+
+Example:
+
+  my $result = FS::API->insert_refund(
+    'secret'  => 'sharingiscaring',
+    'custnum' => 181318,
+    'payby'   => 'CASH',
+    'refund'  => '54.32',
+
+    #optional
+    '_date'   => 1397977200, #UNIX timestamp
+  );
+
+  if ( $result->{'error'} ) {
+    die $result->{'error'};
+  } else {
+    #refund was inserted
+    print "refundnum ". $result->{'crednum'};
+  }
+
+=cut
+
+#Enter cash refund.
+sub insert_refund {
+  my($class, %opt) = @_;
+  my $conf = new FS::Conf;
+  return { 'error' => 'Incorrect shared secret' }
+    unless $opt{secret} eq $conf->config('api_shared_secret');
+
+  # when github pull request #24 is merged,
+  #  will have to change over to default reasonnum like credit
+  # but until then, this will do
+  $opt{'reason'} ||= 'API refund';
+
+  #less "raw" than this?  we are the backoffice API, and aren't worried
+  # about version migration ala cust_main/cust_location here
+  my $cust_refund = new FS::cust_refund { %opt };
+  my $error = $cust_refund->insert;
+  return { 'error'     => $error,
+           'refundnum' => $cust_refund->refundnum,
+         };
+}
+
+# pass the phone number ( from svc_phone ) 
+sub insert_refund_phonenum {
+  my($class, %opt) = @_;
+  my $conf = new FS::Conf;
+  return { 'error' => 'Incorrect shared secret' }
+    unless $opt{secret} eq $conf->config('api_shared_secret');
+
+  $class->_by_phonenum('insert_refund', %opt);
+
+}
+
+#---
+
+# "2 way syncing" ?  start with non-sync pulling info here, then if necessary
+# figure out how to trigger something when those things change
+
+# long-term: package changes?
+
+=item new_customer
+
+=cut
+
+#certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
+# but approaching this from a clean start / back-office perspective
+#  i.e. no package/service, no immediate credit card run, etc.
+
+sub new_customer {
+  my( $class, %opt ) = @_;
+  my $conf = new FS::Conf;
+  return { 'error' => 'Incorrect shared secret' }
+    unless $opt{secret} eq $conf->config('api_shared_secret');
+
+  #default agentnum like signup_server-default_agentnum?
+  #same for refnum like signup_server-default_refnum
+
+  my $cust_main = new FS::cust_main ( {
+      'agentnum'      => $agentnum,
+      'refnum'        => $opt{refnum}
+                         || $conf->config('signup_server-default_refnum'),
+      'payby'         => 'BILL',
+
+      map { $_ => $opt{$_} } qw(
+        agentnum refnum agent_custid referral_custnum
+        last first company 
+        daytime night fax mobile
+        payby payinfo paydate paycvv payname
+      ),
+
+  } );
+
+  my @invoicing_list = $opt{'invoicing_list'}
+                         ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
+                         : ();
+  push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
+
+  my ($bill_hash, $ship_hash);
+  foreach my $f (FS::cust_main->location_fields) {
+    # avoid having to change this in front-end code
+    $bill_hash->{$f} = $opt{"bill_$f"} || $opt{$f};
+    $ship_hash->{$f} = $opt{"ship_$f"};
+  }
+
+  my $bill_location = FS::cust_location->new($bill_hash);
+  my $ship_location;
+  # we don't have an equivalent of the "same" checkbox in selfservice^Wthis API
+  # so is there a ship address, and if so, is it different from the billing 
+  # address?
+  if ( length($ship_hash->{address1}) > 0 and
+          grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
+         ) {
+
+    $ship_location = FS::cust_location->new( $ship_hash );
+  
+  } else {
+    $ship_location = $bill_location;
+  }
+
+  $cust_main->set('bill_location' => $bill_location);
+  $cust_main->set('ship_location' => $ship_location);
+
+  $error = $cust_main->insert( {}, \@invoicing_list );
+  return { 'error'   => $error } if $error;
+  
+  return { 'error'   => '',
+           'custnum' => $cust_main->custnum,
+         };
+
+}
+
+=item customer_info
+
+=cut
+
+#some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
+
+use vars qw( @cust_main_editable_fields @location_editable_fields );
+@cust_main_editable_fields = qw(
+  first last company daytime night fax mobile
+);
+#  locale
+#  payby payinfo payname paystart_month paystart_year payissue payip
+#  ss paytype paystate stateid stateid_state
+@location_editable_fields = qw(
+  address1 address2 city county state zip country
+);
+
+sub customer_info {
+  my( $class, %opt ) = @_;
+  my $conf = new FS::Conf;
+  return { 'error' => 'Incorrect shared secret' }
+    unless $opt{secret} eq $conf->config('api_shared_secret');
+
+  my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
+    or return { 'error' => 'Unknown custnum' };
+
+  my %return = (
+    'error'           => '',
+    'display_custnum' => $cust_main->display_custnum,
+    'name'            => $cust_main->first. ' '. $cust_main->get('last'),
+    'balance'         => $cust_main->balance,
+    'status'          => $cust_main->status,
+    'statuscolor'     => $cust_main->statuscolor,
+  );
+
+  $return{$_} = $cust_main->get($_)
+    foreach @cust_main_editable_fields;
+
+  for (@location_editable_fields) {
+    $return{$_} = $cust_main->bill_location->get($_)
+      if $cust_main->bill_locationnum;
+    $return{'ship_'.$_} = $cust_main->ship_location->get($_)
+      if $cust_main->ship_locationnum;
+  }
+
+  my @invoicing_list = $cust_main->invoicing_list;
+  $return{'invoicing_list'} =
+    join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
+  $return{'postal_invoicing'} =
+    0 < ( grep { $_ eq 'POST' } @invoicing_list );
+
+  #generally, the more useful data from the cust_main record the better.
+  # well, tell me what you want
+
+  return \%return;
+
+}
+
+#I also monitor for changes to the additional locations that are applied to
+# packages, and would like for those to be exportable as well.  basically the
+# location data passed with the custnum.
+sub location_info {
+  my( $class, %opt ) = @_;
+  my $conf = new FS::Conf;
+  return { 'error' => 'Incorrect shared secret' }
+    unless $opt{secret} eq $conf->config('api_shared_secret');
+
+  my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
+
+  my %return = (
+    'error'           => '',
+    'locations'       => [ map $_->hashref, @cust_location ],
+  );
+
+  return \%return;
+}
+
+#Advertising sources?
+
+=back
+
+1;
index 862cceb..4f41219 100644 (file)
@@ -555,8 +555,8 @@ sub customer_info_short {
                       1, ##nobalance
                     );
 
-warn    $return{first}  = $cust_main->first;
-warn    $return{'last'} = $cust_main->get('last');
+    $return{first}  = $cust_main->first;
+    $return{'last'} = $cust_main->get('last');
     $return{name}   = $cust_main->first. ' '. $cust_main->get('last');
 
     $return{payby} = $cust_main->payby;
index 29ec239..5407a8f 100644 (file)
@@ -532,7 +532,7 @@ sub new_customer {
                          || $conf->config('signup_server-default_refnum'),
 
       ( map { $_ => $template_cust->$_ } qw( 
-              last first company daytime night fax 
+              last first company daytime night fax mobile
             )
       ),
 
@@ -563,7 +563,8 @@ sub new_customer {
 
       map { $_ => $packet->{$_} } qw(
         last first ss company 
-        daytime night fax stateid stateid_state
+        daytime night fax mobile
+        stateid stateid_state
         payby
         payinfo paycvv paydate payname paystate paytype
         paystart_month paystart_year payissue
@@ -930,7 +931,7 @@ sub new_customer_minimal {
 
       map { $_ => $packet->{$_} } qw(
         last first ss company 
-        daytime night fax
+        daytime night fax mobile
       ),
 
   } );
index 6ebdcec..62f61d6 100644 (file)
@@ -30,7 +30,7 @@ L<FS::SelfService::XMLRPC>, L<FS::SelfService>
 use strict;
 
 use vars qw($DEBUG $AUTOLOAD);
-use XMLRPC::Lite; # for XMLRPC::Data
+use FS::XMLRPC_Lite; #XMLRPC::Lite, for XMLRPC::Data
 use FS::ClientAPI;
 
 $DEBUG = 0;
@@ -188,17 +188,4 @@ sub ss2clientapi {
   };
 }
 
-
-#XXX submit patch to SOAP::Lite
-
-use XMLRPC::Transport::HTTP;
-
-package XMLRPC::Transport::HTTP::Server;
-
-@XMLRPC::Transport::HTTP::Server::ISA = qw(SOAP::Transport::HTTP::Server);
-
-sub initialize; *initialize = \&XMLRPC::Server::initialize;
-sub make_fault; *make_fault = \&XMLRPC::Transport::HTTP::CGI::make_fault;
-sub make_response; *make_response = \&XMLRPC::Transport::HTTP::CGI::make_response;
-
 1;
index 48b39c5..34254c6 100644 (file)
@@ -2920,6 +2920,7 @@ and customer address. Include units.',
     'section'     => 'self-service',
     'description' => 'Suspend reason when customers suspend their own packages. Set to nothing to disallow self-suspension.',
     'type'        => 'select-sub',
+    #false laziness w/api_credit_reason
     'options_sub' => sub { require FS::Record;
                            require FS::reason;
                            my $type = qsearchs('reason_type', 
@@ -5606,6 +5607,52 @@ and customer address. Include units.',
     'type'        => 'text',
   },
 
+  {
+    'key'         => 'api_shared_secret',
+    'section'     => 'API',
+    'description' => 'Shared secret for back-office API authentication',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'xmlrpc_api',
+    'section'     => 'API',
+    'description' => 'Enable the back-office API XML-RPC server (on port 8008).',
+    'type'        => 'checkbox',
+  },
+
+#  {
+#    'key'         => 'jsonrpc_api',
+#    'section'     => 'API',
+#    'description' => 'Enable the back-office API JSON-RPC server (on port 8081).',
+#    'type'        => 'checkbox',
+#  },
+
+  {
+    'key'         => 'api_credit_reason',
+    'section'     => 'API',
+    'description' => 'Default reason for back-office API credits',
+    'type'        => 'select-sub',
+    #false laziness w/api_credit_reason
+    'options_sub' => sub { require FS::Record;
+                           require FS::reason;
+                           my $type = qsearchs('reason_type', 
+                             { class => 'R' }) 
+                              or return ();
+                          map { $_->reasonnum => $_->reason }
+                               FS::Record::qsearch('reason', 
+                                 { reason_type => $type->typenum } 
+                               );
+                        },
+    'option_sub'  => sub { require FS::Record;
+                           require FS::reason;
+                          my $reason = FS::Record::qsearchs(
+                            'reason', { 'reasonnum' => shift }
+                          );
+                           $reason ? $reason->reason : '';
+                        },
+  },
+
   { 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/Daemon/Preforking.pm b/FS/FS/Daemon/Preforking.pm
new file mode 100644 (file)
index 0000000..98b4fa6
--- /dev/null
@@ -0,0 +1,358 @@
+package FS::Daemon::Preforking;
+use base 'Exporter';
+
+=head1 NAME
+
+FS::Daemon::Preforking - A preforking web server
+
+=head1 SYNOPSIS
+
+  use FS::Daemon::Preforking qw( freeside_init1 freeside_init2 daemon_run );
+
+  my $me = 'mydaemon'; #keep unique among fs daemons, for logfiles etc.
+
+  freeside_init1($me); #daemonize, drop root and connect to freeside
+
+  #do setup tasks which should throw an error to the shell starting the daemon
+
+  freeside_init2($me); #move logging to logfile and disassociate from terminal
+
+  #do setup tasks which will warn/error to the log file, such as declining to
+  # run if our config is not in place
+
+  daemon_run(
+    'port'           => 5454, #keep unique among fs daemons
+    'handle_request' => \&handle_request,
+  );
+
+  sub handle_request {
+    my $request = shift; #HTTP::Request object
+
+    #... do your thing
+
+    return $response; #HTTP::Response object
+
+  }
+
+=head1 AUTHOR
+
+Based on L<http://www.perlmonks.org/?node_id=582781> by Justin Hawkins
+
+and L<http://poe.perl.org/?POE_Cookbook/Web_Server_With_Forking>
+
+=cut
+
+use warnings;
+use strict;
+
+use constant DEBUG         => 0;       # Enable much runtime information.
+use constant MAX_PROCESSES => 10;      # Total server process count.
+#use constant TESTING_CHURN => 0;       # Randomly test process respawning.
+
+use vars qw( @EXPORT_OK $FREESIDE_LOG $SERVER_PORT $user $handle_request );
+@EXPORT_OK = qw( freeside_init1 freeside_init2 daemon_run );
+$FREESIDE_LOG = '%%%FREESIDE_LOG%%%';
+
+use POE 1.2;                     # Base features.
+use POE::Filter::HTTPD;          # For serving HTTP content.
+use POE::Wheel::ReadWrite;       # For socket I/O.
+use POE::Wheel::SocketFactory;   # For serving socket connections.
+
+use FS::Daemon qw( daemonize1 drop_root logfile daemonize2 );
+use FS::UID qw( adminsuidsetup forksuidsetup dbh );
+
+#use FS::TicketSystem;
+
+sub freeside_init1 {
+  my $name = shift;
+
+  $user = shift @ARGV or die &usage($name);
+
+  $FS::Daemon::NOSIG = 1;
+  $FS::Daemon::PID_NEWSTYLE = 1;
+  daemonize1($name);
+
+  POE::Kernel->has_forked(); #daemonize forks...
+
+  drop_root();
+
+  adminsuidsetup($user);
+}
+
+sub freeside_init2 {
+  my $name = shift;
+
+  logfile("$FREESIDE_LOG/$name.log");
+
+  daemonize2();
+
+}
+
+sub daemon_run {
+  my %opt = @_;
+  $SERVER_PORT = $opt{port};
+  $handle_request = $opt{handle_request};
+
+  #parent doesn't need to hold a DB connection open
+  dbh->disconnect;
+  undef $FS::UID::dbh;
+
+  server_spawn(MAX_PROCESSES);
+  POE::Kernel->run();
+  #exit;
+
+}
+
+### Spawn the main server.  This will run as the parent process.
+
+sub server_spawn {
+    my ($max_processes) = @_;
+
+    POE::Session->create(
+      inline_states => {
+        _start         => \&server_start,
+        _stop          => \&server_stop,
+        do_fork        => \&server_do_fork,
+        got_error      => \&server_got_error,
+        got_sig_int    => \&server_got_sig_int,
+        got_sig_child  => \&server_got_sig_child,
+        got_connection => \&server_got_connection,
+        _child         => sub { undef },
+      },
+      heap => { max_processes => MAX_PROCESSES },
+    );
+}
+
+### The main server session has started.  Set up the server socket and
+### bookkeeping information, then fork the initial child processes.
+
+sub server_start {
+    my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+
+    $heap->{server} = POE::Wheel::SocketFactory->new
+      ( BindPort     => $SERVER_PORT,
+        SuccessEvent => "got_connection",
+        FailureEvent => "got_error",
+        Reuse        => "yes",
+      );
+
+    $kernel->sig( INT  => "got_sig_int" );
+    $kernel->sig( TERM => "got_sig_int" ); #huh
+
+    $heap->{children}   = {};
+    $heap->{is_a_child} = 0;
+
+    warn "Server $$ has begun listening on port $SERVER_PORT\n";
+
+    $kernel->yield("do_fork");
+}
+
+### The server session has shut down.  If this process has any
+### children, signal them to shutdown too.
+
+sub server_stop {
+    my $heap = $_[HEAP];
+    DEBUG and warn "Server $$ stopped.\n";
+
+    if ( my @children = keys %{ $heap->{children} } ) {
+        DEBUG and warn "Server $$ is signaling children to stop.\n";
+        kill INT => @children;
+    }
+}
+
+### The server session has encountered an error.  Shut it down.
+
+sub server_got_error {
+    my ( $heap, $syscall, $errno, $error ) = @_[ HEAP, ARG0 .. ARG2 ];
+      warn( "Server $$ got $syscall error $errno: $error\n",
+        "Server $$ is shutting down.\n",
+      );
+    delete $heap->{server};
+}
+
+### The server has a need to fork off more children.  Only honor that
+### request form the parent, otherwise we would surely "forkbomb".
+### Fork off as many child processes as we need.
+
+sub server_do_fork {
+    my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+
+    return if $heap->{is_a_child};
+
+    #my $current_children = keys %{ $heap->{children} };
+    #for ( $current_children + 2 .. $heap->{max_processes} ) {
+    while (scalar(keys %{$heap->{children}}) < $heap->{max_processes}) {
+
+        DEBUG and warn "Server $$ is attempting to fork.\n";
+
+        my $pid = fork();
+
+        unless ( defined($pid) ) {
+            DEBUG and
+              warn( "Server $$ fork failed: $!\n",
+                "Server $$ will retry fork shortly.\n",
+              );
+            $kernel->delay( do_fork => 1 );
+            return;
+        }
+
+        # Parent.  Add the child process to its list.
+        if ($pid) {
+            $heap->{children}->{$pid} = 1;
+            $kernel->sig_child($pid, "got_sig_child");
+            next;
+        }
+
+        # Child.  Clear the child process list.
+        $kernel->has_forked();
+        DEBUG and warn "Server $$ forked successfully.\n";
+        $heap->{is_a_child} = 1;
+        $heap->{children}   = {};
+
+        #freeside db connection, etc.
+        forksuidsetup($user);
+
+        #why isn't this needed ala freeside-selfservice-server??
+        #FS::TicketSystem->init();
+
+        return;
+    }
+}
+
+### The server session received SIGINT.  Don't handle the signal,
+### which in turn will trigger the process to exit gracefully.
+
+sub server_got_sig_int {
+    my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+    DEBUG and warn "Server $$ received SIGINT/TERM.\n";
+
+    if ( my @children = keys %{ $heap->{children} } ) {
+        DEBUG and warn "Server $$ is signaling children to stop.\n";
+        kill INT => @children;
+    }
+
+    delete $heap->{server};
+    $kernel->sig_handled();
+}
+
+### The server session received a SIGCHLD, indicating that some child
+### server has gone away.  Remove the child's process ID from our
+### list, and trigger more fork() calls to spawn new children.
+
+sub server_got_sig_child {
+    my ( $kernel, $heap, $child_pid ) = @_[ KERNEL, HEAP, ARG1 ];
+
+    return unless delete $heap->{children}->{$child_pid};
+
+   DEBUG and warn "Server $$ reaped child $child_pid.\n";
+   $kernel->yield("do_fork") if exists $_[HEAP]->{server};
+}
+
+### The server session received a connection request.  Spawn off a
+### client handler session to parse the request and respond to it.
+
+sub server_got_connection {
+    my ( $heap, $socket, $peer_addr, $peer_port ) = @_[ HEAP, ARG0, ARG1, ARG2 ];
+
+    DEBUG and warn "Server $$ received a connection.\n";
+
+    POE::Session->create(
+      inline_states => {
+        _start      => \&client_start,
+        _stop       => \&client_stop,
+        got_request => \&client_got_request,
+        got_flush   => \&client_flushed_request,
+        got_error   => \&client_got_error,
+        _parent     => sub { 0 },
+      },
+      heap => {
+        socket    => $socket,
+        peer_addr => $peer_addr,
+        peer_port => $peer_port,
+      },
+    );
+
+#    # Gracefully exit if testing process churn.
+#    delete $heap->{server}
+#      if TESTING_CHURN and $heap->{is_a_child} and ( rand() < 0.1 );
+}
+
+### The client handler has started.  Wrap its socket in a ReadWrite
+### wheel to begin interacting with it.
+
+sub client_start {
+    my $heap = $_[HEAP];
+
+    $heap->{client} = POE::Wheel::ReadWrite->new
+      ( Handle => $heap->{socket},
+        Filter       => POE::Filter::HTTPD->new(),
+        InputEvent   => "got_request",
+        ErrorEvent   => "got_error",
+        FlushedEvent => "got_flush",
+      );
+
+    DEBUG and warn "Client handler $$/", $_[SESSION]->ID, " started.\n";
+}
+
+### The client handler has stopped.  Log that fact.
+
+sub client_stop {
+    DEBUG and warn "Client handler $$/", $_[SESSION]->ID, " stopped.\n";
+}
+
+### The client handler has received a request.  If it's an
+### HTTP::Response object, it means some error has occurred while
+### parsing the request.  Send that back and return immediately.
+### Otherwise parse and process the request, generating and sending an
+### HTTP::Response object in response.
+
+sub client_got_request {
+    my ( $heap, $request ) = @_[ HEAP, ARG0 ];
+
+    DEBUG and
+      warn "Client handler $$/", $_[SESSION]->ID, " is handling a request.\n";
+
+    if ( $request->isa("HTTP::Response") ) {
+        $heap->{client}->put($request);
+        return;
+   }
+
+    forksuidsetup($user) unless dbh && dbh->ping;
+
+    my $response = &{ $handle_request }( $request );
+
+    $heap->{client}->put($response);
+}
+
+### The client handler received an error.  Stop the ReadWrite wheel,
+### which also closes the socket.
+
+sub client_got_error {
+    my ( $heap, $operation, $errnum, $errstr ) = @_[ HEAP, ARG0, ARG1, ARG2 ];
+    DEBUG and
+      warn( "Client handler $$/", $_[SESSION]->ID,
+        " got $operation error $errnum: $errstr\n",
+        "Client handler $$/", $_[SESSION]->ID, " is shutting down.\n"
+      );
+    delete $heap->{client};
+}
+
+### The client handler has flushed its response to the socket.  We're
+### done with the client connection, so stop the ReadWrite wheel.
+
+sub client_flushed_request {
+    my $heap = $_[HEAP];
+    DEBUG and
+      warn( "Client handler $$/", $_[SESSION]->ID,
+        " flushed its response.\n",
+        "Client handler $$/", $_[SESSION]->ID, " is shutting down.\n"
+      );
+    delete $heap->{client};
+}
+
+sub usage {
+  my $name = shift;
+  die "Usage:\n\n  freeside-$name user\n";
+}
+
+1;
index 7bf5446..caa2e60 100644 (file)
@@ -78,8 +78,6 @@ if ( -e $addl_handler_use_file ) {
   use HTML::FormatText;
   use HTML::Defang;
   use JSON::XS;
-#  use XMLRPC::Transport::HTTP;
-#  use XMLRPC::Lite; # for XMLRPC::Serializer
   use MIME::Base64;
   use IO::Handle;
   use IO::File;
@@ -215,7 +213,6 @@ if ( -e $addl_handler_use_file ) {
   use FS::usage_class;
   use FS::payment_gateway;
   use FS::agent_payment_gateway;
-  use FS::XMLRPC;
   use FS::payby;
   use FS::cdr;
   use FS::cdr_batch;
@@ -377,6 +374,7 @@ if ( -e $addl_handler_use_file ) {
   use FS::part_fee;
   use FS::cust_bill_pkg_fee;
   use FS::part_fee_msgcat;
+  use FS::part_fee_usage;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index 9c18961..c598507 100644 (file)
@@ -267,7 +267,7 @@ sub send_email {
   }
 
   # Logging
-  if ( $conf->exists('log_sent_mail') and $options{'custnum'} ) {
+  if ( $conf->exists('log_sent_mail') ) {
     my $cust_msg = FS::cust_msg->new({
         'env_from'  => $options{'from'},
         'env_to'    => join(', ', @to),
@@ -278,6 +278,7 @@ sub send_email {
         'custnum'   => $options{'custnum'},
         'msgnum'    => $options{'msgnum'},
         'status'    => ($error ? 'failed' : 'sent'),
+        'msgtype'   => $options{'msgtype'},
     });
     $cust_msg->insert; # ignore errors
   }
@@ -337,7 +338,7 @@ sub generate_email {
 
   my $me = '[FS::Misc::generate_email]';
 
-  my @fields = qw(from to bcc subject custnum msgnum);
+  my @fields = qw(from to bcc subject custnum msgnum msgtype);
   my %return;
   @return{@fields} = @args{@fields};
 
index 7f59384..17b12ae 100644 (file)
@@ -141,7 +141,7 @@ sub payments {
 sub credits {
   my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
   $self->scalar_sql("
-    SELECT SUM(amount)
+    SELECT SUM(cust_credit.amount)
       FROM cust_credit
         LEFT JOIN cust_main USING ( custnum )
       WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum).
@@ -390,9 +390,6 @@ unspecified, defaults to all three.
 'use_override': for line items generated by an add-on package, use the class
 of the add-on rather than the base package.
 
-'freq': limit to packages with this frequency.  Currently uses the part_pkg 
-frequency, so term discounted packages may give odd results.
-
 'distribute': for non-monthly recurring charges, ignore the invoice 
 date.  Instead, consider the line item's starting/ending dates.  Determine 
 the fraction of the line item duration that falls within the specified 
@@ -421,7 +418,8 @@ my $cust_bill_pkg_join = '
     LEFT JOIN cust_main USING ( custnum )
     LEFT JOIN cust_pkg USING ( pkgnum )
     LEFT JOIN part_pkg USING ( pkgpart )
-    LEFT JOIN part_pkg AS override ON pkgpart_override = override.pkgpart';
+    LEFT JOIN part_pkg AS override ON pkgpart_override = override.pkgpart
+    LEFT JOIN part_fee USING ( feepart )';
 
 sub cust_bill_pkg_setup {
   my $self = shift;
@@ -434,7 +432,7 @@ sub cust_bill_pkg_setup {
   $agentnum ||= $opt{'agentnum'};
 
   my @where = (
-    'pkgnum != 0',
+    '(pkgnum != 0 OR feepart IS NOT NULL)',
     $self->with_classnum($opt{'classnum'}, $opt{'use_override'}),
     $self->with_report_option(%opt),
     $self->in_time_period_and_agent($speriod, $eperiod, $agentnum),
@@ -461,7 +459,7 @@ sub cust_bill_pkg_recur {
   my $cust_bill_pkg = $opt{'project'} ? 'v_cust_bill_pkg' : 'cust_bill_pkg';
 
   my @where = (
-    'pkgnum != 0',
+    '(pkgnum != 0 OR feepart IS NOT NULL)',
     $self->with_classnum($opt{'classnum'}, $opt{'use_override'}),
     $self->with_report_option(%opt),
   );
@@ -476,13 +474,14 @@ sub cust_bill_pkg_recur {
     $item_usage = 'usage'; #already calculated
   }
   else {
-    $item_usage = '( SELECT COALESCE(SUM(amount),0)
+    $item_usage = '( SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
       FROM cust_bill_pkg_detail
       WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum )';
   }
   my $recur_fraction = '';
 
   if ( $opt{'distribute'} ) {
+    $where[0] = 'pkgnum != 0'; # specifically exclude fees
     push @where, "cust_main.agentnum = $agentnum" if $agentnum;
     push @where,
       "$cust_bill_pkg.sdate <  $eperiod",
@@ -521,7 +520,8 @@ Arguments as for C<cust_bill_pkg>, plus:
 sub cust_bill_pkg_detail {
   my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
 
-  my @where = ( "cust_bill_pkg.pkgnum != 0" );
+  my @where = 
+    ( "(cust_bill_pkg.pkgnum != 0 OR cust_bill_pkg.feepart IS NOT NULL)" );
 
   push @where, 'cust_main.refnum = '. $opt{'refnum'} if $opt{'refnum'};
 
@@ -536,7 +536,9 @@ sub cust_bill_pkg_detail {
     ;
 
   if ( $opt{'distribute'} ) {
-    # then limit according to the usage time, not the billing date
+    # exclude fees
+    $where[0] = 'cust_bill_pkg.pkgnum != 0';
+    # and limit according to the usage time, not the billing date
     push @where, $self->in_time_period_and_agent($speriod, $eperiod, $agentnum,
       'cust_bill_pkg_detail.startdate'
     );
@@ -547,7 +549,7 @@ sub cust_bill_pkg_detail {
     );
   }
 
-  my $total_sql = " SELECT SUM(amount) ";
+  my $total_sql = " SELECT SUM(cust_bill_pkg_detail.amount) ";
 
   $total_sql .=
     " / CASE COUNT(cust_pkg.*) WHEN 0 THEN 1 ELSE COUNT(cust_pkg.*) END "
@@ -561,6 +563,7 @@ sub cust_bill_pkg_detail {
         LEFT JOIN cust_pkg ON cust_bill_pkg.pkgnum = cust_pkg.pkgnum
         LEFT JOIN part_pkg USING ( pkgpart )
         LEFT JOIN part_pkg AS override ON pkgpart_override = override.pkgpart
+        LEFT JOIN part_fee USING ( feepart )
       WHERE ".join( ' AND ', grep $_, @where );
 
   $self->scalar_sql($total_sql);
@@ -683,14 +686,14 @@ sub with_classnum {
   @$classnum = grep /^\d+$/, @$classnum;
   my $in = 'IN ('. join(',', @$classnum). ')';
 
-  if ( $use_override ) {
-    "(
+  my $expr = "
          ( COALESCE(part_pkg.classnum, 0) $in AND pkgpart_override IS NULL)
-      OR ( COALESCE(override.classnum, 0) $in AND pkgpart_override IS NOT NULL )
-    )";
-  } else {
-    "COALESCE(part_pkg.classnum, 0) $in";
+      OR ( COALESCE(part_fee.classnum, 0) $in AND feepart IS NOT NULL )";
+  if ( $use_override ) {
+    $expr .= "
+      OR ( COALESCE(override.classnum, 0) $in AND pkgpart_override IS NOT NULL )";
   }
+  "( $expr )";
 }
 
 sub with_usageclass {
@@ -834,7 +837,8 @@ sub init_projection {
       # sdate/edate overlapping the ROI, for performance
       "INSERT INTO v_cust_bill_pkg ( 
         SELECT cust_bill_pkg.*,
-          (SELECT COALESCE(SUM(amount),0) FROM cust_bill_pkg_detail 
+          (SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
+          FROM cust_bill_pkg_detail 
           WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum),
           cust_bill._date,
           cust_pkg.expire
index ec4a1b3..bf756d1 100644 (file)
@@ -943,6 +943,7 @@ sub tables_hashref {
         'eventnum',       'int', '', '', '', '',
         'billpkgnum',     'int', 'NULL', '', '', '',
         'feepart',        'int', '', '', '', '',
+        'nextbill',      'char', 'NULL',  1, '', '',
       ],
       'primary_key'  => 'eventfeenum', # I'd rather just use eventnum
       'unique' => [ [ 'billpkgnum' ], [ 'eventnum' ] ], # one-to-one link
@@ -2234,6 +2235,7 @@ sub tables_hashref {
         'gatewaynum',   'int',     'NULL',  '', '', '',
         #'cust_balance', @money_type,            '', '',
         'paynum',       'int',     'NULL',  '', '', '',
+        'void_paynum',  'int',     'NULL',  '', '', '',
         'jobnum',    'bigint',     'NULL',  '', '', '', 
         'invnum',       'int',     'NULL',  '', '', '',
         'manual',       'char',    'NULL',   1, '', '',
@@ -2256,6 +2258,10 @@ sub tables_hashref {
                           { columns    => [ 'paynum' ],
                             table      => 'cust_pay',
                           },
+                          { columns    => [ 'void_paynum' ],
+                            table      => 'cust_pay_void',
+                            references => [ 'paynum' ],
+                          },
                           { columns    => [ 'jobnum' ],
                             table      => 'queue',
                           },
@@ -3188,6 +3194,26 @@ sub tables_hashref {
                         ],
     },
 
+    'part_fee_usage' => {
+      'columns' => [
+        'feepartusagenum','serial',     '',        '', '', '',
+        'feepart',           'int',     '',        '', '', '',
+        'classnum',          'int',     '',        '', '', '',
+        'amount',   @money_type,                '', '',
+        'percent',     'decimal',    '', '7,4', '', '',
+      ],
+      'primary_key'  => 'feepartusagenum',
+      'unique'       => [ [ 'feepart', 'classnum' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'feepart' ],
+                            table      => 'part_fee',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'usage_class',
+                          },
+                        ],
+    },
 
     'part_pkg_link' => {
       'columns' => [
@@ -4427,9 +4453,9 @@ sub tables_hashref {
       'unique'       => [ [ 'blocknum', 'routernum' ] ],
       'index'        => [],
       'foreign_keys' => [
-                          { columns    => [ 'routernum' ],
-                            table      => 'router',
-                          },
+                          #{ columns    => [ 'routernum' ],
+                            table      => 'router',
+                          #},
                           { columns    => [ 'agentnum' ],
                             table      => 'agent',
                           },
@@ -5991,7 +6017,7 @@ sub tables_hashref {
     'cust_msg' => {
       'columns' => [
         'custmsgnum', 'serial',     '',     '', '', '',
-        'custnum',       'int',     '',     '', '', '',
+        'custnum',       'int', 'NULL',     '', '', '',
         'msgnum',        'int', 'NULL',     '', '', '',
         '_date',    @date_type,                 '', '',
         'env_from',  'varchar', 'NULL',    255, '', '',
@@ -6000,6 +6026,7 @@ sub tables_hashref {
         'body',         'blob', 'NULL',     '', '', '',
         'error',     'varchar', 'NULL',    255, '', '',
         'status',    'varchar',     '',$char_d, '', '',
+        'msgtype',   'varchar', 'NULL',     16, '', '',
       ],
       'primary_key'  => 'custmsgnum',
       'unique'       => [ ],
index bf857a9..fa20c24 100644 (file)
@@ -61,14 +61,19 @@ sub desc {
   my( $self, $locale ) = @_;
 
   if ( $self->pkgnum > 0 ) {
-    $self->itemdesc || $self->part_pkg->pkg_locale($locale);
+    return $self->itemdesc if $self->itemdesc;
+    my $part_pkg = $self->part_pkg or return 'UNKNOWN';
+    return $part_pkg->pkg_locale($locale);
+
   } elsif ( $self->feepart ) {
-    $self->part_fee->itemdesc_locale($locale);
+    return $self->part_fee->itemdesc_locale($locale);
+
   } else { # by the process of elimination it must be a tax
     my $desc = $self->itemdesc || 'Tax';
     $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
-    $desc;
+    return $desc;
   }
+
 }
 
 =item time_period_pretty PART_PKG, AGENTNUM
index c4c2d7f..131a236 100644 (file)
@@ -2452,6 +2452,8 @@ sub _items_cust_bill_pkg {
 
         warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
           if $DEBUG > 1;
+        # quotation_pkgs are never fees, so don't worry about the case where
+        # part_pkg is undefined
 
         if ( $cust_bill_pkg->setup != 0 ) {
           my $description = $desc;
@@ -2471,7 +2473,7 @@ sub _items_cust_bill_pkg {
           };
         }
 
-      } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
+      } elsif ( $cust_bill_pkg->pkgnum > 0 ) { # and it's not a quotation_pkg
 
         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
           if $DEBUG > 1;
@@ -2739,29 +2741,21 @@ sub _items_cust_bill_pkg {
 
         } # recurring or usage with recurring charge
 
-      } else { #pkgnum tax or one-shot line item (??)
+      } else { # taxes and fees
 
         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
           if $DEBUG > 1;
 
-        if ( $cust_bill_pkg->setup != 0 ) {
-          push @b, {
-            'description' => $desc,
-            'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
-          };
-        }
-        if ( $cust_bill_pkg->recur != 0 ) {
-          push @b, {
-            'description' => "$desc (".
-                             $self->time2str_local('short', $cust_bill_pkg->sdate). ' - '.
-                             $self->time2str_local('short', $cust_bill_pkg->edate). ')',
-            'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
-          };
-        }
+        # items of this kind should normally not have sdate/edate.
+        push @b, {
+          'description' => $desc,
+          'amount'      => sprintf('%.2f', $cust_bill_pkg->setup 
+                                           + $cust_bill_pkg->recur)
+        };
 
-      }
+      } # if quotation / package line item / other line item
 
-    }
+    } # foreach $display
 
     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
                                 && $conf->exists('discount-show-always'));
diff --git a/FS/FS/XMLRPC.pm b/FS/FS/XMLRPC.pm
deleted file mode 100644 (file)
index 62ae43d..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
- package FS::XMLRPC;
-
-use strict;
-use vars qw( $DEBUG );
-use Frontier::RPC2;
-
-# Instead of 'use'ing freeside modules on the fly below, just preload them now.
-use FS;
-use FS::CGI;
-use FS::Conf;
-use FS::Record;
-use FS::cust_main;
-
-use Data::Dumper;
-
-$DEBUG = 0;
-
-=head1 NAME
-
-FS::XMLRPC - Object methods for handling XMLRPC requests
-
-=head1 SYNOPSIS
-
-  use FS::XMLRPC;
-
-  $xmlrpc = new FS::XMLRPC;
-
-  ($error, $response_xml) = $xmlrpc->serve($request_xml);
-
-=head1 DESCRIPTION
-
-The FS::XMLRPC object is a mechanisim to access read-only data from freeside's subroutines.  It does not, at least not at this point, give you the ability to access methods of freeside objects remotely.  It can, however, be used to call subroutines such as FS::cust_main::smart_search and FS::Record::qsearch.
-
-See the serve method below for calling syntax.
-
-=head1 METHODS
-
-=over 4
-
-=item new
-
-Provides a FS::XMLRPC object used to handle incoming XMLRPC requests.
-
-=cut
-
-sub new {
-
-  my $class = shift;
-  my $self = {};
-  bless($self, $class);
-
-  $self->{_coder} = new Frontier::RPC2;
-
-  return $self;
-
-}
-
-=item serve REQUEST_XML_SCALAR
-
-The serve method takes a scalar containg an XMLRPC request for one of freeside's subroutines (not object methods).  Parameters passed in the 'methodCall' will be passed as a list to the subroutine untouched.  The return value of the called subroutine _must_ be a freeside object reference (eg. qsearchs) or a list of freeside object references (eg. qsearch, smart_search), _and_, the object(s) returned must support the hashref method.  This will be checked first by calling UNIVERSAL::can('FS::class::subroutine', 'hashref').
-
-Return value is an XMLRPC methodResponse containing the results of the call.  The result of the subroutine call itself will be coded in the methodResponse as an array of structs, regardless of whether there was many or a single object returned.  In other words, after you decode the response, you'll always have an array.
-
-=cut
-
-sub serve {
-
-  my ($self, $request_xml) = (shift, shift);
-  my $response_xml;
-
-  my $coder = $self->{_coder};
-  my $call = $coder->decode($request_xml);
-  
-  warn "Got methodCall with method_name='" . $call->{method_name} . "'"
-    if $DEBUG;
-
-  $response_xml = $coder->encode_response(&_serve($call->{method_name}, $call->{value}));
-
-  return ('', $response_xml);
-
-}
-
-sub _serve { #Subroutine, not method
-
-  my ($method_name, $params) = (shift, shift);
-
-
-  #die 'Called _serve without parameters' unless ref($params) eq 'ARRAY';
-  $params = [] unless (ref($params) eq 'ARRAY');
-
-  if ($method_name =~ /^(\w+)\.(\w+)/) {
-
-    #my ($class, $sub) = split(/\./, $method_name);
-    my ($class, $sub) = ($1, $2);
-    my $fssub = "FS::${class}::${sub}";
-    warn "fssub: ${fssub}" if $DEBUG;
-    warn "params: " . Dumper($params) if $DEBUG;
-
-    my @result;
-
-    if ($class eq 'Conf') { #Special case for FS::Conf because we need an obj.
-
-      if ($sub eq 'config') {
-        my $conf = new FS::Conf;
-        @result = ($conf->config(@$params));
-      } else {
-       warn "FS::XMLRPC: Can't call undefined subroutine '${fssub}'";
-      }
-
-    } else {
-
-      unless (UNIVERSAL::can("FS::${class}", $sub)) {
-        warn "FS::XMLRPC: Can't call undefined subroutine '${fssub}'";
-        # Should we encode an error in the response,
-        # or just break silently to the remote caller and complain locally?
-        return [];
-      }
-
-      eval { 
-        no strict 'refs';
-        my $fssub = "FS::${class}::${sub}";
-        @result = (&$fssub(@$params));
-      };
-
-      if ($@) {
-        warn "FS::XMLRPC: Error while calling '${fssub}': $@";
-        return [];
-      }
-
-    }
-
-    if ( scalar(@result) == 1 && ref($result[0]) eq 'HASH' ) {
-      return $result[0];
-    } elsif (grep { UNIVERSAL::can($_, 'hashref') ? 0 : 1 } @result) {
-      #warn "FS::XMLRPC: One or more objects returned from '${fssub}' doesn't " .
-      #     "support the 'hashref' method.";
-      
-      # If they're not FS::Record decendants, just return the results unmap'd?
-      # This is more flexible, but possibly more error-prone.
-      return [ @result ];
-    } else {
-      return [ map { $_->hashref } @result ];
-    }
-  } elsif ($method_name eq 'version') {
-    return [ $FS::VERSION ];
-  } # else...
-
-  warn "Unhandled XMLRPC request '${method_name}'";
-  return {};
-
-}
-
-=head1 BUGS
-
-Probably lots.
-
-=head1 SEE ALSO
-
-L<Frontier::RPC2>.
-
-=cut
-
-1;
-
diff --git a/FS/FS/XMLRPC_Lite.pm b/FS/FS/XMLRPC_Lite.pm
new file mode 100644 (file)
index 0000000..9d3059d
--- /dev/null
@@ -0,0 +1,17 @@
+package FS::XMLRPC_Lite;
+
+use XMLRPC::Lite;
+
+use XMLRPC::Transport::HTTP;
+
+#XXX submit patch to SOAP::Lite
+
+package XMLRPC::Transport::HTTP::Server;
+
+@XMLRPC::Transport::HTTP::Server::ISA = qw(SOAP::Transport::HTTP::Server);
+
+sub initialize; *initialize = \&XMLRPC::Server::initialize;
+sub make_fault; *make_fault = \&XMLRPC::Transport::HTTP::CGI::make_fault;
+sub make_response; *make_response = \&XMLRPC::Transport::HTTP::CGI::make_response;
+
+1;
index 83ddb65..3c0e3e7 100644 (file)
@@ -1065,6 +1065,8 @@ sub generate_email {
   my %return = (
     'from'      => $args{'from'},
     'subject'   => ($args{'subject'} || $self->email_subject),
+    'custnum'   => $self->custnum,
+    'msgtype'   => 'invoice',
   );
 
   $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
index a943921..066ddf1 100644 (file)
@@ -968,6 +968,31 @@ sub tax_locationnum {
   }
 }
 
+sub tax_location {
+  my $self = shift;
+  FS::cust_location->by_key($self->tax_locationnum);
+}
+
+=item part_X
+
+Returns the L<FS::part_pkg> or L<FS::part_fee> object that defines this
+charge.  If called on a tax line, returns nothing.
+
+=cut
+
+sub part_X {
+  my $self = shift;
+  if ( $self->override_pkgpart ) {
+    return FS::part_pkg->by_key($self->override_pkgpart);
+  } elsif ( $self->pkgnum ) {
+    return $self->cust_pkg->part_pkg;
+  } elsif ( $self->feepart ) {
+    return $self->part_fee;
+  } else {
+    return;
+  }
+}
+
 =back
 
 =head1 CLASS METHODS
index 8ea73c9..b9adfaf 100644 (file)
@@ -26,8 +26,8 @@ FS::cust_bill_pkg_fee - Object methods for cust_bill_pkg_fee records
 =head1 DESCRIPTION
 
 An FS::cust_bill_pkg_fee object records the origin of a fee.  
-.  FS::cust_bill_pkg_fee inherits from
-FS::Record.  The following fields are currently supported:
+FS::cust_bill_pkg_fee inherits from FS::Record.  The following fields 
+are currently supported:
 
 =over 4
 
@@ -70,8 +70,8 @@ sub check {
   my $error = 
     $self->ut_numbern('billpkgfeenum')
     || $self->ut_number('billpkgnum')
-    || $self->ut_foreign_key('origin_invnum', 'cust_bill', 'invnum')
-    || $self->ut_foreign_keyn('origin_billpkgnum', 'cust_bill_pkg', 'billpkgnum')
+    || $self->ut_foreign_key('base_invnum', 'cust_bill', 'invnum')
+    || $self->ut_foreign_keyn('base_billpkgnum', 'cust_bill_pkg', 'billpkgnum')
     || $self->ut_money('amount')
   ;
   return $error if $error;
index 567be21..58bd475 100644 (file)
@@ -815,14 +815,9 @@ sub credit_lineitems {
 
     # recalculate taxes with new amounts
     $taxlisthash{$invnum} ||= {};
-    my $part_pkg = $cust_bill_pkg->part_pkg;
-    $cust_main->_handle_taxes( $part_pkg,
-                               $taxlisthash{$invnum},
-                               $cust_bill_pkg,
-                               $cust_bill_pkg->cust_pkg,
-                               $cust_bill_pkg->cust_bill->_date, #invoice time
-                               $cust_bill_pkg->cust_pkg->pkgpart,
-                             );
+    my $part_pkg = $cust_bill_pkg->part_pkg
+      if $cust_bill_pkg->pkgpart_override;
+    $cust_main->_handle_taxes( $taxlisthash{$invnum}, $cust_bill_pkg );
   }
 
   ###
@@ -918,12 +913,12 @@ sub credit_lineitems {
 
       # we still have to deal with the possibility that the tax links don't
       # cover the whole amount of tax because of an incomplete upgrade...
-      if ($amount > 0) {
+      if ($amount > 0.005) {
         $cust_credit_bill{$invnum} += $amount;
         push @{ $cust_credit_bill_pkg{$invnum} },
           new FS::cust_credit_bill_pkg {
             'billpkgnum' => $tax_item->billpkgnum,
-            'amount'     => $amount,
+            'amount'     => sprintf('%.2f', $amount),
             'setuprecur' => 'setup',
           };
 
index be9cd70..1f741b2 100644 (file)
@@ -166,11 +166,16 @@ sub insert {
         'amount'           => sprintf('%.2f', 0-$amount),
       };
 
-      my $error = $cust_tax_exempt_pkg->insert;
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return "error inserting cust_tax_exempt_pkg: $error";
+      if ( $cust_tax_exempt_pkg->cust_main_county ) {
+
+        my $error = $cust_tax_exempt_pkg->insert;
+        if ( $error ) {
+          $dbh->rollback if $oldAutoCommit;
+          return "error inserting cust_tax_exempt_pkg: $error";
+        }
+
       }
+
     } #foreach $exemption
   }
 
index 78794fd..181640d 100644 (file)
@@ -45,6 +45,9 @@ time billing runs for the customer.
 
 =item feepart - key of the fee definition (L<FS::part_fee>).
 
+=item nextbill - 'Y' if the fee should be charged on the customer's next
+bill, rather than causing a bill to be produced immediately.
+
 =back
 
 =head1 METHODS
@@ -93,6 +96,7 @@ sub check {
     || $self->ut_foreign_key('eventnum', 'cust_event', 'eventnum')
     || $self->ut_foreign_keyn('billpkgnum', 'cust_bill_pkg', 'billpkgnum')
     || $self->ut_foreign_key('feepart', 'part_fee', 'feepart')
+    || $self->ut_flag('nextbill')
   ;
   return $error if $error;
 
@@ -108,7 +112,8 @@ sub check {
 =item by_cust CUSTNUM[, PARAMS]
 
 Finds all cust_event_fee records belonging to the customer CUSTNUM.  Currently
-fee events can be cust_main or cust_bill events; this will return both.
+fee events can be cust_main, cust_pkg, or cust_bill events; this will return 
+all of them.
 
 PARAMS can be additional params to pass to qsearch; this really only works
 for 'hashref' and 'order_by'.
@@ -141,6 +146,15 @@ sub by_cust {
     extra_sql => "$where eventtable = 'cust_bill' ".
                  "AND cust_bill.custnum = $custnum",
     %params
+  }),
+  qsearch({
+    table     => 'cust_event_fee',
+    addl_from => 'JOIN cust_event USING (eventnum) ' .
+                 'JOIN part_event USING (eventpart) ' .
+                 'JOIN cust_pkg ON (cust_event.tablenum = cust_pkg.pkgnum)',
+    extra_sql => "$where eventtable = 'cust_pkg' ".
+                 "AND cust_pkg.custnum = $custnum",
+    %params
   })
 }
 
index 6bd82d1..8d38992 100644 (file)
@@ -533,8 +533,6 @@ sub bill {
 
     my @cust_bill_pkg = _omit_zero_value_bundles(@{ $cust_bill_pkg{$pass} });
 
-    next unless @cust_bill_pkg; #don't create an invoice w/o line items
-
     warn "$me billing pass $pass\n"
            #.Dumper(\@cust_bill_pkg)."\n"
       if $DEBUG > 2;
@@ -547,13 +545,26 @@ sub bill {
       hashref => { 'billpkgnum' => '' }
     );
     warn "$me found pending fee events:\n".Dumper(\@pending_event_fees)."\n"
-      if @pending_event_fees;
+      if @pending_event_fees and $DEBUG > 1;
+
+    # determine whether to generate an invoice
+    my $generate_bill = scalar(@cust_bill_pkg) > 0;
+
+    foreach my $event_fee (@pending_event_fees) {
+      $generate_bill = 1 unless $event_fee->nextbill;
+    }
+    
+    # don't create an invoice with no line items, or where the only line 
+    # items are fees that are supposed to be held until the next invoice
+    next if !$generate_bill;
 
+    # calculate fees...
     my @fee_items;
     foreach my $event_fee (@pending_event_fees) {
       my $object = $event_fee->cust_event->cust_X;
+      my $part_fee = $event_fee->part_fee;
       my $cust_bill;
-      if ( $object->isa('FS::cust_main') ) {
+      if ( $object->isa('FS::cust_main') or $object->isa('FS::cust_pkg') ) {
         # Not the real cust_bill object that will be inserted--in particular
         # there are no taxes yet.  If you want to charge a fee on the total 
         # invoice amount including taxes, you have to put the fee on the next
@@ -564,12 +575,20 @@ sub bill {
             'charged'       => ${ $total_setup{$pass} } +
                                ${ $total_recur{$pass} },
         });
+
+        # If this is a package event, only apply the fee to line items 
+        # from that package.
+        if ($object->isa('FS::cust_pkg')) {
+          $cust_bill->set('cust_bill_pkg', 
+            [ grep  { $_->pkgnum == $object->pkgnum } @cust_bill_pkg ]
+          );
+        }
+
       } elsif ( $object->isa('FS::cust_bill') ) {
         # simple case: applying the fee to a previous invoice (late fee, 
         # etc.)
         $cust_bill = $object;
       }
-      my $part_fee = $event_fee->part_fee;
       # if the fee def belongs to a different agent, don't charge the fee.
       # event conditions should prevent this, but just in case they don't,
       # skip the fee.
@@ -581,11 +600,14 @@ sub bill {
       # also skip if it's disabled
       next if $part_fee->disabled eq 'Y';
       # calculate the fee
-      my $fee_item = $event_fee->part_fee->lineitem($cust_bill);
+      my $fee_item = $part_fee->lineitem($cust_bill) or next;
       # link this so that we can clear the marker on inserting the line item
       $fee_item->set('cust_event_fee', $event_fee);
       push @fee_items, $fee_item;
+
     }
+    
+    # add fees to the invoice
     foreach my $fee_item (@fee_items) {
 
       push @cust_bill_pkg, $fee_item;
@@ -596,12 +618,9 @@ sub bill {
       my $fee_location = $self->ship_location; # I think?
 
       my $error = $self->_handle_taxes(
-        $part_fee,
         $taxlisthash{$pass},
         $fee_item,
-        $fee_location,
-        $options{invoice_time},
-        {} # no options
+        location => $fee_location
       );
       return $error if $error;
 
@@ -1319,14 +1338,7 @@ sub _make_lines {
       # handle taxes
       ###
 
-      my $error = $self->_handle_taxes(
-        $part_pkg,
-        $taxlisthash,
-        $cust_bill_pkg,
-        $cust_location,
-        $options{invoice_time},
-        \%options # I have serious objections to this
-      );
+      my $error = $self->_handle_taxes( $taxlisthash, $cust_bill_pkg );
       return $error if $error;
 
       $cust_bill_pkg->set_display(
@@ -1423,15 +1435,13 @@ sub _transfer_balance {
   return @transfers;
 }
 
-=item _handle_taxes PART_ITEM TAXLISTHASH CUST_BILL_PKG CUST_LOCATION TIME [ OPTIONS ]
+=item handle_taxes TAXLISTHASH CUST_BILL_PKG [ OPTIONS ]
 
 This is _handle_taxes.  It's called once for each cust_bill_pkg generated
-from _make_lines, along with the part_pkg (or part_fee), cust_location,
-invoice time, a flag indicating whether the package is being canceled, and a 
-partridge in a pear tree.
+from _make_lines.
 
-The most important argument is 'taxlisthash'.  This is shared across th
-entire invoice.  It looks like this:
+TAXLISTHASH is a hashref shared across the entire invoice.  It looks lik
+this:
 {
   'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
   'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
@@ -1444,16 +1454,27 @@ That "..." is a list of FS::cust_bill_pkg objects that will be fed to
 the 'taxline' method to calculate the amount of the tax.  This doesn't
 happen until calculate_taxes, though.
 
+OPTIONS may include:
+- part_item: a part_pkg or part_fee object to be used as the package/fee 
+  definition.
+- location: a cust_location to be used as the billing location.
+
+If not supplied, part_item will be inferred from the pkgnum or feepart of the
+cust_bill_pkg, and location from the pkgnum (or, for fees, the invnum and 
+the customer's default service location).
+
 =cut
 
 sub _handle_taxes {
   my $self = shift;
-  my $part_item = shift;
   my $taxlisthash = shift;
   my $cust_bill_pkg = shift;
-  my $location = shift;
-  my $invoice_time = shift;
-  my $options = shift;
+  my %options = @_;
+
+  # at this point I realize that we have enough information to infer all this
+  # stuff, instead of passing around giant honking argument lists
+  my $location = $options{location} || $cust_bill_pkg->tax_location;
+  my $part_item = $options{part_item} || $cust_bill_pkg->part_X;
 
   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
 
@@ -1473,9 +1494,8 @@ sub _handle_taxes {
     my @classes;
     #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
     push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
-    # debatable
-    push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
-    push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
+    push @classes, 'setup' if $cust_bill_pkg->setup;
+    push @classes, 'recur' if $cust_bill_pkg->recur;
 
     my $exempt = $conf->exists('cust_class-tax_exempt')
                    ? ( $self->cust_class ? $self->cust_class->tax : '' )
@@ -1543,10 +1563,7 @@ sub _handle_taxes {
           warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
           # calculate the tax amount that the tax_on_tax will apply to
           my $hashref_or_error = 
-            $tax_object->taxline( $localtaxlisthash{$tax},
-                                  'custnum'      => $self->custnum,
-                                  'invoice_time' => $invoice_time,
-                                );
+            $tax_object->taxline( $localtaxlisthash{$tax} );
           return $hashref_or_error
             unless ref($hashref_or_error);
           
index c9cf686..8d57a54 100644 (file)
@@ -22,9 +22,9 @@ FS::cust_msg - Object methods for cust_msg records
 
 =head1 DESCRIPTION
 
-An FS::cust_msg object represents a template-generated message sent to 
-a customer (see L<FS::msg_template>).  FS::cust_msg inherits from
-FS::Record.  The following fields are currently supported:
+An FS::cust_msg object represents an email message generated by Freeside 
+and sent to a customer (see L<FS::msg_template>).  FS::cust_msg inherits 
+from FS::Record.  The following fields are currently supported:
 
 =over 4
 
@@ -34,6 +34,8 @@ FS::Record.  The following fields are currently supported:
 
 =item msgnum - template number
 
+=item msgtype - the message type
+
 =item _date - the time the message was sent
 
 =item env_from - envelope From address
@@ -125,8 +127,8 @@ sub check {
 
   my $error = 
     $self->ut_numbern('custmsgnum')
-    || $self->ut_number('custnum')
-    || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
+    || $self->ut_numbern('custnum')
+    || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum')
     || $self->ut_numbern('msgnum')
     || $self->ut_foreign_keyn('msgnum', 'msg_template', 'msgnum')
     || $self->ut_numbern('_date')
@@ -136,6 +138,11 @@ sub check {
     || $self->ut_anything('body')
     || $self->ut_enum('status', \@statuses)
     || $self->ut_textn('error')
+    || $self->ut_enum('msgtype', [  '',
+                                    'invoice',
+                                    'receipt',
+                                    'admin',
+                                 ])
   ;
   return $error if $error;
 
index 63d7c48..10b51ad 100644 (file)
@@ -414,12 +414,17 @@ sub void {
   } );
   $cust_pay_void->reason(shift) if scalar(@_);
   my $error = $cust_pay_void->insert;
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return $error;
+
+  my $cust_pay_pending =
+    qsearchs('cust_pay_pending', { paynum => $self->paynum });
+  if ( $cust_pay_pending ) {
+    $cust_pay_pending->set('void_paynum', $self->paynum);
+    $cust_pay_pending->set('paynum', '');
+    $error ||= $cust_pay_pending->replace;
   }
 
-  $error = $self->delete;
+  $error ||= $self->delete;
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -611,11 +616,12 @@ sub send_receipt {
         'custnum' => $cust_main->custnum,
       };
       $error = $queue->insert(
-         FS::msg_template->by_key($msgnum)->prepare(
+        FS::msg_template->by_key($msgnum)->prepare(
           'cust_main'   => $cust_main,
           'object'      => $self,
           'from_config' => 'payment_receipt_from',
-        )
+        ),
+        'msgtype' => 'receipt', # override msg_template's default
       );
 
     } elsif ( $conf->exists('payment_receipt_email') ) {
@@ -658,6 +664,7 @@ sub send_receipt {
         'job'     => 'FS::Misc::process_send_generated_email',
         'paynum'  => $self->paynum,
         'custnum' => $cust_main->custnum,
+        'msgtype' => 'receipt',
       };
       $error = $queue->insert(
         'from'    => $conf->config('invoice_from', $cust_main->agentnum),
index f5de73d..63274b1 100644 (file)
@@ -135,6 +135,10 @@ L<FS::payment_gateway> id.
 
 Payment number (L<FS::cust_pay>) of the completed payment.
 
+=item void_paynum
+
+Payment number of the payment if it's been voided.
+
 =item invnum
 
 Invoice number (L<FS::cust_bill>) to try to apply this payment to.
@@ -224,6 +228,7 @@ sub check {
     || $self->ut_foreign_keyn('paynum', 'cust_pay', 'paynum' )
     || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
     || $self->ut_foreign_keyn('invnum', 'cust_bill', 'invnum')
+    || $self->ut_foreign_keyn('void_paynum', 'cust_pay_void', 'paynum' )
     || $self->ut_flag('manual')
     || $self->ut_numbern('discount_term')
     || $self->payinfo_check() #payby/payinfo/paymask/paydate
index 55b6c67..b2f777b 100644 (file)
@@ -133,12 +133,16 @@ sub unvoid {
     map { $_ => $self->get($_) } fields('cust_pay')
   } );
   my $error = $cust_pay->insert;
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return $error;
+
+  my $cust_pay_pending =
+    qsearchs('cust_pay_pending', { void_paynum => $self->paynum });
+  if ( $cust_pay_pending ) {
+    $cust_pay_pending->set('paynum', $cust_pay->paynum);
+    $cust_pay_pending->set('void_paynum', '');
+    $error ||= $cust_pay_pending->replace;
   }
 
-  $error = $self->delete;
+  $error ||= $self->delete;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
index 668de75..4ea3966 100644 (file)
@@ -923,6 +923,8 @@ sub cancel {
         'to'      => \@invoicing_list,
         'subject' => ( $conf->config('cancelsubject') || 'Cancellation Notice' ),
         'body'    => [ map "$_\n", $conf->config('cancelmessage') ],
+        'custnum' => $self->custnum,
+        'msgtype' => '', #admin?
       );
     }
     #should this do something on errors?
@@ -1343,6 +1345,8 @@ sub suspend {
           'Package : #'. $self->pkgnum. " (". $self->part_pkg->pkg_comment. ")\n",
           ( map { "Service : $_\n" } @labels ),
         ],
+        'custnum' => $self->custnum,
+        'msgtype' => 'admin'
       );
 
       if ( $error ) {
@@ -1589,6 +1593,8 @@ sub unsuspend {
           : ''
         ),
       ],
+      'custnum' => $self->custnum,
+      'msgtype' => 'admin',
     );
 
     if ( $error ) {
index 43b8703..47efd31 100644 (file)
@@ -397,12 +397,18 @@ sub search {
   );
 
   if( exists($params->{'active'} ) ) {
-    # This overrides all the other date-related fields
+    # This overrides all the other date-related fields, and includes packages
+    # that were active at some time during the interval.  It excludes:
+    # - packages that were set up after the end of the interval
+    # - packages that were canceled before the start of the interval
+    # - packages that were suspended before the start of the interval
+    #   and are still suspended now
     my($beginning, $ending) = @{$params->{'active'}};
     push @where,
       "cust_pkg.setup IS NOT NULL",
       "cust_pkg.setup <= $ending",
       "(cust_pkg.cancel IS NULL OR cust_pkg.cancel >= $beginning )",
+      "(cust_pkg.susp   IS NULL OR cust_pkg.susp   >= $beginning )",
       "NOT (".FS::cust_pkg->onetime_sql . ")";
   }
   else {
index 7bf41ee..be5a9eb 100644 (file)
@@ -5,6 +5,7 @@ use strict;
 use vars qw( $DEBUG $me $ignore_quantity $conf $ticket_system );
 use Carp;
 #use Scalar::Util qw( blessed );
+use List::Util qw( max );
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs dbh str2time_sql );
 use FS::part_pkg;
@@ -363,15 +364,26 @@ sub check {
   return "Unknown svcpart" unless $part_svc;
 
   if ( $self->pkgnum && ! $ignore_quantity ) {
-    my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
-    return "Unknown pkgnum" unless $cust_pkg;
-    ($part_svc) = grep { $_->svcpart == $self->svcpart } $cust_pkg->part_svc;
-    return "No svcpart ". $self->svcpart.
-           " services in pkgpart ". $cust_pkg->pkgpart
-      unless $part_svc || $ignore_quantity;
-    return "Already ". $part_svc->get('num_cust_svc'). " ". $part_svc->svc.
+
+    #slightly inefficient since ->pkg_svc will also look it up, but fixing
+    # a much larger perf problem and have bigger fish to fry
+    my $cust_pkg = $self->cust_pkg;
+
+    my $pkg_svc = $self->pkg_svc
+      or return "No svcpart ". $self->svcpart.
+                " services in pkgpart ". $cust_pkg->pkgpart;
+
+    my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
+
+    #false laziness w/cust_pkg->part_svc
+    my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
+                            - $num_cust_svc
+                       );
+
+    return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
            " services for pkgnum ". $self->pkgnum
-      if !$ignore_quantity && $part_svc->get('num_avail') <= 0 ;
+      if $num_avail <= 0;
+
   }
 
   $self->SUPER::check;
index c1dda22..74adbed 100644 (file)
@@ -134,13 +134,7 @@ sub delete {
   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;
-  }
-  
   my $pkey = $self->primary_key;
   #my $option_table = $self->option_table;
 
@@ -152,6 +146,12 @@ sub delete {
     }
   }
 
+  my $error = $self->SUPER::delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   '';
index 8eb86fa..a49782d 100644 (file)
@@ -2,6 +2,7 @@ package FS::part_event::Action::Mixin::fee;
 
 use strict;
 use base qw( FS::part_event::Action );
+use FS::Record qw( qsearch );
 
 sub event_stage { 'pre-bill'; }
 
@@ -15,16 +16,34 @@ sub option_fields {
                     value_col => 'feepart',
                     disable_empty => 1,
                   },
-  );
+  ),
+
 }
 
 sub default_weight { 10; }
 
+sub hold_until_bill { 1 }
+
 sub do_action {
   my( $self, $cust_object, $cust_event ) = @_;
 
-  die "no fee definition selected for event '".$self->event."'\n"
-    unless $self->option('feepart');
+  my $feepart = $self->option('feepart')
+    or die "no fee definition selected for event '".$self->event."'\n";
+  my $tablenum = $cust_object->get($cust_object->primary_key);
+
+  # see if there's already a pending fee for this customer/invoice
+  my @existing = qsearch({
+      table     => 'cust_event_fee',
+      addl_from => 'JOIN cust_event USING (eventnum)',
+      hashref   => { feepart    => $feepart,
+                     billpkgnum => '' },
+      extra_sql => " AND tablenum = $tablenum",
+  });
+  if (scalar @existing > 0) {
+    warn $self->event." event, object $tablenum: already scheduled\n"
+      if $FS::part_fee::DEBUG;
+    return;
+  }
 
   # mark the event so that the fee will be charged
   # the logic for calculating the fee amount is in FS::part_fee
@@ -32,8 +51,9 @@ sub do_action {
   # FS::cust_bill_pkg
   my $cust_event_fee = FS::cust_event_fee->new({
       'eventnum'    => $cust_event->eventnum,
-      'feepart'     => $self->option('feepart'),
+      'feepart'     => $feepart,
       'billpkgnum'  => '',
+      'nextbill'    => $self->hold_until_bill ? 'Y' : '',
   });
 
   my $error = $cust_event_fee->insert;
index fc185e4..5d962b1 100644 (file)
@@ -9,4 +9,20 @@ sub eventtable_hashref {
     { 'cust_bill' => 1 };
 }
 
+sub option_fields {
+  (
+    __PACKAGE__->SUPER::option_fields,
+    'nextbill'  => { label    => 'Hold fee until the customer\'s next bill',
+                     type     => 'checkbox',
+                     value    => 'Y'
+                   },
+  )
+}
+
+# it makes sense for this to be optional for previous-invoice fees
+sub hold_until_bill {
+  my $self = shift;
+  $self->option('nextbill');
+}
+
 1;
index a6f1078..9373091 100644 (file)
@@ -9,6 +9,8 @@ sub eventtable_hashref {
     { 'cust_main' => 1 };
 }
 
+sub hold_until_bill { 1 }
+
 # Otherwise identical to cust_bill_fee.  We only have a separate event 
 # because it behaves differently as an invoice event than as a customer
 # event, and needs a different description.
index c2b4673..f1d5891 100644 (file)
@@ -1,5 +1,7 @@
 package FS::part_event::Action::fee;
 
+# DEPRECATED; will most likely be removed in 4.x
+
 use strict;
 use base qw( FS::part_event::Action );
 
@@ -53,11 +55,9 @@ sub _calc_fee {
       my $part_pkg = FS::part_pkg->new({
           taxclass => $self->option('taxclass')
       });
-      my $error = $cust_main->_handle_taxes(
-        FS::part_pkg->new({ taxclass => ($self->option('taxclass') || '') }),
-        $taxlisthash,
-        $charge,
-        FS::cust_pkg->new({custnum => $cust_main->custnum}),
+      my $error = $cust_main->_handle_taxes( $taxlisthash, $charge,
+        location  => $cust_main->ship_location,
+        part_item => $part_pkg,
       );
       if ( $error ) {
         warn "error estimating taxes for breakage charge: custnum ".$cust_main->custnum."\n";
diff --git a/FS/FS/part_event/Action/pkg_fee.pm b/FS/FS/part_event/Action/pkg_fee.pm
new file mode 100644 (file)
index 0000000..7e409a5
--- /dev/null
@@ -0,0 +1,16 @@
+package FS::part_event::Action::pkg_fee;
+
+use strict;
+use base qw( FS::part_event::Action::Mixin::fee );
+
+sub description { 'Charge a fee when this package is billed'; }
+
+sub eventtable_hashref {
+    { 'cust_pkg' => 1 };
+}
+
+sub hold_until_bill { 1 }
+
+# Functionally identical to cust_fee.
+
+1;
index 8e10ea7..9d261f0 100644 (file)
@@ -161,6 +161,10 @@ sub delete {
     'link_table'    => 'export_nas',
     'target_table'  => 'nas',
     'params'        => [],
+  ) || $self->process_m2m(
+    'link_table'    => 'export_svc',
+    'target_table'  => 'part_svc',
+    'params'        => [],
   ) || $self->SUPER::delete;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
index 1fcb828..41f0409 100644 (file)
@@ -6,7 +6,6 @@ use FS::part_export;
 use FS::Record qw(qsearch qsearchs);
 use FS::Conf;
 use FS::msg_template;
-use FS::Misc qw(send_email);
 
 @ISA = qw(FS::part_export);
 
index 67da245..ccf1351 100644 (file)
@@ -5,7 +5,7 @@ use base qw( FS::o2m_Common FS::Record );
 use vars qw( $DEBUG );
 use FS::Record qw( qsearch qsearchs );
 
-$DEBUG = 1;
+$DEBUG = 0;
 
 =head1 NAME
 
@@ -126,6 +126,9 @@ and replace methods.
 sub check {
   my $self = shift;
 
+  $self->set('amount', 0) unless $self->amount;
+  $self->set('percent', 0) unless $self->percent;
+
   my $error = 
     $self->ut_numbern('feepart')
     || $self->ut_textn('comment')
@@ -138,28 +141,25 @@ sub check {
     || $self->ut_floatn('credit_weight')
     || $self->ut_agentnum_acl('agentnum',
                               [ 'Edit global package definitions' ])
-    || $self->ut_moneyn('amount')
-    || $self->ut_floatn('percent')
+    || $self->ut_money('amount')
+    || $self->ut_float('percent')
     || $self->ut_moneyn('minimum')
     || $self->ut_moneyn('maximum')
     || $self->ut_flag('limit_credit')
-    || $self->ut_enum('basis', [ '', 'charged', 'owed' ])
+    || $self->ut_enum('basis', [ 'charged', 'owed', 'usage' ])
     || $self->ut_enum('setuprecur', [ 'setup', 'recur' ])
   ;
   return $error if $error;
 
-  return "For a percentage fee, the basis must be set"
-    if $self->get('percent') > 0 and $self->get('basis') eq '';
-
-  if ( ! $self->get('percent') and ! $self->get('limit_credit') ) {
-    # then it makes no sense to apply minimum/maximum
-    $self->set('minimum', '');
-    $self->set('maximum', '');
-  }
   if ( $self->get('limit_credit') ) {
     $self->set('maximum', '');
   }
 
+  if ( $self->get('basis') eq 'usage' ) {
+    # to avoid confusion, don't also allow charging a percentage
+    $self->set('percent', 0);
+  }
+
   $self->SUPER::check;
 }
 
@@ -175,7 +175,7 @@ sub explanation {
   my $money_char = FS::Conf->new->config('money_char') || '$';
   my $money = $money_char . '%.2f';
   my $percent = '%.1f%%';
-  my $string;
+  my $string = '';
   if ( $self->amount > 0 ) {
     $string = sprintf($money, $self->amount);
   }
@@ -190,7 +190,14 @@ sub explanation {
     } elsif ( $self->basis('owed') ) {
       $string .= 'unpaid invoice balance';
     }
+  } elsif ( $self->basis eq 'usage' ) {
+    if ( $string ) {
+      $string .= " plus \n";
+    }
+    # append per-class descriptions
+    $string .= join("\n", map { $_->explanation } $self->part_fee_usage);
   }
+
   if ( $self->minimum or $self->maximum or $self->limit_credit ) {
     $string .= "\nbut";
     if ( $self->minimum ) {
@@ -219,11 +226,17 @@ representing the invoice line item for the fee, with linked
 L<FS::cust_bill_pkg_fee> record(s) allocating the fee to the invoice or 
 its line items, as appropriate.
 
+If the fee is going to be charged on the upcoming invoice (credit card 
+processing fees, postal invoice fees), INVOICE should be an uninserted
+L<FS::cust_bill> object where the 'cust_bill_pkg' property is an arrayref
+of the non-fee line items that will appear on the invoice.
+
 =cut
 
 sub lineitem {
   my $self = shift;
   my $cust_bill = shift;
+  my $cust_main = $cust_bill->cust_main;
 
   my $amount = 0 + $self->get('amount');
   my $total_base;  # sum of base line items
@@ -235,37 +248,72 @@ sub lineitem {
   warn "Calculating fee: ".$self->itemdesc." on ".
     ($cust_bill->invnum ? "invoice #".$cust_bill->invnum : "current invoice").
     "\n" if $DEBUG;
-  if ( $self->percent > 0 and $self->basis ne '' ) {
-    warn $self->percent . "% of amount ".$self->basis.")\n"
-      if $DEBUG;
-
-    # $total_base: the total charged/owed on the invoice
-    # %item_base: billpkgnum => fraction of base amount
-    if ( $cust_bill->invnum ) {
-      my $basis = $self->basis;
-      $total_base = $cust_bill->$basis; # "charged", "owed"
+  my $basis = $self->basis;
+
+  # $total_base: the total charged/owed on the invoice
+  # %item_base: billpkgnum => fraction of base amount
+  if ( $cust_bill->invnum ) {
 
-      # calculate the fee on an already-inserted past invoice.  This may have 
-      # payments or credits, so if basis = owed, we need to consider those.
+    # calculate the fee on an already-inserted past invoice.  This may have 
+ &nb